Android 에이전트 적용
WhaTap 모바일 에이전트를 Android 앱에 적용하면 화면 로딩, 네트워크 호출, 크래시, ANR, 리소스 사용량, WebView 페이지 성능을 수집할 수 있습니다. 라이브러리 추가부터 데이터 수집 확인까지 적용 절차를 안내합니다. WebView 안의 웹 페이지까지 수집하려면 웹 담당자의 작업도 필요합니다. 자세한 내용은 WebView 페이지 수집 설정에서 다룹니다.
지원 환경
앱 설정
| Item | Value |
|---|---|
minSdk | 21(Android 5.0) 이상 |
| 외부 의존성 | 없음 |
| AAR 크기 | 약 380 ~ 400KB |
빌드 도구
Android Gradle Plugin(AGP) 7.0 이상이 필요합니다. JDK와 Gradle 버전은 사용하는 AGP의 요건을 그대로 따릅니다.
| AGP | JDK | Gradle |
|---|---|---|
| 7.x | 11 이상 | 7.0 이상 |
| 8.x | 17 이상 | 8.x |
| 9.x | 17 이상 | 9.5 이상 |
WebView 연동 요구 버전
앱 안의 WebView 페이지까지 함께 수집하려면 Android 에이전트와 브라우저 에이전트가 모두 요구 버전을 충족해야 합니다. 한쪽이라도 버전이 낮으면 일부 항목이 수집되지 않으며, 앱과 웹 어느 로그에도 오류가 남지 않습니다.
| Component | Minimum version | Owner |
|---|---|---|
| Android 에이전트 | 2.3.5 | 앱 개발자 |
| 브라우저 에이전트 | 3.2.0 | 웹 개발자 |
적용을 마친 뒤에는 대시보드의 agent_version 값으로 실제 동작 중인 버전을 확인하세요. 파일을 교체하는 것만으로는 반영 여부를 알 수 없습니다. 브라우저 에이전트 파일의 버전은 첫 4줄의 배너로 확인합니다.
head -4 whatap-browser-agent.js
에이전트 설치
다음 순서로 진행합니다.
여기까지 마치면 화면 전환, 리소스, 크래시, ANR 수집이 시작됩니다. 네트워크, WebView, 메서드 추적에는 추가 작업이 필요합니다. 자세한 내용은 수집 항목에서 확인하세요.
1. 라이브러리 추가
두 가지 방법 중 하나를 선택합니다. Maven Central 방식을 권장합니다.
Maven Central(권장)
mavenCentral()이 이미 설정되어 있다면 저장소를 추가로 설정하지 않아도 됩니다. Maven Central에서 최신 버전을 확인해 <최신 버전> 자리에 넣으세요.
dependencies {
implementation("io.whatap.android:whatap-android-agent:<최신 버전>")
}
dependencies {
implementation 'io.whatap.android:whatap-android-agent:<최신 버전>'
}
저장소 선언이 없다면 추가합니다.
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
}
버전을 +나 latest.release로 지정하지 마세요. 빌드할 때마다 에이전트 버전이 달라져 문제가 생겼을 때 원인을 찾기 어렵습니다. Maven Central에서 최신 버전을 확인하고 명시적으로 지정하세요.
AAR 파일 직접 추가
폐쇄망이나 사내망으로 Maven Central에 접근할 수 없을 때 사용합니다. 전달받은 AAR 파일을 app/libs/에 복사합니다. 아래 예제의 파일명은 예시이며, 실제 전달받는 파일명은 상황에 따라 다를 수 있습니다. 복사한 파일의 실제 이름으로 바꿔 지정하세요.
dependencies {
implementation(files("libs/whatap-agent-bom-complete.aar"))
}
dependencies {
implementation files('libs/whatap-agent-bom-complete.aar')
}
2. Manifest 설정
AndroidManifest.xml에 권한과 Application 클래스를 등록합니다.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:name=".MyApplication"
...>
</application>
</manifest>
android:name을 등록하지 않으면 Application 클래스가 실행되지 않아 SDK가 초기화되지 않습니다. WebView를 연동한 앱에서는 브릿지가 "에이전트 없음"을 반환합니다. 그러면 웹 데이터가 표준 HTTP 전송으로 폴백하고, 대시보드에서는 일반 브라우저(BROWSER)로 분류됩니다. 디버그 로그를 켜면 앱 로그에 Agent가 초기화되지 않았습니다가 남습니다.
3. SDK 초기화
Application.onCreate()에서 한 번만 호출합니다. 앱에 이미 Application 서브클래스가 있다면 그 클래스의 onCreate()에 코드를 추가하세요.
import android.app.Application
import io.whatap.android.agent.WhatapAgent
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
WhatapAgent.Builder.newBuilder()
.setProjectKey("<PROJECT_ACCESS_KEY>")
.setPCode(12345) // Int 입니다. L 접미어를 붙이면 컴파일 오류
.setServerUrl("https://<수집 서버 도메인>/m") // 끝의 /m 필수
.setSampling(1.0)
.build(this)
}
}
import android.app.Application;
import io.whatap.android.agent.WhatapAgent;
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
WhatapAgent.Builder.newBuilder()
.setProjectKey("<PROJECT_ACCESS_KEY>")
.setPCode(12345)
.setServerUrl("https://<수집 서버 도메인>/m")
.setSampling(1.0)
.build(this);
}
}
필수 값은 세 가지입니다.
| Method | Description |
|---|---|
setProjectKey(String) | 발급받은 project access key |
setPCode(int) | 프로젝트 코드. int 타입 |
setServerUrl(String) | 수집 서버 base URL. 빠지면 데이터가 전송되지 않음 |
나머지 옵션은 Builder 옵션에서 확인하세요.
초기화 순서를 지키세요.
네트워크 라이브러리 계측부터 나오는 모든 API는 build(this)가 끝난 뒤 호출해야 합니다. 그 전에 호출하면 네트워크 계측은 무시되며, 커스텀 로그에서 IllegalStateException이 발생합니다.
4. ProGuard 설정
minifyEnabled false인 앱은 이 절을 건너뛰어도 됩니다. R8이 실행되지 않아 클래스와 메서드 이름이 바뀌지 않습니다.
minifyEnabled true인 경우, AAR 안에 consumer ProGuard 규칙이 들어 있어 자동으로 적용됩니다. 화면, 네트워크, 크래시, ANR, 리소스 같은 네이티브 계측만 쓴다면 별도 keep 규칙이 필요 없습니다.
WebView 를 사용하는 앱
AAR에 동봉된 consumer 규칙에는 WebView 브릿지 메서드를 보존하는 규칙이 없습니다. 브라우저 에이전트는 브릿지 메서드를 이름으로 호출하므로, R8이 @JavascriptInterface 메서드 이름을 바꾸면 브릿지 호출이 모두 실패합니다. 이때 window.whatapBridge 객체는 그대로 남아 전송 경로는 브릿지로 유지됩니다. 개별 호출만 실패하기 때문에 HTTP 폴백도 동작하지 않고 WebView 데이터가 전량 유실됩니다.
증상은 "release 빌드에서만 웹뷰 데이터 0건, 디버그 빌드는 정상"으로 나타납니다. 다음 규칙을 추가하세요.
# WhaTap WebView 브릿지. JS가 메서드명으로 직접 호출하므로 이름 보존 필수
-keep class io.whatap.android.agent.webview.** { *; }
-keepclassmembers class * {
@android.webkit.JavascriptInterface <methods>;
}
-keepattributes *Annotation*
-dontwarn io.whatap.android.**
build.gradle의 proguardFiles에 getDefaultProguardFile("proguard-android-optimize.txt") 또는 getDefaultProguardFile("proguard-android.txt")가 포함되어 있다면 AGP 기본 파일이 @JavascriptInterface 규칙을 이미 담고 있습니다. 위 규칙이 필수인 경우는 기본 파일을 포함하지 않고 커스텀 규칙만 사용하는 프로젝트입니다. 기본 파일을 쓰는 프로젝트도 proguardFiles 구성이 바뀔 때를 대비해 명시적으로 추가하는 것을 권장합니다.
화면 이름 가독성
대시보드의 화면 이름과 ScreenGroup 이름은 Activity 클래스의 단순 이름으로 만들어집니다. R8이 Activity와 Fragment 클래스명을 바꾸는 구성이라면 화면 이름이 난독화된 값으로 적재됩니다. 데이터가 유실되지는 않지만 어느 화면인지 알아보기 어렵습니다. 필요하면 다음 규칙을 추가하세요. 브릿지 동작에는 필요하지 않습니다.
-keep class * extends android.app.Activity
-keep class * extends androidx.fragment.app.Fragment
-keepclassmembers class * extends android.app.Activity {
public void *(android.view.View);
}
5. 설치 확인
앱을 실행한 뒤 logcat으로 확인합니다. 에이전트의 logcat 태그는 모두 whatap으로 시작합니다.
adb logcat | grep -i whatap
Android Studio의 Logcat 창을 사용한다면 필터에 tag:whatap을 입력하세요.
초기화 성공
✅ WhatapAgent가 성공적으로 초기화되었습니다.
서버 전송 성공
✅ [SpanExporter] HTTP 200 Success (123ms)
데이터는 기본적으로 10초마다 전송되므로 앱을 실행한 직후에는 이 로그가 나오지 않습니다. 화면을 몇 번 이동한 뒤 다시 확인하세요. 이 로그가 나오면 HTTP 요청이 성공한 것입니다. 수집 서버가 데이터를 정상적으로 처리했는지는 대시보드에 데이터가 보이는지 확인하세요.
네트워크 요청 수집
🌐 [SpanExporter] Starting export for Span: GET (TraceId: ...)
- network_library: OkHttp / Volley / HttpUrlConnection
- http_request_method: GET
- status_code: 200
수집 항목
build(this)가 정상 종료되면 다음 항목을 자동으로 수집합니다.
- Activity와 Fragment 화면 로딩, ScreenGroup
- 크래시와 ANR
- 사용자 로그
- CPU, 메모리, 온도 등 리소스 사용량
- heartbeat
setCollectScreenLoading 또는 setCollectHeartbeat를 끄면 해당 항목만 수집하지 않습니다. 크래시와 ANR 수집은 build(this)가 성공하면 항상 켜집니다. 크래시와 ANR 수집을 끄거나 리포터 종류를 선택하는 Builder 옵션은 없습니다. iOS의 useNativeCrashReporter(), usePLCrashReporter()는 iOS 전용이므로 Android 코드로 옮겨 쓸 수 없습니다.
다음 세 항목은 자동으로 켜지지 않으며 추가 작업이 필요합니다.
| Item | 필요한 작업 |
|---|---|
| 네트워크 라이브러리 요청, 응답 정보 | 네트워크 라이브러리 계측의 wrap(), onRequest() 호출 또는 자동 계측 적용 |
| 메서드 실행시간 | 메서드 실행시간 추적의 CallStackTracer 호출 |
| WebView 페이지 로드와 성능 정보 | WebView 페이지 수집 설정의 브릿지 등록 |
Builder 옵션
모든 Builder 옵션은 선택 사항이며, 지정하지 않으면 기본값이 적용됩니다.
기본 설정
| Method | Default | Description |
|---|---|---|
setProjectKey(String) | (필수) | 발급받은 project access key |
setPCode(int) | (필수) | 프로젝트 코드 |
setServerUrl(String) | (필수) | base URL. /trace, /log가 자동으로 붙음 |
setTraceServerUrl(String) | (자동) | trace 엔드포인트를 직접 지정할 때만 사용 |
setLogServerUrl(String) | (자동) | log 엔드포인트를 직접 지정할 때만 사용 |
setSampling(double) | 1.0 | 수집 비율. 0.0 ~ 1.0 스케일이며 50%는 0.5. 범위를 벗어난 값은 무시하고 기본값 유지 |
setSampling(double)은 0.0 ~ 1.0 스케일입니다. 브라우저 에이전트의 sampleRate는 0 ~ 100 스케일이므로 값을 그대로 옮겨 적으면 안 됩니다. 모바일의 1.0을 웹에 그대로 넣으면 수집 비율이 1%가 됩니다.
수집 항목 켜고 끄기
| Method | Default | Description |
|---|---|---|
setCollectScreenLoading(boolean) | true | 화면 로딩 수집 |
setCollectNetwork(boolean) | true | 네트워크 수집 |
setCollectHeartbeat(boolean) | true | 30초 주기 heartbeat |
setScreenGroupDelaySeconds(int) | 0 | 화면 그룹 닫힘 지연(초). 0은 비활성화 |
setExcludeLifecycleEventsFromScreenGroup(boolean) | true | 화면 그룹에서 생명주기 이벤트 제외 |
setCollectNetwork(false)로 두면 네트워크 라이브러리 계측의 wrap(), onRequest() 호출이 전부 아무 일도 하지 않습니다. 네트워크를 계측하려면 기본값 true를 유지하세요.
setScreenGroupDelaySeconds(int)의 기본값은 0입니다. 0이면 마지막 task가 끝나는 즉시 ScreenGroup이 닫힙니다. 화면에 진입한 뒤 WebView의 loadUrl()을 호출하기 까지 시간이 오래 걸리면, 웹뷰 데이터가 네이티브 화면과 다른 그룹으로 묶일 수 있습니다. 이 경우 3 정도를 지정해 그룹이 바로 닫히지 않도록 하세요.
전송과 버퍼링
트래픽을 줄여야 할 때 조정합니다.
| Method | Default | Description |
|---|---|---|
setFlushIntervalMs(long) | 10000 | 배치 전송 주기(ms) |
setQueueSize(int) | 1000 | 메모리 큐 크기 |
setMaxDiskBytes(int) | 512000 | 오프라인 디스크 버퍼 상한(bytes) |
setMaxDiskFiles(int) | 5 | 디스크 버퍼 파일 개수. 초과 시 회전 |
setKeepAliveEnabled(boolean) | true | HTTP keep-alive |
setMaxConnections(int) | 5 | 최대 동시 연결 수 |
setDisconnectAfterSend(boolean) | false | 전송 후 연결 즉시 종료 |
WhatapAgent.Builder.newBuilder()
.setProjectKey("<PROJECT_ACCESS_KEY>")
.setPCode(12345)
.setServerUrl("https://<수집 서버 도메인>/m")
.setCollectHeartbeat(false)
.setFlushIntervalMs(60_000L)
.build(this)
백그라운드 상태에서도 heartbeat와 리소스 샘플러가 전송될 수 있습니다. 사용량이 적은 시간대의 트래픽을 줄이려면 위 예제처럼 heartbeat 수집을 끄거나 flush 간격을 늘리세요.
setKeepAliveEnabled(boolean)과 setMaxConnections(int)는 각각 http.keepAlive와 http.maxConnections JVM system property를 전역으로 설정합니다. 따라서 앱에서 Volley나 HttpURLConnection을 사용하고 있다면 이들의 동작도 함께 바뀝니다. 기본값을 사용하는 것을 권장합니다.
setUserId(String)과 setSessionId(String)에 값을 지정해도 실제 전송되는 값에는 반영되지 않습니다(현재 배포 버전 기준). 세션 ID는 build() 안에서 새로 만든 값으로 덮어씁니다. 사용자 ID는 SharedPreferences 값으로 결정됩니다. 로그인 사용자를 반영하려면 로그인 사용자 ID의 WhatapExtra.set(WhatapExtra.USER_ID, ...)를 사용하세요.
네트워크 헤더 캡처
에이전트 2.3.4부터 지원합니다. 이전 버전에는 아래 세 메서드가 없어 빌드가 실패합니다.
| Method | Default | Description |
|---|---|---|
setCaptureRequestHeaders(boolean) | false | request 헤더를 http.request.header.* attribute로 캡처 |
setCaptureResponseHeaders(boolean) | false | response 헤더를 http.response.header.* attribute로 캡처 |
setHeaderValueMaxLength(int) | 512 | 헤더 값 최대 길이(bytes). 초과 시 절삭. 0은 무제한 |
옵션을 켜면 OkHttp, HttpURLConnection, Volley, Apache HttpClient를 통과하는 모든 HTTP 헤더가 다음 규칙에 따라 부착됩니다. 이 규칙은 OpenTelemetry Semantic Conventions를 따릅니다.
| Rule | Example |
|---|---|
헤더 이름을 소문자로 바꾸고 -를 _로 변환 | X-Request-ID → http.request.header.x_request_id |
| 응답 헤더도 같은 규칙 적용 | Set-Cookie → http.response.header.set_cookie |
| 다중값 헤더는 쉼표로 연결 | a, b, c |
| 길이 초과 시 앞부분만 남기고 표시 | ...[TRUNCATED to <N>B] |
민감 헤더 자동 마스킹이 없습니다.
Authorization, Cookie, Set-Cookie, X-API-Key, Proxy-Authorization 같은 민감 헤더도 옵션을 켜면 값 그대로 캡처되어 수집 서버로 전송됩니다. 켜기 전에 캡처 범위를 반드시 검토하고, 필요 없다면 기본값 false로 두세요.
위 옵션과 무관하게 다음 attribute는 항상 자동으로 부착됩니다(2.3.4 이상).
| Attribute | Type | Description |
|---|---|---|
http.request.body.size | long | request body 크기(bytes) |
http.response.body.size | long | response body 크기(bytes) |
네트워크 라이브러리 계측
AAR을 추가하는 것만으로는 네트워크 데이터가 수집되지 않습니다. 사용하는 네트워크 라이브러리에 맞는 코드를 직접 추가해야 합니다. 계측 초기화는 build(this)가 자동으로 처리하므로 앱에서 init()을 따로 호출할 필요는 없습니다.
OkHttp3와 Retrofit
import io.whatap.android.agent.instrumentation.okhttp.OkHttp3Instrumentation
val base = OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.build()
val client = OkHttp3Instrumentation.wrap(base)
val retrofit = Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(client)
.build()
wrap()이 반환한 client를 사용해야 계측됩니다. 원본 base를 그대로 쓰면 아무것도 수집되지 않습니다.
요청 단위 attribute가 필요하면 wrap(client, Config) 오버로드를 사용하세요. 요청 단위 사용자 정의 attribute를 참조하세요.
Volley
import io.whatap.android.agent.instrumentation.volley.VolleyInstrumentation;
StringRequest req = new StringRequest(Request.Method.GET, url,
response -> {
// 응답 처리
VolleyInstrumentation.onResponseExit(req, response);
},
error -> {
// 에러 처리
VolleyInstrumentation.onErrorExit(req, error);
});
VolleyInstrumentation.onRequest(req);
queue.add(req);
onRequest()는 queue.add() 직전에, onResponseExit()와 onErrorExit()는 각 콜백 안에서 호출합니다. 세 지점이 모두 들어가야 합니다.
Volley는 성공 콜백에서 실제 상태 코드를 알 수 없어, 성공 응답의 status_code는 일률적으로 200으로 기록됩니다. 실패는 VolleyError.networkResponse.statusCode에서 정확한 값을 가져옵니다. OkHttp와 HttpURLConnection은 실제 코드가 그대로 들어갑니다.
HttpURLConnection
import io.whatap.android.agent.instrumentation.httpurlconnection.HttpUrlConnectionInstrumentation;
HttpURLConnection raw = (HttpURLConnection) url.openConnection();
HttpURLConnection conn = HttpUrlConnectionInstrumentation.wrap(raw);
try {
int code = conn.getResponseCode();
// body 처리
} finally {
conn.disconnect(); // span 은 wrap 이 자동으로 닫습니다
}
wrap()이 반환한 conn을 사용해야 계측됩니다. 원본 raw를 그대로 쓰면 수집되지 않습니다.
Apache HttpClient
두 가지 방법이 있습니다.
import io.whatap.android.agent.instrumentation.httpclient.ApacheHttpClientInstrumentation;
import io.whatap.android.agent.instrumentation.httpclient.InstrumentedDefaultHttpClient;
// 방법 1. 기존 HttpClient 를 감쌉니다
HttpClient client = ApacheHttpClientInstrumentation.wrap(existingClient);
// 방법 2. DefaultHttpClient 를 쓰고 있었다면 드롭인 교체 (2.2.6 이상)
HttpClient client2 = new InstrumentedDefaultHttpClient();
InstrumentedDefaultHttpClient는 attribute 체이닝을 제공합니다.
HttpClient client3 = new InstrumentedDefaultHttpClient()
.addAttribute("module", "payment")
.setResponseAttributeExtractor(resp -> {
Map<String, Object> a = new HashMap<>();
a.put("biz.status", resp.getStatusLine().getStatusCode());
return a;
});
Apache HttpClient는 Android 6.0부터 플랫폼에서 제거되었으므로 빌드 설정에 다음이 필요합니다.
android {
useLibrary("org.apache.http.legacy")
}
이 선행 조건을 적용할 수 없으면 OkHttp나 HttpURLConnection 같은 다른 네트워크 클라이언트를 사용하세요.
요청 단위 사용자 정의 attribute
에이전트 2.2.6부터 요청마다 추가 attribute를 붙일 수 있습니다. 다음은 Volley 기준 예시입니다.
VolleyInstrumentation.configure(req)
.addAttribute("module", "payment")
.addAttribute("tenant_id", "demo")
.setRequestAttributeExtractor(r -> {
Map<String, Object> a = new HashMap<>();
a.put("biz.user_id", extractFromQuery(r.getUrl()));
return a;
})
.setResponseAttributeExtractor(resp -> {
Map<String, Object> a = new HashMap<>();
if (resp instanceof String) a.put("biz.body_len", ((String) resp).length());
return a;
});
queue.add(req);
OkHttp와 HttpURLConnection은 wrap(client, Config) 오버로드에 같은 형태의 Config를 넘깁니다. 부착한 값은 전송 JSON의 metrics.extras로 들어가 대시보드에 표시됩니다.
자동 계측(선택)
Gradle 플러그인을 적용하면 바이트코드를 주입해 네트워크 라이브러리와 메서드 실행시간을 자동으로 계측합니다. 따라서 앞 절에서 설명한 wrap()이나 onRequest() 호출을 코드에 직접 넣지 않아도 됩니다.
plugins {
id("io.whatap.android") version "<최신 버전>" apply false
}
plugins {
id("com.android.application")
id("io.whatap.android")
}
plugins {
id 'io.whatap.android' version '<최신 버전>' apply false
}
plugins {
id 'com.android.application'
id 'io.whatap.android'
}
저장소를 따로 선언할 필요는 없습니다. 기본 gradlePluginPortal()이 Maven Central을 프록시합니다.
플러그인을 적용한 모듈의 소스만 계측합니다.
다음 코드는 자동 계측되지 않습니다. 해당 구간에는 네트워크 라이브러리 계측의 패턴을 직접 넣으세요.
- 플러그인을 적용하지 않은 다른 Gradle 모듈. 모듈마다
id("io.whatap.android")가 필요합니다. 앱 모듈에만 넣으면:network모듈의 요청은 0건입니다. - 의존성 JAR과 AAR 내부 코드. Retrofit이 내부에서 만드는 기본
OkHttpClient등이 해당합니다. OkHttpClient()생성자. 플러그인이 바꾸는 것은OkHttpClient.Builder.build()를 호출하는 지점뿐입니다.
폐쇄망에서는 와탭이 전달한 whatap-android-plugin-<버전>-offline-repo.zip의 압축을 풀고, 그 폴더를 settings.gradle.kts의 pluginManagement 저장소로 등록하세요. 플러그인이 ASM에 의존하므로 플러그인 JAR 파일 하나만 두면 동작하지 않습니다. 앞의 plugins {} 블록은 그대로 사용합니다.
pluginManagement {
repositories {
maven { url = uri("whatap-plugin-repo-<version>") }
gradlePluginPortal()
}
}
WebView 브릿지 등록은 플러그인을 적용해도 자동으로 처리되지 않습니다. WebView 페이지 수집 설정을 따로 진행하세요.
메서드 실행시간 추적
특정 메서드가 실행되는 데 걸리는 시간을 측정합니다. WhatapAgent가 이미 초기화되어 있다면 처음 호출할 때 자동으로 준비됩니다. 따라서 별도로 초기화할 필요가 없습니다.
계측 패턴
패턴 1부터 3은 에이전트 2.2.8, 패턴 4는 2.2.9부터 지원합니다.
import java.util.concurrent.Callable;
import io.whatap.android.agent.instrumentation.stacktrace.CallStackTracer;
import io.whatap.android.agent.instrumentation.stacktrace.StackSpan;
// 패턴 1. try-with-resources. 메서드 본문 전체를 감쌀 때
try (AutoCloseable s = CallStackTracer.scope("PaymentService", "charge")) {
// 측정할 로직
}
// 패턴 2. 반환값 없음
CallStackTracer.measure("PaymentService", "charge", () -> doCharge());
// 패턴 3. 반환값 있음, checked exception 전파
Receipt r = CallStackTracer.measure("PaymentService", "charge",
(Callable<Receipt>) () -> doChargeReturn());
// 패턴 4. StackSpan. 시작 지점과 종료 지점이 떨어져 있을 때
StackSpan span = CallStackTracer.start("PaymentService", "charge");
try {
// 측정할 로직
span.end();
} catch (RuntimeException e) {
span.endWithError(e); // 에러 정보와 함께 종료
}
| 상황 | 권장 패턴 |
|---|---|
| 메서드 본문 전체를 한 줄로 감싸기 | 패턴 1 scope |
| 람다로 감쌀 수 있고 반환값 없음 | 패턴 2 measure(Runnable) |
| 람다로 감쌀 수 있고 반환값 있음 | 패턴 3 measure(Callable) |
| 시작과 종료가 떨어져 있거나 다른 스레드에서 종료 | 패턴 4 StackSpan |
StackSpan은 AutoCloseable을 상속하므로 try-with-resources로도 쓸 수 있습니다. 이미 종료하거나 취소한 span에 다시 호출해도 아무 일도 일어나지 않습니다. span.cancel()은 적재도 전송도 하지 않습니다.
계측 대상 고르기
모든 메서드를 계측할 필요는 없습니다. 너무 많은 메서드를 계측하면 불필요한 데이터만 늘어납니다.
| 우선순위 | 대상 | 예시 |
|---|---|---|
| 1 | 사용자 액션 진입점 | Activity.onCreate, Fragment.onViewCreated, 클릭 핸들러 |
| 2 | 비즈니스 트랜잭션 경계 | 결제, 송금, 인증, 정산 |
| 3 | 네트워크와 DB 호출 wrapper | Repository.findX, ApiService.callY |
| 4 | 백그라운드 무거운 작업 | 이미지 처리, 암복호화, 직렬화 |
| 부적합 | getter, setter, 단순 변환, 짧은 루프 | 10ms 필터에 걸려 버려지고 노이즈만 증가 |
동작 방식
- 실행 시간이 10ms 미만이면 전송하지 않습니다. 설정으로 바꿀 수 없고,
endWithError()로 끝난 span도 같습니다. - 10초마다 배치로 전송합니다.
- 백그라운드 스레드에서도 독립 동작합니다. ThreadLocal 기반입니다.
- 재귀 호출은 안전합니다. 다만 깊이 누적에 주의하세요.
logcat에서 다음과 같이 수집 상태를 확인할 수 있습니다.
CallStackLogRecordCollector initialized
CallStack LogRecord Collector started - interval: 10000ms
First log added: MainActivity.loadData
Scheduled flush triggered - buffer size: 3
Collected 3 records from buffer
Sending batch with 3 records
✅ [LogExporter] HTTP 200 Success (85ms)
Skipping (< 10ms): MainActivity.onCreate duration=4ms는 정상 동작입니다. 10ms 미만 메서드를 걸러낸 것입니다.
화면 그룹 설정
화면 전환 데이터는 build(this)만 호출해도 수집됩니다. 여러 Activity나 Fragment에 걸친 흐름을 하나의 그룹으로 묶고 싶을 때만 이 절의 설정이 필요합니다.
에이전트 2.3.x에서 기존 startGroup(), addTask(), endGroup() API가 startChain(), endChain()으로 바뀌었습니다.