Logging hiệu quả hơn với Timber

Nếu đã làm việc với Android thì hẳn là các bạn đều đã nghe tới log và mình đoán ít ai là chưa sử dụng tới class Log của Android phải không nào. Thực ra thì mình còn sử dụng log rất nhiều là đằng khác, vẫn còn nhớ cái thời mà chưa biết debug là gì thì log chính là cứu cánh đắc lực của mình. Và kể cả đến bây giờ thì mình vẫn còn dùng log khá nhiều, để log api response, exception, ... Tuy nhiên, liệu bạn đã bao giờ đặt ra câu hỏi rằng chúng ta đã sử dụng Log đúng cách và hiệu quả hay chưa?

Giả sử chúng ta có rất nhiều log, ta cần phải loại bỏ chúng trước khi publish ứng dụng của mình, nhưng bằng cách nào? Ta có thể sử dụng IDE và tìm mọi dòng code chứa cụm Log., nhưng nếu nó là hàng trăm dòng log thì sao nhỉ? Liệu ta có thể xóa, comment hết tất cả được không? Mà nếu đươc, sẽ ra sao nếu chúng ta lại cần chúng khi tiếp tục phát triển ứng dụng đó? À, có một cách rất là đơn giản, chỉ cần kiểm tra xem ta có đang ở môi trường debug hay không, nếu có thì ta log còn không thì thôi:

import android.util.Log;

public class Logger {
    public static void d(final String tag, final String message) {
        if (BuildConfig.DEBUG)
            Log.d(tag, message);
    }
}

Vấn đề của chúng ta có thể đã được giải quyết, nhưng nếu muốn sử dụng các chế độ log khác (Log.e, Log.i, Log.v, ...) ta sẽ phải viết từng đó phương thức để triển khai chúng hoặc thêm một tham số vào để sử dụng được log theo ý mình muốn.

Và đó chính là khi chúng ta nên sử dụng Timber để giải quyết vấn đề này một cách triệt để hơn!

1) Timber là gì?

Timber là một API cho class Log của Android. Nó bổ trợ và nâng cấp cho chức năng log của Android. Ta làm việc đó bằng cách cấy một đối tương Tree và mỗi lần ta log một thứ gì đó, hành vi log có thể thay đổi dựa vào việc Tree nào đang được triển khai.

Để sử dụng Timber, bạn có thể thêm dòng sau vào file build.gradle trong project của mình:

implementation 'com.jakewharton.timber:timber:4.7.1'

2) Timber hoạt động như thế nào?

Tạo một đối tượng Tree và cài đặt nó sớm nhất có thể (thường là trong phương thức onCreate() của class Application). Và chỉ cần như vậy, ta có thể bắt đầu sử dụng các phương thức mà Timber cung cấp để log thay vì class Log thông thường của Android. Rất đơn giản phải không nào.

Tiếp theo, hãy thử động tay động chân với Timber để hiểu rõ hơn về cách Timber hoạt động và tại sao bạn nên sử dụng nó nhé.

3) Logging với Timber

Sau khi đã thiết lập đối tượng Tree của mình, ta nên bắt đầu sử dụng class Timber và nó thực sự rất đơn giản. Nó chứa các phương thức tương tự như class Log, ví dụ: .v, .d, .i, .w.e. Bởi vì các phương thức này là tương tự nhau, bạn có thể dễ dàng thay thế Log bởi Timber bằng cách đổi tất cả các lời gọi Log. thành Timber.:

  Log.v(TAG, "some verbose logs here");
  Timber.v("some verbose logs here");

  Log.d(TAG, "some debug logs here");
  Timber.d("some debug logs here");

  Log.i(TAG, "some info logs here");
  Timber.i("some info logs here");
  
  Log.w(TAG, "some warning logs here");
  Timber.w("some warning logs here");

  Log.e(TAG, "some error logs here");
  Timber.e("some error logs here");
  
  Log.wtf(TAG, "some error logs here");
  Timber.wtf("some error logs here");

Ủa như vậy thì có gì đặc biệt hơn đâu nhỉ? Nhưng bạn có để ý rằng các lời gọi của Timber không hề chứa thành phần tag không? Đó là bởi vì Timber sẽ tìm xem class nào đang gọi nó và tự động thêm tên class đó vào thành tag.

3.1) Lint

Timber chứa một số lint rules tương đối hữu ích (lint là quá trình phân tích code để tìm ra các lỗi tiểm ẩn). Chúng được chia thành hai loại: errorwarning. Error ngăn không cho ứng dụng của bạn biên dịch, còn warning là cảnh báo được hiển thị để báo cho bạn biết nhưng vẫn cho phép ứng dụng biên dịch bình thường.
Nếu bạn cung cấp sai số lượng tham số truyền vào, sai kiểu tham số khi sử dụng string interpolation (ví dụ như String.format("%d", "some text"), ... nó sẽ kích hoạt một error rule.
Nếu bạn sử dụng class Log để log, hay String.format() trong một lời gọi Timber hay log một exception thì nó sẽ kich hoạt một warning rule.
Sau đây là một vài ví dụ về lint rules của Timber, nó có thể phát hiện nếu số lượng tham số truyền vào một lời gọi tới Timber sử dụng String.format() không đúng:

Example.java:35: Error: Wrong argument count, format string Hello %s %s! requires 2 but format call supplies 1 [TimberArgCount]
    Timber.d("Hello %s %s!", firstName);
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Hay phát hiện nếu độ dài tag của chúng ta đang dài hơn so với quy định của Android (23 kí tự):

Example.java:35: Error: The logging tag can be at most 23 characters, was 35 (TagNameThatIsReallyReallyReallyLong) [TimberTagLength]
    Timber.tag("TagNameThatIsReallyReallyReallyLong").d("Hello %s %s!", firstName, lastName);
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Bạn có thể đọc thêm về các rule của Timber tại đây.

3.2) Triển khai Tree

Như mình đã đề cập từ đầu, ta nên tạo một đối tượng Tree và "trồng" nó ngay khi có thể, tuy nhiên nó là gì nhỉ?
Một Tree đơn giản là một class chứa hành vi của các log. Để tạo nó, ta phải extends từ lớp Tree và triển khai phương thức log. Mỗi lần chúng ta log sử dụng Timber, nó sẽ đi qua đối tượng Tree chúng ta khởi tạo; nếu ta "trồng" nhiều Tree thì nó sẽ đi qua tất cả chúng và chứa tất cả hành vi chúng ta muốn.
Ví dụ, nếu ta không muốn log ra thông tin gì khi ứng dụng được release, ta chỉ cần tạo một Tree và không triển khai gì trong phương thức log của nó:

public class NotLoggingTree extends Timber.Tree {
    @Override
    protected void log(int priority, @Nullable String tag,
                       @NotNull String message, @Nullable Throwable t) {
        // Do nothing here
    }
}

Sau đó ta "trồng" nó ở phương thức onCreate của class application:

public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        if (!BuildConfig.DEBUG) {
            Timber.plant(new NotLoggingTree());
        }
    }
}

Khi đó, mọi lời gọi tới Timber.* sẽ không log ra thông tin gì cả khi ứng dụng không ở trong môi trường DEBUG. Tuy nhiên, nếu bạn chạy thử ứng dụng của mình, thì Timber cũng vẫn sẽ không log ngay cả khi ứng dụng đang ở môi trường DEBUG. Đó là bởi vì trong trường hợp đó, chúng ta chưa thiết lập một Tree nào cho Timber cả.
Đúng vậy, bạn phải luôn thiết lập ít nhất một Tree trước khi sử dụng Timber. Nhưng trong tình huống này, bạn không cần phải tạo ra riêng một Tree cho mục đích debugging, bạn chỉ cần phải "trồng" nó với DebugTree - một class được cung cấp sẵn bởi Timber và ứng dụng của bạn sẽ hoạt động một cách hoàn hảo:

public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        if (!BuildConfig.DEBUG) {
            Timber.plant(new NotLoggingTree());
        } else {
            Timber.plant(new Timber.DebugTree());
        }
    }
}

Nếu đó là một ứng dụng debug, nó sẽ sử dụng DebugTree và ngược lại, sử dụng NotLoggingTree cho môi trường release. Tuy nhiên, chúng ta không thể log khi ứng dụng được release, nếu có chuyện gì đó không tốt xảy ra, làm sao chúng ta có thể biết được? Hãy kết hợp sử dụng một thư viện báo cáo crash và Timber xem sao. Ta sẽ custom lại một Tree cho việc báo cáo error và warning tới thư viện crash của mình:

public class ReleaseTree extends Timber.Tree {
    @Override
    protected void log(int priority, @Nullable String tag,
                       @NotNull String message, @Nullable Throwable t) {
        if (priority == Log.ERROR || priority == Log.WARN)
            YourCrashLibrary.log(priority, tag, message);
    }
}

Thật dễ dàng phải không, mọi error và warning sẽ được gửi tới thư viện crash khi ứng dụng được release và các log sẽ bị bỏ qua.

Vậy bây giờ nếu ta muốn nhiều thông tin hơn trong log thì sao nhỉ, giả sử như class và dòng chứa log đó. Ta có thể tự viết tay vào ví dụ như Timber.d("MyClass:13-Something happened here!") nhưng ... tại sao lại tự làm khó bản thân như vậy chứ, hãy để Timber làm việc đó cho bạn:

public class DebugTree extends Timber.DebugTree {
    @Override
    protected @Nullable String createStackElementTag(@NotNull StackTraceElement element) {
        return String.format("C:%s:%s",
                super.createStackElementTag(element),
                element.getLineNumber());
    }
}

Class Application của chúng ta:

public class MyApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        if (BuildConfig.DEBUG) {
            Timber.plant(new DebugTree());
        } else {
            Timber.plant(new ReleaseTree());
        }
    }
}

Và log chúng ta thu được sẽ như sau, có đủ cả tên class và số dòng của log đó và điều tuyệt vời nhất là nó hoàn toàn tự động:

10-21 13:36:42.648 2391-2391/? D/C:MainActivity:20: Something happened here!

4. Tài liệu tham khảo

https://github.com/JakeWharton/timber
https://medium.com/@caueferreira/timber-enhancing-your-logging-experience-330e8af97341
https://medium.com/@rashi.karanpuria/timber-simple-logging-api-for-android-afb10a1dc30f
https://medium.com/@wdziemia/timber-c733b1faa5b6