3

I'm trying to get OpenTelemetry tracing working with FastAPI and Requests. Currently, my setup looks like this:

import requests
from opentelemetry.baggage.propagation import W3CBaggagePropagator
from opentelemetry.propagators.composite import CompositePropagator
from fastapi import FastAPI
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.instrumentation.requests import RequestsInstrumentor
from opentelemetry.propagate import set_global_textmap
from opentelemetry.propagators.b3 import B3MultiFormat
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator

set_global_textmap(CompositePropagator([B3MultiFormat(), TraceContextTextMapPropagator(), W3CBaggagePropagator()]))

app = FastAPI()

FastAPIInstrumentor.instrument_app(app)
RequestsInstrumentor().instrument()

@app.get("/")
async def get_things():
    r = requests.get("http://localhost:8081")

    return {
        "Hello": "world",
        "result": r.json()
    }

The / endpoint just does a GET to another service that looks basically like this one, just with some middleware to log the incoming headers.

If I send a request like this (httpie format),

http :8000 'x-b3-traceid: f8c83f4b5806299983da51de66d9a242' 'x-b3-spanid: ba24f165998dfd8f' 'x-b3-sampled: 1'

I expect that the downstream service, i.e. the one being requested by requests.get("http://localhost:8081"), to receive headers that look something like

{
  "x-b3-traceid": "f8c83f4b5806299983da51de66d9a242",
  "x-b3-spanid": "xxxxxxx",  # some generated value from the upstream service
  "x-b3-parentspanid": "ba24f165998dfd8f", 
  "x-b3-sampled": "1"
}

But what I'm getting is basically exactly what I sent to the upstream service:

{
  "x-b3-traceid": "f8c83f4b5806299983da51de66d9a242",
  "x-b3-spanid": "ba24f165998dfd8f",
  "x-b3-sampled": "1"
}

I must be missing something obvious, but can't seem to figure out exactly what.

Sending a W3C traceparent header results in the same exact situation (just with traceparent in the headers that are received downstream). Any pointers would be appreciated.

EDIT - I'm not using any exporters, as in our environment, Istio is configured to export the traces. So we just care about the HTTP traces for now.

iLikeBreakfast
  • 1,545
  • 23
  • 46

1 Answers1

4

The B3MultiFormat propagator doesn't consider the parent span id field while serialising the context into HTTP headers since X-B3-ParentSpanId is an optional header https://github.com/openzipkin/b3-propagation#multiple-headers. You can expect the X-B3-TraceId and X-B3-SpanId to be always present but not the remaining ones.

Edit:

Are you setting the concrete tracer provider? It doesn't look like from the shared snippet but I don't know if you are actual application code. It's all no-op if you do not set the sdk tracer provider i.e no recording spans are created in FastAPI service. Please do the following.

...
from opentelemetry.trace import set_tracer_provider
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.resources import Resource

set_tracer_provider(TracerProvider(
    resource=Resource.create({"serice.name": "my-service"})
))

...

Another edit:

OpenTelemetry does not store the parent span ID in the context https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/api.md#spancontext. The context propagation client libraries from OTEL are limited to serialise and pass on this info only. I don't think you can have the parentSpanId propagated.

Srikanth Chekuri
  • 1,944
  • 1
  • 9
  • 19
  • Thanks for your answer. How do "parent spans" then get propagated between services? And why does my spanid stay the same between the incoming request and the call to the downstream service? That doesn't seem logical, or am I missing something? – iLikeBreakfast Dec 07 '21 at 08:26
  • Are you saying span_id received in downstream service "http://localhost:8081" is same as the span_id received by FastAPI service? That shouldn't be the case. Are you seeing any traces for the "/" route? – Srikanth Chekuri Dec 07 '21 at 12:07
  • I updated the answer based on another round of detailed look. Please let me know if it doesn't solve the issue. – Srikanth Chekuri Dec 09 '21 at 08:36
  • Well, at least now I have different span ID's in both services! But still no obvious way to tie them together, unless I export the traces directly from the app, which I don't actually want to do. There must be some way to have the parent span ID propagated to the downstream service? – iLikeBreakfast Dec 09 '21 at 12:48
  • What do you mean by tie them together without exporting the traces. If you are not going to export the traces from app/service how do you tie them? In opentelemetry data model parent id is the top level field of Span message and all backends use that for representation https://github.com/open-telemetry/opentelemetry-proto/blob/main/opentelemetry/proto/trace/v1/trace.proto#L112 – Srikanth Chekuri Dec 09 '21 at 13:11
  • From my knowledge parent_span_id in trace context propagation is either deprecated or fully ignored (in your case). You don't even see it's mention in the W3C trace context propagation spec https://www.w3.org/TR/trace-context/#traceparent-header-field-values. – Srikanth Chekuri Dec 09 '21 at 13:24
  • Added another edit to provide more info. – Srikanth Chekuri Dec 10 '21 at 02:24
  • That's unfortunate. So basically the only way for me to get this information (as to which spans come from which upstream spans) is to export them from within the application itself? Thanks for your input and links to the specs, quite helpful indeed! – iLikeBreakfast Dec 10 '21 at 08:11
  • Yes, the id info you want is parent of parent span which is never propagated through header and only can be derived from the full spans exported. – Srikanth Chekuri Dec 10 '21 at 09:44