本文介紹如何快速使用Log ServiceAndroid SDK採集日誌。
前提條件
快速使用
參考如下樣本,初始化SDK並調用addLog介面上報日誌。
SDK支援初始化多個執行個體,
LogProducerConfig執行個體與LogProducerClient執行個體需成對使用。上報日誌到Log Service時需使用阿里雲帳號或RAM使用者的AccessKey,用於鑒權及防篡改。為避免將AccessKey儲存在移動端應用中,造成安全風險,推薦您使用移動端日誌直傳服務配置AccessKey。具體操作,請參見採集-搭建移動端日誌直傳服務。
// 建議您全域儲存LogProducerConfig執行個體和LogProducerClient執行個體。
private LogProducerConfig config = null;
private LogProducerClient client = null;
/**
* 初始化SDK
*/
private void initProducer() {
try {
// Log Service的服務存取點。此處必須是以https://或http://開頭。
final String endpoint = "your endpoint";
final String project = "your project";
final String logstore = "your logstore";
config = new LogProducerConfig(context, endpoint, project, logstore);
// 設定日誌主題。
config.setTopic("example_topic");
// 設定tag資訊,此tag資訊將被附加在每條日誌上。
config.addTag("example", "example_tag");
// 是否丟棄到期日誌。0表示不丟棄,把日誌時間修改為目前時間; 1表示丟棄。預設值為1。
config.setDropDelayLog(0);
// 是否丟棄鑒權失敗的日誌。0表示不丟棄,1表示丟棄。預設值為0。
config.setDropUnauthorizedLog(0);
// LogProducerCallback為可選配置, 如果您不需要關注日誌的發送成功或失敗狀態, 可以不註冊callback。
// 如果需要動態化配置AccessKey,建議設定LogProducerCallback,並在onCall方法被調用時更新AccessKey。
final LogProducerCallback callback = new LogProducerCallback() {
@Override
public void onCall(int resultCode, String reqId, String errorMessage, int logBytes, int compressedBytes) {
// resultCode: 狀態代碼, 更多資訊,請參見錯誤碼。
// reqId: 請求ID,已廢棄。
// errorMessage: 失敗資訊。
// logBytes: 日誌原始位元組數。
// compressedBytes: 日誌壓縮位元組數。
final LogProducerResult result = LogProducerResult.fromInt(resultCode);
if (LogProducerResult.LOG_PRODUCER_SEND_UNAUTHORIZED == result || LogProducerResult.LOG_PRODUCER_PARAMETERS_INVALID == result) {
// 更新AccessKey或者SDK的初始化參數。
}
}
};
// 需要關注日誌的發送成功或失敗狀態時, 第二個參數需要傳入一個callback。
client = new LogProducerClient(config, callback);
} catch (LogProducerException e) {
e.printStackTrace();
}
}
// 請求AccessKey資訊。
private void requestAccessKey() {
// 推薦您先使用移動端日誌直傳服務配置AccessKey資訊。
// ...
// 擷取AccessKey資訊後,完成更新。
updateAccessKey(accessKeyId, accessKeySecret, securityToken);
}
// 更新AccessKey資訊。
private void updateAccessKey(String accessKeyId, String accessKeySecret, String securityToken) {
// 通過STS服務擷取的AccessKey包含securityToken,需要使用以下方式更新。
if (null != securityToken && !"".equals(securityToken)) {
config.resetSecurityToken(accessKeyId, accessKeySecret, securityToken);
} else {
// 不是通過STS服務擷取的AccessKey,使用以下方式更新。
config.setAccessKeyId(accessKeyId);
config.setAccessKeySecret(accessKeySecret);
}
}
/**
* 上報日誌
*/
public void addLog() {
Log log = new Log();
// 您可以根據實際業務需要調整需上報的欄位。
log.putContent("content_key_1", 123456);
log.putContent("content_key_2", 23.34f);
log.putContent("content_key_3", "中文️");
log.putContent(null, "null");
log.putContent("null", (String) null);
client.addLog(log);
}進階用法
動態配置參數
Android SDK支援動態化設定Endpoint、ProjectName、Logstore、AccessKey等參數。
動態化配置Endpoint、ProjectName、Logstore。
// 支援獨立配置或一起配置Endpoint、ProjectName、Logstore等參數。 // 更新Endpoint。 config.setEndpoint("your new-endpoint"); // 更新Project名稱。 config.setProject("your new-project"); // 更新Logstore名稱。 config.setLogstore("your new-logstore");動態化配置AccessKey。
動態化配置AccessKey時,一般建議與LogProducerCallback結合使用。
// 如果您在初始化LogProducerClient時已經完成了LogProducerCallback的初始化,以下代碼可忽略。 final LogProducerCallback callback = new LogProducerCallback() { @Override public void onCall(int resultCode, String reqId, String errorMessage, int logBytes, int compressedBytes) { // resultCode: 狀態代碼。更多資訊,請參見錯誤碼。 // reqId: 請求ID, 已廢棄。 // errorMessage: 失敗資訊。 // logBytes: 日誌原始位元組數。 // compressedBytes: 日誌壓縮位元組數。 final LogProducerResult result = LogProducerResult.fromInt(resultCode); if (LogProducerResult.LOG_PRODUCER_SEND_UNAUTHORIZED == result || LogProducerResult.LOG_PRODUCER_PARAMETERS_INVALID == result) { // 需要更新AccessKey或者SDK的初始化參數。 requestAccessKey(); } } }; // 需要關注日誌的發送成功或失敗狀態時, 第二個參數需要傳入一個callback。 client = new LogProducerClient(config, callback); // 請求AccessKey資訊。 private void requestAccessKey() { // 推薦您先使用移動端日誌直傳服務配置AccessKey資訊。 // ... // 擷取AccessKey資訊後,完成更新。 updateAccessKey(accessKeyId, accessKeySecret, securityToken); } // 更新AccessKey資訊。 private void updateAccessKey(String accessKeyId, String accessKeySecret, String securityToken) { // 通過STS服務擷取的AccessKey包含securityToken,需要使用以下方式更新。 if (null != securityToken && !"".equals(securityToken)) { config.resetSecurityToken(accessKeyId, accessKeySecret, securityToken); } else { // 不是通過STS服務擷取的AccessKey,使用以下方式更新。 config.setAccessKeyId(accessKeyId); config.setAccessKeySecret(accessKeySecret); } }動態化配置source、topic、tag。
重要source、topic、tag無法針對某類日誌進行設定。一旦設定後,所有未成功發送到Log Service的日誌,都可能會更新。如果您需要通過source、topic、tag來跟蹤具體類別的日誌,可能會導致與您的業務預期不相符合。建議您在產生Log時新增欄位來標識對應的類別資訊。
// 設定日誌來源。 config.setSource("your new-source"); // 設定日誌主題。 config.setTopic("your new-topic"); // 設定tag資訊,此tag資訊將被附加在每條日誌上。 config.addTag("test", "your new-tag");
斷點續傳
Android SDK支援斷點續傳。開啟斷點續傳後,每次通過addLog方法寫入成功的日誌都會先儲存在本地binlog檔案中,確認日誌發送成功後才會刪除本機資料,確保日誌上傳實現At Least Once。
您可以在SDK初始化時加入以下代碼,實現斷點續傳。
初始化多個LogProducerConfig執行個體時,
LogProducerConfig類的setPersistentFilePath方法需要傳入不同的值。如果您的App存在多進程且開啟了斷點續傳功能,您應只在主進程初始化SDK。如果子進程也有採集資料的需求,您需要確保
setPersistentFilePath方法傳入的檔案路徑的唯一性,否則可能會導致日誌資料錯亂、丟失等問題。使用時應注意多線程導致的LogProducerConfig重複初始化問題。
private void initProducer() {
// 1表示開啟斷點續傳功能,0表示關閉。預設值為0。
config.setPersistent(1);
// 持久化的檔案名稱,需要保證檔案所在的檔案夾已建立。
final String persistentFilePath = getFilesDir() + File.separator + "log_data";
config.setPersistentFilePath(persistentFilePath);
// 持久化檔案滾動個數,建議設定為10。
config.setPersistentMaxFileCount(10);
// 每個持久化檔案的大小,單位為Byte,格式為N*1024*1024。建議N的取值範圍為1~10。
config.setPersistentMaxFileSize(N*1024*1024);
// 本地最多緩衝的日誌數量,不建議超過1048576,預設為65536。
config.setPersistentMaxLogCount(65536);
}混淆規則
-keep class com.aliyun.sls.android.producer.** { *; }
-keep interface com.aliyun.sls.android.producer.** { *; }Android許可權
<uses-permission android:name="android.permission.INTERNET" />配置參數說明
所有的配置參數由LogProducerConfig類提供,詳細說明如下表所示。
參數 | 資料類型 | 說明 |
setTopic | String | 設定topic欄位的值。預設值為空白符串。 |
addTag | String | 設定tag,格式為 |
setSource | String | 設定source欄位的值。預設值為Android。 |
setPacketLogBytes | Int | 每個緩衝的日誌包大小上限。超過上限後,日誌會被立即發送。 取值範圍為1~5242880,預設值為1024 * 1024,單位為位元組。 |
setPacketLogCount | Int | 每個緩衝的日誌包中包含日誌數量的最大值。超過上限後日誌會被立即發送。 取值範圍為1~4096,預設值為1024。 |
setPacketTimeout | Int | 被緩衝日誌的發送逾時時間,如果緩衝逾時,日誌會被立即發送。 預設值為3000,單位為毫秒。 |
setMaxBufferLimit | Int | 單個Producer Client執行個體可以使用的記憶體上限,超出緩衝時add_log介面會立即返回失敗。 預設值為64 * 1024 * 1024。 |
setSendThreadCount | Int | 發送線程數。開啟斷點續傳後,該值強製為1。 |
setPersistent | Int | 是否開啟斷點續傳功能。
|
setPersistentFilePath | String | 持久化的檔案名稱,需保證檔案所在的檔案夾已建立。配置多個LogProducerConfig執行個體時,需確保唯一性。 預設值為空白。 |
setPersistentForceFlush | Int | 是否開啟每次AddLog強制重新整理功能。
在高可靠性情境時建議開啟, |
setPersistentMaxFileCount | Int | 持久化檔案滾動個數,建議設定成10。預設值為0。 |
setPersistentMaxFileSize | Int | 每個持久化檔案的大小,單位為Byte,格式為N*1024*1024。建議N的取值範圍為1~10。 |
setPersistentMaxLogCount | Int | 本地最多緩衝的日誌數量,不建議超過1048576,預設為65536。 |
setConnectTimeoutSec | Int | 網路連接逾時時間。預設值為10,單位為秒。 |
setSendTimeoutSec | Int | 日誌發送逾時時間。預設值為15,單位為秒。 |
setDestroyFlusherWaitSec | Int | flusher線程銷毀最大等待時間。預設值為1,單位為秒。 |
setDestroySenderWaitSec | Int | sender線程池銷毀最大等待時間。預設值為1,單位為秒。 |
setCompressType | Int | 資料上傳時的壓縮類型。
|
setNtpTimeOffset | Int | 裝置時間與標準時間的差值,值為標準時間-裝置時間。一般這種差值是由於使用者用戶端裝置時間不同步情境。預設值為0,單位秒。 |
setMaxLogDelayTime | Int | 日誌時間與本機時間的差值。超過該差值後,SDK會根據setDropDelayLog選項進行處理。單位秒,預設值為7243600,即7天。 |
setDropDelayLog | Int | 是否丟棄超過setMaxLogDelayTime的到期日誌。
|
setDropUnauthorizedLog | Int | 是否丟棄鑒權失敗的日誌。
|
setCallbackFromSenderThread | Boolean | 是否從sender線程回調callback。
|
錯誤碼
全部的錯誤碼定義在枚舉類LogProducerResult,詳細說明如下表所示。
錯誤碼 | 數值 | 說明 | 解決方案 |
LOG_PRODUCER_OK | 0 | 成功。 | 不涉及。 |
LOG_PRODUCER_INVALID | 1 | SDK已銷毀或無效。 |
|
LOG_PRODUCER_WRITE_ERROR | 2 | 資料寫入錯誤,可能原因是Project寫入流量已達上限。 | 調整Project寫入資料傳輸量上限。具體操作,請參見調整資源配額。 |
LOG_PRODUCER_DROP_ERROR | 3 | 磁碟或記憶體緩衝已滿,日誌無法寫入。 | 調整maxBufferLimit、persistentMaxLogCount、persistentMaxFileSize參數值後重試。 |
LOG_PRODUCER_SEND_NETWORK_ERROR | 4 | 網路錯誤。 | 檢查Endpoint、Project、Logstore的配置情況。 |
LOG_PRODUCER_SEND_QUOTA_ERROR | 5 | Project寫入流量已達上限。 | 調整Project寫入資料傳輸量上限。具體操作,請參見調整資源配額。 |
LOG_PRODUCER_SEND_UNAUTHORIZED | 6 | AccessKey到期、無效或AccessKey權限原則配置不正確。 | 檢查AccessKey。 RAM使用者需具備動作記錄服務資源的許可權。具體操作,請參見為RAM使用者授權。 |
LOG_PRODUCER_SEND_SERVER_ERROR | 7 | 服務錯誤。 | 提請工單聯絡支援人員。 |
LOG_PRODUCER_SEND_DISCARD_ERROR | 8 | 資料被丟棄,一般是裝置時間與伺服器時間不同步導致。 | SDK會自動重新發送。 |
LOG_PRODUCER_SEND_TIME_ERROR | 9 | 與伺服器時間不同步。 | SDK會自動修複該問題。 |
LOG_PRODUCER_SEND_EXIT_BUFFERED | 10 | SDK銷毀時快取資料還沒有上報。 | 建議開啟斷點續傳功能可避免資料丟失。 |
LOG_PRODUCER_PARAMETERS_INVALID | 11 | SDK初始化參數錯誤。 | 檢查AccessKey、Endpoint、Project、Logstore等參數配置。 |
LOG_PRODUCER_PERSISTENT_ERROR | 99 | 快取資料寫入磁碟失敗。 | 1、檢查快取檔案路徑配置不正確。 2、檢查快取檔案是否寫滿 3、檢查系統磁碟空間是否充足。 |
常見問題
為什麼會存在重複日誌?
Android SDK發送日誌的過程是非同步,受網路狀態影響,日誌可能會發送失敗並重新發送。由於SDK只在介面返回200狀態代碼時才認為發送成功,因此日誌會存在一定的重複率。建議您通過SQL查詢分析語句對日誌進行去重。
如果日誌重複率較高,您需要先排查下SDK初始化是否存在問題。主要原因和解決方案如下:
斷點續傳配置錯誤
針對斷點續傳配置錯誤,您需要確認
setPersistentFilePath方法傳入的檔案路徑是否全域唯一。SDK重複初始化
造成SDK重複初始化的常見原因是單個執行個體寫法錯誤,或者沒有使用單例模式封裝SDK的初始化,建議您參考以下方式完成SDK初始化。
public class AliyunLogHelper { private LogProducerConfig config; private LogProducerClient client; private static class Holder { private static final AliyunLogHelper INSTANCE = new AliyunLogHelper(); } public static AliyunLogHelper getInstance() { return Holder.INSTANCE; } private AliyunLogHelper() { initProducer(); } private void initProducer() { // 以下代碼替換為您的初始化代碼。 try { config = new LogProducerConfig(); client = new LogProducerClient(config); } catch (LogProducerException e) { e.printStackTrace(); } } public void addLog(Log log) { if (null == client) { return; } client.addLog(log); } }另外一個造成SDK重複初始化的原因是多進程。建議您只在主進程初始化SDK,或者在不同進程中初始化SDK時,設定
setPersistentFilePath為不同的值,確保唯一性。
弱網環境配置最佳化
如果您的應用是在弱網環境下使用,建議您參考如下樣本最佳化SDK配置參數。
// 初始化SDK。 private void initProducer() { // 調整HTTP連結和發送逾時時間,有利於減少日誌重複率。 // 您可以根據實際情況調整具體的逾時時間。 config.setConnectTimeoutSec(20); config.setSendTimeoutSec(20); // 其他初始化參數。 // ... }
日誌缺失,如何處理?
日誌上報的過程是非同步,如果在上報日誌前App被關閉,則日誌有可能無法被上報,造成日誌丟失。建議您開啟斷點續傳功能,具體操作,請參見斷點續傳。
日誌上報延時,如何處理?
SDK發送日誌的過程是非同步,受網路環境以及應用使用情境的影響,日誌可能不會立即上報。如果只有個別裝置出現日誌上報延時,這種情況是正常的,否則請您根據如下錯誤碼進行排查。
錯誤碼 | 說明 |
LOG_PRODUCER_SEND_NETWORK_ERROR | 檢查Endpoint、Project、Logstore的配置是否正確。 |
LOG_PRODUCER_SEND_UNAUTHORIZED | 檢查AccessKey是否到期、有效或AccessKey權限原則配置是否正確。 |
LOG_PRODUCER_SEND_QUOTA_ERROR | 調整Project寫入流量已達上限。具體操作,請參見調整資源配額。 |
Android SDK是否支援DNS預解析和緩衝策略?
以下提供Android SDK結合HTTPDNS SDK以及OkHttp實現DNS預解析和緩衝策略的樣本。
實現自訂 DNS 介面。
/** * 基於OkHttp Dns 介面,實現自訂 Dns 解析服務,供參考。 */ public class OkHttpDns implements Dns { private Context mContext; public OkHttpDns(Context context) { mContext = context; } @Override public List<InetAddress> lookup(String host) throws UnknownHostException { // !!注意!! // accountId 需要替換為您在HTTPDNS服務申請的值,具體請參考HTTPDNS文檔 HTTPDNSResult httpdnsResult = HttpDns.getService(mContext, accountId) .getHttpDnsResultForHostSync(host, RequestIpType.both); List<InetAddress> inetAddresses = new ArrayList<>(); InetAddress address; try { if (httpdnsResult.getIps() != null) { //處理IPv4地址 for (String ipv4 : httpdnsResult.getIps()) { address = InetAddress.getByName(ipv4); inetAddresses.add(address); } } if (httpdnsResult.getIpv6s() != null) { //處理IPv6地址 for (String ipv6 : httpdnsResult.getIpv6s()) { address = InetAddress.getByName(ipv6); inetAddresses.add(address); } } } catch (UnknownHostException e) { } if (!inetAddresses.isEmpty()) { return inetAddresses; } return okhttp3.Dns.SYSTEM.lookup(host); } }實現自訂NetworkInterface。
/** * 基於 OkHttp 實現 SLS SDK 的 NetworkInterface,供參考 */ private static class OkHttpNetworkInterface implements LogProducerHttpTool.NetworkInterface { private OkHttpClient client; public OkHttpNetworkInterface(Context context) { client = new OkHttpClient.Builder() .connectTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .writeTimeout(30, TimeUnit.SECONDS) .dns(new OkHttpDns(context)) .build(); } @Override public NetworkResponse send(String urlString, String method, String[] header, byte[] body) { // 建議先調用一次SDK原始的實現,在日誌上報失敗時再通過OkHttp上報。 LogHttpResponse logHttpResponse = LogProducerHttpTool.android_http_post(urlString, method, header, body); if (logHttpResponse.getStatusCode() != -1) { NetworkResponse response = new NetworkResponse(); response.statusCode = logHttpResponse.getStatusCode(); response.errorMessage = logHttpResponse.getErrorMessage(); return response; } // 通過 OkHttp 上報日誌 Request.Builder builder = new Request.Builder(); builder.url(urlString); builder.post(RequestBody.create(body)); for (int i = 0; i < header.length; i += 2) { builder.addHeader(header[i], header[i + 1]); } // 不管請求成功或失敗,都需要返回 NetworkResponse,否則SDK的運行可能會出錯 try (okhttp3.Response response = client.newCall(builder.build()).execute()) { NetworkResponse httpResponse = new NetworkResponse(); httpResponse.statusCode = response.code(); httpResponse.headers = response.headers().toMultimap(); httpResponse.errorMessage = response.message(); return httpResponse; } catch (IOException e) { NetworkResponse httpResponse = new NetworkResponse(); httpResponse.statusCode = -1; httpResponse.headers = null; httpResponse.errorMessage = e.getLocalizedMessage(); return httpResponse; } } }完成SLS SDK初始化。
public class AliyunSLS { private void initSLS(Context context) { // 給SLS SDK設定自訂 NetworkInterface // !!!注意!!! // 該設定會對所有SLS SDK的樣本生效 LogProducerHttpTool.setNetworkInterface(new OkHttpNetworkInterface(context)); // 初始化SLS SDK initProducer(); } private void initProducer() { // 這裡正常實現 LogProducerConfig 和 LogProducerClient 的初始化。 // ... } }