Django and Flask apps can feel like black boxes until you instrument them with the Elastic APM Python agent, which turns them into transparent systems where you can see every request’s journey.

Let’s see it in action with a tiny Flask app.

from flask import Flask
from elasticapm.contrib.flask import ElasticAPM

app = Flask(__name__)
app.config['ELASTIC_APM'] = {
    'SERVICE_NAME': 'my-flask-app',
    'SERVER_URL': 'http://localhost:8200',
    'ENVIRONMENT': 'development'
}
apm = ElasticAPM(app)

@app.route('/')
def hello_world():
    return 'Hello, World!'

if __name__ == '__main__':
    app.run(debug=True)

When you run this app and hit http://localhost:5000/, the Elastic APM agent automatically captures the incoming HTTP request, notes the route handler (hello_world), and sends this data to your Elastic APM server. You’ll see a transaction appear in the Kibana APM UI, showing the duration, status code, and any errors.

The real power comes when you add external calls or database queries. Let’s extend our Flask app:

from flask import Flask
from elasticapm.contrib.flask import ElasticAPM
import requests
from elasticapm import capture_span

app = Flask(__name__)
app.config['ELASTIC_APM'] = {
    'SERVICE_NAME': 'my-flask-app',
    'SERVER_URL': 'http://localhost:8200',
    'ENVIRONMENT': 'development'
}
apm = ElasticAPM(app)

@app.route('/')
def hello_world():
    external_service_call()
    return 'Hello, World!'

@capture_span("external_service_call", span_type="external")
def external_service_call():
    try:
        response = requests.get('https://httpbin.org/delay/1', timeout=2)
        response.raise_for_status() # Raise an exception for bad status codes
    except requests.exceptions.RequestException as e:
        # Capture the exception, APM agent will automatically associate it with the current transaction
        apm.capture_exception()
        return "Error calling external service"
    return "External service called successfully"

if __name__ == '__main__':
    app.run(debug=True)

Now, when you visit /, the external_service_call function is executed. The @capture_span decorator tells the APM agent to specifically time this function and label it as an "external" span. If requests.get takes longer than 2 seconds or returns an error status, the try...except block catches it, and apm.capture_exception() explicitly sends the error to the APM server.

In Kibana, you’d see the initial transaction for /, and nested within it, a span for external_service_call. If the requests.get call timed out, you’d also see an error associated with that transaction, and the span would show its duration. This allows you to pinpoint whether slow responses are due to your application logic, external dependencies, or database operations.

The agent works by monkey-patching certain libraries (like requests, psycopg2, sqlalchemy) and using instrumentation APIs (like @capture_span and capture_exception) to hook into your code’s execution. For Django, it integrates via middleware.

The core problem this solves is observability in distributed systems. Before APM, if a user reported a slow page load, you’d have to guess: was it the database? An external API? Your own code? APM provides a unified view. You see the entire request trace from the web server, through your application code, to database queries and any outbound HTTP calls. This drastically reduces debugging time by showing you exactly where time is spent and where errors originate.

The agent automatically instruments many common libraries. For requests, it captures the HTTP request and response details as spans. For database interactions (like psycopg2 or SQLAlchemy), it captures the query, its duration, and parameters. You can further customize this by using the @capture_span decorator to measure specific functions or blocks of code, and apm.capture_exception() to record exceptions that might not otherwise be caught.

The SERVICE_NAME is crucial; it’s how the APM server groups all the data from a single application. The SERVER_URL points to your APM server (usually Kibana or the dedicated APM server). ENVIRONMENT lets you differentiate between development, staging, and production.

A common misconception is that you need to manually wrap every single function. While you can do that with @capture_span, the agent’s strength lies in its automatic instrumentation of popular libraries. For many applications, just configuring the agent and letting it run will provide significant value by tracing database calls and external HTTP requests. You only need manual instrumentation for custom business logic or specific code paths that are performance-critical or prone to errors.

The next step is to explore distributed tracing, where you can see requests flow across multiple microservices.

Want structured learning?

Take the full Elastic-apm course →