Opentelemetry + Typescript
Published: 2024-02-05 10:54:00

## Inserindo Observabilidade nos seus códigos Typescript

Nesse artigo irei apresentar funcionalidades básicas de observabilidade para inserir nos seus projetos em Nest Js, para que o gerenciamento da sua aplicação e tratamento de erros se torne algo cada vez mais simples no seu desenvolvimento e a correção de incidentes, mais rápida.

### O que é o Opentelemetry
OpenTelemetry é uma ferramenta para observabilidade utilizada para criação e gerenciamento de traços, métricas e logs de uma aplicação. É uma funcionalidade open source com integração disponível a diversos sistemas de APM (Jaeger, Grafana, Dynatrace e Prometheus).

### Jaeger
Jaeger é uma plataforma open-source para gerenciamento de `trace`. Por ser simples, utilizaremos para monitoramento da nossa aplicação em ambiente local.

### 1 - Organize seu docker compose
Para conseguir testar em ambiente de desenvolvimento, utilizaremos uma ferramenta para gerenciamento de traces open source. Jaeger

Crie um arquivo chamado `docker-compose.yaml`

e insira o seguinte texto:
```
version: '3.0'
services:
  jaeger:
    platform: linux/x86_64
    image: jaegertracing/all-in-one:1.53
    environment:
      - COLLECTOR_ZIPKIN_HTTP_PORT=9411
    ports:
      - "5775:5775/udp"
      - "6831:6831/udp"
      - "6832:6832/udp"
      - "5778:5778"
      - "4317:4317"
      - "4318:4318"
      - "16686:16686"
      - "14250:14250"
      - "14268:14268"
      - "14269:14269"
      - "9411:9411"
```
rode um 
```
docker compose up -d
```

E o Jaeger já estará instrumentado.

Para testar se a instrumentação está correta abra a página
`http://localhost:16686/`


### 2 - Instrumentando os traces no Jaeger

As nossas métricas da aplicação, traces e logs de sistema serão instrumentados a partir de 'exporters' que são configurados a partir do SDK do NodeJS.

Para instrumentar o sdk do OpenTelemetry é necessário instalar a biblioteca necessária para implantação dessa classe

```
yarn add @opentelemetry/sdk-node
```

crie um arquivo para inicializar o sdk, inserindo o seguinte código:
```
import { NodeSDK } from "@opentelemetry/sdk-node";

const sdk = new NodeSDK({

})

sdk.start();
```

Após a criação do sdk, precisamos dizer ao OpenTelemetry para onde queremos exportar todos os logs que traços de sistema que ele gerar. Para facilitar esse trabalho, dentro do objeto de configuração do NodeSDK, podemos definir qual `exporter` iremos utilizar. O próprio `OpenTelemetry Protocol (OTLP)` nos fornece uma boa experiência e facilidade para exportar esses dados.

Há varios protocolos disponíveis para envio dessas informações aos coletores, por exemplo, JSON, protobuf ou gRPC. 

Iremos utilizar utilizar o protocolo JSON, mas fique a vontade para utilizar o protocolo que quiser, todos irão funcionar bem.

Instale as dependencias abaixo para gerenciar os `exporters`

```
yarn add @opentelemetry/exporter-trace-otlp-http \
  @opentelemetry/exporter-metrics-otlp-http \
  @opentelemetry/sdk-metrics
```

Atualizando nossa instancia do SDK, ficaremos com o código da seguinte forma: 

```
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
import { NodeSDK } from "@opentelemetry/sdk-node";

const sdk = new NodeSDK({
    traceExporter: new OTLPTraceExporter({}),
    metricReader: new PeriodicExportingMetricReader({
        exporter: new OTLPMetricExporter({})
    })
})

sdk.start();
```

OBS: Dentro de cada coletor, podemos definir URL's de destino da aplicação. Exemplo, ao utilizar o Dynatrace, temos que definir uma URL de destino dos `traces` que criamos. Para realizar essa modificação. Dentro do objeto de `OTLPTraceExporter` e `OTLPMetricExporter` existe um campo, chamado `url`, onde definimos o destino da informação

Exemplo: 

```
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
import { NodeSDK } from "@opentelemetry/sdk-node";

const sdk = new NodeSDK({
    traceExporter: new OTLPTraceExporter({
        url: 'http://localhost:4318/v1/traces'
    }),
    metricReader: new PeriodicExportingMetricReader({
        exporter: new OTLPMetricExporter({
            url: 'http://localhost:4318/v1/metrics'
        })
    })
})

sdk.start();
```

Antes de finalizar a configuração do NodeSDK. Precisamos dizer ao NodeSDK do OpenTelemetry os dados da nossa aplicação, para conseguirmos identificar os traces de maneira mais rápida e como serão as instrumentações automáticas.

Para definir dados da aplicação para o OpenTelemetry, precisamos instalar a biblioteca  `@opentelemetry/resources` e `@opentelemetry/semantic-conventions`

```
yarn add @opentelemetry/resources \
  @opentelemetry/semantic-conventions
```

Onde o código ficará da seguinte forma:

```
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { Resource } from '@opentelemetry/resources';
import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
import { NodeSDK } from '@opentelemetry/sdk-node';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';

const sdk = new NodeSDK({
  traceExporter: new OTLPTraceExporter({
    url: 'http://localhost:4318/v1/traces',
  }),
  metricReader: new PeriodicExportingMetricReader({
    exporter: new OTLPMetricExporter({
      url: 'http://localhost:4318/v1/metrics',
    }),
  }),
  resource: new Resource({
    [SemanticResourceAttributes.SERVICE_NAMESPACE]: 'NOME DO SERVICO',
    [SemanticResourceAttributes.SERVICE_VERSION]: 'VERSAO',
  }),
});

sdk.start();
```

Para finalizar, a instrumentação automática, é criada a partir da `@opentelemetry/auto-instrumentations-node`

Então

```
yarn add @opentelemetry/auto-instrumentations-node
```

E modificamos o código, deixando-o da seguinte forma:

```
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { Resource } from '@opentelemetry/resources';
import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
import { NodeSDK } from '@opentelemetry/sdk-node';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';

const sdk = new NodeSDK({
  traceExporter: new OTLPTraceExporter({
    url: 'http://localhost:4318/v1/traces',
  }),
  metricReader: new PeriodicExportingMetricReader({
    exporter: new OTLPMetricExporter({
      url: 'http://localhost:4318/v1/metrics',
    }),
  }),
  resource: new Resource({
    [SemanticResourceAttributes.SERVICE_NAMESPACE]: 'NOME',
    [SemanticResourceAttributes.SERVICE_VERSION]: '0.2',
    [SemanticResourceAttributes.SERVICE_NAME]: 'Nome Identificador da aplicação',
  }),
  instrumentations: [getNodeAutoInstrumentations()]
});

sdk.start();

```

### 3 - Criando Traces Manuais

Para criarmos os nossos traços na aplicação de forma manual, utilizaremos decorators que vão nos facilitar a não ficar repetindo código na aplicação inteira, para pegar todo o caminho de uma requisição ou lógica.

Criaremos um decorator, que por enquanto apenas executará o método sem modificá-lo em nada 

```
export const Trace = (value?: string) => {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor,
  ) {
    const method = descriptor.value
    
    const args = arguments;

    descriptor.value = function() {
      const that = this
      return method?.apply(that, args)
    }
  };
};

```

Para iniciarmos a criação dos tracers, é necessário instalar mais uma biblioteca chamada `@opentelemetry/api`

```
yarn add @opentelemetry/api
```

Modificamos o decorator, para ficar do seguinte formato:
```
import { Span, trace } from '@opentelemetry/api';

export const Trace = (value?: string) => {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor,
  ) {
    const method = descriptor.value;
    
    const args = arguments;

    const tracer = trace.getTracer('');

    descriptor.value = function () {
      const that = this;
      return tracer.startActiveSpan(
        value ?? `${target.constructor.name}.${propertyKey}`,
        (span: Span) => {
          console.log(propertyKey)
          const result = method?.apply(that, args);
          span.end();
          return result
        }
      );
    };
  };
};

```

Dessa forma conseguiremos todos os traços dos métodos que inserirem esse `decorator`

Criaremos também uma função de traceError para conseguir ter mapeados todos os erros que acontecerem na aplicação.

```
import { Exception, SpanStatusCode, trace } from "@opentelemetry/api";

export function traceError(error: any) {
  const activeSpan = trace.getActiveSpan();

  activeSpan?.recordException(error as Exception);
  activeSpan?.setAttribute(
    "exception.response",
    JSON.stringify(
      {
        data: "Erro AXIOS",
      },
      null,
      2
    )
  );
  activeSpan?.setStatus({
    code: SpanStatusCode.ERROR,
    message: `Error: ${error.message}`,
  });
}

```