트레이스 브로드캐스팅

트레이스 브로드캐스팅

트레이스 브로드캐스팅(trace broadcasting)은 같은 OpenTelemetry 트레이스를 여러 목적지로 한 번에 보내는 방법이에요. 예를 들어, 장기 보관을 위한 자체 데이터 웨어하우스에 보내고, 동시에 LLM 관찰과 온라인 평가용으로 Confident AI에도 보낼 수 있죠.

Confident AI는 표준 OTLP/HTTP를 받아들이기 때문에, OTLP를 만들 수 있는 파이프라인이면 어떤 것이든 모든 트레이스의 사본을 https://otel.confident-ai.com/v1/traces로 보낼 수 있어요. 전용 프로토콜이나 래퍼 SDK는 필요 없어요.

출처: 문서

본문

개요

팀이 트레이스를 브로드캐스팅하는 흔한 이유는 다음과 같아요.

  • 규정 준수 / 데이터 거주(residency) — 네트워크 밖으로 나가기 전에 모든 트레이스 사본을 내부 웨어하우스에 보관
  • 벤더 독립성 — 나중에 옵저버빌리티 벤더를 바꾸거나 추가할 수 있도록 원시 스팬을 자체 인프라에 보관
  • 전문 백엔드 — 서비스 모니터링에는 범용 APM(Datadog, Tempo, Jaeger)을, LLM 전용 평가에는 Confident AI를 사용
  • 샘플링 분리 — 로컬 디버깅용으로는 100% 트레이스를 보관하고, 외부로는 샘플링된 일부만 전송

브로드캐스팅할 수 있는 동등한 방법이 두 가지 있어요. 트레이스를 퍼뜨리는 OpenTelemetry Collector(작은 독립형 바이너리)를 구성하거나, 애플리케이션 안에 exporter를 여러 개 직접 붙이면 돼요. 두 방법 모두 같은 결과를 만들어요.

Confident AI는 OTLP에 대해 gRPC를 지원하지 않고 HTTP만 지원해요. Collector에서는 otlphttp를, SDK에서는 proto-http 패키지의 OTLPSpanExporter를 사용하세요.

아키텍처

Collector 경유

sequenceDiagram
    participant App as Your Application
    participant Collector as OpenTelemetry Collector
    participant Warehouse as Data Warehouse
    participant Confident as Confident AI

    App->>Collector: Export OTLP spans

    par Broadcast
        Collector->>Warehouse: Send via OTLP / Kafka / file exporter
    and
        Collector->>Confident: Send via OTLP/HTTP<br/>(x-confident-api-key)
    end

버퍼링, 재시도, 샘플링, PII 스크러빙이 모두 한 곳에 모이기 때문에 프로덕션에 권장돼요.

SDK 경유

sequenceDiagram
    participant Code as Your Code
    participant Provider as TracerProvider
    participant ProcA as BatchSpanProcessor A
    participant ProcB as BatchSpanProcessor B
    participant Warehouse as Data Warehouse
    participant Confident as Confident AI

    Code->>Provider: span ends

    par Independent export
        Provider->>ProcA: onEnd(span)
        ProcA->>Warehouse: OTLP export
    and
        Provider->>ProcB: onEnd(span)
        ProcB->>Confident: OTLP/HTTP export
    end

더 단순해서 단일 서비스 앱에 좋아요. 각 BatchSpanProcessor가 독립적으로 배칭·재시도하므로, 한 목적지에서 실패해도 다른 쪽은 영향받지 않아요.

설정

스택에 맞는 방식을 골라 쓰면 돼요 — 둘 다 같은 브로드캐스트를 달성합니다.

Collector (YAML)

receivers:
  otlp:
    protocols:
      http:
      grpc:

exporters:
  otlphttp/warehouse:
    endpoint: https://traces.internal.yourcompany.com
    headers:
      authorization: Bearer ${env:...KEY}

  otlphttp/confident:
    endpoint: https://otel.confident-ai.com
    headers:
      x-confident-api-key: ${env:...KEY}

service:
  pipelines:
    traces:
      receivers: [otlp]
      exporters: [otlphttp/warehouse, otlphttp/confident]

이 구성을 실행 중인 OpenTelemetry Collector에 로드하세요 — 배포 옵션은 공식 Collector 문서를 참고하세요. 같은 파이프라인에 exporter를 둘 다 나열하기만 하면 되고, 그러면 모든 스팬이 둘 다로 갑니다.

Python

from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter

provider = TracerProvider()

provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter(
    endpoint=f"{WAREHOUSE_ENDPOINT}/v1/traces",
    headers={"authorization": f"Bearer {WAREHOUSE_API_KEY}"},
)))

provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter(
    endpoint="https://otel.confident-ai.com/v1/traces",
    headers={"x-confident-api-key": CONFIDENT_API_KEY},
)))

trace.set_tracer_provider(provider)

TypeScript

import { trace } from "@opentelemetry/api";
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";

const provider = new NodeTracerProvider({
  spanProcessors: [
    new BatchSpanProcessor(new OTLPTraceExporter({
      url: `${process.env.WAREHOUSE_ENDPOINT}/v1/traces`,
      headers: { authorization: `Bearer ${process.env.WAREHOUSE_API_KEY}` },
    })),
    new BatchSpanProcessor(new OTLPTraceExporter({
      url: "https://otel.confident-ai.com/v1/traces",
      headers: { "x-confident-api-key": process.env.CONFIDENT_API_KEY ?? "" },
    })),
  ],
});

trace.setGlobalTracerProvider(provider);

Go

warehouseExp, _ := otlptracehttp.New(ctx,
    otlptracehttp.WithEndpoint(os.Getenv("WAREHOUSE_ENDPOINT")),
    otlptracehttp.WithHeaders(map[string]string{
        "authorization": "Bearer " + os.Getenv("WAREHOUSE_API_KEY"),
    }),
)

confidentExp, _ := otlptracehttp.New(ctx,
    otlptracehttp.WithEndpoint("otel.confident-ai.com"),
    otlptracehttp.WithHeaders(map[string]string{
        "x-confident-api-key": os.Getenv("CONFIDENT_API_KEY"),
    }),
)

tp := sdktrace.NewTracerProvider(
    sdktrace.WithBatcher(warehouseExp),
    sdktrace.WithBatcher(confidentExp),
)
otel.SetTracerProvider(tp)

Java

OtlpHttpSpanExporter warehouse = OtlpHttpSpanExporter.builder()
    .setEndpoint(System.getenv("WAREHOUSE_ENDPOINT") + "/v1/traces")
    .addHeader("authorization", "Bearer " + System.getenv("WAREHOUSE_API_KEY"))
    .build();

OtlpHttpSpanExporter confident = OtlpHttpSpanExporter.builder()
    .setEndpoint("https://otel.confident-ai.com/v1/traces")
    .addHeader("x-confident-api-key", System.getenv("CONFIDENT_API_KEY"))
    .build();

SdkTracerProvider provider = SdkTracerProvider.builder()
    .addSpanProcessor(BatchSpanProcessor.builder(warehouse).build())
    .addSpanProcessor(BatchSpanProcessor.builder(confident).build())
    .build();

OpenTelemetrySdk.builder()
    .setTracerProvider(provider)
    .buildAndRegisterGlobal();

Ruby

require 'opentelemetry/sdk'
require 'opentelemetry/exporter/otlp'

OpenTelemetry::SDK.configure do |c|
  c.add_span_processor(
    OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(
      OpenTelemetry::Exporter::OTLP::Exporter.new(
        endpoint: "#{ENV['WAREHOUSE_ENDPOINT']}/v1/traces",
        headers: { 'authorization' => "Bearer #{ENV['WAREHOUSE_API_KEY']}" },
      )
    )
  )

  c.add_span_processor(
    OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(
      OpenTelemetry::Exporter::OTLP::Exporter.new(
        endpoint: 'https://otel.confident-ai.com/v1/traces',
        headers: { 'x-confident-api-key' => ENV['CONFIDENT_API_KEY'] },
      )
    )
  )
end

C#

using var provider = Sdk.CreateTracerProviderBuilder()
    .AddSource("my-llm-app")
    .AddOtlpExporter(o => {
        o.Endpoint = new Uri($"{Environment.GetEnvironmentVariable("WAREHOUSE_ENDPOINT")}/v1/traces");
        o.Headers  = $"authorization=Bearer {Environment.GetEnvironmentVariable("WAREHOUSE_API_KEY")}";
        o.Protocol = OtlpExportProtocol.HttpProtobuf;
    })
    .AddOtlpExporter(o => {
        o.Endpoint = new Uri("https://otel.confident-ai.com/v1/traces");
        o.Headers  = $"x-confident-api-key={Environment.GetEnvironmentVariable("CONFIDENT_API_KEY")}";
        o.Protocol = OtlpExportProtocol.HttpProtobuf;
    })
    .Build();

이후에는 평소처럼 스팬을 만들면 돼요 — 모든 스팬이 두 목적지로 흐릅니다.

고급 Collector 기능

이 기능들은 Collector 경로에서만 쓸 수 있어요. 애플리케이션 코드를 건드리지 않고 브로드캐스트 동작을 바꿀 수 있게 해 줍니다.

선택적 브로드캐스트

웨어하우스에는 100%를 보관하면서 LLM 태그가 붙은 스팬만 Confident AI로 보내려면 routing 커넥터를 사용하세요.

connectors:
  routing:
    default_pipelines: [traces/warehouse]
    table:
      - context: span
        statement: route() where attributes["confident.span.type"] != nil
        pipelines: [traces/warehouse, traces/confident]

샘플링

로컬에는 100%를 보관하되 Confident AI에는 10%만(플러스 모든 오류) 보내려면:

processors:
  tail_sampling/confident:
    decision_wait: 10s
    policies:
      - name: errors
        type: status_code
        status_code: { status_codes: [ERROR] }
      - name: random
        type: probabilistic
        probabilistic: { sampling_percentage: 10 }

이걸 Confident AI 파이프라인에만 적용하고, 웨어하우스 파이프라인은 샘플링 없이 두세요.

PII 스크러빙

네트워크 밖으로 나가기 전에 민감 필드를 해시하거나 제거합니다.

processors:
  attributes/redact:
    actions:
      - { key: user.email, action: hash }
      - { key: http.request.header.authorization, action: delete }

그런 다음 파이프라인의 processors 목록에 attributes/redact를 추가하세요.

분산 추적과 결합하기

이미 여러 서비스에 걸쳐 분산 추적을 쓰고 있다면, 모든 서비스가 공유 Collector를 가리키게 하고 그 Collector가 브로드캐스트를 처리하게 하세요.

sequenceDiagram
    participant ServiceA as Service A
    participant ServiceB as Service B
    participant ServiceC as Service C
    participant Collector as OpenTelemetry Collector
    participant Warehouse as Data Warehouse
    participant Confident as Confident AI

    ServiceA->>ServiceB: Request + traceparent
    ServiceB->>ServiceC: Request + traceparent

    par
        ServiceA->>Collector: OTLP spans
    and
        ServiceB->>Collector: OTLP spans
    and
        ServiceC->>Collector: OTLP spans
    end

    par
        Collector->>Warehouse: All spans
    and
        Collector->>Confident: All spans
    end

traceparent가 종단 간 전파되기 때문에, 모든 목적지가 완전하고 통합된 트레이스를 받아요.

모든 서비스는 동일한 CONFIDENT_API_KEY 를 사용해야 합니다. 키가 다르면 서로 다른 프로젝트로 라우팅되어 트레이스 통합이 깨져요.

모범 사례

Confident AI 속성 설정

브로드캐스팅은 스팬이 어디로 가는지만 바꾸지, 스팬이 무엇을 담는지는 바꾸지 않아요. 스팬은 여전히 confident.* 속성(예: confident.span.type, confident.span.input, confident.llm.model)을 담고 있어야 Observatory에서 제대로 렌더링됩니다. 스팬 수준 속성 매핑을 참고하세요.

진입 스팬에 confident.trace.user.id와 confident.trace.customer.id를 설정해 활동을 최종 사용자와 그 계정에 귀속시키세요. 아이덴티티 시맨틱은 사용자 및 고객 가이드를 참고하세요.

프로덕션에서는 Collector를 선호

서비스가 둘 이상이 되면 Collector를 권장해요.

  • 단일 버퍼가 스파이크를 흡수해서, 모든 앱이 자체 큐를 들고 있을 필요가 없어요.
  • 어느 목적지로의 네트워크 문제도 Collector에만 영향 — 앱은 쾌적하게 유지돼요.
  • 앱 코드를 재배포하지 않고도 목적지, 샘플링, PII 규칙을 바꿀 수 있어요.

HTTP를 사용하고 gRPC는 피하기

Confident AI의 OTLP 엔드포인트는 HTTP만 받아들여요. Collector에서는 otlphttp를, SDK에서는 opentelemetry-exporter-otlp-proto-http의 OTLPSpanExporter를 사용하세요.

파이프라인별 환경 설정

OTEL_RESOURCE_ATTRIBUTES를 사용해 트레이스가 어느 Confident AI 환경에 들어갈지 제어하세요.

OTEL_RESOURCE_ATTRIBUTES="confident.trace.environment=production"

목적지별로 서로 다른 환경이 필요하면 서로 다른 리소스 프로세서를 가진 Collector 파이프라인 두 개를 돌리세요.

디버그 싱크는 분리해서

트레이스가 이상해 보이면 exporter를 한 번에 하나씩 꺼서, 문제가 상류인지 특정 목적지 때문인지 확인하세요.

더 알아보기