EXAMPLES.md — Detailed Check and Simulation Descriptions¶
This document describes each check/simulation implemented in the samples, its objective, how it works internally, and how to query results in Grafana.
SampleApi — Endpoints¶
1. GET /order/{id} — Trace distribuída completa¶
Objetivo: Demonstrar propagação de contexto entre 3 serviços via gRPC.
Fluxo:
1. API cria span api.get-order com tag order.id
2. Chama Backend via gRPC ProcessOrder
3. Backend cria spans backend.db-query-order (10-80ms) + backend.external-enrich-order (httpbin ~1s)
4. Resposta retorna com dados do pedido
Consultar no Grafana:
# Tempo — trace completa
{resource.service.name="sample-api" && name="api.get-order"}
# Ver os 3 serviços no mesmo trace
{resource.service.name=~"sample-api|sample-backend|sample-process" && name="api.get-order"}
2. POST /order — Criação com body JSON¶
Objetivo: Demonstrar instrumentação de endpoint POST com body parsing.
Fluxo:
1. Lê body {"product": "...", "quantity": N}
2. Gera ID aleatório
3. Chama Backend via gRPC ProcessOrder
4. Retorna 201 com dados
Consultar no Grafana:
3. GET /order/{id}/cancel — Cancelamento¶
Objetivo: Demonstrar operação de escrita (UPDATE) no Backend.
Fluxo:
1. API cria span api.cancel-order
2. Chama Backend via gRPC CancelOrder
3. Backend cria span backend.db-cancel-order (5-30ms)
Consultar no Grafana:
4. GET /slow — Operação lenta¶
Objetivo: Demonstrar detecção de alta latência pelo tail sampling (traces >1s sempre mantidos).
Fluxo:
1. API cria span api.slow-operation
2. Chama Backend via gRPC SlowOperation
3. Backend: backend.db-heavy-query (3-5s) + backend.external-slow-enrichment (1-2s)
4. Total: 4-7s
Consultar no Grafana:
# Traces lentas
{resource.service.name="sample-api" && duration > 3s}
# Especificamente o slow
{resource.service.name="sample-api" && name="api.slow-operation"}
5. GET /batch — Fan-out sequencial¶
Objetivo: Demonstrar trace com muitos spans (fan-out pattern).
Fluxo:
1. API cria span api.batch-orders
2. Gera 5 IDs aleatórios
3. Para cada ID, cria span api.batch-item e chama Backend ProcessOrder sequencialmente
4. Total: ~37 spans no trace (5 × backend spans)
Consultar no Grafana:
6. GET /error — Exception simulada¶
Objetivo: Demonstrar que traces com erro são sempre mantidos pelo sampling (100% em qualquer ambiente).
Fluxo:
1. API cria span api.error-simulated com StatusCode.Error
2. Lança InvalidOperationException
3. Retorna 500
Consultar no Grafana:
7. GET /health/ready — Readiness probe¶
Objetivo: Demonstrar health check que valida dependência (Backend).
Fluxo:
1. API cria span api.readiness-check
2. Chama Backend via gRPC ProcessOrder com ID 0
3. Se sucesso → 200, se falha → 503
Consultar no Grafana:
8. GET /order/{id}/trace — Baggage propagation¶
Objetivo: Demonstrar propagação de metadados (baggage) entre serviços sem adicionar ao span diretamente. Também demonstra scoped logging.
Como funciona:
1. API seta 3 baggage items: tenant.id, order.id, feature.flag
2. Usa logger.BeginScope(...) para adicionar contexto ao log
3. Chama Backend via gRPC ReadBaggage
4. Backend lê Baggage.Current e retorna os items recebidos
5. Baggage é propagado automaticamente via headers gRPC (sem código extra)
O que valida:
- Baggage propaga entre serviços automaticamente
- Backend recebe os 3 items sem configuração adicional
- Scoped logging adiciona TenantId e OrderId aos logs
Consultar no Grafana:
# Tempo
{resource.service.name="sample-api" && name="api.trace-with-baggage"}
# Loki — ver baggage nos logs do Backend
{service_name="sample-backend"} |= "ReadBaggage"
9. GET /order/{id}/events — Span Events¶
Objetivo: Demonstrar eventos dentro de um span (pontos no tempo, sem criar sub-spans).
Como funciona:
1. API cria span api.order-with-events
2. Adiciona 4 events ao span:
- order.received (com tags: order.id, order.source)
- order.validated (após delay de validação)
- order.enriched (após chamada ao Backend)
- order.completed (com tag: status)
3. Events aparecem como timeline dentro do span no Grafana
O que valida: - Events são visíveis no detalhe do span no Tempo - Cada event tem timestamp próprio (mostra duração entre etapas) - Tags nos events adicionam contexto sem poluir o span
Consultar no Grafana:
# Tempo — abrir o span e ver a aba "Events"
{resource.service.name="sample-api" && name="api.order-with-events"}
10. GET /parallel/{count} — Parallel fan-out¶
Objetivo: Demonstrar spans concorrentes (paralelos) dentro de um trace.
Como funciona:
1. API cria span api.parallel-fan-out com tag fan_out.count
2. Dispara N chamadas ao Backend em paralelo via Task.WhenAll
3. Cada chamada cria span api.parallel-item (children concorrentes)
4. No Grafana, os spans aparecem lado a lado (não sequenciais)
O que valida: - Spans paralelos são visualizados corretamente no Tempo - Duração total ≈ duração do item mais lento (não soma) - Cada item tem seu próprio span com tags
Consultar no Grafana:
11. GET /retry/{id} — Retry com spans por tentativa¶
Objetivo: Demonstrar padrão de retry onde cada tentativa é um span separado, com ERROR nas falhas e SUCCESS na última.
Como funciona:
1. API cria span pai api.retry-operation
2. Loop de até 3 tentativas, cada uma com span api.retry-attempt
3. Chama Backend UnstableOperation (configurado para falhar 2x)
4. Tentativas 1 e 2: span com StatusCode.Error + log warning
5. Tentativa 3: sucesso
6. Backoff exponencial entre tentativas (100ms, 200ms)
O que valida: - Cada tentativa é visível como span separado no trace - Spans de falha têm status ERROR (ícone vermelho no Grafana) - O span pai mostra a operação completa - Log com exception stack trace correlacionado ao trace
Consultar no Grafana:
# Trace com retries
{resource.service.name="sample-api" && name="api.retry-operation"}
# Só as falhas
{resource.service.name="sample-api" && name="api.retry-attempt" && status=error}
12. GET /cache/{id} — Cache hit/miss¶
Objetivo: Demonstrar padrão de cache com métricas de hit rate e spans diferenciados.
Como funciona:
1. API cria span api.cache-lookup com tag order.id
2. Verifica cache in-memory (ConcurrentDictionary)
3. HIT: tag cache.hit=true, incrementa cache.hits_total, retorna imediato
4. MISS: tag cache.hit=false, incrementa cache.misses_total, chama Backend, armazena resultado
O que valida:
- Primeira chamada: MISS (span longo, chama Backend)
- Segunda chamada mesmo ID: HIT (span curto, sem Backend)
- Métricas cache.hits_total e cache.misses_total exportadas
- Tag cache.hit no span permite filtrar no Tempo
Consultar no Grafana:
# Tempo — filtrar por hit/miss
{resource.service.name="sample-api" && name="api.cache-lookup" && span.cache.hit=true}
# PromQL — cache hit rate
rate(cache_hits_total[5m]) / (rate(cache_hits_total[5m]) + rate(cache_misses_total[5m]))
SampleBackend — RPCs¶
UnstableOperation¶
Objetivo: Servir como target para o endpoint /retry da API. Simula serviço instável.
Como funciona:
- Mantém contador global de chamadas
- Falha nas primeiras N chamadas (retorna StatusCode.Unavailable)
- Sucesso a cada N+1 chamadas
- Span backend.unstable-operation com tag attempt
ReadBaggage¶
Objetivo: Demonstrar que baggage propagado via gRPC é acessível no serviço downstream.
Como funciona:
- Lê Baggage.Current (populado automaticamente pelo OTel SDK via headers gRPC)
- Retorna todos os items como mapa key→value
- Adiciona cada item como tag no span
SampleProcess — Workers¶
ApiHealthWorker (a cada 1 minuto)¶
Objetivo: Gerar traces distribuídas continuamente, validando a comunicação entre os 3 serviços.
Como funciona:
- 6 chamadas sequenciais à API, cada uma com StartRootActivity (trace independente)
- Span names descritivos por endpoint:
- process-api-check → GET /
- process-api-order → GET /order/1
- process-api-cancel → GET /order/1/cancel
- process-api-health-ready → GET /health/ready
- process-api-batch → GET /batch
- process-api-simulate-error → GET /error
Métricas:
- api_health.checks_total (por endpoint)
- api_health.checks_failed_total (por endpoint)
- api_health.check_duration_seconds (por endpoint)
Consultar no Grafana:
# Tempo — todos os health checks
{resource.service.name="sample-process" && name=~"process-api-.*"}
# PromQL — success rate
1 - (rate(api_health_checks_failed_total[1h]) / rate(api_health_checks_total[1h]))
HeavyProcessWorker (a cada 2 minutos)¶
Objetivo: Gerar carga de CPU e memória para observar no runtime metrics (.NET GC, thread pool).
Como funciona:
- Batch de 8-20 itens em paralelo (StartRootActivity)
- Cada item: SHA256 ×2000 (string 50KB) + 500 buffers (4-32KB)
- 5% de chance de falha simulada por item
- Span process.heavy-batch (root) → N× process.heavy-process-item (children paralelos)
Métricas:
- heavy_work.batches_total
- heavy_work.items_total (tag: status)
- heavy_work.errors_total
- heavy_work.batch_duration_seconds
- heavy_work.item_duration_seconds
- heavy_work.items_active (gauge)
Consultar no Grafana:
# Tempo
{resource.service.name="sample-process" && name="process.heavy-batch"}
# PromQL — throughput
rate(heavy_work_items_total[5m])
# PromQL — error rate
rate(heavy_work_errors_total[5m]) / rate(heavy_work_items_total[5m])
# Runtime — GC pressure (correlacionar com batch execution)
dotnet_gc_collections_total
QueueConsumerWorker (a cada 30 segundos)¶
Objetivo: Demonstrar o padrão de instrumentação para consumers de fila (SQS, Kafka, RabbitMQ).
Como funciona:
- Simula poll de fila: gera 1-5 "mensagens" fictícias por ciclo
- Cada mensagem é um trace independente (StartRootActivity)
- Dentro do trace, 3 etapas: parse → validate → persist
- 10% de chance de falha na validação (span com ERROR)
- Observable gauge queue.depth mostra profundidade da fila
Span hierarchy por mensagem:
process.queue-consume (root)
├── process.queue-parse
├── process.queue-validate
└── process.queue-persist
Tags no span root:
- messaging.message_id — ID único da mensagem
- messaging.system — "sqs"
- messaging.destination — "orders-queue"
Métricas:
- queue.messages_processed_total (tag: status)
- queue.messages_failed_total
- queue.message_duration_seconds
- queue.depth (observable gauge)
Consultar no Grafana:
# Tempo — traces de mensagens
{resource.service.name="sample-process" && name="process.queue-consume"}
# Tempo — só falhas de validação
{resource.service.name="sample-process" && name="process.queue-validate" && status=error}
# PromQL — throughput de mensagens
rate(queue_messages_processed_total[5m])
# PromQL — queue depth
queue_depth
# PromQL — failure rate
rate(queue_messages_failed_total[5m]) / rate(queue_messages_processed_total[5m])
ScheduledJobWorker (a cada 3 minutos)¶
Objetivo: Demonstrar job agendado com timeout, detecção de falha, e circuit breaker.
Como funciona:
1. Job executa 3 etapas: fetch-data → transform → persist
2. fetch-data tem delay aleatório (100ms-7s) — às vezes excede o timeout de 5s
3. Se timeout: span com ERROR + tag timeout=true
4. Após 3 falhas consecutivas: circuit breaker abre
5. Circuit aberto: job é rejeitado (span com tag circuit_breaker.action=rejected)
6. Após 30s: circuit vai para half-open, tenta novamente
7. Se sucesso: circuit fecha, contador reseta
Span hierarchy (sucesso):
process.scheduled-job (root)
├── process.job-fetch-data
├── process.job-transform
└── process.job-persist
Span hierarchy (timeout):
process.scheduled-job (root, ERROR, timeout=true)
└── process.job-fetch-data (cancelado pelo timeout)
Estados do circuit breaker:
- 0 = closed (normal, jobs executam)
- 1 = open (jobs rejeitados, aguardando recovery)
- 2 = half-open (tentando recovery)
Métricas:
- scheduled_job.runs_total (tag: result = success/timeout/error/circuit-open)
- scheduled_job.timeouts_total
- scheduled_job.duration_seconds
- circuit_breaker.state (observable gauge: 0/1/2)
Consultar no Grafana:
# Tempo — todos os jobs
{resource.service.name="sample-process" && name="process.scheduled-job"}
# Tempo — só timeouts
{resource.service.name="sample-process" && name="process.scheduled-job" && span.timeout=true}
# PromQL — timeout rate
rate(scheduled_job_timeouts_total[1h]) / rate(scheduled_job_runs_total[1h])
# PromQL — circuit breaker state
circuit_breaker_state
# PromQL — job duration P95
histogram_quantile(0.95, rate(scheduled_job_duration_seconds_bucket[5m]))
Resumo de funcionalidades demonstradas¶
| Funcionalidade | Onde | Span/Métrica |
|---|---|---|
| Trace distribuída (3 serviços) | API /order/{id} |
api.get-order |
| Baggage propagation | API /order/{id}/trace |
api.trace-with-baggage |
| Span events | API /order/{id}/events |
api.order-with-events |
| Parallel fan-out | API /parallel/{count} |
api.parallel-fan-out |
| Retry com spans | API /retry/{id} |
api.retry-operation |
| Cache hit/miss | API /cache/{id} |
api.cache-lookup + cache.hits_total |
| Error trace (sempre mantido) | API /error |
api.error-simulated |
| High latency (sempre mantido) | API /slow |
api.slow-operation |
| gRPC instrumentation | API → Backend | Auto-spans gRPC client/server |
| StartRootActivity (workers) | Process todos | Traces independentes por iteração |
| Observable gauge | Process queue/circuit | queue.depth, circuit_breaker.state |
| Consumer pattern | Process queue | process.queue-consume |
| Timeout detection | Process job | process.scheduled-job + timeout=true |
| Circuit breaker | Process job | circuit_breaker.state gauge |
| CPU/memory stress | Process heavy | process.heavy-batch |
| Scoped logging | API /order/{id}/trace |
logger.BeginScope(...) |
| RecordException | Todos (automático) | Exception stack trace no span |
| Debug tracestate | Todos (quando debug=true) | tracestate: debug=true |
Dashboards Grafana¶
3 dashboards prontos em example/dashboards/. Para importar: Dashboards → Import → Upload JSON.
| Arquivo | Título | O que mostra |
|---|---|---|
01-api-business-metrics.json |
Sample — API & Business Metrics | Request rate, error rate, P95 latency, cache hit/miss, revenue, DB/external duration, traces recentes |
02-workers-background.json |
Sample — Workers & Background Processing | Health check success rate, heavy work throughput, queue depth, circuit breaker state, scheduled job timeouts |
03-traces-reliability.json |
Sample — Traces & Reliability Patterns | Retry attempts, parallel fan-out, baggage propagation, span events, timeout traces, queue consumer, all errors |
Datasources necessários:
- victoriametrics (type: prometheus) — métricas
- tempo (type: tempo) — traces
Variável de filtro (dashboard 01): $service — seleciona entre sample-api, sample-backend, sample-process.