Muestreo
Uno de los problemas de OpenTelemetry es controlar el coste. Aunque sea una herramienta de código abierto, en algún lugar necesitarás tráfico, procesamiento y almacenamiento para los datos recopilados. Ya sea en una infraestructura propia, en la nube, habrá un coste asociado.
¿Pero cuánto voy a gastar? Ahí está el problema... no sabemos la cantidad de trazado que se generará, especialmente cuando utilizamos la auto-instrumentación, ya que creará los spans que considere necesarios.
¿Lo vas a enviar a Grafana? ¿A Dynatrace? ¿A Datadog? ¿A un Jaeger interno?
Si hablamos de logs, tenemos cierto control, ya que podemos definir la gravedad para registrar (INFO, WARNING, ERROR, DEBUG), pero no tenemos control de la cantidad de spans que se crearán.
Por ello, necesitamos elaborar un plan para garantizar que el coste no suba demasiado y ahí es donde entra el Muestreo (Sampling).
Existen dos tipos de Muestreo:
-
Head Sampling: Una instrumentación configurada en la aplicación decidirá si debemos mantener el span o no.
-
Tail Sampling: La aplicación no se preocupa por esto, todo será dirigido al collector y este definirá si queremos mantenerlo o no.
Cuando digo mantener me refiero a enviar para ser almacenado. Podemos generar todos los spans siempre, pero no vamos a guardarlos.
Por ahora, vamos a hablar solamente sobre el Head, ya que aún no utilizamos el collector.
En el primer span generado, el padre, tomará la decisión de si debe o no mantener y esa decisión necesita ser propagada a todos los hijos que deben respetar esa decisión. Es la misma idea del TraceID y de las flags que forman el contexto del trace.
Estamos tomando una decisión al inicio sin saber lo que viene por delante. No sabemos cómo terminará, si con éxito o error, cuánto tiempo llevó, etc. Esta decisión está siendo tomada por un único intervalo de tiempo sin tener en consideración cualquier otro.
Existen 4 samplers predefinidos.
AlwaysOffSampler(Todos los datos serán descartados): Solamente para fines de desarrollo y pruebas.AlwaysOnSampler(Todos los datos serán mantenidos): Solamente para fines de desarrollo y pruebas.ParentBasedSampler(Más usado en producción): Respeta la decisión de quien lo llamó. Si el parent directo, no el primero, decidir que no se debe mantener entonces no se mantiene.TraceIdRatioBasedSampler(También usado en producción): Obtiene una muestra de una cantidad (ej: 50%) de los datos del propio span para que la decisión pueda ser tomada.
Podemos combinar el ParentBasedSampler y TraceIdRatioBasedSampler. Si el span es el primero entonces puede decidir y si son los hijos respetarán la decisión del padre.
El sampler también se pasa al instrumentador, así que vamos a crearlo. El código estará disponible en el proyecto en la branch sampler.
const sampler = new ParentBasedSampler({
root: new TraceIdRatioBasedSampler(0.5)
})
const sdk = new NodeSDK({
resource,
traceExporter,
instrumentations,
sampler,
});
-
ParentBasedSampler: Este es un sampler que toma decisiones basadas en el estado del parent (padre) del span:
- Si el span tiene un padre (parent), sigue la decisión de muestreo del padre
- Si el span no tiene padre (es un root span), usa el sampler definido en root
-
TraceIdRatioBasedSampler(0.5): Este es el sampler usado para spans root (sin padre):
- El valor 0.5 significa 50% de muestreo, es decir, aproximadamente la mitad de los traces root serán muestreados. Si fuera 1.0 sería 100% de los traces, 0.1 sería 10% de los traces.
Entonces, en resumen, este código configura:
- Para spans que tienen padre: sigue la decisión del padre
- Para spans root (sin padre): muestrea 50% de ellos aleatoriamente
Así es como lo utilizaríamos de forma rápida, pero aún tenemos un problema. Cuando retiramos el 50% de los datos, básicamente estamos cogiendo el 50% más común de nuestro tráfico, que puede contener cosas que simplemente no nos interesan. Vamos a probarlo.

Eliminando el 50% de los datos, lo único que se grabó fue un GET que vino del scrape de prometheus, lo que no tiene ningún sentido para depurar una aplicación.
Lo que podemos hacer es crear nuestro propio sampler que será definido por el padre y pasado a los hijos.
Para hacer esto ahora vamos a crear un sampler separadamente que será el archivo customSampler.ts e importarlo en nuestro instrumentation.ts.
// Código...
import { CustomSampler } from './customSampler'
// Código...
function start(serviceName: string) {
const sampler = new ParentBasedSampler({
root: new CustomSampler(), // El padre va a coger un sampler personalizado.
});
const sdk = new NodeSDK({
resource,
traceExporter,
instrumentations,
sampler,
});
// Código...
}
Nuestro customSampler.ts será...
import { Attributes, Context, Link, SpanKind } from "@opentelemetry/api";
import { PrometheusExporter } from "@opentelemetry/exporter-prometheus";
import {
Sampler,
SamplingResult,
SamplingDecision,
} from "@opentelemetry/sdk-trace-base";
// La clase que vamos a implementar
export class CustomSampler implements Sampler {
// Método obligatorio de la interfaz Sampler que decide si un span específico debe ser muestreado o no
shouldSample(
context: Context,
traceId: string,
spanName: string,
spanKind: SpanKind,
attributes: Attributes,
links: Link[]
): SamplingResult {
// attributes["http.target"] contiene la ruta de la petición HTTP. Si es /metrics...
if (
attributes["http.target"] === PrometheusExporter.DEFAULT_OPTIONS.endpoint
) {
// Log para debug que vamos a mostrar en la consola
console.log("¡No vamos a mantener!", { attributes });
// Retorna decisión de NO registrar este span
return {
decision: SamplingDecision.NOT_RECORD,
};
}
// Para todos los demás casos, retorna decisión de registrar y muestrear
return {
decision: SamplingDecision.RECORD_AND_SAMPLED,
};
}
}
Cuando ejecutamos tendremos esto en la consola, mostrando que todo el GET que prometheus hace para el scrape es desechado.
auth-1 | }
todo-1 | ¡No vamos a mantener! {
todo-1 | attributes: {
todo-1 | 'http.url': 'http://todo:9464/metrics',
todo-1 | 'http.host': 'todo:9464',
todo-1 | 'net.host.name': 'todo',
todo-1 | 'http.method': 'GET',
todo-1 | 'http.scheme': 'http',
todo-1 | 'http.target': '/metrics',
todo-1 | 'http.user_agent': 'Prometheus/3.1.0',
todo-1 | 'http.flavor': '1.1',
todo-1 | 'net.transport': 'ip_tcp'
todo-1 | }
todo-1 | }
auth-1 | ¡No vamos a mantener! {
auth-1 | attributes: {
auth-1 | 'http.url': 'http://auth:9464/metrics',
auth-1 | 'http.host': 'auth:9464',
auth-1 | 'net.host.name': 'auth',
auth-1 | 'http.method': 'GET',
auth-1 | 'http.scheme': 'http',
auth-1 | 'http.target': '/metrics',
auth-1 | 'http.user_agent': 'Prometheus/3.1.0',
auth-1 | 'http.flavor': '1.1',
auth-1 | 'net.transport': 'ip_tcp'
auth-1 | }
auth-1 | }
todo-1 | ¡No vamos a mantener!
#...
¡Eliminando algunas cosas podemos reducir el coste!