# Observabilidade no Flutter com Open Telemetry e Elastic APM

# **O que é Observabilidade?**

Observabilidade (ou **o11y** ) nos permite entender um sistema de fora, permitindo-nos fazer perguntas sem conhecer o funcionamento interno desse sistema, baseado apenas nas suas saídas externas, como **logs**, **métricas** e **traces.**

*Com isso podemos solucionar e lidar facilmente com problemas novos (ou seja, "desconhecidos").*

Ela oferece monitoramento de desempenho de ponta a ponta, permitindo que as organizações monitorem todo o ciclo de vida de uma aplicação, desde as interações do usuário no front-end até os processos de back-end.

Se você está apenas começando com esse tema, as postagens abaixo ajudarão você a se aprofundar um pouco mais:

* [**Desvendando a Observabilidade:**](https://storiesfromtheherd.com/unpacking-observability-a-beginners-guide-833258a0591f) Um guia para iniciantes
    
* [**Logs, Events, Traces, and Spans**](https://medium.com/dzerolabs/observability-journey-understanding-logs-events-traces-and-spans-836524d63172) : Os Fundamentos
    

# **O que** **é Open Telemetry?**

OpenTelemetry (OTel) segundo o [site](https://opentelemetry.io/): **é uma coleção de APIs, SDKs e ferramentas. Use-o para instrumentar, gerar, coletar e exportar dados de telemetria (métricas, logs e rastros) para ajudar você a analisar o desempenho e o comportamento do seu software.**

É também um framework de código aberto para instrumentação de código, e muitos dos principais fornecedores de ferramentas de observabilidade o suportam.

Ele é independente de fornecedor, então, se você optar por trocar sua ferramenta de coleta de dados para uma nova de fornecedor diferente, não será prejudicado pois isso significa que ele não está vinculado a nenhum fornecedor específico de monitoramento ou observabilidade.

A ferramenta que eu uso atualmente para coletar os dados se chama [Elastic APM](https://www.elastic.co/pt/observability/application-performance-monitoring) e vou falar um pouco dela mais tarde mas você pode usar qualquer outra de sua preferência.

Antes de começar a usar o OpenTelemetry , é importante entender alguns conceitos básicos, como **Spans** e **Traces**.

A instrumentação de código com a OpenTelemetry é suportada por muitas linguagens de programação populares como o exemplo abaixo:

[![](https://cdn.hashnode.com/res/hashnode/image/upload/v1745322222244/83b73586-1989-4623-a01e-62161ef9c3a0.png align="center")](https://opentelemetry.io/docs/languages/)

## **Instrumentação manual vs. automática**

Existem duas formas de instrumentar uma aplicação com o OpenTelemetry: manual e automática. No caso do Flutter, como ainda não há um SDK oficial, utilizamos a instrumentação manual.

Já em outras stacks, é possível usar a instrumentação automática, que oferece um conjunto de módulos predefinidos e prontos para uso. Com isso, é possível coletar dados de telemetria com pouca ou nenhuma necessidade de alterações no código.

# O que é Elastic APM (Application Performance Monitoring)

É uma solução de observabilidade oferecida pelo Elastic para para instrumentalizar e observar aplicações backend ou frontend. Com ela é possível monitorar o desempenho de aplicações em tempo real coletando dados sobre como sua aplicação está se comportando — por exemplo:

* Quanto tempo as requisições levam para serem processadas.
    
* Onde estão os gargalos (funções lentas, chamadas de banco de dados demoradas, etc.).
    
* Quais erros estão acontecendo e com que frequência.
    
* Como os serviços estão interagindo em uma arquitetura distribuída (ex: microserviços).
    

Ele possui integração com as principais ferramentas do mercado e inclusive com a Open telemetry.

Agora vamos adicionar nossa aplicação ao elastic, veja o passo a passo abaixo:

**1 -** No painel do Elastic no canto superior direito clique em **Add data:**

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1745330599159/92a98e17-8cfb-4378-95e7-a4f84b598d4f.png align="center")

**2 -** Depois selecione **Application** e **Open Telemetry:**

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1745330530839/2207296e-dfe5-46c5-8dc9-e8216b2116f7.png align="center")

**3 -** E por último mais abaixo selecione **Open Telemetry** novamente:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1745330303079/9ab5ff45-f235-4c7c-ba79-4e492bb86c4a.png align="center")

A variável **OTEL\_EXPORTER\_OTLP\_ENDPOINT** contém o endpoint para coleta dos dados da sua aplicação e vamos usá-la mais tarde.

# Conceitos-Chave do OpenTelemetry

<mark>Os exemplos abaixo estão na linguagem Dart.</mark>

### 1\. **Tracer**

* O objeto principal e um ponto de entrada para criar ***spans***. Um [**Trace**](https://medium.com/dzerolabs/observability-journey-understanding-logs-events-traces-and-spans-836524d63172) é composto por uma árvore de Spans e oferece uma visão holística do seu sistema.
    
* Você pega ele com algo tipo:
    

```dart
    final tracer = tracerProvider.getTracer('meu-serviço');
```

Você pode também ter vários **traces** e um pra cada contexto e assim organizar melhor os dados no Elastic, filtrar por tipo de operação ou subsistema, ter granularidade no controle.

Veja o exemplo abaixo:

```dart
final apiTracer = tracerProvider.getTracer('api-tracer');
final uiTracer = tracerProvider.getTracer('ui-tracer');
```

No seu app Flutter, **na maioria dos casos, você pode usar apenas um** `Tracer` global que já é o suficiente. Se no futuro seu app crescer muito e tiver múltiplos módulos/libraries com integração separada, aí pode fazer sentido separar `Tracers`

Apesar de você **poder ter vários tracers**, o que realmente importa são os **spans e atributos** que você gera.

```dart
final span = tracer.startSpan('api.chamada.login');
span.setAttribute('feature', 'login');
span.setAttribute('http.url', '/auth/login');
```

### 2\. **Span**

* Representa uma **unidade de trabalho** (ex: carregar dados, enviar mensagem, fazer login…). **Spans** contêm **Eventos** , logs estruturados (JSON) que descrevem ocorrências pontuais durante aquele período e podem ser usados para:
    
    * Medir tempo de tela
        
    * Performance de chamadas de APIs
        
    * Acompanhamento de eventos e jornada do usuário
        
* Combine com `try/catch` pra capturar erros:
    
    ```dart
    final span = tracer.startSpan("fazer-login");
    try {
      await login();
    } catch (e) {
      span.setStatus(StatusCode.error, description: e.toString());
    } finally {
      span.end();
    }
    ```
    
* Um Span pode ter:
    
    * **Nome** (`operation-flutter`) O nome do microsserviço que está sendo executado ou uma chamada de função.
        
    * **Tempo de início/fim**
        
    * **Atributos** (ex: status, erro, nome do usuário) Lista de pares de chave-valor usados ​​para agregação ou filtragem de dados de rastreamento (por exemplo, identificador do cliente, nome do host do processo). Usados ​​para descrever e contextualizar o trabalho realizado em um Span.
        
    * **Hierarquia** (spans filhos)
        
    * **Eventos** : Strings com registro de data e hora compostas por registro de data e hora, nome e atributos opcionais usadas para descrever e contextualizar o trabalho realizado em um período.
        

```dart
final span = tracer.startSpan("carregar-dados");
// faz algo
span.end();
```

Para enriquecer com atributos use o `span.setAttribute(...)` para dar contexto:

```dart
  void _spanWithAttribute() {
    final span = tracer.startSpan(
      'GET /resource/catalog',
      kind: SpanKind.client,
    );
    span.setAttribute(Attribute.fromString('http.method', 'GET'));
    span.setAttribute(Attribute.fromString('http.url', 'your http url'));
    //faça algo
    span.end();
  }
```

### 3\. **Traces**

* É como o OpenTelemetry conecta vários spans (inclusive em diferentes serviços).
    
* Permite criar um *span filho* ligado ao *span pai*:
    

```dart
Context.current.withSpan(parentSpan).execute(() {
  final child = tracer.startSpan("operação-filho");
  child.end();
});
```

### 4\. **TracerProvider**

* Gerencia os tracers e controla como eles se comportam. Nele você configurar:
    
    * Processadores de span (ex: batch ou simples)
        
    * Exportadores (pra onde os dados vão: console, Elastic, etc)
        
    * Metadata sobre o app (nome do serviço, versão…)
        

### 5\. **Exporter**

* Responsável por **enviar os dados dos spans** pro backend (Elastic, Zipkin, Jaeger, etc).
    
* Ex: `CollectorExporter`, `ConsoleExporter`, ou `SlsExporter`.
    

### 6\. **Span Processor**

Eles são responsáveis por **processar e exportar os spans** assim que eles são criados (ou finalizados).

* `SimpleSpanProcessor`: exporta logo após o `.end()` imediatamente
    
* `BatchSpanProcessor`: agrupa vários e exporta em lote (mais eficiente e indicado para produção)
    
* Recomendado: sempre usar `BatchSpanProcessor` no app real.
    
* Exibir no console (`SimpleSpanProcessor` com `ConsoleExporter`)
    

Você **pode registrar vários ao mesmo tempo**:

```dart
final tracerProvider = TracerProviderBase(
  processors: [
    BatchSpanProcessor(exporterElastic),
    SimpleSpanProcessor(ConsoleExporter()),
  ],
);
```

Neste caso:

* Um envia os dados pra sua ferramenta que vai receber os dados que nesse caso usamos o [Elastic APM](https://www.elastic.co/observability/application-performance-monitoring)
    
* Outro imprime os spans no console para debug localmente
    

### 7\. **Resource**

* Metadados sobre o app: nome, versão, ambiente (dev/prod), etc.
    
* Isso ajuda a identificar o app no painel de observabilidade.
    

```dart
Resource([
  Attribute.fromString("service.name", "meu_app"),
  Attribute.fromString("deployment.environment", "dev"),
])
```

### 8\. **Metrics e Logs (Futuramente no Flutter)**

* OpenTelemetry também suporta **métricas** e **logs estruturados**, mas a maior parte do suporte em Flutter ainda é centrada em *traces* (spans).
    
* A comunidade tá evoluindo isso aos poucos em relação ao package que usamos.
    

# Utilizando o package do open telemetry no Flutter

**1— Adicionando a dependência em seu projeto**

Vamos começar adicionando a dependência ao nosso arquivo ***pubspec.yaml.***

Instale o pacote a partir da linha de comando com o Flutter:

```
 $ flutter pub add opentelemetry
```

Ou adicione `opentelemetry` diretamente em seu `pubspec.yaml` e execute `flutter pub get` no momento da criação desse post ele está na versão **0.18.10 .**

```dart
dependencies:
 opentelemetry: ^0.18.10
```

Agora, no seu código Dart, você pode importar o package:

```dart
import 'package:opentelemetry/api.dart';
import 'package:opentelemetry/sdk.dart';
```

**2 -** Para inicializar o SDK vamos criar um arquivo chamado ***telemetry\_initializer.dart*** *veja como ficou abaixo:*

```dart
import 'package:opentelemetry/api.dart' as otel_api;
import 'package:opentelemetry/sdk.dart' as otel_sdk;

late otel_api.Tracer tracer;

Future<void> initTelemetry() async {
  final exporter = otel_sdk.CollectorExporter(
    Uri.parse(
      'https://<seu-endpoint>/v1/traces', // 👈 não esqueça de incluir o /v1/traces!
    ),
    headers: {
      'Authorization': 'Bearer <sua-api-key>',
      'Content-Type': 'application/json',
    },
  );

  final simpleProcessor = otel_sdk.SimpleSpanProcessor(exporter);
  final batchProcessor = otel_sdk.BatchSpanProcessor(exporter);

  final tracerProvider = otel_sdk.TracerProviderBase(
    processors: [batchProcessor, simpleProcessor],
    resource: otel_sdk.Resource([
      otel_api.Attribute.fromString('service.name', 'main'),
      otel_api.Attribute.fromString('service.version', '1.0.0'),
      otel_api.Attribute.fromString('deployment.environment', 'dev'),
    ]),
  );

  otel_api.setTracerProvider(tracerProvider);

  tracer = tracerProvider.getTracer('flutter-app');
}
```

Acima, importamos o TraceProviderBase, que é o ponto inicial do open telemetry, ele fornece acesso ao Tracer, a classe responsável pela criação de spans.

Além disso, especificamos o uso do BatchSpanProcessor ele agrupa spans e os envia em massa.

## **Isolando a regra da coleta de erros com o padrão Service (**error\_reporter\_service.dart)

A finalidade da camada de serviço, por outro lado, é encapsular a lógica de negócios em um único local para promover a reutilização de código e a separação de interesses.

Basicamente seria isolar uma tarefa específica em um outro objeto sendo assim assumindo uma responsabilidade muito estreita de realizar alguma atividade útil.

Então, vamos criar um serviço para integrar apenas a coleta de erros ao seu projeto Flutter e gerenciar os relatórios de erros. 👇🏻

```dart
import 'dart:isolate';

import 'package:flutter/foundation.dart';
import 'package:opentelemetry/api.dart';
import 'package:opentelemetry/sdk.dart';

class ErrorReporterService {
  static void initialize() {
    FlutterError.onError = (FlutterErrorDetails details) {
      _report(
        details.exception,
        details.stack ?? StackTrace.empty,
        'FLUTTER_ERROR',
      );
    };

    Isolate.current.addErrorListener(_isolateErrorListener);
  }

  static Future<void> _report(
    dynamic exception,
    StackTrace stack,
    String tag,
  ) async {
    final tracer = globalTracerProvider.getTracer('flutter-my-app');
    final span = tracer.startSpan('error: $tag');

    span.setStatus(StatusCode.error, exception.toString());
    span
      ..addEvent('date', timestamp: DateTimeTimeProvider().now)
      ..setStatus(StatusCode.error, exception.toString())
      ..recordException(
        exception,
        stackTrace: stack,
        attributes: [
          Attribute.fromString(
            'exception.type',
            exception.runtimeType.toString(),
          ),
          Attribute.fromString('exception.message', exception.toString()),
          Attribute.fromString('exception.stacktrace', stack.toString()),
          Attribute.fromString('error.tag', tag),
        ],
      );

    span.end();

    debugPrint('Erro capturado: $exception');
    debugPrintStack(label: tag, stackTrace: stack);
  }

  static SendPort get _isolateErrorListener {
    return RawReceivePort((pair) async {
      final List errorAndStacktrace = pair;
      final exception = errorAndStacktrace[0];
      final stackTrace = errorAndStacktrace[1] as StackTrace;
      await _report(exception, stackTrace, 'ISOLATE');
    }).sendPort;
  }

  static void reportExternalFailure(
    dynamic exception,
    StackTrace? stack,
    String? label,
  ) {
    if (stack != null && label != null) {
      _report(exception, stack, 'EXTERNAL_FAILURE: $label');
    }
  }
}
```

* `Isolate.current.addErrorListener(_isolateErrorListener)`: adiciona um ouvinte de erros para isolados (ou seja, quando múltiplos threads estão rodando em paralelo no app), garantindo que erros ocorridos fora do thread principal sejam capturados e reportados.
    

## **Exemplo de uso do** ErrorReportService

Essa abordagem centraliza a manipulação de exceções e falhas no aplicativo, garante que erros sejam capturados e enviados para monitoramento via Elastic APM, e oferece flexibilidade para criar diferentes tipos de erros com pouca repetição de código.

```dart
import 'package:flutter/foundation.dart';

import '../service/crashlytics_service.dart';

abstract class Failure implements Exception {
  final String errorMessage;

  Failure({
    StackTrace? stackTrace,
    String? label,
    dynamic exception,
    this.errorMessage = '',
  }) {
    // Loga a stack trace apenas no modo debug para evitar poluição de logs em produção.
    if (stackTrace != null && kDebugMode) {
      debugPrintStack(label: label, stackTrace: stackTrace);
    }

    // Reporta o erro ao service se houver exception
    ErrorReport.externalFailureError(exception, stackTrace, label);
  }
}

class UnknownError extends Failure {
  UnknownError({
    String? label,
    dynamic exception,
    StackTrace? stackTrace,
  }) : super(
          stackTrace: stackTrace,
          label: label,
          exception: exception,
          errorMessage:
              'Unknown Error', // Passa a mensagem diretamente para a classe base
        );
}
```

## **Possíveis extensões**

Se no futuro você quiser criar outras subclasses de `Failure` para diferentes tipos de erros, será muito fácil. Por exemplo, você pode criar uma classe `NetworkError` para representar erros relacionados à rede, e utilizar o mesmo padrão de construção.

```dart
class NetworkError extends Failure {
  NetworkError({
    String? label,
    dynamic exception,
    StackTrace? stackTrace,
  }) : super(
          stackTrace: stackTrace,
          label: label,
          exception: exception,
          errorMessage: 'Network Error',
        );
}
```

### Visualizando os dados no Elastic APM:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1745333354965/59e43069-0266-4652-a17f-4624c3331dfb.png align="center")

Abaixo na aba **Traces** veremos todos os erros coletados do nosso aplicativo, se você clicar nele verá vários dados e insights sobre ocorrido:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1745333555242/1ef03ead-9567-49f3-a527-8e3a7028b780.png align="center")

# **Conclusão**

Bom é isso 😎.

Neste artigo, você aprendeu como configurar e preparar nossos aplicativos Flutter para serem usados ​​com o padrão da Open Telemetry o que é incrível pois é uma ferramenta robusta para coletar e analisar dados.

Aqui está o código do [projeto](https://github.com/iang12/flutter_open_telemetry_example) de exemplo no github. 🔗

Espero que você tenha gostado! Compartilhe-o com seus amigos e colegas!

Juntos, vamos construir apps incríveis que transformam o mundo!

Se tiver alguma dúvida ou contribuição, deixe nos comentários!

Me siga para estar sempre por dentro dos próximos artigos 📲 🚀

# **🌐 Minhas redes sociais 🌐**

[**GitHub**](https://www.github.com/iang12) | [**LinkedIn**](https://www.linkedin.com/in/ianoliveirag12/) | [**Instagram**](https://www.instagram.com/ianoliveira.dev/) | [**Twitter**](https://twitter.com/ianoliveirag12) **(X)** | [**Medium**](https://medium.com/@ianoliveirag12)
