Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Android

ปรับปรุง Logging ด้วย Timber ใน Android


Timber คืออะไร?

Timber เป็น Library Wrapper รอบ Android Log (android.util.Log) ทำให้ Logging ง่ายขึ้น ปลอดภัยกว่า และจัดการได้ดีขึ้น พัฒนาโดย Jake Wharton (Square)

ทำไมต้องใช้ Timber แทน Log ปกติ?

ปัญหา Log ปกติTimber แก้ไขอย่างไร
ต้อง TAG เองทุกครั้งAuto Tag จาก Class Name
Release Build ยัง Log ออกไปAuto Disable ใน Release (via BuildConfig.DEBUG)
Format ข้อความยุ่งยากString Formatting เหมือน String.format
Crash ไม่ได้ Log Stack Trace อัตโนมัติLog Exception ได้โดยตรง
ยากต่อการ Redirect ไป File/ServerCustom Tree สำหรับ Crashlytics, File, Remote

การติดตั้ง

// build.gradle (Module: app)
dependencies {
    implementation 'com.jakewharton.timber:timber:5.0.1'
    // สำหรับ Kotlin ใช้ได้เลย (มี Extension Functions)
}

การตั้งค่า (Initialization)

ต้อง Plant Tree ใน Application Class (ไม่ใช่ Activity) เพื่อให้ใช้งานได้ทั่วแอป

MyApplication.java

import android.app.Application;
import timber.log.Timber;

public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        
        if (BuildConfig.DEBUG) {
            // Debug Tree: Log ไป Logcat พร้อม Tag, Line Number
            Timber.plant(new Timber.DebugTree());
        } else {
            // Release Tree: ส่งไป Crashlytics, Server, File
            Timber.plant(new ReleaseTree());
        }
    }
}

AndroidManifest.xml


    ...

การใช้งานพื้นฐาน

import timber.log.Timber;

// Log Levels
Timber.v("Verbose: %s", "detail");      // Verbose
Timber.d("Debug: %d items", count);     // Debug
Timber.i("Info: User %s logged in", name); // Info
Timber.w("Warning: %s", "low battery"); // Warning
Timber.e("Error: %s", errorMessage);    // Error

// Exception/Stack Trace (สำคัญมาก!)
try {
    riskyOperation();
} catch (Exception e) {
    Timber.e(e, "Operation failed for user %s", userId);
    // หรือ
    Timber.e(e); // Log แค่ Exception
}

// Tag อัตโนมัติ = Class Name (MainActivity, UserRepository, etc.)

Advanced: Custom Tree สำหรับ Production

1. Crashlytics Tree

import com.google.firebase.crashlytics.FirebaseCrashlytics;
import timber.log.Timber;

public class CrashlyticsTree extends Timber.Tree {
    @Override
    protected void log(int priority, @Nullable String tag, @Nullable String message, @Nullable Throwable t) {
        if (priority == Log.VERBOSE || priority == Log.DEBUG || priority == Log.INFO) {
            return; // ไม่ส่ง Debug/Info ไป Crashlytics
        }
        
        if (t != null) {
            FirebaseCrashlytics.getInstance().recordException(t);
        } else {
            FirebaseCrashlytics.getInstance().log(message);
            // หรือ recordCustomKey
        }
    }
}

2. File Logging Tree (เก็บ Log ไฟล์)

import android.util.Log;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import timber.log.Timber;

public class FileLoggingTree extends Timber.Tree {
    private final File logFile;
    private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.US);

    public FileLoggingTree(File logDir) {
        this.logFile = new File(logDir, "app_log_" + dateFormat.format(new Date()) + ".txt");
    }

    @Override
    protected void log(int priority, @Nullable String tag, @Nullable String message, @Nullable Throwable t) {
        if (message == null && t == null) return;
        
        String level = priorityToString(priority);
        String logLine = String.format(Locale.US, "%s %s/%s: %s%n", 
            dateFormat.format(new Date()), level, tag, message);
        
        if (t != null) {
            logLine +="\n" + Log.getStackTraceString(t) + "\n";
        }
        
        try (FileWriter writer = new FileWriter(logFile, true)) {
            writer.write(logLine);
        } catch (IOException e) {
            // Fail silently
        }
    }

    private String priorityToString(int priority) {
        switch (priority) {
            case Log.VERBOSE: return "V";
            case Log.DEBUG: return "D";
            case Log.INFO: return "I";
            case Log.WARN: return "W";
            case Log.ERROR: return "E";
            default: return "?";
        }
    }
}

3. Plant หลาย Tree พร้อมกัน

if (BuildConfig.DEBUG) {
    Timber.plant(new Timber.DebugTree());
    // Plant File Tree ด้วยใน Debug ถ้าอยากเก็บไฟล์
    Timber.plant(new FileLoggingTree(getCacheDir()));
} else {
    Timber.plant(new CrashlyticsTree());
    Timber.plant(new FileLoggingTree(getExternalFilesDir(null))); // หรือ Internal
}

Kotlin Extensions (ถ้าใช้ Kotlin)

// timber.kt (Extension)
inline fun  T.logDebug(message: () -> String): T {
    Timber.tag(this::class.java.simpleName).d(message())
    return this
}

inline fun  T.logError(message: () -> String, throwable: Throwable? = null): T {
    Timber.tag(this::class.java.simpleName).e(throwable, message())
    return this
}

// ใช้งาน
userRepository.logDebug { "Fetching user $userId" }
   .fetchUser(userId)
   .logError { "Failed to fetch user $userId" }

Best Practices

  1. Plant ใน Application Class - ครอบคลุมทั้งแอป
  2. ใช้ String Formatting - Timber.d("User %s has %d items", name, count) ไม่ใช้ + Concatenation
  3. Log Exception ด้วย Timber.e(e, ...) - Stack Trace ครบถ้วน
  4. ไม่ Log Sensitive Data - Password, Token, PII ใน Production
  5. Custom Tree สำหรับ Release - ส่งไป Crashlytics, Sentry, Server Log
  6. ใช้ Tag สำหรับ Filter - Logcat กรองได้ตาม Class Name

Migration จาก Log ปกติ

// Before
private static final String TAG = "MainActivity";
Log.d(TAG, "onCreate: " + savedInstanceState);
Log.e(TAG, "Error: " + e.getMessage(), e);

// After
Timber.d("onCreate: %s", savedInstanceState);
Timber.e(e, "Error occurred");

สรุป

  • Timber = Better Log API + Auto Tag + Release Safety
  • Plant Tree ใน Application.onCreate()
  • Debug: Timber.DebugTree()
  • Release: Custom Tree (Crashlytics, File, Remote)
  • ใช้ Timber.e(throwable, message) สำหรับ Exception
  • Kotlin ใช้ Extension Functions ทำให้สะดวกกว่า