Skip to content

7. Logging

James Shvarts edited this page Jan 17, 2019 · 6 revisions

Meaningful logs are one of the biggest benefits in a Unidirectional/MVI architecture. Logs can help during development or while writing unit tests and even in production to help debug various edge cases.

Logging Actions as they come in and the resulting States is all that's necessary and Roxie does it for you inside the BaseViewModel (look for Roxie.log statements).

Configuration

By default, Actions and States are not logged. You can enable logging in your Application's onCreate() using one of 2 ways:

Default logger:

    // Actions will be logged using println()
    override fun onCreate() {
        super.onCreate()
        Roxie.enableLogging()
    }

Custom logger:

    // Actions will be logged using Timber with the severity specified
    override fun onCreate() {
        super.onCreate()
        Roxie.enableLogging(object : Roxie.Logger {
            override fun log(msg: String) {
                Timber.tag("Roxie").d(msg)
            }
        })
    }

Displaying a note with ID of 2, for instance, generates the following logs:

D/Roxie: NoteDetailViewModel: Received action: LoadNoteDetail(noteId=1)
D/Roxie: NoteDetailViewModel: Received state: State(note=Note(id=1, text=note1), isIdle=false, isLoading=false, isLoadError=false, isNoteDeleted=false, isDeleteError=false)

The logs give us helpful insight into what's going on. For instance, it would be easy to confirm that rotating the device will not produce any more logs (we probably don't want to reload the note again on rotation). The latest State wrapped in LiveData will simply get rendered again after rotation. Having the logging mechanism built-in is very helpful when optimizing your potentially expensive data operations.

Logging sensitive data

MVI with its single pipeline to dispatch Actions and produce States makes logging effective and easy. This simple but powerful quality can aid greatly in both development and production. However, with great power comes great responsibility. When logging user data, for instance, avoid logging sensitive user-identifiable data. One way to do it is to redact certain properties from the User object (for instance, by overwriting the toString() on the data class).

Roxie comes equipped with a very basic form of obfuscation with obfuscatedString() for Actions and States.

Without obfuscating:

sealed class Action : BaseAction {
    data class MyAction(val data: String) : Action()
}

print(MyAction(data="test")) // outputs: MyAction(data=test)

With obfuscating:

sealed class Action : BaseAction {
    data class MyObfuscatedAction(val data: String) : Action() {
        override fun toString() = obfuscatedString()
    }
}

print(MyObfuscatedAction(data="test")) // outputs: MyObfuscatedAction@123 - where "123" is the hash code.

Logs as JSON

If you have large State objects with many properties, logging them using built-in toString() may not be optimal as the output won't be easy to read. A good solution to this problem would be logging these objects as JSON. This way you can format the output by viewing it using some sort of a JSON viewer (Android Studio has a good plugin for his).

Logs in unit tests

When working on unit tests, it's helpful to see the States being emitted. You can do so in doOnNext() in the ViewModel being tested. For instance,

    disposables += loadNotesChange
        .scan(initialState, reducer)
        .filter { !it.isIdle }
        .distinctUntilChanged()
        .doOnNext { println("Received state: $it") }
        .subscribe(state::postValue, Timber::e)
    }
Clone this wiki locally