# Nici trace-urile tale nu ar trebui să știe în ce cloud rulează

Am argumentat că [logurile aplicației n-ar trebui deținute de cloud](/ro/blog/your-logs-should-not-know-which-cloud): aplicația emite, un agent ștampilează cloud-ul pe ele, un backend neutru le stochează. Logurile erau jumătatea ușoară. O linie de log e de sine stătătoare. Un trace nu e: e un arbore de span-uri produse de cinci servicii pe trei clouduri, cusute laolaltă de un ID care trebuie să supraviețuiască fiecărui hop dintre ele, inclusiv unei cozi și unei funcții care nu vorbește HTTP. Dacă ID-ul nu ajunge dincolo, n-ai un trace, ai cinci span-uri fără legătură și aceeași corelare cu ochiul de la 02:40 de care încercam să scăpăm.

E aceeași arhitectură aplicată la tracing, cu părțile care sunt diferite: propagarea, sampling-ul și cum arată query-ul când merge.

## Cele trei straturi, din nou

**Aplicația** folosește SDK-ul OpenTelemetry și nimic specific unui vendor. În Node asta înseamnă `@opentelemetry/sdk-node` cu auto-instrumentare pentru HTTP, driverul de bază de date, SDK-ul AWS și clientul de coadă. Exportă span-uri prin OTLP către `localhost:4317`. Habar n-are dacă collector-ul de la celălalt capăt trimite mai departe în Tempo, X-Ray sau într-un fișier.

```ts
// tracing.ts, încărcat cu --require înaintea aplicației
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc';
import { Resource } from '@opentelemetry/resources';

new NodeSDK({
  resource: new Resource({ 'service.name': process.env.OTEL_SERVICE_NAME }),
  traceExporter: new OTLPTraceExporter({ url: 'http://localhost:4317' }),
  instrumentations: [getNodeAutoInstrumentations()],
}).start();
```

`service.name` e singurul atribut pe care îl setează aplicația. Tot ce ține de *unde* rulează e adăugat mai târziu.

**Collector-ul** e [aceeași configurație de DaemonSet ca pentru loguri](/ro/blog/your-logs-should-not-know-which-cloud), cu un pipeline `traces` adăugat. Procesorul `resourcedetection` ștampilează `cloud.provider`, `cloud.region` și `k8s.cluster.name` pe fiecare span, exact cum a făcut pe fiecare linie de log, din același endpoint de metadata, cu zero ramificare între EKS, GKE și AKS.

**Backend-ul** e Grafana Tempo: OTLP la intrare, TraceQL la ieșire, object storage dedesubt, niciun index de dimensionat. Tempo și Loki stau unul lângă altul în aceeași Grafana, și asta face ca „sari de la linia asta de log la trace-ul ei” să fie un click, nu un copy-paste.

## Partea grea: ID-ul trebuie să traverseze golurile

În interiorul unui serviciu, SDK-ul gestionează contextul automat. Între servicii, peste HTTP, header-ul W3C `traceparent` e injectat de instrumentarea clientului și extras de instrumentarea serverului, și pur și simplu merge. Golurile sunt tot ce nu e un apel HTTP sincron.

<div class="article-figure">
<svg viewBox="0 0 900 260" width="100%" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Un request care traversează trei clouduri. Browserul apelează api-ul de pe EKS prin HTTP cu header traceparent. Api-ul publică în SQS și injectează contextul de trace într-un atribut de mesaj. Worker-ul de comenzi de pe GKE consumă mesajul, extrage contextul și apelează serviciul de facturare de pe AKS prin HTTP cu traceparent. Facturarea invocă o Lambda; instrumentarea Lambda extrage contextul din eveniment. Toate span-urile au același trace ID și ajung într-un singur Tempo.">
<defs><marker id="arrT" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L10,5 L0,10 z" fill="#4fffb0"/></marker></defs>
<g font-family="Inter,system-ui,sans-serif" font-size="12">
<rect x="15" y="60" width="130" height="64" rx="10" fill="#151b2e" stroke="#9aa3c7" stroke-width="1.5"/><text x="80" y="86" text-anchor="middle" fill="#f1f3ff" font-weight="700">browser</text><text x="80" y="106" text-anchor="middle" fill="#9aa3c7" font-size="11">POST /orders</text>
<line x1="147" y1="92" x2="188" y2="92" stroke="#4fffb0" stroke-width="1.5" marker-end="url(#arrT)"/><text x="167" y="80" text-anchor="middle" fill="#4fffb0" font-size="10">HTTP</text>
<rect x="190" y="60" width="140" height="64" rx="10" fill="#151b2e" stroke="#ffd166" stroke-width="1.5"/><text x="260" y="80" text-anchor="middle" fill="#f1f3ff" font-weight="700">api</text><text x="260" y="98" text-anchor="middle" fill="#ffd166" font-size="11">EKS · aws</text><text x="260" y="114" text-anchor="middle" fill="#9aa3c7" font-size="10">traceparent extras</text>
<line x1="332" y1="92" x2="373" y2="92" stroke="#ff6b8a" stroke-width="1.5" marker-end="url(#arrT)"/><text x="352" y="80" text-anchor="middle" fill="#ff6b8a" font-size="10">SQS</text>
<rect x="375" y="60" width="140" height="64" rx="10" fill="#151b2e" stroke="#ff6b8a" stroke-width="1.5" stroke-dasharray="5,3"/><text x="445" y="80" text-anchor="middle" fill="#f1f3ff" font-weight="700">coada</text><text x="445" y="98" text-anchor="middle" fill="#ff6b8a" font-size="11">golul</text><text x="445" y="114" text-anchor="middle" fill="#9aa3c7" font-size="10">context într-un atribut de mesaj</text>
<line x1="517" y1="92" x2="558" y2="92" stroke="#ff6b8a" stroke-width="1.5" marker-end="url(#arrT)"/>
<rect x="560" y="60" width="140" height="64" rx="10" fill="#151b2e" stroke="#ffd166" stroke-width="1.5"/><text x="630" y="80" text-anchor="middle" fill="#f1f3ff" font-weight="700">order-worker</text><text x="630" y="98" text-anchor="middle" fill="#ffd166" font-size="11">GKE · gcp</text><text x="630" y="114" text-anchor="middle" fill="#9aa3c7" font-size="10">context extras, span legat</text>
<line x1="702" y1="92" x2="743" y2="92" stroke="#4fffb0" stroke-width="1.5" marker-end="url(#arrT)"/><text x="722" y="80" text-anchor="middle" fill="#4fffb0" font-size="10">HTTP</text>
<rect x="745" y="60" width="140" height="64" rx="10" fill="#151b2e" stroke="#ffd166" stroke-width="1.5"/><text x="815" y="80" text-anchor="middle" fill="#f1f3ff" font-weight="700">billing</text><text x="815" y="98" text-anchor="middle" fill="#ffd166" font-size="11">AKS · azure</text><text x="815" y="114" text-anchor="middle" fill="#9aa3c7" font-size="10">→ Lambda, context în eveniment</text>
<line x1="80" y1="126" x2="80" y2="190" stroke="#2a3150" stroke-dasharray="3,3"/><line x1="815" y1="126" x2="815" y2="190" stroke="#2a3150" stroke-dasharray="3,3"/>
<rect x="80" y="190" width="735" height="40" rx="8" fill="#151b2e" stroke="#7b8cff" stroke-width="1.5"/>
<text x="447" y="215" text-anchor="middle" fill="#7b8cff" font-weight="700">un singur trace_id · 4bf92f3577b34da6a3ce929d0e0e4736 · fiecare span într-un singur Tempo</text>
<text x="450" y="252" text-anchor="middle" fill="#9aa3c7">Trei clouduri, două protocoale, o coadă. ID-ul supraviețuiește pentru că fiecare gol are un inject explicit pe o parte și un extract pe cealaltă.</text>
</g>
</svg>
</div>

**Cozile.** SQS, Pub/Sub și Service Bus nu poartă header-e HTTP. Producătorul trebuie să pună contextul undeva unde consumatorul îl va găsi, iar consumatorul trebuie să-l scoată și să-și pornească span-ul ca un copil (sau, mai sincer, ca un *link*, pentru că consumatorul rulează mai târziu și posibil într-un batch). Instrumentarea SDK-ului AWS face asta pentru SQS dacă o folosești, prin atribute de mesaj; pentru alți clienți scrii zece linii:

```ts
// producător
const carrier: Record<string, string> = {};
propagation.inject(context.active(), carrier);
await queue.send({ body, attributes: { traceparent: carrier.traceparent } });

// consumator
const parent = propagation.extract(context.active(), { traceparent: msg.attributes.traceparent });
const span = tracer.startSpan('process order', { links: [{ context: trace.getSpanContext(parent)! }] }, parent);
```

**Lambda.** Funcția n-are un proces cu viață lungă în care să trăiască SDK-ul, așa că layer-ul OpenTelemetry pentru Lambda împachetează handler-ul, extrage contextul din eveniment (header-e HTTP pentru API Gateway, atribute de mesaj pentru SQS sau un câmp `traceparent` pe care îl punem noi în payload pentru invocări directe) și golește span-urile înainte ca funcția să înghețe. Golirea e detaliul care contează: fără ea, ultimul span al fiecărei invocări se pierde.

**HTTP între clouduri.** Nimic special. `traceparent` e un header ca oricare altul și load balancer-ele de pe toate cele trei clouduri îl lasă să treacă. Singurul lucru de verificat e ca API gateway-ul sau WAF-ul să nu taie header-ele necunoscute; unul de-al nostru a făcut-o, o săptămână, și fiecare trace se termina la margine.

## Sampling-ul e locul unde trăiește egress-ul

Trace-urile sunt mai mari decât logurile per request: un singur flux de comandă produce patruzeci de span-uri cu atribute. La scară, să le trimiți pe toate peste granițele cloud-urilor ar costa mai mult decât logurile. Răspunsul e în collector și trebuie să fie *tail* sampling, nu head sampling: nu poți decide la începutul unui request dacă va fi interesant.

```yaml
processors:
  tail_sampling:
    decision_wait: 10s
    policies:
      - { name: errors,  type: status_code, status_code: { status_codes: [ERROR] } }
      - { name: slow,    type: latency, latency: { threshold_ms: 2000 } }
      - { name: baseline, type: probabilistic, probabilistic: { sampling_percentage: 10 } }
```

Păstrezi fiecare trace cu eroare, fiecare trace mai lent de două secunde și unul din zece din rest. Asta ne-a tăiat volumul de trace-uri cu cam 85 % și, în șase luni, n-am vrut niciodată un trace care fusese aruncat: cele interesante sunt, prin construcție, cele păstrate.

Există o capcană specifică multi-cloud-ului. Tail sampling-ul are nevoie ca toate span-urile unui trace să ajungă la *aceeași* instanță de collector ca să ia o decizie, iar span-urile noastre sunt produse pe trei clustere. Soluția e un collector pe două niveluri: DaemonSet-ul per nod face doar detecția resurselor și batching-ul și trimite mai departe către un deployment mic de collector central (per cloud, sau unul comun) care face tail sampling-ul, cu un load balancer care rutează după trace ID. Încă zece linii de config și o componentă care există doar pentru că trace-ul traversează o graniță.

## Cum arată query-ul

Trace-ul pentru incidentul de la 02:40, în TraceQL:

```
{ trace:id = "4bf92f3577b34da6a3ce929d0e0e4736" }
```

Fiecare flux de comandă eșuat care a atins clusterul Azure în ultima oră:

```
{ resource.service.name = "billing" && resource.cloud.provider = "azure" && status = error }
```

Comenzi mai lente de două secunde unde span-ul lent a fost pe GCP, adică query-ul care găsește „worker-ul e lent, nu API-ul”:

```
{ resource.service.name = "api" && duration > 2s } >> { resource.cloud.provider = "gcp" && duration > 1500ms }
```

Și din orice linie de log din Loki cu un `trace_id`, un click deschide trace-ul în Tempo, pentru că amândouă sunt în aceeași Grafana și amândouă au primit același ID de la același SDK.

<div class="article-figure">
<svg viewBox="0 0 900 230" width="100%" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Vedere de tip waterfall a trace-ului între clouduri, așa cum îl randează Grafana. Span rădăcină POST /orders pe api, aws, 2,9 secunde. Span copil SQS publish, 12 milisecunde. Span legat process order pe order-worker, gcp, care începe 90 de milisecunde mai târziu, 2,7 secunde. Span-ul lui copil HTTP POST billing pe billing, azure, 2,6 secunde, status error, connection reset. Eroarea e vizibil în span-ul azure și timpul e vizibil petrecut așteptându-l.">
<g font-family="Inter,system-ui,sans-serif" font-size="12">
<text x="20" y="24" fill="#f1f3ff" font-size="14" font-weight="700">Trace 4bf9…4736 · 2,9 s · 4 servicii · 3 clouduri</text>
<text x="20" y="58" fill="#9aa3c7">api · aws</text><rect x="200" y="46" width="660" height="16" rx="3" fill="#7b8cff"/><text x="210" y="58" fill="#0d1120" font-size="11">POST /orders · 2,9 s</text>
<text x="20" y="88" fill="#9aa3c7">api · aws</text><rect x="212" y="76" width="8" height="16" rx="2" fill="#7b8cff"/><text x="226" y="88" fill="#9aa3c7" font-size="11">SQS publish · 12 ms</text>
<text x="20" y="118" fill="#9aa3c7">order-worker · gcp</text><rect x="240" y="106" width="610" height="16" rx="3" fill="#4fffb0"/><text x="250" y="118" fill="#0d1120" font-size="11">process order · 2,7 s · legat din mesajul SQS</text>
<text x="20" y="148" fill="#9aa3c7">billing · azure</text><rect x="262" y="136" width="580" height="16" rx="3" fill="#ff6b8a"/><text x="272" y="148" fill="#0d1120" font-size="11">HTTP POST /charge · 2,6 s · status=error · connection reset by peer</text>
<text x="20" y="178" fill="#9aa3c7">billing-fn · azure</text><rect x="270" y="166" width="20" height="16" rx="2" fill="#9aa3c7"/><text x="296" y="178" fill="#9aa3c7" font-size="11">invocare Lambda · 40 ms · context din eveniment</text>
<line x1="200" y1="196" x2="860" y2="196" stroke="#2a3150"/><text x="200" y="212" fill="#9aa3c7" font-size="10">0 s</text><text x="860" y="212" text-anchor="end" fill="#9aa3c7" font-size="10">2,9 s</text>
<text x="450" y="226" text-anchor="middle" fill="#9aa3c7">Timpul e petrecut pe span-ul azure. Eroarea e pe span-ul azure. Nimeni n-a trebuit să deschidă portalul Azure ca să afle asta.</text>
</g>
</svg>
</div>

## Ce rămâne nativ

X-Ray, Cloud Trace și Application Insights sunt produse bune și fiecare e blocat pe un singur cloud. Nu le folosim pentru trace-urile aplicației din același motiv pentru care nu folosim CloudWatch pentru logurile aplicației. Unde își câștigă locul e în interiorul serviciilor managed pe care nu le instrumentăm noi: o execuție AWS Step Functions sau o integrare API Gateway produce segmente X-Ray gratis și sunt utile pentru depanarea *acelui serviciu*. Nu trebuie să se alăture trace-ului aplicației, iar forțarea lor e mai multă muncă decât merită.

SDK-ul OTel poate propaga formatul de header al X-Ray alături de cel W3C, dacă ai nevoie ca cele două lumi să se întâlnească la o graniță. L-am pornit o dată, pentru un API Gateway care insista, și l-am oprit când gateway-ul s-a mutat.

## Versiunea scurtă

Aceeași configurație de collector, încă un pipeline. Aceleași atribute de resurse, încă un backend. Munca nouă e la goluri: inject la producător, extract la consumator, flush înainte ca o funcție să înghețe, sampling la coadă într-un collector care vede tot trace-ul. Odată făcut asta, „de ce e lentă comanda asta” e un query care întoarce răspunsul cu cloud-ul atașat ca label, iar mutarea unui serviciu între clouduri schimbă valoarea label-ului și nimic altceva.

Dacă trace-urile tale se opresc la o coadă sau la o graniță de cloud, [am mai cusut golul ăsta](/contact).
