Многабукаф, не читал: я напилил очень простую реализацию удаленного логгинга. Может быть полезно, когда у вашего клиента (или, например, тестировщика) выскакивает непонятная ошибка, а вам хочется видеть сиюсекундно, что происходит в приложении. Заинтересованных прошу ниже.




Часть 1. Проблема


Вообще проблема появилась так: у тестировщика баг воспроизводился, а у меня — нет. Надо было глянуть логи, по adb подключиться было нельзя (потому что тестировщик в другмо городе и доступа к админке роутера не имел), а перекидываться файликами — это какой-то отстой.

Часть 2. Описываем проблему


Есть такая штука — Timber. Для тех — кто не знает: это библиотека, которая расширяет стандартные возможности Android'овского класса Log. При логгинге библиотека автоматически добавляет в качестве TAG название класса и название метода. Но что для нас еще важнее — там можно оверрайднуть метод логгинга и что-то сделать еще внутри.

image

План такой: пишем отправку сообщения на сервер со стороны Android, а потом пишем сервер для приемки сообщений.

Часть 3. Пишем для Android


В build.gradle кладем вещи: одна — что у нас DEBUG режим, вторая — SERVER_LOGGING.
Подключение логгера будет выглядеть вот так (этот код надо вызвать в Application):

private fun plantTimberTree() {
     if (BuildConfig.DEBUG) {
         if (BuildConfig.SERVER_LOGGING) {
             val logsSource = Injection.provideLogsDataSource()
             Timber.plant(ServerLoggingTree(logsSource))
         }

         else {
             Timber.plant(Timber.DebugTree())
         }
     }
}

Как у нас вообще выглядит отправка лога? У лога есть приоритет, сообщение, тэг, дата. И урл, куда его отправлять, в случае удаленного логгинга. Все это дело возвращает Completable.

interface LogsDataSource {
    fun sendLog(
            priority: Int, 
            tag: String?,
            message: String,
            date: Date,
            url: String
    ): Completable
}

Нам надо отправлять это все на сервер с помощью, например, Retrofit'a:

interface LogsService {

    @POST
    @FormUrlEncoded
    fun sendLog(
            @Url url: String,
            @Field("priority") priority: Int,
            @Field("tag") tag: String?,
            @Field("message") message: String,
            @Field("date") date: Date
    ): Single<BaseResponse>
}

Теперь давайте напишем саму отправку.

class LogsRepository(
        val rxSchedulers: RxSchedulers
) : LogsDataSource {
    private val logsService = RestApi.createService(LogsService::class.java)

    override fun sendLog(
            priority: Int, 
            tag: String?,
            message: String,
            date: Date, 
            url: String
    ): Completable {
        val request = logsService.sendLog(
                        url, 
                        priority, 
                        tag, 
                        message,
                        date
                )
                .subscribeOn(rxSchedulers.io)
        return Completable.fromSingle(request)
    }
}

Теперь давайте посмотрим на наш класс ServerLoggingTree. Там мы оверрайдим метод логгинга и вызываем в нем метод отправки на сервер.

class ServerLoggingTree(
        private val logsDataSource: LogsDataSource
) : Timber.DebugTree() {
    companion object {
        private const val LOG_HOST = "abcdef.ddns.net"
        private const val LOG_PORT = 8443
        private const val LOG_PATH = "api/v1/logs.send"
        const val LOG_URL = "https://$LOG_HOST:$LOG_PORT/$LOG_PATH"
    }

    override fun log(
            priority: Int,
            tag: String?,
            message: String,
            t: Throwable?
    ) {
        val disposable = logsDataSource
                .sendLog(priority, tag, message, Date(), LOG_URL)
                .subscribe({}, {
                    Log.e("ServerLoggingTree", "Failed to send log")
                })

        super.log(priority, tag, message, t)
    }
}

На этом, собственно, часть для Android закончена.

Часть 4. Пишем сервер


Писать будем на Node.JS, но вообще можно на чем угодно.

Для логгинга мы используем библиотеку Bunyan, потому что она, во-первых, поддерживает запись в файл, а во-вторых, она позволяет выводить сообщения красиво раскрашенными.

Давайте напишем штуку, которая отвечает за логгинг. Пишем, что у нас под каждый тип послания свой файлик.

let bunyan = require('bunyan');
let bformat = require('bunyan-format');
let formatOut = bformat({ outputMode: 'short' });
let fs = require('fs');
let path = process.cwd();

module.exports = function(name) {
    return bunyan.createLogger(
    {
        name: name,
        streams: [
            {
                level: 'error',
                stream: getStream('error.json')
            },
            {
                level: 'trace',
                stream: getStream('trace.json')
            },
            {
                level: 'debug',
                stream: getStream('debug.json')
            },
            {
                level: 'info',
                stream: getStream('info.json')
            },
            {
                level: 'warn',
                stream: getStream('warn.json')
            },
            {
                stream: formatOut
            }]
    });
};

function getStream(file) {
    return fs.createWriteStream(path + '/logs/' + file);
}

Теперь напишем метод для приема логов от Android'a. Константы, отвечающие за приорити, были определены опытным путем.

const logger = require('../../../utils/Bunyan')("logs.send");

const PRIORITY_INFO = "4";
const PRIORITY_WARN = "5";
const PRIORITY_VERBOSE = "2";
const PRIORITY_DEBUG = "3";
const PRIORITY_ERROR = "6";

module.exports = async function(req, res) {
    const priority = req.body.priority;
    const line = getLogLine(req);

    if (priority === PRIORITY_INFO) {
        logger.info(line)
    }

    else if (priority === PRIORITY_WARN) {
        logger.warn(line)
    }

    else if (priority === PRIORITY_VERBOSE) {
        logger.trace(line)
    }

    else if (priority === PRIORITY_DEBUG) {
        logger.debug(line)
    }

    else if (priority === PRIORITY_ERROR) {
        logger.error(line)
    }

    res.send({status: "ok"})
};

function getLogLine(req) {
    const tag = req.body.tag;
    const message = req.body.message;
    const date = req.body.date;

    return tag + ": " + message + ", when: " + date
}

Вот это мы примерно пишем в app.js, дабы подключить нашу штучку для логгинга.

const express = require('express');
const helmet = require('helmet');
const bodyParser = require('body-parser');
const logs_send = require('./routes/v1/logs/send');
const fs = require('fs');
const https = require('https');
const constants = require('./config');
const app = express();

app.set('view engine', 'jade');
app.use(helmet());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));

app.use('/api/v1/logs.send', logs_send);
app.listen(3600);

module.exports = app;

Но вообще тут два пути: если сделать так, как описано выше (без https), то в Android придется вкатывать network_security_config, разрешающий CLEARTEXT коммуникацию. Поэтому по-хорошему надо сделать вот что: на своем роутере сделать DDNS, потом получить для этого домена сертификат (через letsencrypt), ну и поднимать сервак уже с сертификатом.

Часть 5. Заключение


Всем спасибо за прочтение, надеюсь, я кому-то помог.

Можете почитать другие мои статьи:
Добавляем графики в Notion
Делаем адаптивную загрузку контента на сайте
Разрабатываем приложение, которое отсылает данные другим приложениям (экосистемное приложение)

Еще можно подписаться на telegram-канал моего стартапа, иногда там тоже интересно.