DEV Community

Vinicius Pereira de Oliveira
Vinicius Pereira de Oliveira

Posted on • Edited on

Opentelemetry + Typescript

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"
Enter fullscreen mode Exit fullscreen mode

rode um

docker compose up -d
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

crie um arquivo para inicializar o sdk, inserindo o seguinte código:

import { NodeSDK } from "@opentelemetry/sdk-node";

const sdk = new NodeSDK({

})

sdk.start();
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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

Então

yarn add @opentelemetry/auto-instrumentations-node
Enter fullscreen mode Exit fullscreen mode

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();

Enter fullscreen mode Exit fullscreen mode

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)
    }
  };
};

Enter fullscreen mode Exit fullscreen mode

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

yarn add @opentelemetry/api
Enter fullscreen mode Exit fullscreen mode

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
        }
      );
    };
  };
};

Enter fullscreen mode Exit fullscreen mode

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}`,
  });
}

Enter fullscreen mode Exit fullscreen mode

Top comments (0)