Sampling
Um dos problemas do OpenTelemetry é controlar o custo. Apesar de ser uma ferramenta open source em algum lugar você irá precisar de tráfego, processamento e armazenamento para os dados coletados. Seja em uma infra própria, na cloud, um custo estará associado.
Mas quanto irei gastar? Ai que tá.. não sabemos a quantidade de tracing que será gerado, principalmente quando utilizamos a auto instrumentacão pois ele irá criar os spans que ele achar que deve.
Vai mandar pro Grafana? Pro Dynatrace? Pro Datadog? Pra uma Jaeger interno?
Se falarmos de logs temos um certo controle pois podemos definir a gravidade para registrar (INFO, WARNING, ERROR, DEBUG), mas não estamos controle da quantidade de spans que serão criados.
Por conta disso precisamos elaborar um plano para garantir que o custo não suba demais e é ai que entra o Sampling.
Existem dois tipos de Sampling:
-
Head Sampling: Uma instrumentação configurada na aplicação irá decidir se devemos manter o span ou não.
-
Tail Sampling: A aplicação não se importa com isso, tudo será direcionado para o collector e este irá definir se queremos manter ou não.
Quando digo manter é enviar para ser armazenado. Podemos gerar todos os spans sempre, mas não vamos guardá-lo.
Por agora, vamos falar somente sobre o Head, pois não utilizamos o collector ainda.
No primeiro span gerado, o pai, tomará a decisão se deve ou não manter e essa decisão precisa ser propagada para todos os filhos que devem respeitar essa decisão. É a mesma idéia do TraceID e das flags que formam o contexto do trace.
Estamos tomando uma decisão no início sem saber o que vem pela frente. Não sabemos como ele irá terminar, se com sucesso ou erro, quanto tempo levou, etc. Essa decisão esta sendo tomada por um único intervalo de tempo sem levar em consideração qualquer outro.
Existem 4 spans pre definidos.
AlwaysOffSampler
(Todos os dados serão descartados): Somente para fins de desenvolvimento e teste.AlwaysOnSampler
(Todos os dados serão mantidos): Somente para fins de desenvolvimento e teste.ParentBasedSampler
(Mais usado em produção): Respeita a decisão de quem o chamou. Se o parent direto, não primeiro, decidir que não é para manter então não mantém.TraceIdRatioBasedSampler
(Também usado em produção): Obtem uma amostra uma quantidade (ex: 50%) dos dados do próprio span para que a decisão possa ser tomada.
Podemos combinar o ParentBasedSampler e TraceIdRatioBasedSampler. Se o span for o primeiro então ele pode decidir e se for os filhos eles respeitarão a decisão do pai.
O sampler também é passado para o instrumentador, então vamos criá-lo. O código estará disponível no projeto na branch sampler.
const sampler = new ParentBasedSampler({
root: new TraceIdRatioBasedSampler(0.5)
})
const sdk = new NodeSDK({
resource,
traceExporter,
instrumentations,
sampler,
});
-
ParentBasedSampler: Este é um sampler que toma decisões baseadas no estado do parent (pai) da span:
- Se a span tem um pai (parent), ela segue a decisão de amostragem do pai
- Se a span não tem pai (é uma root span), ele usa o sampler definido no root
-
TraceIdRatioBasedSampler(0.5): Este é o sampler usado para spans root (sem pai):
- O valor 0.5 significa 50% de amostragem u seja, aproximadamente metade das traces root serão amostradas. Se fosse 1.0 seria 100% das traces, 0.1 seria 10% das traces.
Então, em resumo, este código configura:
- Para spans que têm pai: segue a decisão do pai
- Para spans root (sem pai): amostra 50% delas aleatoriamente
Assim seria como utilizaríamos de forma rápida, mas ainda temos um problema. Quando retiramos 50% dos dados, basicamente estamos pegando os 50% mais comuns do nosso tráfego que pode haver coisas que simplesmente não interessam. Vamos testar isso.
Eliminando 50% dos dados, a única coisa que foi gravada foi um GET que veio do scrape do prometheus o que não faz sentido nenhum para debugarmos uma aplicação.
O que podemos fazer é criar o nosso próprio sampler que será definido pelo pai e passado aos filhos.
Para fazer isso agora vamos criar um sampler separadamente que será o arquivo customSampler.ts e importar no nosso instrumentation.ts.
// Código...
import { CustomSampler } from './customSampler'
// Código...
function start(serviceName: string) {
const sampler = new ParentBasedSampler({
root: new CustomSampler(), // O pai vai pegar um sampler customizado.
});
const sdk = new NodeSDK({
resource,
traceExporter,
instrumentations,
sampler,
});
// Código...
}
O nosso 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";
// A classe que vamos implementar
export class CustomSampler implements Sampler {
// Método obrigatório da interface Sampler que decide se um span específico deve ser amostrado ou não
shouldSample(
context: Context,
traceId: string,
spanName: string,
spanKind: SpanKind,
attributes: Attributes,
links: Link[]
): SamplingResult {
// attributes["http.target"] contém o caminho da requisição HTTP. Se for /metrics...
if (
attributes["http.target"] === PrometheusExporter.DEFAULT_OPTIONS.endpoint
) {
// Log para debug que vamos mostrar no console
console.log("Não vamos manter!", { attributes });
// Retorna decisão de NÃO registrar este span
return {
decision: SamplingDecision.NOT_RECORD,
};
}
// Para todos os outros casos, retorna decisão de registrar e amostrar
return {
decision: SamplingDecision.RECORD_AND_SAMPLED,
};
}
}
Quando rodamos teremos isso no console, mostrando que todo o get que o prometheus faz para o scrape é desprezado.
auth-1 | }
todo-1 | Não vamos manter! {
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 | Não vamos manter! {
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 | Não vamos manter!
#...
Removendo algumas coisas podemos diminuir o custo!