Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integration with fastapi #1020

Closed
NixBiks opened this issue Apr 7, 2021 · 10 comments · Fixed by #1124
Closed

Integration with fastapi #1020

NixBiks opened this issue Apr 7, 2021 · 10 comments · Fixed by #1124

Comments

@NixBiks
Copy link

NixBiks commented Apr 7, 2021

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

@gautam-ergo
Copy link

gautam-ergo commented Apr 8, 2021

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

@NixBiks
Copy link
Author

NixBiks commented Apr 8, 2021

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.

@gkocjan
Copy link

gkocjan commented Jan 24, 2022

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))

@mkrdip
Copy link

mkrdip commented Jan 26, 2022

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!

askldjd added a commit to askldjd/azure-docs that referenced this issue Feb 5, 2022
There is a memory leak in the FastAPI / Tracer sample code. In the code, the Tracer is created on every request, and that leaks hundreds of KB of memory for each request. Each AzureExporer instantiation will spin up worker threads and will remain allocated even after the request is over.

We should only instantiate the Tracer once outside of the middleware function.

This bug was found separately in the opencensus repo.
census-instrumentation/opencensus-python#1020 (comment)
@jayanttw
Copy link

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

@ikait
Copy link
Contributor

ikait commented May 2, 2022

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

@NixBiks
Copy link
Author

NixBiks commented May 2, 2022

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

@ikait
Copy link
Contributor

ikait commented May 3, 2022

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?

@NixBiks
Copy link
Author

NixBiks commented May 3, 2022

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

@cirezd
Copy link

cirezd commented Feb 6, 2023

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants