Envio de logs com Flutter

logo_flutter_post

Olá, amigos e amigas! Hoje trago mais um conteúdo sobre logs — mas, dessa vez, com o nosso querido Flutter.

Ok, você deve estar se perguntando: qual é a diferença? Enviar logs não é só fazer uma requisição para a API e pronto?
E, para ser sincero, se você pensa assim… está parcialmente certo.

Porém, existem alguns detalhes importantes aqui. Primeiro: você sabe o que está realmente sendo enviado? Tem ideia de como esses dados estão sendo processados?
Complicou, né?

Pois bem, a ideia deste tutorial vai muito além de apenas enviar logs via HTTP. A proposta é definir um padrão e um formato correto, que seja facilmente interpretado e que não exija manipulações bizarras para ser apresentado ou metrificado (obter métricas).

Então dada a nossa introdução bora colocar a mão na massa.

E caso queira também pode acompanhar pelo youtube

Porem antes de darmos inicio precisamos instalar o package Dio para conseguir realizar as requisições http

use o comando: flutter pub add dio

detalhes podem ser obtidos aqui!

Logo de inicio vamos definir a estrutura para nossos logs:

Bem então a primeira coisa a fazer é criar nossas classes de modelo

Vamos criar a classe log.dart

import 'dart:convert';
import 'log_level.dart';
import 'log_type.dart';
import 'namespace.dart';

class Log {
  final LogType logType;
  final String acronym;
  final LogLevel level;
  final String serviceName;
  final Namespace namespace;
  final String operation;
  final dynamic content;
  final int? duration;
  final double? value;
  final LogExtraInfo? extra;

  Log({
    required this.logType,
    required this.acronym,
    required this.level,
    required this.serviceName,
    required this.namespace,
    required this.operation,
    required this.content,
    this.duration,
    this.value,
    this.extra,
  });

  Map<String, dynamic> toJson() {
    return {
      'timestamp': DateTime.now().toUtc().toIso8601String(),
      'logType': logType.name,
      'acronym': acronym,
      'level': level.name,
      'service.name': serviceName,
      'service.namespace': namespace.name,
      'operation': operation,
      'content': content is String ? content : jsonEncode(content),
      if (duration != null) 'duration': duration,
      if (value != null) 'value': value,
      if (extra != null) ...extra!.toJson(),
    };
  }
}

Agora vamos criar a classe de suporte log_extra_info.dart

Importante essa classe ela deve respeitar a sua regra de negocio então tenha em mente que ela deve possuir informações que façam sentido a sua realidade. Outro ponto que gosto de usar em classes de suporte ao log e deixar seus valores opcionais mas pensando sempre em como ela pode apresentar os dados de negocio, troubleshooting e falhas criticas (até o stackTrace completo sem quebrar o log todo).

class LogExtraInfo {
  final String? userId;
  final String? transactionId;
  final int? statusCode;
  final dynamic message;
  final String? exception;
  final String? errorMessage;
  final List<String>? errorStack;

  LogExtraInfo({
    this.userId,
    this.transactionId,
    this.statusCode,
    this.message,
    this.exception,
    this.errorMessage,
    this.errorStack,
  });

  Map<String, dynamic> toJson() {
    return {
      if (userId != null) 'userId': userId,
      if (transactionId != null) 'transactionId': transactionId,
      if (statusCode != null) 'statusCode': statusCode,
      if (message != null) 'message': message,
      if (exception != null) 'exception': exception,
      if (errorMessage != null) 'errorMessage': errorMessage,
      if (errorStack != null) 'errorStack': errorStack,
    };
  }
}

E claro os nossos enums: log_level.dart, log_type.dart e namespace.dart

Apenas para entendimento geral é padrão de mercado assim como boa praticar ter uma segmentação do LogType nestes quatro tipos: Troubleshooting, Audit, Security e Business para uma identificação correta e aplicabilidade do log.

enum LogType {
  TBS, // Troubleshooting
  AUD, // Audit
  SEC, // Security
  BUS, // Business
}
enum LogLevel {
  DEBUG,
  INFO,
  WARN,
  ERROR,
}
enum Namespace { 
  prd, 
  uat, 
  des 
}

Agora um ponto bem importante, no meu exemplo estou carregando as credenciais a partir de um objeto na raiz do projeto, aqui fica a sua vontade/necessidade se quiser carregar de outra forma por exemplo como o dotend, aqui vou deixar um package que uso!

Se quiser seguir a mesma forma

Crie um arquivo chamado secrets.json na raiz do projeto, e adicione o seguinte conteúdo, com suas respectivas API e secret:

{
    "URL_LOG_API": "<SUA API PARA INGESTÃO DE LOGS>",
    "URL_LOG_API_DEV": "<SUA KEY PARA INGESTÃO DE LOGS>"
}

E por fim vamos criar o arquivo responsável por fazer o envio dos logs o logger.dart

IMPORTANTE: Observe que a classe necessita de dois valores iniciais sendo a URL e a apiKey, ambas estão definidas no arquivo anterior, mas fique a vontade para alterar caso necessário.

import 'package:dio/dio.dart';

class Logger {
  static late Dio _dio;

  static void initialize({required String endpoint, required String apiKey}) {
    _dio = Dio(BaseOptions(
      baseUrl: endpoint,
      headers: {
        'Content-Type': 'application/json; charset=utf-8',
        'Authorization': 'Api-Token $apiKey',
      },
      connectTimeout: const Duration(seconds: 5),
      receiveTimeout: const Duration(seconds: 5),
    ));
  }

  static Future<void> send(Log log) async {
    try {
      final response = await _dio.post(
        '',
        data: log,
      );

      if (response.statusCode != null && response.statusCode! > 299) {
        print(
            'Erro ao enviar log: ${response.statusCode} - ${response.statusMessage}');
      }
    } on DioException catch (e) {
      print('Erro de Dio ao enviar log: ${e.message}');
    } catch (e) {
      print('Erro desconhecido ao enviar log: $e');
    }
  }
}

Um pequeno parênteses aqui se você criou o arquivo secrets vai precisar carregar os dados, então na inicialização do seu projeto adicione esse método:

Future<void> loadConfig() async {
  final contents = await rootBundle.loadString('secrets.json');
  config = json.decode(contents);
}

E agora passe os valores para a classe Logger

Logger.initialize(
      endpoint: config?['URL_LOG_API'],
      apiKey: config?['URL_LOG_API_DEV'],
    );

Ufa! Tudo configurado.

Basta então enviar nossos logs, e para isso basta criar uma chamada com estrutura próxima ao exemplo e seguindo o padrão pre estabelecido.

No exemplo abaixo trata um log de erro simples, mas lembre-se caso queira enviar o errorStack é importante converter o mesmo em uma lista de strings dessa forma a sua apresentação no resultado ficara igualmente ao log da aplicação.

Logger.send(Log(
          logType: LogType.TBS,
          acronym: 'smart',
          level: LogLevel.ERROR,
          serviceName: 'smart_recycling',
          namespace: Namespace.prd,
          operation: 'Obter localizaçao',
          content: 'Permissões negadas pelo usuario',
          extra: LogExtraInfo(
            errorMessage: 'Permission not granted',
            exception: e.exception
          )));

E no próximo exemplo temos um log comum, observem que apenas alteramos e omitimos alguns valores

Logger.send(Log(
          logType: LogType.BUS,
          acronym: 'smart',
          level: LogLevel.INFO,
          serviceName: 'smart_recycling',
          namespace: Namespace.prd,
          operation: 'Obter localizaçao',
          content: 'user location',
          extra: LogExtraInfo(
            message: 'Localização obtida com sucesso',
          )));

E por fim temos o resultado em nossa ferramenta que está recebendo os dados.

IMPORTANTE: Essa forma que exemplifiquei a vocês é totalmente agnóstica e funciona da mesma forma em qualquer ferramenta de mercado bastando alterar o endpoint para envio dos logs, e dessa forma não corremos o risco de alterar visões e ou estratégias de negócios baseadas em logs uma vez que sempre chegaram da mesma forma.

CATEGORIES:

Flutter

Tags:

No responses yet

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Latest Comments