OpenTelemetry: gemeinsamer Standard für Trace, Logs und Metrics

1. Überblick

Wenn das System in viele Services aufgeteilt ist, kann ein Request durch fünf, zehn verschiedene Prozesse gehen. Im Fehlerfall musst du wissen, wo der Request hingegangen ist, wo er sich verlangsamt hat und welche Logs zu diesem Request gehören. OpenTelemetry (OTel) ist ein offener Standard, der genau dieses Problem löst. Es vereinheitlicht die Art und Weise, wie Traces, Metrics und Logs gesammelt werden, und sendet sie dann an ein beliebiges Backend.

Nach dem Lesen dieses Artikels wirst du die Kernkonzepte von OTel verstehen und warum es so viel Aufmerksamkeit bekommt. Du wirst auch wissen, wie du OTel in eine Spring-Boot-3-Anwendung integrierst, den Collector laufen lässt, Tests für die Instrumentation schreibst und erkennst, wann du es nicht verwenden solltest.

2. OpenTelemetry ist was?

OpenTelemetry ist ein Open-Source-Projekt der CNCF. Es definiert wie man Telemetrie-Daten erzeugt, beschreibt und übermittelt. OTel ist nicht der Ort, an dem Daten gespeichert werden oder die Ansichtsschnittstelle. Das kümmern sich um Backends wie Jaeger, Prometheus, Grafana Tempo/Loki oder kommerzielle Dienste.

2.1. Ba loại tín hiệu (signals)

  • Trace: die Reise eines Requests. Jeder Trace besteht aus mehreren Spans, jeder Span ist eine Arbeitseinheit, z. B. „PaymentService aufrufen” oder „orders-Tabelle abfragen”. Ein Span hat eine Startzeit, eine Endzeit und Attribute.
  • Metric: aggregierte Metriken über die Zeit, wie Requests pro Sekunde, p99-Latenz oder fehlgeschlagene Bestellungen.
  • Log: diskrete Ereigniseinträge. OTel fügt traceId und spanId zu Logs hinzu, damit kannst du von einer Logzeile zum entsprechenden Trace springen.

Der größte Wert liegt in der Korrelation. Alle drei Signale verwenden einen gemeinsamen Kontext und eine gemeinsame Ressourcenbeschreibung, z. B. service.name.

2.2. Các thành phần chính

Komponente Rolle
API Schnittstelle für Code zum Erstellen von Spans und Metrics, herstellerunabhängig
SDK Implementierung der API: Sampling, Batching, Export
OTLP Protokoll zur Datenübertragung (gRPC oder HTTP/Protobuf)
Collector Eigenständiger Prozess, der Telemetrie empfängt, verarbeitet und weiterleitet
Semantic conventions Benennungskonventionen für Attribute wie http.request.method, db.system…
Instrumentation Bibliotheken oder Agenten, die automatisch Telemetrie für populäre Frameworks erzeugen

2.3. Context propagation

Damit Spans mehrerer Services sich zu einem Trace zusammensetzen, muss der Kontext dem Request folgen. Bei HTTP verwendet OTel standardmäßig den W3C Trace Context Standard über den Header traceparent:

traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01

Dieser Header hat vier Teile: Version, traceId, spanId des übergeordneten Spans und ein Sampling-Flag. Der empfangende Service liest diesen Header und erstellt einen untergeordneten Span, der zum gleichen Trace gehört.

3. Vì sao OpenTelemetry đang được quan tâm?

  • Herstellerunabhängig: Instrumentiere deinen Code einmal. Wenn du das Backend wechseln möchtest, änderst du nur die Exporter- oder Collector-Konfiguration, nicht deinen Code.
  • Vereinigt ältere Standards: OTel entstand durch die Zusammenführung von OpenTracing und OpenCensus. Die Community muss sich nicht mehr zwischen zwei Richtungen entscheiden.
  • Ein Standard für alle drei Signale: Früher hatte jeder Datentyp normalerweise seinen eigenen Agent und sein eigenes Format.
  • Großes Ökosystem: Viele Frameworks, Cloud Provider und Observability-Tools unterstützen OTLP. Spring Boot 3 integriert es auch über Micrometer.
  • Flexibler Collector: Du kannst sensible Daten filtern, Sampling durchführen und Daten auf Infrastrukturebene an mehrere Backends weiterleiten, ohne die Anwendung zu berühren.

4. Áp dụng OpenTelemetry vào Spring Boot 3

Spring Boot 3 nutzt Micrometer Observation als Abstraktionsschicht. Du schreibst eine Observation, Micrometer erzeugt sowohl eine Metrik (Timer) als auch einen Span. Eine Brücke zu OTel kümmert sich um den Export.

4.1. Abhängigkeit

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <!-- Brücke von Micrometer Tracing zu OpenTelemetry -->
    <dependency>
        <groupId>io.micrometer</groupId>
        <artifactId>micrometer-tracing-bridge-otel</artifactId>
    </dependency>
    <!-- Export von Traces über OTLP -->
    <dependency>
        <groupId>io.opentelemetry</groupId>
        <artifactId>opentelemetry-exporter-otlp</artifactId>
    </dependency>
    <!-- Export von Metrics über OTLP -->
    <dependency>
        <groupId>io.micrometer</groupId>
        <artifactId>micrometer-registry-otlp</artifactId>
    </dependency>
    <!-- Für Tests -->
    <dependency>
        <groupId>io.micrometer</groupId>
        <artifactId>micrometer-observation-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

Du musst keine Versionen angeben. Spring Boot verwaltet die Versionen der Micrometer- und OpenTelemetry-Bibliotheken über eine BOM.

4.2. Cấu hình

spring:
  application:
    name: order-service

management:
  tracing:
    sampling:
      probability: 1.0   # dev: 100% Sampling; production sollte reduziert werden
  otlp:
    tracing:
      endpoint: http://localhost:4318/v1/traces
    metrics:
      export:
        url: http://localhost:4318/v1/metrics
        step: 30s

logging:
  pattern:
    level: "%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]"

Die Bedeutung jeder Konfigurationsgruppe:

  • spring.application.name wird als service.name verwendet. Dies ist das wichtigste Attribut für Backends, um Services zu unterscheiden.
  • management.tracing.sampling.probability bestimmt, welcher Anteil der Traces aufgezeichnet wird.
  • Die beiden otlp Endpunkte verweisen auf den Collector über OTLP/HTTP, der Standard-Port ist 4318.
  • logging.pattern.level fügt traceId und spanId aus MDC in jede Logzeile ein. Dies ist der einfachste Weg, um Logs mit Traces zu korrelieren.

4.3. Instrument nghiệp vụ bằng Observation API

HTTP-Anfragen an Controller und Aufrufe über RestClient/WebClient, die von Spring erstellt werden, werden automatisch instrumentiert. Was du selbst hinzufügen musst, sind die wichtigen Geschäftsschritte:

public record Order(String id, String paymentMethod, long amount) {}
@FunctionalInterface
public interface PaymentService {
    String charge(Order order);
}
@Service
public class CheckoutService {

    private static final Logger log = LoggerFactory.getLogger(CheckoutService.class);

    private final ObservationRegistry registry;
    private final PaymentService paymentService;

    public CheckoutService(ObservationRegistry registry, PaymentService paymentService) {
        this.registry = registry;
        this.paymentService = paymentService;
    }

    public String checkout(Order order) {
        return Observation.createNotStarted("order.checkout", registry)
                .lowCardinalityKeyValue("payment.method", order.paymentMethod())
                .highCardinalityKeyValue("order.id", order.id())
                .observe(() -> {
                    log.info("Processing order {}", order.id());
                    return paymentService.charge(order);
                });
    }
}

Punkte, die im obigen Code zu beachten sind:

  • Observation.createNotStarted("order.checkout", registry) erstellt eine Observation namens order.checkout.
  • lowCardinalityKeyValue fügt ein Attribut mit wenigen Werten hinzu (CARD, CASH, WALLET). Dieses Attribut erscheint sowohl in Metriken als auch in Spans.
  • highCardinalityKeyValue fügt ein Attribut mit vielen möglichen Werten hinzu, wie eine Bestellungs-ID. Es wird nur an den Span angehängt, nicht an die Metrik.
  • observe(...) öffnet den Span, misst die Zeit, zeichnet Ausnahmen auf und schließt den Span, wenn der Supplier zurückkommt.

Beim Ausführen sieht das Log wie folgt aus:

 INFO [order-service,4bf92f3577b34da6a3ce929d0e0e4736,a1b2c3d4e5f60718] ... Processing order A-1001

Das Backend erhält zwei Dinge:

  • Einen order.checkout Span, der unter dem HTTP-Span der Anfrage verschachtelt ist.
  • Eine order.checkout Timer-Metrik, aufgeteilt nach payment.method. Aus dieser Metrik kannst du die Anzahl der Aufrufe, die Gesamtzeit und die maximale Zeit berechnen.

Hinweis: Bevorzuge die Observation API gegenüber direkten Aufrufen von io.opentelemetry.api.trace.Tracer in Spring Code. Dein Code wird nicht von einer bestimmten Tracing-Bibliothek abhängig sein. Eine Messung erzeugt sowohl eine Metrik als auch einen Trace.

4.4. Phương án thay thế: Java agent

Wenn du deinen Code nicht ändern oder Abhängigkeiten hinzufügen möchtest, kannst du den OpenTelemetry Java Agent verwenden. Der Agent instrumentiert automatisch viele populäre Bibliotheken wie JDBC, HTTP-Client und Kafka:

export OTEL_SERVICE_NAME=order-service
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
export OTEL_TRACES_SAMPLER=parentbased_traceidratio
export OTEL_TRACES_SAMPLER_ARG=0.1

java -javaagent:./opentelemetry-javaagent.jar -jar order-service.jar

Der Agent eignet sich für ältere Anwendungen oder wenn das Operations-Team die Instrumentation zentral kontrollieren möchte. Der Nachteil ist, dass du weniger detaillierte Kontrolle hast, die Startzeit länger wird und der Agent wie eine „Black Box” agiert, die bytecode manipuliert.

Hinweis: Führe nicht gleichzeitig den Java Agent und die Micrometer Bridge für denselben Signal-Typ aus. Du wirst duplizierte Spans oder verschobene Kontexte erhalten.

5. Chạy OpenTelemetry Collector

Anwendungen sollten Daten an den Collector senden, anstatt sie direkt an das Backend zu senden. Der Collector ist der Ort, wo du Daten batching, filterst und routest. Hier ist eine minimale Konfiguration:

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

processors:
  batch: {}
  attributes/strip-pii:
    actions:
      - key: customer.email
        action: delete

exporters:
  debug:
    verbosity: detailed

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [attributes/strip-pii, batch]
      exporters: [debug]
    metrics:
      receivers: [otlp]
      processors: [batch]
      exporters: [debug]
    logs:
      receivers: [otlp]
      processors: [batch]
      exporters: [debug]

Diese Konfiguration besteht aus vier Teilen:

  • receivers.otlp empfängt Daten über gRPC (4317) und HTTP (4318).
  • attributes/strip-pii löscht das Attribut customer.email, bevor Daten deine Infrastruktur verlassen.
  • batch fasst Daten zusammen, bevor sie gesendet werden, um die Anzahl der Anfragen zu reduzieren.
  • Der debug Exporter gibt Daten auf der Konsole aus, damit du sie überprüfen kannst. In einer produktiven Umgebung ersetzt du ihn durch den Exporter des Backends, das du verwendest. Ältere Collector-Versionen verwenden den Namen logging für diesen Exporter.

Starte den Collector mit Docker:

docker run --rm -p 4317:4317 -p 4318:4318 \
  -v "$(pwd)/otel-collector.yaml:/etc/otelcol-contrib/config.yaml" \
  otel/opentelemetry-collector-contrib

Rufe die Checkout-API auf und schau dann in die Collector-Konsole. Du wirst einen ähnlichen Span sehen:

Span #0
    Trace ID       : 4bf92f3577b34da6a3ce929d0e0e4736
    Parent ID      : 9a8b7c6d5e4f3a2b
    Name           : order.checkout
    Attributes:
         -> payment.method: Str(CARD)
         -> order.id: Str(A-1001)

6. Test phần instrumentation

Instrumentation ist auch Code und muss daher getestet werden. TestObservationRegistry ermöglicht es dir, Observations zu prüfen, ohne einen Spring-Kontext oder Collector zu starten:

import io.micrometer.observation.tck.TestObservationRegistry;
import io.micrometer.observation.tck.TestObservationRegistryAssert;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class CheckoutServiceTest {

    @Test
    void whenCheckout_thenObservationIsRecordedWithPaymentMethod() {
        TestObservationRegistry registry = TestObservationRegistry.create();
        PaymentService paymentService = order -> "PAID-" + order.id();
        CheckoutService service = new CheckoutService(registry, paymentService);

        String result = service.checkout(new Order("A-1001", "CARD", 250_000));

        assertThat(result).isEqualTo("PAID-A-1001");
        TestObservationRegistryAssert.assertThat(registry)
                .hasObservationWithNameEqualTo("order.checkout")
                .that()
                .hasLowCardinalityKeyValue("payment.method", "CARD")
                .hasHighCardinalityKeyValue("order.id", "A-1001")
                .hasBeenStarted()
                .hasBeenStopped();
    }
}

Dieser Test bestätigt drei Dinge:

  • Das Geschäftsergebnis ist korrekt.
  • Die Observation order.checkout wurde erstellt, mit den richtigen Attributen.
  • Die Observation wurde geöffnet und geschlossen. Ein vergessenes Schließen eines Spans ist ein sehr schwer zu erkennender Fehler in der Produktion.

7. Lỗi hay gặp

  • Kardinalitätsexplosion: Wenn du userId, orderId oder URLs mit Parametern in Metric-Tags einfügst, explodiert die Anzahl der Metrik-Serien und die Speicherkosten steigen. Diese Werte sollten nur an Spans angehängt werden.
  • 100% Sampling in der Produktion: Datenmenge und Kosten wachsen linear mit dem Traffic. Reduziere probability, oder verwende Tail Sampling im Collector, um Fehler-Traces oder langsame Traces zu behalten.
  • Kontextverlust bei asynchroner Ausführung: Bei @Async, CompletableFuture oder selbst erstellten Thread Pools folgt der Kontext nicht automatisch zum neuen Thread. Du musst den Executor mit einem Context-Propagations-Mechanismus wrappen, z. B. ContextExecutorService aus der context-propagation Bibliothek, oder einen von Spring verwalteten, vorkonfigurierten Executor verwenden.
  • Lecks von sensiblen Daten: E-Mails, Kartennummern oder Token können in Attribute oder Logs eindringen. Filtere sie direkt in deinem Code und füge eine zusätzliche Filterschicht im Collector hinzu.
  • Willkürliche Benennung: Wenn man sich nicht an Semantic Conventions hält, sind Dashboards und Alerts schwer zu realisieren.
  • Collector überspringen: Wenn du direkt vom Application zum Backend sendest, musst du alle Services neu deployen, jedes Mal wenn du das Backend wechselst.

8. Khi nào KHÔNG nên dùng (hoặc chưa cần)

  • Kleine Anwendung, eine Instanz, wenige Abhängigkeiten: Strukturierte Logs zusammen mit ein paar Metrics von Actuator reichen normalerweise aus. Distributed Tracing bringt fast keinen Wert, wenn es nichts Verteiltes gibt.
  • Das Team hat kein Backend und keinen Operator: OTel ist nur eine Pipeline. Wenn niemand die Daten anschaut, zahlst du nur zusätzliche CPU-, Netzwerk- und Speicherkosten.
  • System ist extrem latenzempfindlich oder ressourcenbeschränkt: z. B. eingebettete Anwendungen oder Hot Paths im Mikrosekundenbereich. Miss den Overhead, bevor du Instrumentation breit aktivierst.
  • Erwartung, dass OTel alle bestehenden Tools auf einmal ersetzt: Der Reifegrad jedes Signals und jeder Sprache kann unterschiedlich sein. Prüfe den Support-Status dessen, was du verwenden möchtest, und migriere schrittweise, beginne mit Tracing.

9. Kết luận

OpenTelemetry vereinheitlicht die Art und Weise, wie Traces, Metrics und Logs über eine gemeinsame API, das OTLP-Protokoll und den Collector erzeugt und übertragen werden. Dadurch kannst du die Instrumentation in deinem Code von der Backend-Auswahl trennen.

Bei Spring Boot 3 besteht der einfachste Weg aus vier Schritten. Erstens: Verwende Micrometer Observation, damit eine Messung sowohl eine Metrik als auch einen Span erzeugt. Zweitens: Exportiere über OTLP zum Collector. Drittens: Füge traceId in Logs ein. Viertens: Schreibe Tests für wichtige Observations. Parallel dazu: Kontrolliere Kardinalität, Sampling und sensible Daten von Anfang an.

Wenn dein System noch klein ist, brauchst du dich nicht zu beeilen. Wenn die Anzahl der Services wächst und Debugging stundenlang dauert, ist OTel eine Plattform, die es wert ist, Geld darin zu investieren.

opentelemetryobservabilityspring-boottracingtech-trends