Trace failed fastapi requests with opencensus - fastapi

I'm using opencensus-python to track requests to my python fastapi application running in production, and exporting the information to Azure AppInsights using the opencensus exporters. I followed the Azure Monitor docs and was helped out by this issue post which puts all the necessary bits in a useful middleware class.
Only to realize later on that requests that caused the app to crash, i.e. unhandled 5xx type errors, would never be tracked, since the call to execute the logic for the request fails before any tracing happens. The Azure Monitor docs only talk about tracking exceptions through the logs, but this is separate from the tracing of requests, unless I'm missing something. I certainly wouldn't want to lose out on failed requests, these are super important to track! I'm accustomed to using the "Failures" tab in app insights to monitor any failing requests.
I figured the way to track these requests is to explicitly handle any internal exceptions using try/catch and export the trace, manually setting the result code to 500. But I found it really odd that there seems to be no documentation of this, on opencensus or Azure.
The problem I have now is: this middleware function is expected to pass back a "response" object, which fastapi then uses as a callable object down the line (not sure why) - but in the case where I caught an exception in the underlying processing (i.e. at await call_next(request)) I don't have any response to return. I tried returning None but this just causes further exceptions down the line (None is not callable).
Here is my version of the middleware class - its very similar to the issue post I linked, but I'm try/catching over await call_next(request) rather than just letting it fail unhanded. Scroll down to the final 5 lines of code to see that.
import logging
from fastapi import Request
from opencensus.trace import (
attributes_helper,
execution_context,
samplers,
)
from opencensus.ext.azure.trace_exporter import AzureExporter
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 opencensus.ext.azure.log_exporter import AzureLogHandler
from starlette.types import ASGIApp
from src.settings import settings
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__)
module_logger.addHandler(AzureLogHandler(
connection_string=settings.appinsights_connection_string
))
class AppInsightsMiddleware:
"""
Middleware class to handle tracing of fastapi requests and exporting the data to AppInsights.
Most of the code here is copied from a github issue: https://github.com/census-instrumentation/opencensus-python/issues/1020
"""
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.propagator = (
propagator or trace_context_http_header_format.TraceContextPropagator()
)
self.exporter = exporter or AzureExporter(
connection_string=settings.appinsights_connection_string
)
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:
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)
try:
response = await call_next(request)
tracer.add_attribute_to_current_span(HTTP_STATUS_CODE, response.status_code)
tracer.end_span()
return response
# Explicitly handle any internal exception here, and set status code to 500
except Exception as exception:
module_logger.exception(exception)
tracer.add_attribute_to_current_span(HTTP_STATUS_CODE, 500)
tracer.end_span()
return None
I then register this middleware class in main.py like so:
app.middleware("http")(AppInsightsMiddleware(app, sampler=samplers.AlwaysOnSampler()))

Explicitly handle any exception that may occur in processing the API request. That allows you to finish tracing the request, setting the status code to 500. You can then re-throw the exception to ensure that the application raises the expected exception.
try:
response = await call_next(request)
tracer.add_attribute_to_current_span(HTTP_STATUS_CODE, response.status_code)
tracer.end_span()
return response
# Explicitly handle any internal exception here, and set status code to 500
except Exception as exception:
module_logger.exception(exception)
tracer.add_attribute_to_current_span(HTTP_STATUS_CODE, 500)
tracer.end_span()
raise exception

Related

Azure Web App returns Internal Error Server when executing a Get Request using FastAPI in Python code

We are trying to execute a Get Request from the Azure Web App using our python code. The request will be made to our DevOps Repo following the available API. This is just a part (example) of the entire code:
from fastapi import FastAPI
import requests
import base64
import uvicorn
app = FastAPI()
#app.get("/")
async def root():
return {"Hello": "Pycharm"}
#app.get('/file')
async def sqlcode(p: str = '/ppppppppp',
v: str = 'vvvv',
r: str = 'rrrrrr',
pn: str = 'nnnnnnn'):
organizational_url = f'https://xxxxxxxx/{pn}/_apis/git/repositories/{r}/items?path={p}&versionDescriptor.version={v}&api-version=6.1-preview.1'
username = 'username'
password = 'password'
basic_authentication = base64.b64encode((f'{username}:{password}').encode('utf-8')).decode('utf-8')
headers = {
'Authorization': f'Basic {basic_authentication}',
}
response = requests.get(url=organizational_url, headers=headers)
return {"sqlcode": response.text}
# Code ends for Fastapi with swagger
def print_hi(name):
print(f'Hi, {name}') # Press Ctrl+F8 to toggle the breakpoint.
# Press the green button in the gutter to run the script.
if __name__ == '__main__':
print_hi('PyCharm')
And this is the requirements.txt
Flask==2.0.1
asyncpg==0.21.0
databases==0.4.1
fastapi==0.63.0
gunicorn==20.0.4
pydantic==1.7.3
SQLAlchemy==1.3.22
uvicorn==0.11.5
config==0.3.9
pandas==1.5.3
dask==2023.1.0
azure.storage.blob==12.14.1
pysftp==0.2.9
azure-identity==1.12.0
azure-keyvault-secrets==4.6.0
When we execute the code in Pycharm this works without any issue :
But when we execute the same code from the Web App we've got the Internal Server Error Message
I'm just trying to help the team to understand this error. I'm not a cloud engineer. What could be missing, is it something in the code, a particular port to use, is a permission needed in Azure Web Service?

In gRPC python, the Service class and the Serve method are always in the same file, why?

In gRPC python, the Service-class and the Serve method are always in the same file, why?
for example -
the service class - link and the serve() - link
Similarly - service-class and serve()
I am new to python and grpc. In my project I wrote the serve method in different file importing the service-class, the server seems started but when I invoke it from client code (postman)
it doesn't work
Here is my code - ems_validator_service.py contains the service class
and main.py has the serve() method
File: ems_validator_service.py -
from validator.src.grpc import ems_validator_service_pb2
from validator.src.grpc.ems_validator_service_pb2_grpc import EmsValidatorServiceServicer
class EmsValidatorServiceServicer(EmsValidatorServiceServicer):
def Validate(self, request, context):
# TODO: logic to validate
return ems_validator_service_pb2.GetStatusResponse(
validation_status=ems_validator_service_pb2.VALIDATION_STATUS_IN_PROGRESS)
def GetStatus(self, request, context):
# TODO: logic to get actual status
return ems_validator_service_pb2.GetStatusResponse(
validation_status=ems_validator_service_pb2.VALIDATION_STATUS_IN_PROGRESS)
File: main.py -
from validator.src.grpc.ems_validator_service_pb2_grpc import (
EmsValidatorServiceServicer,
add_EmsValidatorServiceServicer_to_server
)
from concurrent import futures
import grpc
def serve():
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
add_EmsValidatorServiceServicer_to_server(EmsValidatorServiceServicer(), server)
server.add_insecure_port('localhost:50051') # todo change it
server.start()
server.wait_for_termination()
if __name__ == "__main__":
serve()
For the above code I can't invoke the rpc.
but If I move the serve method to the ems_validator_service.py file and call that method from main.py then it works fine. Not sure if it is a python thing or gRPC thing?
The error I get from client.py -
File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/grpc/_channel.py", line 946, in __call__
return _end_unary_response_blocking(state, call, False, None)
File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/grpc/_channel.py", line 849, in _end_unary_response_blocking
raise _InactiveRpcError(state)
grpc._channel._InactiveRpcError: <_InactiveRpcError of RPC that terminated with:
status = StatusCode.UNIMPLEMENTED
details = "Method not implemented!"
debug_error_string = "UNKNOWN:Error received from peer ipv6:%5B::1%5D:50051 {created_time:"2022-10-19T22:10:18.898439-07:00", grpc_status:12, grpc_message:"Method not implemented!"}"
>
As already mentioned, the same client works fine if I move the above serve() method to the service class
These are just simple examples. It's totally fine to call serve() from a different file.

Can't invoke Soap method with Zeep and basic auth

I am trying to access Web Service with Zeep and I get 401 in response. I checked official docs and https://stackoverflow.com/a/48861779/9187682 . I get an error when I try to call a method, like:
from requests import Session
from requests.auth import HTTPBasicAuth # or HTTPDigestAuth, or OAuth1, etc.
from zeep import Client
from zeep.transports import Transport
session = Session()
session.auth = HTTPBasicAuth(user, password)
client = Client('http://my-endpoint.com/production.svc?wsdl',
transport=Transport(session=session))
Items = client.get_type('ns1:ItemsType')
response = client.service.publishService('MyProps', Items ={ #ERROR HAPPENS HERE
'ItemInformation': {
'':''
}
})
The response I get is:
zeep.exceptions.TransportError: Server returned response (401) with invalid XML: Invalid XML content received (Start tag expected, '<' not found, line 1, column 1).
Request in itself is OK (it works without auth if it's disabled on service).
Credentials are OK as well (in fact the whole thing works in Soap UI)
Am I missing something here?

Unable to modify request in middleware using Scrapy

I am in the process of scraping public data regarding metheorology for a project (data science), and in order to effectively do that I need to change the proxy used on my scrapy requests in the event of a 403 response code.
For this, I have defined a download middleware to handle such situation, which is as follows
class ProxyMiddleware(object):
def process_response(self, request, response, spider):
if response.status == 403:
f = open("Proxies.txt")
proxy = random_line(f) # Just returns a random line from the file with a valid structure ("http://IP:port")
new_request = Request(url=request.url)
new_request.meta['proxy'] = proxy
spider.logger.info("[Response 403] Changed proxy to %s" % proxy)
return new_request
return response
After properly adding the class to settings.py, I expected this middleware to deal with 403 responses by generating a new request with the new proxy, hence finishing in a 200 response. The observed behaviour is that it actually gets executed (I can see the Logger info about Changed proxy), but the new request does not seem to be made. Instead, I'm getting this:
2018-12-26 23:33:19 [bot_2] INFO: [Response] Changed proxy to https://154.65.93.126:53281
2018-12-26 23:33:26 [bot_2] INFO: [Response] Changed proxy to https://176.196.84.138:51336
... indefinitely with random proxies, which makes me think that I'm still retrieving 403 errors and the proxy is not changing.
Reading the documentation, regarding process_response, it states:
(...) If it returns a Request object, the middleware chain is halted and the returned request is rescheduled to be downloaded in the future. This is the same behavior as if a request is returned from process_request().
Is it possible that "in the future" is not "right after it is returned"? How should I do to change the proxy for all requests from that moment on?
Scrapy will drop duplicate requests to the same url by default, so that's probably what's happening on your spider. To check if this is your case you can set this settings:
DUPEFILTER_DEBUG=True
LOG_LEVEL='DEBUG'
To solve this you should add dont_filter=True:
new_request = Request(url=request.url, dont_filter=True)
Try this:
class ProxyMiddleware(object):
def process_response(self, request, response, spider):
if response.status == 403:
f = open("Proxies.txt")
proxy = random_line(f)
new_request = Request(url=request.url)
new_request.meta['proxy'] = proxy
spider.logger.info("[Response 403] Changed proxy to %s" % proxy)
return new_request
else:
return response
A better approach would be to use scrapy random proxies module instead:
'DOWNLOADER_MIDDLEWARES' : {
'rotating_proxies.middlewares.RotatingProxyMiddleware': 610,
'rotating_proxies.middlewares.BanDetectionMiddleware': 620
},

Can't yield json response from server with redux-saga

i am trying to use the stack react redux and redux-saga and understand the minimal needed plumbing.
i did a github repo to reproduce the error that i got :
https://github.com/kasra0/react-redux-saga-test.git
running the app : npm run app
url : http://localhost:3000/
the app consists of a simple combo box and a button.
Once selecting a value from the combo, clicking the button dispatch an action that consists simply of fetching some json data .
The server recieves the right request (based on the selected value) but at the line let json = yield call([res, 'json']). i got the error :
the error message that i got from the browser :
index.js:2177 uncaught at equipments at equipments
at takeEvery
at _callee
SyntaxError: Unexpected end of input
at runCallEffect (http://localhost:3000/static/js/bundle.js:59337:19)
at runEffect (http://localhost:3000/static/js/bundle.js:59259:648)
at next (http://localhost:3000/static/js/bundle.js:59139:9)
at currCb (http://localhost:3000/static/js/bundle.js:59212:7)
at <anonymous>
it comes from one of my sagas :
import {call,takeEvery,apply,take} from 'redux-saga/effects'
import action_types from '../redux/actions/action_types'
let process_equipments = function* (...args){
let {department} = args[0]
let fetch_url = `http://localhost:3001/equipments/${department}`
console.log('fetch url : ',fetch_url)
let res = yield call(fetch,fetch_url, {mode: 'no-cors'})
let json = yield call([res, 'json'])
// -> this is the line where something wrong happens
}
export function* equipments(){
yield takeEvery(action_types.EQUIPMENTS,process_equipments)
}
I did something wrong in the plumbing but i can't find where.
thanks a lot for your help !
Kasra
Just another way to call .json() without using call method.
let res = yield call(fetch,fetch_url, {mode: 'no-cors'})
// instead of const json = yield call([res, res.json]);
let json = yield res.json();
console.log(json)
From redux-saga viewpoint all code is slightly correct - two promises are been executed sequentially by call effect.
let res = yield call(fetch,fetch_url, {mode: 'no-cors'})
let json = yield call([res, 'json'])
But using fetch in no-cors mode meant than response body will not be available from program code, because request mode is opaque in this way: https://fetch.spec.whatwg.org/#http-fetch
If you want to fetch information from different origin, use cors mode with appropriate HTTP header like Access-Control-Allow-Origin, more information see here: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS

Resources