opencensus-python icon indicating copy to clipboard operation
opencensus-python copied to clipboard

Integration with fastapi

Open NixBiks opened this issue 4 years ago • 9 comments

I've created my own FastAPI middleware. I figured it could be useful for others and it would be great to maintain it in the contrib library here.

Implementation of opencensus.ext.fastapi

import logging

from fastapi import Request
from opencensus.trace import (
    attributes_helper,
    execution_context,
    print_exporter,
    samplers,
)
from opencensus.trace import span as span_module
from opencensus.trace import tracer as tracer_module
from opencensus.trace import utils
from opencensus.trace.propagation import trace_context_http_header_format
from starlette.types import ASGIApp

HTTP_HOST = attributes_helper.COMMON_ATTRIBUTES["HTTP_HOST"]
HTTP_METHOD = attributes_helper.COMMON_ATTRIBUTES["HTTP_METHOD"]
HTTP_PATH = attributes_helper.COMMON_ATTRIBUTES["HTTP_PATH"]
HTTP_ROUTE = attributes_helper.COMMON_ATTRIBUTES["HTTP_ROUTE"]
HTTP_URL = attributes_helper.COMMON_ATTRIBUTES["HTTP_URL"]
HTTP_STATUS_CODE = attributes_helper.COMMON_ATTRIBUTES["HTTP_STATUS_CODE"]

module_logger = logging.getLogger(__name__)


class FastAPIMiddleware:
    def __init__(
        self,
        app: ASGIApp,
        excludelist_paths=None,
        excludelist_hostnames=None,
        sampler=None,
        exporter=None,
        propagator=None,
    ) -> None:
        self.app = app
        self.excludelist_paths = excludelist_paths
        self.excludelist_hostnames = excludelist_hostnames
        self.sampler = sampler or samplers.AlwaysOnSampler()
        self.exporter = exporter or print_exporter.PrintExporter()
        self.propagator = (
            propagator or trace_context_http_header_format.TraceContextPropagator()
        )

    async def __call__(self, request: Request, call_next):

        # Do not trace if the url is in the exclude list
        if utils.disable_tracing_url(str(request.url), self.excludelist_paths):
            return await call_next(request)

        try:
            span_context = self.propagator.from_headers(request.headers)

            tracer = tracer_module.Tracer(
                span_context=span_context,
                sampler=self.sampler,
                exporter=self.exporter,
                propagator=self.propagator,
            )
        except Exception:  # pragma: NO COVER
            module_logger.error("Failed to trace request", exc_info=True)
            return await call_next(request)

        try:
            span = tracer.start_span()
            span.span_kind = span_module.SpanKind.SERVER
            span.name = "[{}]{}".format(request.method, request.url)
            tracer.add_attribute_to_current_span(HTTP_HOST, request.url.hostname)
            tracer.add_attribute_to_current_span(HTTP_METHOD, request.method)
            tracer.add_attribute_to_current_span(HTTP_PATH, request.url.path)
            tracer.add_attribute_to_current_span(HTTP_URL, str(request.url))
            execution_context.set_opencensus_attr(
                "excludelist_hostnames", self.excludelist_hostnames
            )
        except Exception:  # pragma: NO COVER
            module_logger.error("Failed to trace request", exc_info=True)

        response = await call_next(request)
        try:
            tracer.add_attribute_to_current_span(HTTP_STATUS_CODE, response.status_code)
        except Exception:  # pragma: NO COVER
            module_logger.error("Failed to trace response", exc_info=True)
        finally:
            tracer.end_span()
            return response

Minimal example using it

from fastapi import FastAPI
from opencensus.ext.fastapi import FastAPIMiddleware
from opencensus.trace import samplers

app = FastAPI()

app.middleware("http")(FastAPIMiddleware(app, sampler=samplers.AlwaysOnSampler()))


@app.get("/")
def ping():
    return {"message": "pong!"}

Let me know if I should proceed with a PR

NixBiks avatar Apr 07 '21 14:04 NixBiks

hi @mr-bjerre

This seems interesting, one of the issues that we were facing. on following this example from Azure - OpenCensus FastAPI implementation is that there was a constant OOM(out of memory) problem within our pods, would you be able to suggest any tweaks. Are there any major differences to your implementation from the one I linked.

Any help would be greatful

Thanks

gautam-ergo avatar Apr 08 '21 22:04 gautam-ergo

Shortly speaking they seem similar. The azure example is a minimal example of what I provided (from a quick perspective). If you mean "out of memory" then I don't see how that could be the case.

NixBiks avatar Apr 08 '21 22:04 NixBiks

If you are using Azure App Insights is also good to add HTTP_ROUTE because it is used to group requests in performance view.

For example:

tracer.add_attribute_to_current_span(HTTP_ROUTE, str(request.url.path))

gkocjan avatar Jan 24 '22 07:01 gkocjan

Source: Azure Monitor - Tracking FastAPI applications. This Azure example is good the simple implementation they have.


@app.middleware("http")
async def middlewareOpencensus(request: Request, call_next):
    tracer = Tracer(exporter=AzureExporter(connection_string=f'InstrumentationKey={APPINSIGHTS_INSTRUMENTATIONKEY}'),sampler=ProbabilitySampler(1.0))
    with tracer.span("main") as span:
        span.span_kind = SpanKind.SERVER

        response = await call_next(request)

        tracer.add_attribute_to_current_span(
            attribute_key=HTTP_STATUS_CODE,
            attribute_value=response.status_code)
        tracer.add_attribute_to_current_span(
            attribute_key=HTTP_URL,
            attribute_value=str(request.url))

    return response

@gautam-ergo, the azure example above has the tracer initiated within the function which initiates a new tracer every time and causes OOM. So, please initiate before the function for your program. A simple example is below.

tracer = Tracer(exporter=AzureExporter(connection_string=f'InstrumentationKey={APPINSIGHTS_INSTRUMENTATIONKEY}'),sampler=ProbabilitySampler(1.0))

@app.middleware("http")
async def middlewareOpencensus(request: Request, call_next):
    with tracer.span("main") as span:
        span.span_kind = SpanKind.SERVER

        response = await call_next(request)

        tracer.add_attribute_to_current_span(
            attribute_key=HTTP_STATUS_CODE,
            attribute_value=response.status_code)
        tracer.add_attribute_to_current_span(
            attribute_key=HTTP_URL,
            attribute_value=str(request.url))

    return response

Hope this helps!

mkrdip avatar Jan 26 '22 19:01 mkrdip

If you are using Azure App Insights is also good to add HTTP_ROUTE because it is used to group requests in performance view.

For example:

tracer.add_attribute_to_current_span(HTTP_ROUTE, str(request.url.path))

thanks @gkocjan , this was really helpful

jayanttw avatar Feb 23 '22 13:02 jayanttw

@mr-bjerre Your integration would help much people including me. Could you proceed with a PR?

ikait avatar May 02 '22 15:05 ikait

I'm not really using this anymore but you should be able to simply use it as is?

NixBiks avatar May 02 '22 16:05 NixBiks

I have used your code in some projects and it would be more useful if it maintained in the contrib dir. If you say so, could I make a PR using your code and my unittests?

ikait avatar May 03 '22 02:05 ikait

Yes of course. Go ahead - I agree that would be preferable!

NixBiks avatar May 03 '22 04:05 NixBiks

Would this solution also enable to include custom properties (e.g. header information)? If yes, how would one do this? I tried extending the Middleware by adding span.add_attribute("http.headers.referrer", request.headers.get("referer")) but this is not recognized by the exporter.

cirezd avatar Feb 06 '23 13:02 cirezd