elastic / apm-agent-python

https://www.elastic.co/guide/en/apm/agent/python/current/index.html
BSD 3-Clause "New" or "Revised" License
411 stars 216 forks source link

Support for motor (async pymongo) #1934

Open jeff-goddard opened 10 months ago

jeff-goddard commented 10 months ago

Is your feature request related to a problem? Please describe. The Python Elastic APM agent does not automatically instrument async calls to mongodb using motor

Describe the solution you'd like Support for automatic instrumentation of motor similar to the way pymongo is instrumented

Describe alternatives you've considered For now we have lived without instrumentation, although it looks like it should be possible to adapt the pymongo wrappers

Additional context This is the motor project: https://github.com/mongodb/motor It should be similar to pymongo

basepi commented 10 months ago

Thanks for the request!

glexey commented 6 months ago

I found out that adopting opentelemetry's approach to tracing mongo driver events works well with motor. Here's a simple instrumenter based on this approach:

from elasticapm.traces import execution_context
from pymongo import monitoring

def instrument_app():
    mongo_tracer = MongoCommandTracer()
    monitoring.register(mongo_tracer)

class MongoCommandTracer(monitoring.CommandListener):

    def started(self, event: monitoring.CommandStartedEvent):
        command = event.command.get(event.command_name, "")
        name = event.command_name
        if command:
            name += f".{command}"

        transaction = execution_context.get_transaction()
        if transaction and transaction.is_sampled:
            transaction.begin_span(name=name, span_type="db", leaf=True, span_subtype="mongodb", span_action="query")

    def succeeded(self, event: monitoring.CommandSucceededEvent):
        transaction = execution_context.get_transaction()
        if transaction and transaction.is_sampled:
            transaction.end_span(skip_frames=0, duration=None, outcome="success")

    def failed(self, event: monitoring.CommandFailedEvent):
        transaction = execution_context.get_transaction()
        if transaction and transaction.is_sampled:
            transaction.end_span(skip_frames=0, duration=None, outcome="failure")

Original code adds extra metadata such as server host:port, which wasn't needed in my case.

jeff-goddard commented 4 months ago

Thanks @glexey

I added a few more parameters for my own use case, and this works well.

Not sure if something like this should replace the official instrumentation?