TT Lab
Blog

한국어 English 日本語 中文

Get started
Blog

Blog

Observing LLM Serving with OpenTelemetry: Actual Signals from vLLM, The Distance from Standards, and Filling the Gap with a Collector

Status: Draft (October 7, 2026; October 9, 2026, collector overhead measurement reinforced). All numerical figures in this article were measured directly by the author. The environment consists of a single 8GB-class GPU, vLLM 0.31.0, Qwen3-1.7B, OpenTelemetry Collector contrib 0.162.0, and a single server. Because the GenAI semantic conventions are still in the Development stage , their names are changing (even on the day of writing this article, one metric name changed). Tracing for SGLang and Dynamo, as well as trace storage (Tempo, Jaeger, etc.) and metric storage, are Unmeasured . Writing rule: Measured data is the default, and anything unmeasured is marked as "Unmeasured".

Table of Contents

When an LLM server slows down, you generally want to know three things: how long the request waited in the queue, how long the calculation took, and which service call initiated this request. OpenTelemetry (OTel) is the standard for recording these three things in the same way, and the collector, sampling, semantic conventions, and context propagation tested in the OTCA are all used here. This article attaches these to a single vLLM instance and answers five questions.

  1. What spans and attributes does vLLM leave for a single request, and what names do the metrics appear under?
  2. Does the trace initiated at the client continue all the way to vLLM?
  3. What is the overhead of enabling it?
  4. How different is it from the standard (GenAI semantic conventions), and can it be filled using a collector?
  5. How does sampling that keeps only slow requests work, and how is it different from probabilistic sampling?

Short answer. For each request, a llm_request span remained attached to the traceparent , and simply using its attributes allowed us to separate time-to-first-token into waiting and computation. The overhead of enabling tracing was within the margin of error. Although the attribute names differed from the standard (such asprompt_tokens vs. input_tokens), they could be changed using the collector's transform processor. Tail-based sampling, which keeps only slow requests, kept all 19 slow requests, whereas 10% probabilistic sampling kept only 1 out of 22 slow requests.

1. Configuration

클라이언트(계측, traceparent 전송) ──HTTP──▶ vLLM(--otlp-traces-endpoint) ──OTLP/gRPC──▶ OTel Collector ─▶ 파일(JSON)
                                              └─ /metrics(Prometheus) ◀──스크레이프── Collector(prometheus 수신기)

2. Signals Emitted by vLLM

2.1 Traces: One llm_request per request

The client created a parent span and sent three requests carrying W3C traceparent headers. All three vLLM spans that arrived at the collector were llm_request(SERVER kind), their parent span IDs matched the client span's ID , and their trace IDs matched as well. In other words, the trace started at the client continued into vLLM.

Each span had twelve attributes, and changing the configuration still resulted in the same twelve attributes.

Attribute Example Value Meaning
gen_ai.latency.time_to_first_token 0.0195 (seconds) From request arrival until the first token is produced
gen_ai.latency.time_in_queue 0.00002 (seconds) Time spent waiting in the queue
gen_ai.latency.time_in_model_prefill 0.0176 Prefill (input processing)
gen_ai.latency.time_in_model_decode 0.4504 Decode (token generation)
gen_ai.latency.time_in_model_inference 0.4680 Total model work time
gen_ai.latency.e2e 0.4697 Total request time
gen_ai.usage.prompt_tokens, gen_ai.usage.completion_tokens 10, 32 Input and output token counts
gen_ai.request.id, max_tokens, n, top_p cmpl-…, 32, 1, 1 Request information

Absences are also important. The model name (gen_ai.request.model) and gen_ai.operation.name were absent from this span, and the resource's service.name was unknown_service . If a service name is not assigned, spans from multiple servers get mixed under a single name. You can assign it via the environment variable OTEL_SERVICE_NAME or populate resource using the collector's service.name processor (we used the latter in Section 4).

The server startup process is also recorded as traces. llm_request Aside from Overall Loading , dozens of startup spans emerged as a single trace. As averages across four servers where model files were already present, EngineCoreProc init was 18.7 seconds, Prepare model was 6.7 seconds, Executor init was 6.1 seconds, Load model was 3.4 seconds, Warmup (GPU) was 2.7 seconds, and Load weights was 1.8 seconds (spans contain parent-child relationships, so they should not be summed). For a freshly booted server, downloading weights took 298 seconds and Overall Loading was 341 seconds. You can read "why the server started slowly" through span durations instead of digging through logs.

2.2 Metrics: Prometheus /metrics

instead of OTLP /metricsIn this version, metrics were only confirmed via vllm: (Prometheus format). When fetched every 5 seconds using the collector's prometheus receiver, 93 names appeared, and the useful ones are below (all starting with the

prefix). Metric Type
time_to_first_token_seconds How to Read Histogram
inter_token_latency_seconds Time to first token Histogram
e2e_request_latency_seconds Inter-token latency Histogram
request_queue_time_seconds, request_prefill_time_seconds, request_decode_time_seconds Total request time Histogram
num_requests_running, num_requests_waiting gauge current running and waiting
kv_cache_usage_perc gauge KV cache usage
prefix_cache_queries_total, prefix_cache_hits_total total prefix cache hit
prompt_tokens_total, generation_tokens_total total token throughput
num_preemptions_total total preemption (KV shortage)

We checked three things.

3. Cost of Enabling

The same load (512 inputs, 128 outputs, concurrency 1, 8, 32) was measured across three configurations, restarting the server twice for each. Since the KV cache placement could vary each time the server started (Part 14), server logs were checked, and all six servers showed 26,560 tokens.

Configuration Concurrency 1 Throughput / TTFT Concurrency 8 Concurrency 32
Off 66 tok/s / 73ms, 73ms 384, 384 tok/s / 349, 353ms 833, 831 tok/s / 670, 669ms
--otlp-traces-endpoint 66, 66 / 73, 73ms 385, 384 / 350, 348ms 832, 832 / 668, 668ms
Top + --collect-detailed-traces all 66, 66 / 74, 73ms 385, 384 / 349, 351ms 832, 834 / 667, 667ms

4. Distance from the Standard, and Bridging with a Collector

OpenTelemetry's GenAI semantic conventions have been moved to a separate repository (semantic-conventions-genai) and their status is development . Comparing the contents at the time of verification (latest commit as of 2026-10-06) with what vLLM outputs yields the following.

Item GenAI Convention vLLM 0.31.0
Inference Span Name gen_ai.client.inference , span name {operation} {model}, required attributes gen_ai.operation.name, gen_ai.provider.name llm_request, server-side span, lacks the above required attributes
Token Count gen_ai.usage.input_tokens, gen_ai.usage.output_tokens gen_ai.usage.prompt_tokens, gen_ai.usage.completion_tokens(former name)
Model Name gen_ai.request.model Not in spans, present as a model_name label in metrics
Time to First Token Server metric gen_ai.server.time_to_first_token(histogram, seconds) Span attribute gen_ai.latency.time_to_first_token(custom name), metric vllm:time_to_first_token_seconds
Token interval, request duration gen_ai.server.time_per_output_token, gen_ai.server.request.duration vllm:inter_token_latency_seconds, vllm:e2e_request_latency_seconds

Since the standard is still under development, rather than either side being "wrong," vLLM is simply using names from before the standard was finalized. For users, having a collector handle the translation avoids the need to modify the server. This was verified by actually running the configuration below.

processors:
  resource/service:
    attributes:
      - {key: service.name, value: vllm, action: upsert}
  transform/genai:
    trace_statements:
      - context: span
        statements:
          - set(attributes["gen_ai.usage.input_tokens"], attributes["gen_ai.usage.prompt_tokens"]) where attributes["gen_ai.usage.prompt_tokens"] != nil
          - set(attributes["gen_ai.usage.output_tokens"], attributes["gen_ai.usage.completion_tokens"]) where attributes["gen_ai.usage.completion_tokens"] != nil
          - delete_key(attributes, "gen_ai.usage.prompt_tokens")
          - delete_key(attributes, "gen_ai.usage.completion_tokens")
  metrics_transform/genai:
    transforms:
      - {include: "vllm:time_to_first_token_seconds", action: update, new_name: gen_ai.server.time_to_first_token}
      - {include: "vllm:inter_token_latency_seconds", action: update, new_name: gen_ai.server.time_per_output_token}
      - {include: "vllm:e2e_request_latency_seconds", action: update, new_name: gen_ai.server.request.duration}

As a result, the service name of the span became vllm , the token count attribute was changed to input_tokens, output_tokens (the old name was removed), and three metric names were changed to gen_ai.server.* . However, just from the names, it is not possible to know whether the meaning of the values also matches the standard. For example, the standard's time to first token is defined as "until the first token of a successful response," and it was not verified whether vLLM's value covers the exact same interval (unmeasured). Additionally, as seen earlier, units are empty and must be filled in separately.

5. How to Find Slow Requests

5.1 Isolating Time to First Token Using Only Span Attributes

Using 500 request spans collected from four servers (two with tracing on, two with verbose on), we compared 69 requests where the first token took 1 second or more with 38 requests where it took less than 0.2 seconds.

Request Average TTFT Queueing Prefill
69 taking 1s+ 1,494ms 886ms 491ms
38 under 0.2s 74ms 0ms 65ms

The median proportion of queueing time in slow requests' TTFT was 59%. Slow requests were slow because of waiting in line, not because computation was slow, which points not to "getting a faster GPU" but to "increasing concurrency limits and replica counts or rate-limiting requests." The time to first token was not exactly equal to the sum of queueing and prefill; the median difference was 78ms, reaching up to 221ms (presumed to be tokenization or the initial decode phase, but not broken down). The total request duration was equal to the sum of the time to first token and decode time within 1.1ms.

5.2 Tail Sampling vs. Probabilistic Sampling

We sent 120 requests under a mixed workload (input lengths of 64, 512, and 1,500 tokens, concurrency of 16). The client-measured request latency had a median of 2.08s, p90 of 2.73s, and maximum of 3.02s, with 19 requests having a server-side span duration of 2.5 seconds or more.

Policy Remaining traces Slow requests (>= 2.5s)
Keep all (transformation only) 120 19
Tail sampling, latency Policy >= 2,500ms 19(15.8%) 19 (all)
Probabilistic sampling 10% (300 request load) 39(13%) 1 out of 22

For traces spanning multiple services, they must not go to different collectors; they must be routed to the same place using the trace ID (as explained in the official documentation, though not verified in this test).

7. OTCA Scope and Further Reading

This summarizes which items of the test scope are directly demonstrated by this experiment.

Test Scope What Was Covered in This Article
Context propagation traceparent Client spans and vLLM spans are connected as parent and child using
Resources and semantic conventions service.name Without this, unknown_service, the difference between standard names and server names
Collector pipeline Receivers (otlp, prometheus), processors (resource, transform, metrics_transform, tail_sampling, batch), exporters (file), and the order of processors
Sampling The degree to which probabilistic sampling and tail sampling capture slow requests
Metrics Cumulative histograms, missing units, gauges, and sums

The LabHub OTCA course includes an additional module (otca-llm-serving, Lab Step 7) where you can calculate these contents yourself with actual spans and histograms. It is visible in the deployed environment.

8. Limitations and Unmeasured Items

References

Sign in to like

Comments

No comments yet.

Sign in to leave a comment