Remote Logger

Szanuj Logcata

Podstawowe narzędzie programisty Android — Logcat. Jest tak powszechnie używane, zawsze pod ręką, że trudno sobie wyobrazić sytuację, gdy trzeba pracować bez niego. Mnie jednak udało się w takiej sytuacji znaleźć. I postanowiłem napisać i udostępnić narzędzie, które pomogło mi ten dramat przetrwać.

Wyzwanie

Dostałem zadanie przetestowania pewnej biblioteki w ramach naszego SDK. Z racji, że pracuję w #fintech to często dochodzi dodatkowy rygor bezpieczeństwa. Owa biblioteka nie pozwalała się zainicjalizować, jeśli:

  • aplikacja była zbudowana w trybie debug
  • została zainstalowana przez Android Studio, lub adb install
  • nie została podpisana (to akurat wynika z instalowania aplikacji w wersji release)
  • opcje deweloperskie były włączone
  • została uruchomiona na urządzeniu lub wersji systemu spoza listy dopuszczonych

Brak opcji deweloperskich oznacza brak logów. Już samo zbudowanie aplikacji w wersji release ogranicza logi, ale przynajmniej wyjątki są logowane. Aplikacja wywalała się dość często, podejrzewałem jakieś standardowe NPE, bo np. zapomniałem czegoś zainicjalizować, zastubować itd. ale nie miałem pojęcia gdzie. Nie mogłem po prostu napisać testów ze względu na wymagania biblioteki ani jej zastubować, bo na tym polegało moje zadanie — na przetestowaniu jak się sprawdzi w możliwie “prawdziwym” środowisku.

Crashlytics lub inne narzędzie do zdalnego zbierania raportów crashy aplikacji, mogłoby w pewnym stopniu pomóc. Jednak raportowanie nie jest natychmiastowe, logowane są crashe aplikacji i jakieś metryki, a przydałoby mi się też logowanie zwykłych eventów.

Rozwiązanie

Gdyby tylko dało się wysyłać logi Logcata, np. przez websocket i potem łapać je na swoim komputerze… Nie znalazłem takiego narzędzia, więc je sobie napisałem.

Server

Potrzebowałem czegoś maksymalnie prostego, co pozwoliłoby postawić serwer WebSocket i wyświetlać logi w formie HTMLa, lub logować do pliku. Padło na ExpressJS z którym miałem trochę do czynienia hobbystycznie.

WebSocket

Postawienie serwera WS jest dość proste:

const ws = require('ws');
const wsServer = new ws.Server({ noServer: true});

wsServer.on('connection', socket => {
	events.clear();
	console.log("clearing events");
	socket.on('message', message =>{
		events.add(JSON.parse(message))
	})
});

const server = app.listen(process.env.WS_PORT || '1234')

console.log("Remote Logger is starting on port: ", process.env.WS_PORT || '1234');

server.on('upgrade', (request, socket, head) =>{
	wsServer.handleUpgrade(request, socket, head, socket =>{
		wsServer.emit('connection', socket, request);
	});
});

Serwer nasłuchuje eventów na porcie przekazanym podczas odpalenia aplikacji WS_PORT lub domyślnym 1234, nastepnie wrzuca je do obiektu listy events.

Web UI

Mając już logi zarejestrowane w obiekcie events, mogłem je wyświetlić jako HTML. Użyłem do tego szablonów pug. W kodzie aplikacji wystarczy:

app.set('view engine', 'pug')
app.get('/', function (req, res) {
  res.render('index', { title: 'Remote Logger', message: 'Remote Logger!', data: events.toArray() })
})

A użyty szablon to:

html
  head
    title= title
    style
      include style.css
  body
    h1= message
    ul
      each event in data
          li(class=event.level)!= event.time+' <b>['+event.level+'] '+event.tag+'</b>  : ' + '<span style="white-space: pre-wrap">'+event.message+'</span>'

Czyli prosta lista zasilana elementami z obiektu events i podstawowymi kolorami na tekst w zależności od poziomu logów.

Same logi prezentują się tak: Remote Logger web interface

Extra

Nie mogłem sobie odpuścić dodania jakichś smaczków, więc po uruchomieniu serwera witają nas informacje o użytym porcie, IP do wpisania w kodzie aplikacji Android oraz nazwa narzędzia. Dodatkowo strona z logami otwiera się automatycznie.

❯ node index.js
Remote Logger is starting on port:  1234
Application started on port 3000 at IP: 192.168.1.87
██████╗ ███████╗███╗   ███╗ ██████╗ ████████╗███████╗
██╔══██╗██╔════╝████╗ ████║██╔═══██╗╚══██╔══╝██╔════╝
██████╔╝█████╗  ██╔████╔██║██║   ██║   ██║   █████╗
██╔══██╗██╔══╝  ██║╚██╔╝██║██║   ██║   ██║   ██╔══╝
██║  ██║███████╗██║ ╚═╝ ██║╚██████╔╝   ██║   ███████╗
╚═╝  ╚═╝╚══════╝╚═╝     ╚═╝ ╚═════╝    ╚═╝   ╚══════╝

██╗      ██████╗  ██████╗  ██████╗ ███████╗██████╗
██║     ██╔═══██╗██╔════╝ ██╔════╝ ██╔════╝██╔══██╗
██║     ██║   ██║██║  ███╗██║  ███╗█████╗  ██████╔╝
██║     ██║   ██║██║   ██║██║   ██║██╔══╝  ██╔══██╗
███████╗╚██████╔╝╚██████╔╝╚██████╔╝███████╗██║  ██║
╚══════╝ ╚═════╝  ╚═════╝  ╚═════╝ ╚══════╝╚═╝  ╚═╝

Android Lib

Inicjalizacja części androidowej narzędzia jest bardzo prosta, najlepiej zrobić to w metodzie onCreate() klasy dziedziczącej po Application:

class App : Application() {

    override fun onCreate() {
        super.onCreate()
        RemoteLogger().initialize(
            Config(
                "192.168.1.87",
                1234,
                applicationContext.packageName
            )
        )
    }
}

Inicjalizujemy bibliotekę i zapominamy o sprawie, logi lecą do naszego lokalnego serwera. To nie jest narzędzie, które zastąpi zdalne zbieranie crashy aplikacji albo samego Logcata. Chodzi tylko o rozwiązanie specyficznego problemu, gdzie aplikacja musi być uruchomiona w środowisku niepozwalającym na korzystanie z Logcata, ale nadal w fazie developmentu potrzebujemy dostępu do logów. Inicjalizacja loggera, jak i sama biblioteka nie powinna trafić do publicznie wydanej wersji aplikacji.

Użycie

  1. Pobierz repo
  2. Dodaj bibliotekę do projektu JitPack
  3. Odpal serwer w folderze /server node index.js
  4. Odpal swoją aplikację z dodaną biblioteką
  5. Odśwież stronę remote loggera

Biblioteka sama połączy się do serwera w momencie startu aplikacji, dlatego serwer musi być odpalony wcześniej. Na razie nie chciało mi się tego robić ładniej. To narzędzie jest przeznaczone dla programistów i do rozwiązania konkretnego problemu, a nie stosowania na co dzień w każdym projekcie :)

Wszystkie logi z Androida są zbierane od momentu startu aplikacji, jeszcze przed wywołaniem onCreate() w App. Zwykle pierwszym wpisem jest Fri Aug 06 08:58:54 GMT+02:00 2021 [DEBUG] LOGGER : --------- beginning of system. Logi są zbierane na jednym wątku a po połączeniu z serwerem WS są wysyłane na innym. Zbierane są logi wyłącznie dla pakietu aplikacji, dlatego trzeba go podać w metodzie initialize().

RemoteLogger przejmuje obsługę niewyłapanych wyjątków przez ustawienie Thread.setDefaultUncaughtExceptionHandler. Po wystąpieniu wyjątku, który wywala aplikację, jest on logowany na lokalnym serwerze, po czym aplikacja wyłącza się z kodem błędu 1. Nie miałem okazji sprawdzić, czy nie gryzie się to z Firebase Crashlytics, ale jeśli masz już to skonfigurowane, najprawdopodobniej nie potrzebujesz mojego narzędzia.

Nie chciej tego używać

Zdaję sobie sprawę, że to narzędzie jest raczej spartańskie… ale mi sporo pomogło, a więcej niż jedno popołudnie nie chciałem na nie poświęcać. I życzę Ci, żeby nigdy nie nastała potrzeba użycia go :) Szanuj Logcata.


Adam Świderski

software engineer