Saltar al contenido principal

Implementar Opentelemetry (OTEL) en cloud functions

Instalar dependencias de Opentelemetry

npm install @opentelemetry/api \
@opentelemetry/sdk-node \
@opentelemetry/exporter-trace-otlp-http \
@opentelemetry/core

Crear modulo de opentelemetry

Crear un archivo con extensión .js con el siguiente contenido

import { diag, DiagConsoleLogger, DiagLogLevel } from "@opentelemetry/api";
import dotenv from "dotenv";
import { NodeSDK } from "@opentelemetry/sdk-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { W3CTraceContextPropagator } from "@opentelemetry/core";
import config from "../config";
import logger from "./logger";

dotenv.config();

const startTracing = async () => {
const tempoUser = process.env.TEMPO_USER;
const tempoPass = process.env.TEMPO_PASS;
const auth = Buffer.from(`${tempoUser}:${tempoPass}`).toString("base64");

const traceExporter = new OTLPTraceExporter({
url: process.env.TEMPO_URL,
headers: {
Authorization: `Basic ${auth}`,
},
});

const sdk = new NodeSDK({
traceExporter,
instrumentations: [],
textMapPropagator: new W3CTraceContextPropagator(),
});

await sdk.start();

// For troubleshooting, set the log level to DiagLogLevel.DEBUG
diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.INFO);
};

export const init = async (cacheClient) => {
if (!cacheClient.tracingStarted && config.NODE_ENV !== "test") {
try {
await startTracing();
cacheClient.tracingStarted = true;
} catch (e) {
logger.info("No se pudo iniciar tracing:", e.message);
}
}
};

Nota: En el caso de troubleshooting configurar el logger de otel a modo DEBUG.

diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.DEBUG);

Crear instancia de otel

En el entrypoint de la función ya sea index.js o server.js agregar a la variable global cacheClient la propiedad tracingStarted: false.

const cacheClient = { tracingStarted: false }; // En el caso que existan otros parametros dejarlos por favor...

Nota: La condición config.NODE_ENV !== "test" es para que en el caso de que la función tenga tests unitarios, este modulo no se cargue ya que no es necesario.

Ahora al principio del método principal de la función ejecutar el método init para instanciar el cliente de otel.

await otel.init(cacheClient);

Crear un trace (nodo raíz)

Supongamos que queremos medir metricas desde dos funciones llamadas A, B. que se ejecutan de forma asincronica mediante cloud tasks.

alt text

Para lograr esto se debe crear en un ROOT_CONTEXT que cree un traceparent id distinto para cada ejecución de la función ya que en caso de no setearlo se creara una sola traza con todas las ejecuciones.

Crear un ROOT_CONTEXT

import {
context,
propagation,
ROOT_CONTEXT,
SpanKind,
trace,
} from "@opentelemetry/api";

const tracer = trace.getTracer("mi-tracer");

export const miFuncion = async (req, res) => {
await context.with(ROOT_CONTEXT, async () => {
const span = tracer.startSpan("span-name");

span.setAttribute(
"body",
JSON.stringify({ message: "Procesado en función A" })
);

return res.status(200).json({ message: "Response A" });
});
};

El código especificado anteriormente crea una instancia de un tracer llamado mi-tracer.

const tracer = trace.getTracer("mi-tracer");

Luego en el cuerpo de la función creamos el context raíz con el modulo context de open telemetry, este context es el usado en toda la función para generar spans y propagarlo en todas las funciones o servicios.

await context.with(ROOT_CONTEXT, async () => {
// Contexto de la función
});

Luego creamos un span en el cual podemos recolectar datos e información.

const span = tracer.startSpan("span-name");

En este span podemos setear atributos y eventos:

// Atributo de tipo JSON
span.setAttribute(
"body",
JSON.stringify({ message: "Procesado en función A" })
);

// Atributo de tipo number
span.setAttribute("numerito", JSON.stringify(120));

// Atributo de tipo string
span.setAttribute(
"ohayou.sekai.good.morning.world",
JSON.stringify("Hello world")
);

En el caso de eventos estos se generan con un timestamp y se setan de la siguiente manera:

span.addEvent("some.event", event);

Crear un span cliente

Si lo que deseamos ahora es propagar el contexto a traves de servicios, lo que debemos hacer es crear un span de tipo (o kind) CLIENT. Para esto usamos el metodo startActiveSpan.

const data = { message: "Procesado en A y enviado a B" };

await context.with(ROOT_CONTEXT, async () => {
// Este es el span normal creado anteriormente (no propaga contexto).
const span = tracer.startSpan("span-name");

await tracer.startActiveSpan(
"call-B",
{
attributes: {
"service.name": "funcion-A",
"peer.service": "funcion-B",
"rpc.system": "http",
},
kind: SpanKind.CLIENT,
},
async (span) => {
// Propagamos en los headers del context el traceparent id.
const headers = {};
propagation.inject(context.active(), headers);

const body = {
...data,
next: "http://funcion-B.sils.tech",
};

// IMPORTANTE: Los headers siempre se deben pasar en cada consulta a cloud tasks.
await tasks.createTask(url, body, headers);

span.setAttribute("next.url", JSON.stringify(url));
span.setAttribute("next.body", JSON.stringify(body));

span.end();
}
);
});

Crear un span server

Una vez configurado el span CLIENT en la función A necesitamos configurar en la funcion B un span de kind SERVER.

**
Función A
**

// Extraemos el context padre en el context activo o actual
// Este si o si necesita que llegue en los headers de la consulta
// un atributo llamado traceparent.
const extractedContext = propagation.extract(context.active(), req.headers);

// Instanciamos
const tracer = trace.getTracer("mi-tracer");

// (Opcional) Si no quieres tener spans huerfanos en el trace genera un ROOT CONTEXT.
const parentContext = req.headers.traceparent ? extractedContext : ROOT_CONTEXT;

export const miFuncionB = async (req, res) => {
await context.with(parentContext, async () => {
await tracer.startActiveSpan(
"call-B",
{
attributes: {
"service.name": "funcion-B",
"rpc.system": "http",
},
kind: SpanKind.SERVER,
},
async (span) => {
JSON.stringify({ message: "Procesado en función A" })
});
});
};

Variables de entorno

Para pushear las traces a tempo y para detectar el nombre del servicio hacen falta un par de envs. Para setear el nombre del service es necesario tener el env llamado OTEL_SERVICE_NAME

OTEL_SERVICE_NAME=mi-funcion-A
TEMPO_URL=http://${ip-de-tempo}/v1/traces
TEMPO_USER=XXXX
TEMPO_PASS=XXXX

Pedir las envs a juli.

Si todo salio bien deberiamos ver en grafana los traces. Proximamente sacaremos un tutorial de como navegar en las metricas, mientras tanto vayan jugando con la herramienta, ante cualquier incoveniente hablarle juli

Muchas gracias y saluditos 😘