zalando-zmon / opentracing-utils

Convenient utilities for adding OpenTracing support in your python projects
MIT License
20 stars 8 forks source link

Nested lib spans #25

Closed while-loop closed 6 years ago

while-loop commented 6 years ago

Does this library support spans across multiple libraries? Like using flask and sqlalchemy within the same trace?

def test_nested_libs(self):
    engine = create_engine('sqlite://')
    User.metadata.create_all(engine)
    session = sessionmaker(bind=engine)()
    app = Flask('test_app')

    @app.route('/')
    def root():
        user = User(name='Tracer', is_active=True)
        session.add(user)
        session.commit()
        return user

    trace_flask(app)
    trace_sqlalchemy()

    with app.app_context():
        print app.test_client().get('/').data

With 1 flask trace with a child sql span

mohabusama commented 6 years ago

Hi @while-loop ,

Yes, it can be done, but with a caveat! here is a test case that records the spans and asserts their hierarchy:

def test_trace_flask_sqlalchemy_nested():
    recorder = Recorder()
    recorder.spans = []
    tracer = BasicTracer(recorder=recorder)
    tracer.register_required_propagators()
    opentracing.tracer = tracer

    engine = create_engine('sqlite://')
    User.metadata.create_all(engine)
    session = sessionmaker(bind=engine)()
    app = Flask('test_app')

    @app.route('/')
    @trace(span_extractor=extract_span_from_flask_request)  # We need this to provide a clear parent for any child spans (in our case sqlalchemy operations)
    def root():
        user = User(name='Tracer', is_active=True)
        session.add(user)
        session.commit()
        return 'OK'

    trace_flask(app)
    trace_sqlalchemy()

    with app.app_context():
        print(app.test_client().get('/').data)

    assert len(recorder.spans) == 3

    sql_span = recorder.spans[0]
    assert sql_span.operation_name == 'insert'
    assert sql_span.tags['db.statement'] == 'INSERT INTO users (name, is_active) VALUES (?, ?)'

    trace_span = recorder.spans[1]
    assert trace_span.operation_name == 'root'

    flask_span = recorder.spans[2]
    assert flask_span.operation_name == 'root'

    assert sql_span.parent_id == trace_span.context.span_id
    assert trace_span.parent_id == flask_span.context.span_id

The reason we need the @trace around the root() handler is because trace_flask just inserts the span in the flask.request object, but root does not know about it. So we need to tell root that you should pick up the parent span from the request and from this point on, any traced function calls can be child spans. This of course adds an extra span in the trace (we have 3 spans instead of 2), but I feel this is the most straight forward way to do it, and we are using it in production.

If you want to show only two spans, then you have 2 options:

trace_sqlalchemy(span_extractor=extract_span_from_flask_request)

I believe with opentracing==2.0 this can be handled even better if the Tracer implements the new scope API. But it is still not supported yet via opentracing-utils.

I have created couple of issue to enhance the quick start and make these usecases clearer: https://github.com/zalando-zmon/opentracing-utils/issues/26 https://github.com/zalando-zmon/opentracing-utils/issues/27

while-loop commented 6 years ago

Awesome! Thanks for the insight and examples!