stephenhillier / starlette_exporter

Prometheus exporter for Starlette and FastAPI

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Exemplar with data from header?

backbord opened this issue · comments

Hi,

I would like to add a trace id from a request as an exemplar, if that makes sense.

The exemplar callback however seems to be called w/o arguments in

extra["exemplar"] = self.exemplars()

Would it be possible, to pass the request to the exemplar callback, allowing for

def my_exemplars(request: Request) -> dict[str, str]:
    return {"trace_id": request.headers.get("Trace-Id", "")}

...
exemplars=my_exemplars
...

or even have individual fields be callbacks like it is done with labels in

async def _default_label_values(self, request: Request):

exemplars=lambda: {"trace_id": from_header("Trace-Id")}

?

Any help would be greatly appreciated.

PS: I'm new to exemplars and might be misinterpreting something. :)

Are you using OpenTelemetry for tracing? If so, see this example app (most of this is just otel boilerplate, but see the get_trace_id function and PrometheusMiddleware config):

from fastapi import FastAPI
from opentelemetry import trace
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from starlette_exporter import PrometheusMiddleware, handle_openmetrics

trace.set_tracer_provider(TracerProvider(resource=Resource.create({"service.name": "exemplar-test"})))
tracer = trace.get_tracer_provider().get_tracer(__name__)

app = FastAPI()

def get_trace_id():
    trace_id = trace.get_current_span().get_span_context().trace_id
    return hex(trace_id)[2:]

app.add_middleware(
  PrometheusMiddleware,
  exemplars=lambda: {"trace_id": get_trace_id()}  # the function above
)

@app.get("/")
async def index():
    with tracer.start_as_current_span("index"):
        return {"message": get_trace_id()}

app.add_route("/metrics", handle_openmetrics, ["GET"])

FastAPIInstrumentor.instrument_app(app)

However since you mention a Trace-Id header I'm wondering if maybe you're using something different. Let me know if that's the case and we can work out a way to pass the request object into the exemplar callback.

Thanks for the quick reply and aplogies for my late reply!

I intended to use a HTTP header that the client (or the app) can set for tracing a request (over multiple services in metrics and/or logs), and I was under the impression, that it might be a good fit for exemplars.

So my use case would be:

  • Client sends a request with a "Trace-Id" header to a service (sometimes called "X-Request-ID" or "X-Trace-ID" or "X-Correlation-ID")
  • The trace id may be an arbitrary string but should be unique, often a uuid 4 string.
  • The service handles the request:
    • When logging, the trace id is added to the log.
    • => When collecting metrics, the trace id should be attached to the metrics for that request, if possible
    • If the service issues requests to other services itself, it might choose to send the same trace id to trace the requests context over multiple services. (As far as I understand, this would be a new trace id within the same span id in OpenTelemetry.)
    • The trace id is repeated in the response header.

It would be great if I could use exemplars with such a header value.

However, I'm not using OpenTelemetry so far and might confuse OpenTelemetry specific traces (or the new HTTP trace context headers?) with those non-standard correlation ID HTTP headers (see also: The Value of Correlation IDs).

That makes sense. It sounds like access to the request object (like you originally said 😄 ) is needed to pull the value from headers. My biggest concern would be adding the request to callbacks might break some existing users' callback functions, but I will see if I can come up with. Any ideas welcome.

As for an alternative solution, I don't have experience with this library so this is not a recommendation but I have come across this https://github.com/tomwojcik/starlette-context

I've opened #92 with a suggestion how to optionally inject the Request object when calling an exemplars function.