Last active
October 23, 2022 01:41
-
-
Save nethergrim/38c6a73084a00d2e2e06a28a15e1c2bc to your computer and use it in GitHub Desktop.
Android advanced logging to Crashlytics with Timber
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import com.crashlytics.android.Crashlytics; | |
import timber.log.Timber; | |
public class CrashlyticsTree extends Timber.DebugTree { | |
@Override | |
protected void log(int priority, String tag, String message, Throwable t) { | |
Crashlytics.log(message); | |
if (t != null) { | |
Crashlytics.logException(t); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import android.content.Intent; | |
import android.os.Bundle; | |
import android.support.annotation.NonNull; | |
import android.support.annotation.Nullable; | |
import android.support.v7.app.AppCompatActivity; | |
import timber.log.Timber; | |
public abstract class LogActivity extends AppCompatActivity { | |
private void logInternal(String message){ | |
Timber.d(getFinalClassName() + ": " + message); | |
} | |
@Override | |
protected void onDestroy() { | |
logInternal("onDestroy() called"); | |
super.onDestroy(); | |
} | |
@Override | |
protected void onCreate(@Nullable Bundle savedInstanceState) { | |
logInternal("onCreate() called with: savedInstanceState = [" + savedInstanceState + "]"); | |
super.onCreate(savedInstanceState); | |
} | |
@Override | |
protected void onStart() { | |
logInternal("onStart() called"); | |
super.onStart(); | |
} | |
@Override | |
protected void onStop() { | |
logInternal("onStop() called"); | |
super.onStop(); | |
} | |
@Override | |
protected void onResume() { | |
logInternal("onResume() called"); | |
super.onResume(); | |
} | |
@Override | |
protected void onPause() { | |
logInternal("onPause() called"); | |
super.onPause(); | |
} | |
@Override | |
protected void onActivityResult(int requestCode, int resultCode, Intent data) { | |
logInternal("onActivityResult() called with: requestCode = [" + requestCode + | |
"], resultCode = [" + resultCode + "], data = [" + data + "]"); | |
super.onActivityResult(requestCode, resultCode, data); | |
} | |
@Override | |
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, | |
@NonNull int[] grantResults) { | |
super.onRequestPermissionsResult(requestCode, permissions, grantResults); | |
logInternal("onRequestPermissionsResult() called with: requestCode = [" + requestCode + | |
"], permissions = [" + permissions + "], grantResults = [" + grantResults + "]"); | |
} | |
private String getFinalClassName(){ | |
return this.getClass().getSimpleName(); | |
} | |
@Override | |
protected void onNewIntent(Intent intent) { | |
logInternal("onNewIntent() called with: intent = [" + intent + "]"); | |
super.onNewIntent(intent); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import android.content.Intent; | |
import android.os.Bundle; | |
import android.support.annotation.NonNull; | |
import android.support.annotation.Nullable; | |
import android.support.v4.app.Fragment; | |
import android.view.LayoutInflater; | |
import android.view.View; | |
import android.view.ViewGroup; | |
import timber.log.Timber; | |
public abstract class LogFragment extends Fragment { | |
@Override | |
public void onCreate(@Nullable Bundle savedInstanceState) { | |
logInternal("onCreate() called with: savedInstanceState = [" + savedInstanceState + "]"); | |
super.onCreate(savedInstanceState); | |
} | |
@Nullable | |
@Override | |
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, | |
@Nullable Bundle savedInstanceState) { | |
logInternal("onCreateView() called with: inflater = [" + inflater + "], container = [" + | |
container + "], savedInstanceState = [" + savedInstanceState + "]"); | |
return super.onCreateView(inflater, container, savedInstanceState); | |
} | |
@Override | |
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { | |
logInternal("onViewCreated() called with: view = [" + view + "], savedInstanceState = [" + | |
savedInstanceState + "]"); | |
super.onViewCreated(view, savedInstanceState); | |
} | |
@Override | |
public void onResume() { | |
logInternal("onResume() called"); | |
super.onResume(); | |
} | |
@Override | |
public void onActivityResult(int requestCode, int resultCode, Intent data) { | |
logInternal("onActivityResult() called with: requestCode = [" + requestCode + | |
"], resultCode = [" + resultCode + "], data = [" + data + "]"); | |
super.onActivityResult(requestCode, resultCode, data); | |
} | |
@Override | |
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, | |
@NonNull int[] grantResults) { | |
logInternal("onRequestPermissionsResult() called with: requestCode = [" + requestCode + | |
"], permissions = [" + permissions + "], grantResults = [" + grantResults + "]"); | |
super.onRequestPermissionsResult(requestCode, permissions, grantResults); | |
} | |
@Override | |
public void onPause() { | |
logInternal("onPause() called"); | |
super.onPause(); | |
} | |
@Override | |
public void onStop() { | |
logInternal("onStop() called"); | |
super.onStop(); | |
} | |
@Override | |
public void onDestroyView() { | |
logInternal("onDestroyView() called"); | |
super.onDestroyView(); | |
} | |
@Override | |
public void onDestroy() { | |
logInternal("onDestroy() called"); | |
super.onDestroy(); | |
} | |
private String getFinalClassName(){ | |
return this.getClass().getSimpleName(); | |
} | |
private void logInternal(String message){ | |
Timber.d(getFinalClassName() + ": " + message); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class LogInterceptor implements Interceptor { | |
private static final Charset UTF8 = Charset.forName("UTF-8"); | |
public enum Level { | |
/** No logs. */ | |
NONE, | |
/** | |
* Logs request and response lines. | |
* <p> | |
* <p>Example: | |
* <pre>{@code | |
* --> POST /greeting http/1.1 (3-byte body) | |
* | |
* <-- 200 OK (22ms, 6-byte body) | |
* }</pre> | |
*/ | |
BASIC, | |
/** | |
* Logs request and response lines and their respective headers. | |
* <p> | |
* <p>Example: | |
* <pre>{@code | |
* --> POST /greeting http/1.1 | |
* Host: example.com | |
* Content-Type: plain/text | |
* Content-Length: 3 | |
* --> END POST | |
* | |
* <-- 200 OK (22ms) | |
* Content-Type: plain/text | |
* Content-Length: 6 | |
* <-- END HTTP | |
* }</pre> | |
*/ | |
HEADERS, | |
/** | |
* Logs request and response lines and their respective headers and bodies (if present). | |
* <p> | |
* <p>Example: | |
* <pre>{@code | |
* --> POST /greeting http/1.1 | |
* Host: example.com | |
* Content-Type: plain/text | |
* Content-Length: 3 | |
* | |
* Hi? | |
* --> END POST | |
* | |
* <-- 200 OK (22ms) | |
* Content-Type: plain/text | |
* Content-Length: 6 | |
* | |
* Hello! | |
* <-- END HTTP | |
* }</pre> | |
*/ | |
BODY | |
} | |
public interface Logger { | |
void log(String message); | |
} | |
public LogInterceptor() { | |
this(new Logger() { | |
@Override | |
public void log(String message) { | |
Timber.d(message); | |
} | |
}); | |
setLevel(Level.BASIC); | |
} | |
public LogInterceptor(Logger logger) { | |
this.logger = logger; | |
setLevel(Level.BASIC); | |
} | |
private final Logger logger; | |
private volatile Level level = Level.NONE; | |
/** Change the level at which this interceptor logs. */ | |
public LogInterceptor setLevel(Level level) { | |
if (level == null) { | |
throw new NullPointerException("level == null. Use Level.NONE instead."); | |
} | |
this.level = level; | |
return this; | |
} | |
public Level getLevel() { | |
return level; | |
} | |
@Override | |
public Response intercept(Chain chain) throws | |
IOException { | |
Level level = this.level; | |
Request request = chain.request(); | |
if (level == Level.NONE) { | |
return chain.proceed(request); | |
} | |
boolean logBody = level == Level.BODY; | |
boolean logHeaders = logBody || level == Level.HEADERS; | |
RequestBody requestBody = request.body(); | |
boolean hasRequestBody = requestBody != null; | |
Connection connection = chain.connection(); | |
Protocol protocol = connection != null ? connection.protocol() : Protocol.HTTP_1_1; | |
String requestStartMessage = | |
"--> " + request.method() + ' ' + request.url() + ' ' + protocol; | |
if (!logHeaders && hasRequestBody) { | |
requestStartMessage += " (" + requestBody.contentLength() + "-byte body)"; | |
} | |
logger.log(requestStartMessage); | |
if (logHeaders) { | |
if (hasRequestBody) { | |
// Request body headers are only present when installed as a network interceptor. Force | |
// them to be included (when available) so there values are known. | |
if (requestBody.contentType() != null) { | |
logger.log("Content-Type: " + requestBody.contentType()); | |
} | |
if (requestBody.contentLength() != -1) { | |
logger.log("Content-Length: " + requestBody.contentLength()); | |
} | |
} | |
Headers headers = request.headers(); | |
for (int i = 0, count = headers.size(); i < count; i++) { | |
String name = headers.name(i); | |
// Skip headers from the request body as they are explicitly logged above. | |
if (!"Content-Type".equalsIgnoreCase(name) && | |
!"Content-Length".equalsIgnoreCase(name)) { | |
logger.log(name + ": " + headers.value(i)); | |
} | |
} | |
if (!logBody || !hasRequestBody) { | |
logger.log("--> END " + request.method()); | |
} else if (bodyEncoded(request.headers())) { | |
logger.log("--> END " + request.method() + " (encoded body omitted)"); | |
} else { | |
Buffer buffer = new Buffer(); | |
requestBody.writeTo(buffer); | |
Charset charset = UTF8; | |
MediaType contentType = requestBody.contentType(); | |
if (contentType != null) { | |
charset = contentType.charset(UTF8); | |
} | |
logger.log(""); | |
if (isPlaintext(buffer)) { | |
logger.log(buffer.readString(charset)); | |
logger.log( | |
"--> END " + request.method() + " (" + requestBody.contentLength() + | |
"-byte body)"); | |
} else { | |
logger.log("--> END " + request.method() + " (binary " + | |
requestBody.contentLength() + "-byte body omitted)"); | |
} | |
} | |
} | |
long startNs = System.nanoTime(); | |
Response response; | |
try { | |
response = chain.proceed(request); | |
} catch (Exception e) { | |
logger.log("<-- HTTP FAILED: " + e); | |
throw e; | |
} | |
long tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs); | |
ResponseBody responseBody = response.body(); | |
long contentLength = responseBody.contentLength(); | |
String bodySize = contentLength != -1 ? contentLength + "-byte" : "unknown-length"; | |
logger.log("<-- " + response.code() + ' ' + response.message() + ' ' + | |
response.request().url() + " (" + tookMs + "ms" + | |
(!logHeaders ? ", " + bodySize + " body" : "") + ')'); | |
if (logHeaders) { | |
Headers headers = response.headers(); | |
for (int i = 0, count = headers.size(); i < count; i++) { | |
logger.log(headers.name(i) + ": " + headers.value(i)); | |
} | |
if (!logBody || !HttpHeaders.hasBody(response)) { | |
logger.log("<-- END HTTP"); | |
} else if (bodyEncoded(response.headers())) { | |
logger.log("<-- END HTTP (encoded body omitted)"); | |
} else { | |
BufferedSource source = responseBody.source(); | |
source.request(Long.MAX_VALUE); // Buffer the entire body. | |
Buffer buffer = source.buffer(); | |
Charset charset = UTF8; | |
MediaType contentType = responseBody.contentType(); | |
if (contentType != null) { | |
charset = contentType.charset(UTF8); | |
} | |
if (!isPlaintext(buffer)) { | |
logger.log(""); | |
logger.log("<-- END HTTP (binary " + buffer.size() + "-byte body omitted)"); | |
return response; | |
} | |
if (contentLength != 0) { | |
logger.log(""); | |
logger.log(buffer.clone().readString(charset)); | |
} | |
logger.log("<-- END HTTP (" + buffer.size() + "-byte body)"); | |
} | |
} | |
return response; | |
} | |
/** | |
* Returns true if the body in question probably contains human readable text. Uses a small sample | |
* of code points to detect unicode control characters commonly used in binary file signatures. | |
*/ | |
static boolean isPlaintext(Buffer buffer) { | |
try { | |
Buffer prefix = new Buffer(); | |
long byteCount = buffer.size() < 64 ? buffer.size() : 64; | |
buffer.copyTo(prefix, 0, byteCount); | |
for (int i = 0; i < 16; i++) { | |
if (prefix.exhausted()) { | |
break; | |
} | |
int codePoint = prefix.readUtf8CodePoint(); | |
if (Character.isISOControl(codePoint) && !Character.isWhitespace(codePoint)) { | |
return false; | |
} | |
} | |
return true; | |
} catch (EOFException e) { | |
return false; // Truncated UTF-8 sequence. | |
} | |
} | |
private boolean bodyEncoded(Headers headers) { | |
String contentEncoding = headers.get("Content-Encoding"); | |
return contentEncoding != null && !contentEncoding.equalsIgnoreCase("identity"); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment