opencensus-python
opencensus-python copied to clipboard
Integration with fastapi
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
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
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.
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))
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!
If you are using Azure App Insights is also good to add
HTTP_ROUTEbecause 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
@mr-bjerre Your integration would help much people including me. Could you proceed with a PR?
I'm not really using this anymore but you should be able to simply use it as is?
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?
Yes of course. Go ahead - I agree that would be preferable!
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.