Многабукаф, не читал: я напилил очень простую реализацию удаленного логгинга. Может быть полезно, когда у вашего клиента (или, например, тестировщика) выскакивает непонятная ошибка, а вам хочется видеть сиюсекундно, что происходит в приложении. Заинтересованных прошу ниже.
Вообще проблема появилась так: у тестировщика баг воспроизводился, а у меня — нет. Надо было глянуть логи, по adb подключиться было нельзя (потому что тестировщик в другмо городе и доступа к админке роутера не имел), а перекидываться файликами — это какой-то отстой.
Есть такая штука — Timber. Для тех — кто не знает: это библиотека, которая расширяет стандартные возможности Android'овского класса Log. При логгинге библиотека автоматически добавляет в качестве TAG название класса и название метода. Но что для нас еще важнее — там можно оверрайднуть метод логгинга и что-то сделать еще внутри.
План такой: пишем отправку сообщения на сервер со стороны Android, а потом пишем сервер для приемки сообщений.
В build.gradle кладем вещи: одна — что у нас DEBUG режим, вторая — SERVER_LOGGING.
Подключение логгера будет выглядеть вот так (этот код надо вызвать в Application):
Как у нас вообще выглядит отправка лога? У лога есть приоритет, сообщение, тэг, дата. И урл, куда его отправлять, в случае удаленного логгинга. Все это дело возвращает Completable.
Нам надо отправлять это все на сервер с помощью, например, Retrofit'a:
Теперь давайте напишем саму отправку.
Теперь давайте посмотрим на наш класс ServerLoggingTree. Там мы оверрайдим метод логгинга и вызываем в нем метод отправки на сервер.
На этом, собственно, часть для Android закончена.
Писать будем на Node.JS, но вообще можно на чем угодно.
Для логгинга мы используем библиотеку Bunyan, потому что она, во-первых, поддерживает запись в файл, а во-вторых, она позволяет выводить сообщения красиво раскрашенными.
Давайте напишем штуку, которая отвечает за логгинг. Пишем, что у нас под каждый тип послания свой файлик.
Теперь напишем метод для приема логов от Android'a. Константы, отвечающие за приорити, были определены опытным путем.
Вот это мы примерно пишем в app.js, дабы подключить нашу штучку для логгинга.
Но вообще тут два пути: если сделать так, как описано выше (без https), то в Android придется вкатывать network_security_config, разрешающий CLEARTEXT коммуникацию. Поэтому по-хорошему надо сделать вот что: на своем роутере сделать DDNS, потом получить для этого домена сертификат (через letsencrypt), ну и поднимать сервак уже с сертификатом.
Всем спасибо за прочтение, надеюсь, я кому-то помог.
Можете почитать другие мои статьи:
Добавляем графики в Notion
Делаем адаптивную загрузку контента на сайте
Разрабатываем приложение, которое отсылает данные другим приложениям (экосистемное приложение)
Еще можно подписаться на telegram-канал моего стартапа, иногда там тоже интересно.
Часть 1. Проблема
Вообще проблема появилась так: у тестировщика баг воспроизводился, а у меня — нет. Надо было глянуть логи, по adb подключиться было нельзя (потому что тестировщик в другмо городе и доступа к админке роутера не имел), а перекидываться файликами — это какой-то отстой.
Часть 2. Описываем проблему
Есть такая штука — Timber. Для тех — кто не знает: это библиотека, которая расширяет стандартные возможности Android'овского класса Log. При логгинге библиотека автоматически добавляет в качестве TAG название класса и название метода. Но что для нас еще важнее — там можно оверрайднуть метод логгинга и что-то сделать еще внутри.
План такой: пишем отправку сообщения на сервер со стороны 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-канал моего стартапа, иногда там тоже интересно.
le1ic
Во-первых надо слать какой-то уникальный device-id. Во-вторых зачем складывать в разные файлы? как вы потом будете смотреть последовательность событий разного уровня логгирования? Надо складывать в один файл, а из него если нужно доставать с фильтрами (grep etc). В третьих есть миллион и еще пять бесплатных онлайн сервисов, которые все это делают, но гораздо удобнее, с управлением логами, расширенным поиском, автоматическим посыланием device-id. Даже у родного гугла есть такой сервис
mixeden Автор
Вообще задача, для которой это дело пилилось — это конкретному человеку (одному, поэтому device-id не нужен) скинуть сборку, на которой бы воспроизвелся баг. Ну и добыть логи.
Про уровни логгирования — там можно сделать запись и по разным уровням, и в один файл.
Про управление логами — если потребность возникла один раз, тянуть лишнее не хочется.