Der Bericht klingt immer gleich: „Die App ist manchmal langsam." Nicht bei jeder Anfrage, nicht für jeden Benutzer und nicht reproduzierbar auf Anfrage – was bedeutet, dass print-Anweisungen und ein an die lokale Maschine angeschlossener Debugger nutzlos sind, weil die langsame Anfrage bereits auf einem Produktions-Replikas passiert ist, das du nicht beobachtet hast, und wenn du schaust, ist der Moment längst vorbei. Logs helfen ein bisschen, wenn die Anfrage zufällig einen Codepfad berührt hat, der etwas loggt, aber Logs von einem FastAPI-Handler, einem Celery-Worker und einem Datenbankverbindungspool sind drei separate Streams ohne gemeinsamen Faden, der sie zurück zur einen Anfrage verbindet, die langsam war.
Das ist das eigentliche Problem, das Distributed Tracing löst, und es ist enger und konkreter, als „Observability" als Buzzword vermuten lässt. Ein Trace ist die kausale Zeitlinie einer Anfrage, zusammengenäht über jeden Service und jeden Hop, den sie berührt hat, mit echten Dauern an jedem Schritt. OpenTelemetry ist der herstellerneutrale Standard zur Erzeugung dieser Zeitlinie – einmal instrumentieren, überall exportieren (Jaeger, Tempo, Honeycomb, Datadog) – und das, was echte Sorgfalt beim Engineering erfordert, ist nicht die Installation der Bibliothek, sondern sicherzustellen, dass der Trace-Kontext jede Grenze überlebt, die eine Anfrage überschreitet, besonders die zwischen einem synchronen API-Handler und einem asynchronen Hintergrundtask.
Du wirst lernen:
- Den konkreten Unterschied zwischen einer Log-Zeile, einer Metrik und einem Trace Span – und warum man alle drei braucht, nicht nur eines
- Wie OpenTelemetrys Trace-, Span- und Kontext-Modell tatsächlich zusammenpasst
- Wie man eine FastAPI-App auto-instrumentiert und Spans für jede Anfrage mit ein paar Zeilen Setup erhält
- Warum Trace-Kontext standardmäßig an einer Celery-Task-Grenze abbricht und wie man ihn manuell propagiert, damit der Span eines Workers korrekt unter seiner ursprünglichen Anfrage verschachtelt wird
- Wie man strukturiertes Logging mit Trace-IDs koppelt, damit jede Log-Zeile einen Klick vom vollständigen Anfrage-Timeline entfernt ist
- Wie Sampling tatsächlich funktioniert und warum „alles tracing" eine Entscheidung ist, die man bei Scale bereuen wird
Inhaltsverzeichnis
- Die Grundlagen
- Die vollständige Architektur
- Erläuterung der Core-Layer
- Durchgehende Erklärung
- Sonderfälle
- Skalierung & Produktionsherausforderungen
- Code-Beispiele
- Häufige Fallstricke
- Best Practices für die Produktion
Die Grundlagen
Logs, Metriken und Traces sind verschiedene Werkzeuge
Ein Log ist ein diskretes Ereignis mit einer Nachricht und etwas Kontext, nützlich für „was ist an einem einzelnen Zeitpunkt passiert." Eine Metrik ist eine aggregierte Zahl über die Zeit – Request-Count, p99-Latenz, Fehlerrate – nützlich für „ist das System gerade gesund" auf einen Blick, aber sie wirft pro-Request-Details weg. Ein Trace ist keines davon – es ist die zusammenhängende Timeline einer bestimmten Anfrage, wie sie sich durch jeden Service, jede Funktion und jeden I/O-Aufruf bewegt, den sie berührt, mit einer Dauer für jeden Schritt. Metriken sagen dir, dass etwas langsam ist, Traces sagen dir wo, und strukturierte Logs, die mit demselben Trace korreliert sind, sagen dir warum am spezifischen Schritt, der langsam war.
Warum das speziell für Python-APIs wichtig ist
- Async- und Hintergrundarbeit fragmentieren die Timeline standardmäßig. Ein FastAPI-Handler, der einen Celery-Task startet oder einen nachgelagerten HTTP-Aufruf awaitet, teilt eine logische Anfrage in mehrere physische Ausführungskontexte – ohne explizite Propagierung verbindet nichts sie zurück zusammen.
- „Manchmal langsam" ist per Konstruktion nicht reproduzierbar. Man kann nachträglich keinen Debugger an ein Produktions-Replikas anhängen. Ein Trace, der zum Zeitpunkt aufgezeichnet wurde, ist das einzige Artefakt, das noch existiert, sobald die Anfrage vorbei ist.
- Mehrere Services bedeuten mehrere Log-Streams. Mehrere Log-Dateien nach einem passenden Zeitstempel zu durchsuchen ist eine schlechte Möglichkeit, den Pfad einer Anfrage zu rekonstruieren – eine gemeinsame Trace-ID ist es nicht.
Die vollständige Architektur
client
│ traceparent: 00-<trace_id>-<span_id>-01
▼
┌─────────────┐ span: http.request ┌───────────────┐
│ FastAPI │──────────────────────────►│ span: db.query │
│ (root span) │ └───────────────┘
└──────┬───────┘
│ task.delay() — trace context injected into task headers
▼
┌─────────────┐
│ Celery worker │ span: celery.task (child of the FastAPI root span)
└─────────────┘
│
▼
OTLP exporter → Collector → Jaeger / Tempo / vendor backend
Das Leitprinzip: Ein Trace ist ein Baum von Spans, jeder Span hat genau einen Elternteil (außer dem Wurzel-Span), und was den Baum über Prozessgrenzen hinweg verbindet, ist ein kleines Stück Kontext – eine Trace-ID und die aktuelle Span-ID – das explizit über jede Grenze getragen wird, die kein einfacher In-Process-Aufruf ist. HTTP trägt ihn im traceparent-Header (der W3C Trace Context Standard); eine Message-Queue wie Celery hat keinen solchen eingebauten Header, also fügt dein Code ihn manuell ein und extrahiert ihn.
Erläuterung der Core-Layer
1. Traces, Spans und Kontext
Was es ist: Ein Span repräsentiert eine Arbeitseinheit – einen HTTP-Request, eine Datenbankabfrage, einen Funktionsaufruf – mit einem Namen, Start- und Endzeit, Attributen (Key-Value-Metadaten) und einer Referenz zum Eltern-Span. Ein Trace ist der vollständige Satz von Spans, die eine Trace-ID teilen. Kontext ist das kleine Bündel (Trace-ID + aktuelle Span-ID + Trace-Flags), das propagiert wird, damit ein neuer Span weiß, zu welchem Trace und Elternteil er gehört.
Warum es wichtig ist: Nichts über „zu welchem Trace gehört dieser Span" ist automatisch über eine Prozess- oder Thread-Grenze hinweg – es wird explizit getragen, entweder In-Process (via Pythons contextvars) oder über das Netz (der traceparent HTTP-Header, oder manuell injizierte Header für alles, das kein HTTP ist).
Produktionstipp: Versuche nicht, das von Grund auf zu bauen – das Python-SDK von OpenTelemetry handhabt contextvars-Propagierung automatisch für alles, was seine Instrumentierung verwendet, einschließlich durch async/await. Der Ort, an dem man explizit darüber nachdenken muss, ist jede Grenze, die das SDK noch nicht kennt, was genau der Celery-Fall unten ist.
2. FastAPI auto-instrumentieren
Was es ist: opentelemetry-instrumentation-fastapi verpackt die Request-Verarbeitung deiner App, sodass jede eingehende Anfrage automatisch einen Root-Span mit Attributen für HTTP-Methode, Route und Statuscode bekommt – keine manuelle Span-Erstellung für den Basisfall erforderlich.
Warum es wichtig ist: Das gibt dir echtes Request-Level-Tracing kostenlos – jede Anfrage zeigt ihre tatsächliche Dauer und Route, was oft allein schon ausreicht, um zu erkennen, dass /reports/export der langsame Endpunkt ist, bevor man einen einzigen manuellen Span geschrieben hat.
Produktionstipp: Auto-Instrumentierung deckt die Request/Response-Grenze ab. Sie weiß nicht automatisch, dass „Schritt 3 in diesem Handler, der eine Drittanbieter-API aufruft" seinen eigenen Span wert ist – dafür verpacke den spezifisch langsam aussehenden Code in einen manuellen Span, sodass er als sein eigenes benanntes Segment im Trace erscheint statt in die Gesamtdauer des Elternteils einzufließen.
3. Strukturiertes Logging korreliert mit Trace-IDs
Was es ist: Strukturiertes Logging bedeutet, Logs als Key-Value-Daten (typischerweise JSON) statt als Freitext-Strings auszugeben – logger.info("order created", order_id=123) statt eines interpolierten Satzes. Das Korrelieren mit Tracing bedeutet, die aktive Trace-ID und Span-ID automatisch in jeden Log-Datensatz einzufügen.
Warum es wichtig ist: Ein strukturierter Log mit einer angehängten Trace-ID ermöglicht es dir, von „diese Log-Zeile sieht verdächtig aus" zu „zeig mir die gesamte Anfrage, zu der das gehört" in einem Klick zu gelangen, in jedem Backend, das Logs mit Traces verknüpft (Grafana mit Loki+Tempo, Datadog, Honeycomb). Ohne das ist diese Log-Zeile ein isolierter Punkt ohne Pfad zurück zu ihrem Kontext.
Produktionstipp: Verwende structlog oder python-json-logger mit einem Prozessor, der die Trace-ID und Span-ID des aktuellen Spans bei jedem Log-Aufruf aus dem Kontext von OpenTelemetry zieht – das sind ein paar Zeilen Setup, die sich beim ersten Mal bezahlen, wenn du etwas debuggst, das drei Services umfasst.
4. Spans über eine Celery-Task-Grenze propagieren
Was es ist: Celery-Tasks laufen in einem vollständig separaten Prozess (dem Worker) und empfangen nur die Daten, die in die Task-Nachricht serialisiert wurden – standardmäßig nur die Funktionsargumente, ohne anhängenden Trace-Kontext. Der Span eines Tasks, wenn du einen erstellst, startet als ein brandneuer, nicht verbundener Root-Trace standardmäßig, ohne Link zurück zur Anfrage, die ihn in die Warteschlange gestellt hat.
Warum es wichtig ist: Das ist die einzige häufigste Lücke im Tracing-Setup eines Python-Services – die API-Seite sieht vollständig instrumentiert aus, die Worker-Seite sieht vollständig instrumentiert aus, aber die Trace-UI zeigt zwei nicht zusammenhängende Traces statt eines verbundenen Timelines, weil nichts den Kontext über die Queue trug.
Produktionstipp: Injiziere den aktuellen Trace-Kontext in die Header des Tasks, wenn du .delay()/.apply_async() aufrufst, und extrahiere ihn am Anfang des Tasks, bevor du seinen Span startest – opentelemetry-instrumentation-celery handhabt einen guten Teil davon automatisch via Celery’s Signal-Hooks, aber es lohnt sich, die manuelle Injection/Extraktion unten zu verstehen, da du dasselbe Muster für jede andere Nicht-HTTP-Grenze (Kafka, SQS, eine rohe Redis-Queue) ohne eine Bibliothek, die das für dich erledigt, brauchen wirst. Wie gut dieses Propagierungsmuster passt, hängt auch davon ab, welche Broker- und Worker-Modell du betreibst – sieh dir diesen Vergleich von Python-Task-Queue-Optionen an, um zu sehen, wie Celerys Architektur im Vergleich zu den Alternativen hier abschneidet.
5. Exporteure und der Collector
Was es ist: Das SDK produziert Spans; ein Exporteur liefert sie irgendwo hin. Man kann direkt zu einem Backend exportieren, oder – das häufigere Muster – über OTLP (OpenTelemetry Protocol) zu einem Collector exportieren, einem separaten Prozess, der Batches verarbeitet, filtert und an ein oder mehrere echte Backends weiterleitet (Jaeger, Tempo, den Ingestion-Endpunkt eines Anbieters).
Warum es wichtig ist: Direkt von jeder Instanz zu exportieren koppelt jeden Service an das Ingestion-Format und die Rate-Limits dieses Backends. Den Weg durch einen Collector zu leiten entkoppelt sie – man kann Backends wechseln, Sampling-Regeln hinzufügen oder an zwei Ziele aufteilen, alles in der Collector-Konfiguration, ohne Anwendungscode zu berühren.
Produktionstipp: Starte auch in der Entwicklung mit einem lokalen Collector – ein Docker-Container, und deine Export-Konfiguration (OTEL_EXPORTER_OTLP_ENDPOINT) muss sich zwischen lokal, Staging und Produktion nie ändern; nur die eigene Ausgabe-Konfiguration des Collectors ändert sich.
Durchgehende Erklärung
Verfolge eine einzelne langsame Anfrage von einem POST /reports-Endpunkt, der einen Celery-Task in die Warteschlange stellt:
- Client sendet
POST /reports. Keintraceparent-Header ist vorhanden, also startet die Instrumentierung von FastAPI einen brandneuen Trace und Root-Span und generiert eine neue Trace-ID. - Handler validiert die Anfrage (ein schneller Child-Span, ein paar Millisekunden) und ruft
generate_report.delay(report_id)auf. - Bevor die Nachricht an den Broker gesendet wird, wird der aktuelle Trace-Kontext (Trace-ID + Span-ID dieses Spans) in die Celery-Nachrichten-Header injiziert.
- Der Handler gibt sofort
202 Acceptedzurück – der Root-Span für den HTTP-Request endet hier, aber der Trace ist noch nicht fertig, da der in die Warteschlange gestellte Task noch nicht ausgeführt wurde. - Ein Celery-Worker übernimmt den Task, möglicherweise Sekunden später auf einer anderen Maschine. Bevor er die Task-Funktion ausführt, extrahiert er den Trace-Kontext aus den Nachrichten-Headern und startet einen neuen Span als Kind des Spans der ursprünglichen Anfrage – dieselbe Trace-ID, korrekt verschachtelt.
- Der Task läuft, einschließlich einer Datenbankabfrage, die 4 der insgesamt 4,3 Sekunden des Tasks dauert – sichtbar als eigener Child-Span mit einer Dauer, die ihn sofort als Engpass identifiziert.
- Das Trace-Backend rendert einen verbundenen Wasserfall: den schnellen HTTP-Handler, dann (nach einer Queue-Lücke, die man auch messen kann) den Task-Span des Workers, dann die langsame Abfrage darin verschachtelt – der genaue Hop, der langsam war, mit einer Zahl daran, statt einer Vermutung.
Sonderfälle
Async-Endpunkt-Instrumentierung. FastAPI’s async def-Handler funktionieren korrekt mit Auto-Instrumentierung, weil das SDK Kontext via contextvars propagiert, was asyncio über await-Grenzen innerhalb desselben Tasks respektiert. Man braucht spezielle Behandlung für asyncio.create_task()-Aufrufe, die genuinlich separate Tasks spawnen, da ein neuer Task den Kontext des Elternteils nicht erbt, außer wenn explizit übergeben.
Hintergrundtasks via BackgroundTasks. FastAPI’s eingebaute BackgroundTasks laufen In-Process nach dem Senden der Antwort, immer noch im selben contextvars-Scope wie die Anfrage – Kontext propagiert hier automatisch, im Gegensatz zum Celery-Fall, weil keine Prozess- oder Serialisierungsgrenze beteiligt ist.
Fire-and-forget Tasks ohne wartenden Aufrufer. Einige Celery-Tasks werden von einem cron-ähnlichen Scheduler in die Warteschlange gestellt, nicht von einer Live-Anfrage – es gibt keine sinnvolle „Elternanfrage", zu der man linken kann. Einen neuen Root-Trace dafür zu starten ist korrekt; ein künstlicher Eltern-Span fügt nur Rauschen hinzu.
Drittanbieter-HTTP-Aufrufe. opentelemetry-instrumentation-requests (oder httpx) auto-instrumentiert ausgehende Aufrufe und injiziert den traceparent-Header automatisch in die ausgehende Anfrage, sodass wenn der nachgelagerte Service auch instrumentiert ist, der Trace über diese Grenze hinweg weitergeht.
Skalierung & Produktionsherausforderungen
Sampling: „Alles tracen" überlebt echten Traffic nicht. Bei bedeutendem Volumen erzeugt der Export eines Spans für jede Anfrage enormes Datenvolumen und Kosten für viele langweilige, schnelle, erfolgreiche Anfragen, die man sich nie ansehen wird. Head-basiertes Sampling (zu Beginn des Traces entscheiden, z.B. 10% der Anfragen) ist einfach, kann aber den seltenen langsamen Ausreißer verpassen, der tatsächlich wichtig ist. Tail-basiertes Sampling (nach Abschluss des Traces entscheiden, alles Langsame oder Fehlerhafte behalten, unabhängig von der Sample-Rate) braucht einen Collector, der so konfiguriert ist, zu puffern und zu entscheiden, aber es ist das Muster, das tatsächlich die Traces behält, die man braucht.
Kardinalität in Span-Attributen. Eine rohe Benutzer-ID oder einen vollständigen Request-Body als Span-Attribut anzuhängen fühlt sich praktisch an, bis die Kosten oder der Index deines Backends von effektiv unbegrenzten einzigartigen Werten explodiert. Halte hoch-kardinalitäre Daten in strukturierten Logs und reserviere Span-Attribute für einen begrenzten Satz von Dimensionen, nach denen du tatsächlich filtern oder gruppieren wirst.
Clock Skew über Services. Eine Waterfall-Ansicht eines Traces hängt davon ab, dass die Uhr jedes Services angemessen synchronisiert ist – NTP-Drift kann dazu führen, dass ein Child-Span scheinbar vor seinem Elternteil beginnt, was verwirrend, aber kein tatsächlicher Tracing-Bug ist.
Collector als neue operationale Abhängigkeit. Sobald er im Telemetrie-Pfad ist, werden seine eigene Verfügbarkeit und sein Backpressure-Verhalten zu etwas, das man überwachen muss – konfiguriere ihn so, dass er Spans unter Überlastung fallen lässt statt die Anwendung zu blockieren oder zum Absturz zu bringen.
Code-Beispiele
Eine FastAPI-App mit automatischem Tracing und OTLP-Export instrumentieren:
# pip install opentelemetry-distro opentelemetry-exporter-otlp \
# opentelemetry-instrumentation-fastapi opentelemetry-instrumentation-celery
from fastapi import FastAPI
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
resource = Resource.create({"service.name": "reports-api"})
provider = TracerProvider(resource=resource)
provider.add_span_processor(
BatchSpanProcessor(OTLPSpanExporter(endpoint="http://otel-collector:4317"))
)
trace.set_tracer_provider(provider)
app = FastAPI()
FastAPIInstrumentor.instrument_app(app)
tracer = trace.get_tracer(__name__)
@app.post("/reports")
async def create_report(report_id: str):
with tracer.start_as_current_span("validate-report-request"):
pass # validation logic here
generate_report.delay(report_id)
return {"status": "queued"}
Trace-Kontext manuell über die Celery-Grenze propagieren:
from celery import Celery
from celery.signals import before_task_publish, task_prerun
from opentelemetry import trace, context
from opentelemetry.propagate import inject, extract
celery_app = Celery("worker", broker="redis://localhost:6379/0")
tracer = trace.get_tracer(__name__)
@before_task_publish.connect
def inject_trace_context(headers=None, **kwargs):
# Runs in the caller (the FastAPI process) — attach the current
# trace context onto the outgoing task's message headers.
if headers is None:
return
inject(headers)
@celery_app.task(name="generate_report")
def generate_report(report_id: str):
# This task's own tracing setup extracts the injected context
# from the task's request headers (wired via task_prerun in a
# real setup, or automatically by opentelemetry-instrumentation-celery)
# and starts this span as a child of the original request's span.
with tracer.start_as_current_span("generate_report") as span:
span.set_attribute("report.id", report_id)
_run_slow_query(report_id)
Strukturierte Logs mit structlog mit dem aktiven Trace korrelieren:
import structlog
from opentelemetry import trace
def add_trace_context(logger, method_name, event_dict):
span = trace.get_current_span()
ctx = span.get_span_context()
if ctx.is_valid:
event_dict["trace_id"] = format(ctx.trace_id, "032x")
event_dict["span_id"] = format(ctx.span_id, "016x")
return event_dict
structlog.configure(
processors=[add_trace_context, structlog.processors.JSONRenderer()]
)
log = structlog.get_logger()
log.info("report_generated", report_id="r_123")
# {"event": "report_generated", "report_id": "r_123",
# "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736", ...}
Eine minimale Collector-Konfiguration für die lokale Entwicklung:
receivers:
otlp:
protocols:
grpc:
http:
exporters:
otlphttp/jaeger:
endpoint: http://jaeger:4318
processors:
batch:
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [otlphttp/jaeger]
Häufige Fallstricke
Fehler: Davon ausgehen, dass Celery-Tasks Trace-Kontext automatisch erben. Ohne explizite Injektion/Extraktion startet jeder Task einen nicht verbundenen Root-Trace, und die UI zeigt nicht zusammenhängende Fragmente statt einer Timeline. Lösung: Verwende opentelemetry-instrumentation-celery oder das manuelle inject/extract-Muster oben.
Fehler: Hoch-kardinalitäre Daten (rohe Benutzer-IDs, vollständige Payloads) in Span-Attributen. Das lässt Ingestion-Kosten und Index-Größe in den meisten Backends explodieren. Lösung: Halte diese Daten in strukturierten Logs, korreliert durch Trace-ID; halte Span-Attribute auf einem begrenzten, filterbaren Satz.
Fehler: Alle Anfragen mit 100% Sample-Rate in der Produktion tracen. Das sind enorme, meist langweilige Datenmengen, die echtes Geld kosten. Lösung: Verwende Tail-basiertes Sampling, sodass langsame und fehlerhafte Anfragen immer behalten werden, während routinemäßig schnelle herunterabgetastet werden.
Fehler: Logs und Traces in zwei nicht verbundenen Systemen ohne gemeinsame ID. Man landet am manuellen Korrelieren durch Augenscanen von Zeitstempeln über zwei UIs. Lösung: Trace-ID und Span-ID in jede strukturierte Log-Zeile einfügen.
Fehler: Die API instrumentieren, aber nie den Worker (oder umgekehrt). Ein halber Trace ist immer noch ein kaputtes Trace – man kann sehen, dass die Anfrage langsam war, aber nicht warum, oder warum, aber nicht zu welcher Anfrage sie gehörte. Lösung: Jeden Prozess instrumentieren, der den Lebenszyklus der Anfrage berührt.
Best Practices für die Produktion
- Instrumentiere zuerst die Request/Response-Grenze mit Auto-Instrumentierung – der höchstwertige, geringste Aufwand-Schritt, von dem die meisten „warum ist das manchmal langsam"-Untersuchungen ausgehen.
- Behandle jede Nicht-HTTP-Grenze (Queues, Pub/Sub, Cron) als manuellen Propagierungspunkt, bis das Gegenteil bewiesen ist.
- Korreliere strukturierte Logs mit Trace-IDs von Tag eins an, nicht nach der ersten schmerzhaften Cross-Service-Debugging-Session, die die Lücke offensichtlich macht.
- Verwende Tail-basiertes Sampling, sobald du über Spielzeug-Traffic-Mengen hinausgehst – vollständiges Sampling skaliert finanziell oder praktisch nicht.
- Leite durch einen Collector statt direkt von jedem Service zu exportieren. Es ist die Naht, die es dir ermöglicht, Backends oder Sampling-Richtlinien zu ändern, ohne Anwendungscode neu zu deployen.
Fazit
Ein Trace ist nur so nützlich wie die Grenzen, die er überschreitet, und in einem typischen Python-Service ist die Grenze, die zuerst bricht, die zwischen einem synchronen Request-Handler und welcher asynchronen Arbeit auch immer er startet – ein Celery-Task, eine Queue, ein Fire-and-forget-Aufruf. Auto-Instrumentierung gibt einem die einfachen 80% kostenlos; die verbleibenden 20%, Kontext genau über diese Grenzen zu propagieren, ist das, wo sich „wir haben Tracing" in „wir können den langsamen Hop in einer nicht reproduzierbaren Anfrage finden" verwandelt.
Wenn dein aktuelles Setup FastAPI instrumentiert hat und Celery nicht verbundene Traces daneben ausführt, ist diese Lücke – nicht ein fehlendes Dashboard, nicht ein fehlender Exporteur – fast sicher der Grund, warum die Trace-UI deine letzten paar „warum war das langsam"-Fragen nicht beantwortet hat.

