Conversation
Extend the EPPs private Prometheus registry with the request, phase, and Signed-off-by: 0z5a <Dezhen.lu@student.uni-tuebingen.de>
Give every ext_proc stream one RequestObservation and close it exactly once with a finite terminal outcome. Classification follows the control flow rather than the gRPC return value: the handler returns Ok(()) both for a completed response and for an immediate rejection it already wrote, so result.is_ok() cannot mean success. The priority order is early rejection, then protocol error, then upstream HTTP error status, then a completed response, then incomplete. - The first non-empty response body chunk records first_response_body. It is the same edge the prefill-complete signal uses, and it is deliberately not described as time to first token: the chunk may be SSE metadata or a role chunk. - Response header handling captures the `:status` pseudo-header when the gateway forwards it. A 4xx/5xx is attributed to the upstream instead of being folded into a completed response. - A stream that ends without a provable backend terminal is `incomplete`, which is what a disconnect or force shutdown actually is. It is not reported as a client cancellation, which this layer cannot distinguish. - A dropped stream task still closes its attempt, because the context owns the observation and the observation owns its own Drop. The recorder is injected into ExtProcServer rather than reached through a global, so tests assert on an isolated registry. Instrumentation does not change routing, status, headers, body, or cleanup behaviour: the existing wire-level assertions and the new metric assertions both run against the same server. Signed-off-by: 0z5a <Dezhen.lu@student.uni-tuebingen.de>
The standalone picker now reports the two phases it actually enters and the lifecycle callbacks it actually invokes, and names where a request stopped when routing fails. - render_tokenize covers the renderer call and its wait, including the upstream renderer's latency. It is not pure tokenizer CPU time. A malformed client body fails the surrounding parse and is reported as a routing failure with an invalid_request code rather than as a renderer error. - selection covers the call and its wait inside the selection service. Queue wait is not separable from scoring work here, so it is not named a compute time. - A phase sample exists only for a phase the request entered. A request shed because the in-flight limit is saturated records no phase sample, only the capacity routing_failed event. - lifecycle_callbacks_total counts callback invocations and their return. It is not an active-reservation gauge: free_reservation and prefill_complete are idempotent and report success for an unknown id without releasing anything. - epp.routing_failed is emitted with a finite stage and error code, so a log line and the client-visible status always agree. No error message text, prompt, header, or worker address is logged. Signed-off-by: 0z5a <Dezhen.lu@student.uni-tuebingen.de>
Extend the existing ext-proc test harness with the metric contract, reusing the mock picker rather than adding a second one. Metric contract: - a complete non-streaming exchange is one response_eos terminal; - an SSE response with several body chunks records one first-body sample and one terminal, and the first sample lands on the first *non-empty* chunk; - an upstream 5xx status is upstream_http_error, not response_eos; - an immediate rejection is early_reject even though the handler returns Ok; - a stream closed mid-response is incomplete and leaves nothing inflight; - a body-free header-only stream still terminates exactly once; - two streams sharing a client-supplied request id are two attempts. Recorder contract: - finish followed by Drop records one terminal, not two; - dropping without finish records `Incomplete` and returns the gauge to zero; - no response body means no first-body sample, and a backend-reported zero cached_tokens is still a sample; - repeated lifecycle callbacks count as calls; - 5,000 observations across the closed enumerations produce one series per observed label combination and none beyond the enumeration product; - isolated recorders do not share samples; - the exposition declares HELP/TYPE for every family. Wire the recorder through the runner so one process-wide instance serves the router and the ext_proc server. Signed-off-by: 0z5a <Dezhen.lu@student.uni-tuebingen.de>
Contributor
|
👋 Hi 0z5a! Thank you for contributing to ai-dynamo/dynamo. Just a reminder: The 🚀 |
Trim the observability module without changing any runtime path. The module documentation carried five overlapping sections plus a second copy of the metric table's own explanations. It is now one glossary table and the readings a dashboard is most likely to get wrong: the phase timings that include wait, the first-body metric that is not TTFT, the callback counter that is not a state gauge, and the zero-versus-unknown rule. 96 lines of docs become 51. Two tests asserted properties nothing can break: - `every_label_value_is_distinct` compared the strings that `as_str` returns for a closed enum, which the compiler and the match arms already guarantee; - `isolated_recorders_do_not_share_samples` tested that two separately built registries are separate, which is a property of the test helper rather than of the code under test. The cardinality test drove 1000 rounds before asserting. It now drives each outcome, phase, operation, and result once, so every series the label schema allows is created and each is observed a known number of times, and the assertions count the expected series per family instead of stating one bound. Not touched: the request, phase, and callback observation paths, the exporter, the model-label binding, and `cached_metrics` semantics. The whole diff is documentation and test structure. Signed-off-by: 0z5a <Dezhen.lu@student.uni-tuebingen.de>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #11661 (Milestone 9 — Embedded EPP Observability). This is the first batch: request/phase instrumentation on the current EPP path. It does not close the milestone.
What this adds
Extends the EPP's existing private Prometheus registry (
deploy/inference-gateway/ext-proc/src/metrics.rs) with the request, phase, and lifecycle families. The exporter, the/metricslistener, the startup/discovery model-label binding, anddynamo_epp_cached_tokensare reused unchanged — no second registry, no second model labeling, no runtime initialization.dynamo_epp_requests_totalmodel,outcomedynamo_epp_request_duration_secondsmodel,outcomedynamo_epp_streams_inflightmodeldynamo_epp_phase_duration_secondsmodel,phase,outcomedynamo_epp_first_response_body_secondsmodeldynamo_epp_lifecycle_callbacks_totalmodel,operation,resultdynamo_epp_cached_tokensmodeloutcome∈ {response_eos,upstream_http_error,early_reject,ext_proc_error,incomplete};phase∈ {render_tokenize,selection};operation∈ {prefill_complete,request_complete};result∈ {ok,error}. There is nocancelledresult — a call site cannot reliably tell a cancelled call from a failed one, so it reports the failure rather than guessing.The metric glossary lives in the module docs: exact observation points, what each family includes and does not include, the zero/absent/unknown rules, and the list of things that are not observable at this layer.
Terminal classification
result.is_ok()is deliberately not consulted. The gRPC handler returnsOk(())both for a completed response and for an immediate rejection it already wrote, so the outcome is derived from the control flow, in a fixed priority order: early rejection → protocol error → upstream HTTP error status → completed response → incomplete.incompleteis what a disconnect or force shutdown actually is. It is not reported as a client cancellation, which this layer cannot distinguish.upstream_http_erroruses the:statuspseudo-header when the gateway forwards response headers, so a 5xx backend is not folded intoresponse_eos.first_response_body_secondssample.cached_tokens = 0remains a real zero sample; a response with no usage information records nothing rather than a fabricated zero.Exactly-once terminal accounting
One
RequestObservationowns one attempt.finish()is consuming, so an attempt records exactly one terminal; if the owning future is dropped first,DroprecordsIncompleteinstead. A dropped stream therefore cannot leak the inflight gauge or disappear from the counters, and no shared request-keyed map is needed.Reservation lifecycle: calls, not state
dynamo_epp_lifecycle_callbacks_totalcounts callback invocations and their return. It is explicitly not an active-reservation gauge:free_reservationandprefill_completeare idempotent and return success for an unknown id without releasing anything, socallback == Okmust not decrement a state counter. AuthoritativeApplied/NoChangetransitions need a core-level observer and are not claimed here.Structured events
epp.request_finished(finiteoutcome,duration_ms) andepp.routing_failed(request_id, finitestage, finitecode,reservation_taken) carry bounded fields only. No prompt, token, authorization, raw handoff, worker address, or error message text is logged. These are tracing events on the existing subscriber; this PR does not add a JSON sink.Cardinality
modelremains the startup/discovery-bound label; it is never read from a request body. Nothing request-scoped (request id, attempt id, reservation/plan/session id, prompt, cache salt, token, worker endpoint, peer IP, path/query/header) is a label. A test drives every label combination the schema allows and asserts the series count per family is exactly the enumeration product.Behavioural neutrality
Instrumentation does not change routing, status, headers, body, callback order, or cleanup semantics. The pre-existing wire-level integration test and all pre-existing unit tests pass unchanged.
Tests
New coverage:
response_eos,upstream_http_error,early_reject,incomplete; exactly-once terminal; inflight back to zero; body-free stream; two streams sharing a client request id;finish+Droprecords one terminal, not two; isolated registries do not share samples; exposition declares HELP/TYPE for every family;Follow-up commits
b8974c7c48condenses the module documentation into one glossary plus the readings most likely to bemisread, and drops two tests that asserted properties the compiler or the test helper already
guarantees. No runtime path changed;
175lib tests pass.Base
402c2611ffcb3dbdeea344677d757dfed5469912Not in this PR
HostTelemetryis a candidate, its snapshot granularity needs checking first).discovered/register/deregisteroperation outcomes follow separately.Notes for review
ExtProcServer::newnow takes the recorder instead of reaching a global, which is what lets the tests assert on an isolated registry.EppRouter::from_selectortakes it for the same reason.:statuscapture only fires when the gateway forwards response headers; when it is absent the attempt is classified without it, and the glossary says so.selectionincludes queue wait andrender_tokenizeincludes renderer latency. They are named for the phase, not for compute, on purpose.