Передаем Android логи через интернет с Timber и Node.JS

от автора

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

Часть 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-канал моего стартапа, иногда там тоже интересно.

ссылка на оригинал статьи https://habr.com/ru/post/467373/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *