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.

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 😘