Package: opentracing-utils
Early stage WIP + ExperimentalConvenient utilities for addingOpenTracingsupport in your python projects.Featuresopentracing-utilsshould provide and aims at the following:No external dependencies, onlyopentracing-python.No threadlocals. Either use tracer scoper manager, pass spans explicitly or fallback to callstack frames inspection!Context agnostic, so no externalcontext implementationdependency (no Tornado, Flask, Django etc …).Support OpenTracing 2.0 API, withscope_manager(opentracing-utils>0.21.0).Try to be less verbose - just add the@tracedecorator.Could be more verbose when needed, without complexity - just accept**kwargsand get the span passed to your traced functions via@trace(pass_span=True).Support asyncio/async-await coroutines. (drop support for py2.7)Supportgevent.Ability to add OpenTracing support to external libs/frameworks/clients:Django (viaOpenTracingHttpMiddleware)Flask (viatrace_flask())Requests (viatrace_requests())SQLAlchemy (viatrace_sqlalchemy())InstallUsing pip (not released yet to PyPi)pipinstall-Uopentracing-utilsor by cloning the repopythonsetup.pyinstallUsageinit_opentracing_tracerThe first step needed in OpenTracing instrumentation is to initialize a tracer. Each vendor defines how the tracer can be initialized. Currently the following tracers are supported:BasicTracerInstanaJaegerLightStepBasicTracerThis is the basic noop tracer. It could be initialized with a recorder (e.g.Memory Recorder), which can be useful in debugging and playing around with OpenTracing concepts.importopentracingfromopentracing_utilsimportOPENTRACING_BASIC,init_opentracing_tracer# Initialize upon application startinit_opentracing_tracer(OPENTRACING_BASIC)# It is possible to pass custom recorder# init_opentracing_tracer(OPENTRACING_BASIC, recorder=custom_recorder)# Now use the opentracing.tracerroot_span=opentracing.tracer.start_span(operation_name='root_span')InstanaConfig VarsThe following config variables can be used in initialization if set as env variablesOPENTRACING_INSTANA_SERVICEThe service name.importopentracingfromopentracing_utilsimportOPENTRACING_INSTANA,init_opentracing_tracer# Initialize upon application startinit_opentracing_tracer(OPENTRACING_INSTANA)# It is possible to pass args# init_opentracing_tracer(OPENTRACING_INSTANA, service='python-server')# Now use the opentracing.tracerroot_span=opentracing.tracer.start_span(operation_name='root_span')DependencyAddinstanato thedependencies.txtof your project.JaegerConfig VarsThe following config variables can be used in initialization if set as env variablesOPENTRACING_JAEGER_SERVICE_NAMEThe service name.NoteJaeger configuration should be passed by the instrumentated code. Default is{}.importopentracingfromopentracing_utilsimportOPENTRACING_JAEGER,init_opentracing_tracer# Initialize upon application startinit_opentracing_tracer(OPENTRACING_JAEGER)# It is possible to pass args# init_opentracing_tracer(OPENTRACING_JAEGER, service_name='python-server', config=custom_config_with_sampling)# Now use the opentracing.tracerroot_span=opentracing.tracer.start_span(operation_name='root_span')DependencyAddjaeger_clientto thedependencies.txtof your project.LightStepConfig VarsThe following config variables can be used in initialization if set as env variablesOPENTRACING_LIGHTSTEP_COMPONENT_NAMEThe component name.OPENTRACING_LIGHTSTEP_ACCESS_TOKENThe LightStep collector access token.OPENTRACING_LIGHTSTEP_COLLECTOR_HOSTThe LightStep collector host. Default:collector.lightstep.com.OPENTRACING_LIGHTSTEP_COLLECTOR_PORTThe LightStep collector port (int). Default:443.OPENTRACING_LIGHTSTEP_VERBOSITYThe verbosity of the tracer (int). Default:0.importopentracingfromopentracing_utilsimportOPENTRACING_LIGHTSTEP,init_opentracing_tracer# Initialize upon application startinit_opentracing_tracer(OPENTRACING_LIGHTSTEP)# It is possible to pass args# init_opentracing_tracer(OPENTRACING_LIGHTSTEP, component_name='python-server', access_token='123', collector_host='production-collector.com')# Now use the opentracing.tracerroot_span=opentracing.tracer.start_span(operation_name='root_span')DependencyAddlightstepto thedependencies.txtof your project.@trace decoratorThe@tracedecorator supports OpenTracingscope_managerAPI (new in opentracing-utils > 0.21.0).The order of detecting a parent span goes as the following:Usingspan_extractorif exists.Detect from passed kwargs.Detectscope_manageractive span (opentracing.tracer.active_span).Detect using call stack frames.importopentracingfromopentracing_utilsimporttrace,extract_span_from_kwargs# decorate all your functions that require tracing# Normal traced function@trace()deftrace_me():pass# Traced function with access to created span in ``kwargs``@trace(operation_name='user.operation',pass_span=True)defuser_operation(user,op,**kwargs):current_span=extract_span_from_kwargs(**kwargs)current_span.set_tag('user.id',user.id)# Then do stuff ...# trace_me will have ``current_span`` as its parent.trace_me()# Traced function using ``follows_from`` instead of ``child_of`` reference.@trace(use_follows_from=True)deftrace_me_later():pass# Start a fresh trace - any parent spans will be ignored@trace(operation_name='epoch',ignore_parent_span=True)defstart_fresh():user={'id':1}# trace decorator will handle trace heirarchyuser_operation(user,'create')# trace_me will have ``epoch`` span as its parent.trace_me()@trace using scope_managerIn case you need to always use thescope_manager, then you can passuse_scope_manager=Trueto@trace.# ``use_scope_manager=True`` will always use scope_manager API for activating the new span.@trace(operation_name='traced',use_scope_manager=True)deftrace_me_via_scope_manager():# @trace will activate the current span using the ``scope_manager``.current_span=opentracing.tracer.active_spanassertcurrent_span.operation_name=='traced'# @trace will detect parent span from the ``scope_manager`` active span and automatically activate the new nested span.@trace(operation_name='nested')deftrace_and_detect_scope():nested_span=opentracing.tracer.active_spanassertnested_span.operation_name=='nested'trace_and_detect_scope()# current_span is back to be the active span.assertcurrent_span==opentracing.tracer.active_span# If the ``scope_manager`` API is activating the parent span, @trace will detect it and use the ``scope_manager`` for the child span as well.@trace()deftrace_and_detect_parent_scope():current_span=opentracing.tracer.active_spanassertcurrent_span.operation_name=='trace_and_detect_parent_scope'withopentracing.tracer.start_active_span('top_span',finish_on_close=True):# the child span will depend on the ``scope_manager`` to detect the ``top_span`` as the parent span for the following function call.trace_and_detect_parent_scope()Skip SpansIn certain cases you might need to skip certain spans while using the@tracedecorator.defskip_this_span(arg1,arg2,**kwargs):ifarg1=='special':# span should be skippedreturnTruereturnFalse@trace(skip_span=skip_this_span)deftraced(arg1,arg2):passtop_span=opentracing.tracer.start_span(operation_name='top_trace')withtop_span:# this call will be traced and have a span!traced('open','tracing')# this call won't be traced and no span to be added!traced('special','tracing')Broken tracesIf you plan to break nested traces, then it is recommended to pass the span to traced functions.top_span=opentracing.tracer.start_span(operation_name='top_trace')withtop_span:# This one gets ``top_span`` as parent spancall_traced()# Here, we break the trace, since we create a new span with no parentsbroken_span=opentracing.tracer.start_span(operation_name='broken_trace')withbroken_span:# This one gets ``broken_span`` as parent span (not consistent in 2.7 and 3.5)call_traced()# pass span as safer/guaranteed trace herecall_traced(span=broken_span)# ISSUE: Due to stack call inspection, next call will get ``broken_span`` instead of ``top_span``, which is wrong!!call_traced()# To get the ``top_span`` as parent span, then pass it to the traced callcall_traced(span=top_span)Multiple tracesIf you plan to use multiple traces then it is better to always pass the span as it is safer/guaranteed.Note: this should not be an issue ifscope_manageris used.first_span=opentracing.tracer.start_span(operation_name='first_trace')withfirst_span:# This one gets ``first_span`` as parent spancall_traced()second_span=opentracing.tracer.start_span(operation_name='second_trace')withsecond_span:# ISSUE: This one **could** get ``first_span`` as parent span (not consistent among Python versions)call_traced()# It is better to pass ``second_span`` explicitlycall_traced(span=second_span)Generators (yield)Using generators could get tricky and leads to invalid parent span inspection. It is recommended to pass the span explicitly.@trace(pass_span=True)defgen(**kwargs):s=extract_span_from_kwargs(**kwargs)# noqa# Extract and pass span to ``f2()`` otherwise it could get ``f1()`` as parent span instead of ``gen()``f2(span=s)foriinrange(10):yieldi@trace()deff2():pass@trace()deff1():list(gen())first_span=opentracing.tracer.start_span(operation_name='first_trace')withfirst_span:f1()External libraries and clientsDjangoFor tracingDjangoapplications. You can use the following:OpenTracingHttpMiddleware: for tracing incoming HTTP requests# In settings.py or equivalent Django configfromopentracing_utilsimportinit_opentracing_tracerinit_opentracing_tracer(YOUR_TRACER)# make sure opentracing.tracer is initialized properly.MIDDLEWARE=('opentracing_utils.OpenTracingHttpMiddleware',# goes first in the list# ... more middlewares here)# Further options# Add default tags to all incoming HTTP requests spans.OPENTRACING_UTILS_DEFAULT_TAGS={'my-default-tag':'tag-value'}# Add error tag on 4XX responses (default is ``True``).OPENTRACING_UTILS_ERROR_4XX=False# Override span operation_name (default is ``view_func.__name__``).OPENTRACING_UTILS_OPERATION_NAME_CALLABLE='my_app.utils.span_operation_name'# Use tracer scope manager (default is ``False``).OPENTRACING_UTILS_USE_SCOPE_MANAGER=True# Exclude certain requests from OpenTracingOPENTRACING_UTILS_SKIP_SPAN_CALLABLE='my_app.utils.skip_span'Here are the callables examples for overriding span operation names and skipping spans:# my_app/utils.pydefspan_operation_name(request,view_func,view_args,view_kwargs):return'edge_{}'.format(view_func.__name__)defskip_span(request,view_func,view_args,view_kwargs):ifview_func.__name__.startswith('no_trace_'):returnTruereturnFalseIn order to follow traces in your views, you can useextract_span_from_django_requestutility function.# my_app/views.pyfromopentracing_utilsimporttrace,extract_span_from_django_request@trace(span_extractor=extract_span_from_django_request,operation_name='custom_view')defmy_traced_view(request):...FlaskFor tracingFlaskapplications. This utility function adds a middleware that handles all incoming requests to the Flask application.fromopentracing_utilsimporttrace_flask,extract_span_from_flask_requestfromflaskimportFlaskapp=Flask(__name__)trace_flask(app)# You can use the ``scope_manager`` for managing all spans.trace_flask(app,use_scope_manager=True)# You can add default_tags or optionally treat 4xx responses as not an error (i.e no error tag in span)# trace_flask(app, default_tags={'always-there': True}, error_on_4xx=False)# Extract current span from request contextdefinternal_function():current_span=extract_span_from_flask_request()current_span.set_tag('internal',True)# You can skip requests spans.defskip_health_checks(request):returnrequest.path=='/health'# trace_flask(skip_span=skip_health_checks)RequestsFor tracingrequestsclient library for all outgoing requests.# trace_requests should be called as early as possible, before importing requestsfromopentracing_utilsimporttrace_requeststrace_requests()# noqa# You can use the ``scope_manager`` for managing all spans.trace_requests(use_scope_manager=True)# noqa# In case you want to include default span tags to be sent with every outgoing request.# trace_requests(default_tags={'account_id': '123'}, set_error_tag=False)# In case you want to keep the URL query args (masked by default in order to avoid leaking auth tokens etc...)# trace_requests(mask_url_query=False)# You can also mask URL path parameters (e.g. http://hostname/1 will be http://hostname/??/)# trace_requests(mask_url_path=True)# The library patches the requests library send functionality. This causes# all requests to propagate the span id's in the headers. Sometimes this is# undesireable so it's also possible to avoid tracing specific URL's or# endpoints. trace_requests accepts a list of regex patterns and matches the# request.url against these patterns, ignoring traces if any pattern matches.# trace_requests(ignore_url_patterns=[r".*hostname/endpoint"])importrequestsdefmain():span=opentracing.tracer.start_span(operation_name='main')withspan:# Following call will be traced as a ``child span`` and propagated via HTTP headers.requests.get('https://example.org')SQLAlchemyFor tracingSQLAlchemyclient library for all SQL queries.# trace_sqlalchemy can be used to trace all SQL queries.# By default, span operation_name will be deduced from the query statement (e.g. select, update, delete).fromopentracing_utilsimporttrace_sqlalchemytrace_sqlalchemy()# You can use the ``scope_manager`` for managing all spans.trace_sqlalchemy(use_scope_manager=True)# You can customize the span operation_name via supplying a callabledefget_sqlalchemy_span_op_name(conn,cursor,statement,parameters,context,executemany):# inspect statement and parameters etc...return'custom_operation_name'# trace_sqlalchemy(operation_name=get_sqlalchemy_span_op_name)# By default, trace_sqlalchemy will not set error tags for SQL errors/exceptions. You can change that via ``set_error_tag`` param.# trace_sqlalchemy(set_error_tag=True)# you can skip spans for certain SQL queries.defskip_inserts(conn,cursor,statement,parameters,context,executemany):returnstatement.lower().startswith('insert')# trace_sqlalchemy(skip_span=skip_inserts)# you can enrich the span with by supplying an ``enrich_span`` callable.defenrich_sql_span_parameters(span,conn,cursor,statement,parameters,context,executemany):span.set_tag('parameters',parameters)# trace_sqlalchemy(enrich_span=enrich_sql_span_parameters)LicenseThe MIT License (MIT)Copyright (c) 2017 Zalando SE,https://tech.zalando.comPermission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the “Software”), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.