Celery is a powerful background task processor for Python applications. It is commonly used to offload time-consuming tasks like sending emails, processing images, or making API calls, allowing your web application to respond faster and stay responsive.
However, out-of-sight background tasks can quickly become front-of-mind concerns. These tasks can fail silently, introduce latency, or cause downstream issues that are hard to detect without visibility. That’s where observability comes in.
OpenTelemetry (OTel) is the open standard for collecting telemetry data such as traces, metrics, and logs. With wide support across the Python ecosystem, it’s the go-to solution for instrumenting your services and gaining full visibility into your application’s behavior, including Celery task queues.
In this post, we’ll walk through how to instrument a Celery app with OTel, add custom spans for deeper insights, and export your telemetry data to SolarWinds® Observability SaaS to visualize traces and troubleshoot issues more easily.
Set up a Sample Celery Task Project
To demonstrate the OpenTelemetry integration, we’ve built a small Python app that uses Celery as the message broker and Redis as the message broker. We’ll also include Flask to simulate a web request that triggers background tasks. Our app will let users submit a task via an HTTP request. Celery will pick it up and simulate a delay before returning a result. You can find the code at this GitHub repository, follow it, and try it out yourself.
Project Structure and Code Highlights
The initial folder structure for our project looks like this:
<span style="font-weight:400;">python-celery-demo/</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">├── app.py</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">├── celery_app.py</span>
<span style="font-weight:400;">├── demo.py</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">├── tasks.py</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">└── requirements.txt</span>
In tasks.py
, we defined a simple task that simulates work with a delay:
<span style="background-color:#fff0cf;"><span style="font-weight:400;">import</span><span style="font-weight:400;"> time</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">import</span><span style="font-weight:400;"> random</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">from</span><span style="font-weight:400;"> celery_app </span><span style="font-weight:400;">import</span><span style="font-weight:400;"> celery_app</span></span>
<span style="background-color:#fff0cf;"><span style="font-weight:400;">from</span><span style="font-weight:400;"> dotenv </span><span style="font-weight:400;">import</span><span style="font-weight:400;"> load_dotenv</span></span>
<span style="background-color:#fff0cf;"><span style="font-weight:400;"><br /></span><span style="font-weight:400;">@celery_app.task(name='process_task')</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">def</span> <span style="font-weight:400;">process_task</span><span style="font-weight:400;">(task_id: str, min_delay: int = </span><span style="font-weight:400;">3</span><span style="font-weight:400;">, max_delay: int = </span><span style="font-weight:400;">10</span><span style="font-weight:400;">) -> dict:</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">"""</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> Simulate a long-running task with a random delay.</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> Args:</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> task_id: Unique identifier for the task</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> min_delay: Minimum delay in seconds (default: 3)</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> max_delay: Maximum delay in seconds (default: 10)</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> Returns:</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> dict: Task result with status and completion time</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> """</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;"># Generate random delay between min_delay and max_delay</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> delay_seconds = random.randint(min_delay, max_delay)</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;"># Simulate processing time</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> time.sleep(delay_seconds)</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">return</span><span style="font-weight:400;"> {</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">'task_id'</span><span style="font-weight:400;">: task_id,</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">'status'</span><span style="font-weight:400;">: </span><span style="font-weight:400;">'completed'</span><span style="font-weight:400;">,</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">'processed_at'</span><span style="font-weight:400;">: time.strftime(</span><span style="font-weight:400;">'%Y-%m-%d %H:%M:%S'</span><span style="font-weight:400;">),</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">'message'</span><span style="font-weight:400;">: </span><span style="font-weight:400;">f'Task </span><span style="font-weight:400;">{task_id}</span><span style="font-weight:400;"> processed successfully'</span><span style="font-weight:400;">,</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">'processing_time'</span><span style="font-weight:400;">: delay_seconds</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> }</span></span>
The Celery background app, defined in celery_app.py
, connects to Redis and processes any tasks found in the queue there.
<span style="background-color:#fff0cf;"><span style="font-weight:400;">from</span><span style="font-weight:400;"> celery </span><span style="font-weight:400;">import</span><span style="font-weight:400;"> Celery</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">import</span><span style="font-weight:400;"> os</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">from</span><span style="font-weight:400;"> dotenv </span><span style="font-weight:400;">import</span><span style="font-weight:400;"> load_dotenv</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"># Load environment variables</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">load_dotenv()</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"># Create Celery instance</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">celery_app = Celery(</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">'tasks'</span><span style="font-weight:400;">,</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> broker=os.getenv(</span><span style="font-weight:400;">'CELERY_BROKER_URL'</span><span style="font-weight:400;">, </span><span style="font-weight:400;">'redis://localhost:6379/0'</span><span style="font-weight:400;">),</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> backend=os.getenv(</span><span style="font-weight:400;">'CELERY_RESULT_BACKEND'</span><span style="font-weight:400;">, </span><span style="font-weight:400;">'redis://localhost:6379/0'</span><span style="font-weight:400;">)</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">)</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"># Optional configuration</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">celery_app.conf.update(</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> task_serializer=</span><span style="font-weight:400;">'json'</span><span style="font-weight:400;">,</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> accept_content=[</span><span style="font-weight:400;">'json'</span><span style="font-weight:400;">],</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> result_serializer=</span><span style="font-weight:400;">'json'</span><span style="font-weight:400;">,</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> timezone=</span><span style="font-weight:400;">'UTC'</span><span style="font-weight:400;">,</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> enable_utc=</span><span style="font-weight:400;">True</span><span style="font-weight:400;">,</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">)</span></span>
Finally, the Flask application in <span style="background-color:#fff0cf;">app.py</span>
sets up a server to accept API requests for submitting a task and checking task statuses.
<span style="background-color:#fff0cf;"><span style="font-weight:400;">from</span><span style="font-weight:400;"> flask </span><span style="font-weight:400;">import</span><span style="font-weight:400;"> Flask, jsonify, request</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">import</span><span style="font-weight:400;"> uuid</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">from</span><span style="font-weight:400;"> tasks </span><span style="font-weight:400;">import</span><span style="font-weight:400;"> process_task</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">app = Flask(__name__)</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">@app.route('/submit-task', methods=['POST'])</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">def</span> <span style="font-weight:400;">submit_task</span><span style="font-weight:400;">():</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">"""</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> Endpoint to submit a new task for processing.</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> Accepts a JSON payload with optional min_delay and max_delay parameters.</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> """</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">try</span><span style="font-weight:400;">:</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> data = request.get_json() </span><span style="font-weight:400;">or</span><span style="font-weight:400;"> {}</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> min_delay = int(data.get(</span><span style="font-weight:400;">'min_delay'</span><span style="font-weight:400;">, </span><span style="font-weight:400;">3</span><span style="font-weight:400;">))</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> max_delay = int(data.get(</span><span style="font-weight:400;">'max_delay'</span><span style="font-weight:400;">, </span><span style="font-weight:400;">10</span><span style="font-weight:400;">))</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;"># Validate delay parameters</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">if</span><span style="font-weight:400;"> min_delay > max_delay:</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> min_delay, max_delay = max_delay, min_delay</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;"># Generate a unique task ID</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> task_id = str(uuid.uuid4())</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;"># Submit the task to Celery</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> task = process_task.delay(task_id, min_delay, max_delay)</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">return</span><span style="font-weight:400;"> jsonify({</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">'task_id'</span><span style="font-weight:400;">: task_id,</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">'celery_task_id'</span><span style="font-weight:400;">: task.id,</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">'status'</span><span style="font-weight:400;">: </span><span style="font-weight:400;">'submitted'</span><span style="font-weight:400;">,</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">'message'</span><span style="font-weight:400;">: </span><span style="font-weight:400;">'Task submitted successfully'</span><span style="font-weight:400;">,</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">'delay_range'</span><span style="font-weight:400;">: </span><span style="font-weight:400;">f'</span><span style="font-weight:400;">{min_delay}</span><span style="font-weight:400;">-</span><span style="font-weight:400;">{max_delay}</span><span style="font-weight:400;"> seconds'</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> }), </span><span style="font-weight:400;">202</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">except</span><span style="font-weight:400;"> Exception </span><span style="font-weight:400;">as</span><span style="font-weight:400;"> e:</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">return</span><span style="font-weight:400;"> jsonify({</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">'error'</span><span style="font-weight:400;">: str(e),</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">'status'</span><span style="font-weight:400;">: </span><span style="font-weight:400;">'error'</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> }), </span><span style="font-weight:400;">400</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">@app.route('/task-status/<task_id>', methods=['GET'])</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">def</span> <span style="font-weight:400;">get_task_status</span><span style="font-weight:400;">(task_id):</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">"""</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> Endpoint to check the status of a submitted task.</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> """</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">try</span><span style="font-weight:400;">:</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> task = process_task.AsyncResult(task_id)</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">if</span><span style="font-weight:400;"> task.ready():</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">return</span><span style="font-weight:400;"> jsonify({</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">'task_id'</span><span style="font-weight:400;">: task_id,</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">'status'</span><span style="font-weight:400;">: </span><span style="font-weight:400;">'completed'</span><span style="font-weight:400;">,</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">'result'</span><span style="font-weight:400;">: task.get()</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> })</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">return</span><span style="font-weight:400;"> jsonify({</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">'task_id'</span><span style="font-weight:400;">: task_id,</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">'status'</span><span style="font-weight:400;">: </span><span style="font-weight:400;">'processing'</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> })</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">except</span><span style="font-weight:400;"> Exception </span><span style="font-weight:400;">as</span><span style="font-weight:400;"> e:</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">return</span><span style="font-weight:400;"> jsonify({</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">'error'</span><span style="font-weight:400;">: str(e),</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">'status'</span><span style="font-weight:400;">: </span><span style="font-weight:400;">'error'</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> }), </span><span style="font-weight:400;">400</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">if</span><span style="font-weight:400;"> __name__ == </span><span style="font-weight:400;">'__main__'</span><span style="font-weight:400;">:</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> app.run(debug=</span><span style="font-weight:400;">True</span><span style="font-weight:400;">, host=</span><span style="font-weight:400;">'0.0.0.0'</span><span style="font-weight:400;">, port=</span><span style="font-weight:400;">5000</span><span style="font-weight:400;">)</span></span>
Demo Run of the Application
In one terminal window, go ahead and start up your Flask application.
<span style="background-color:#fff0cf;"><span style="font-weight:400;">$ </span><b>python app.py</b><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> * Serving Flask app 'app'</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> * Running on http://127.0.0.1:5000</span></span>
<span style="background-color:#fff0cf;"><span style="font-weight:400;"></span></span>
In another terminal window, start up Celery.
<span style="background-color:#fff0cf;"><span style="font-weight:400;">$ </span><b>celery -A tasks worker --loglevel=info</b><span style="font-weight:400;"><br /></span> <span style="font-weight:400;"><br /></span><span style="font-weight:400;">-------------- celery@demo-coder-machine v5.3.6 (emerald-rush)</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">…</span></span>
<span style="background-color:#fff0cf;"><span style="font-weight:400;">[12:46:12,850: INFO/MainProcess] Connected to redis://localhost:6379/0</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">[12:46:12,851: INFO/MainProcess] mingle: searching for neighbors</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">[12:46:13,857: INFO/MainProcess] mingle: all alone</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">[12:46:13,871: INFO/MainProcess] celery@demo-coder-machine ready.</span></span>
This walkthrough also includes a demo script that simulates adding tasks and checking their statuses.
<span style="background-color:#fff0cf;"><span style="font-weight:400;">$ python demo.py </span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">===========================================================================</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">Starting Flask-Celery Demo with 10 tasks</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">===========================================================================</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">===========================================================================</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">Submitting 10 tasks...</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">===========================================================================</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">Task 1/10 submitted…</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">Task 2/10 submitted…</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">Task 3/10 submitted…</span></span>
<span style="background-color:#fff0cf;"><span style="font-weight:400;">Task 4/10 submitted…</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">Task 5/10 submitted…</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">Task 6/10 submitted…</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">Task 7/10 submitted…</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">Task 8/10 submitted…</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">Task 9/10 submitted…</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">Task 10/10 submitted…</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">===========================================================================</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">Polling task statuses...</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">===========================================================================</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">Attempt 1/20 (Elapsed: 0.0s)</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">Progress: 0/10 tasks completed</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">Attempt 2/20 (Elapsed: 1.1s)</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">Progress: 0/10 tasks completed</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">Attempt 3/20 (Elapsed: 2.2s)</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">Progress: 0/10 tasks completed</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">Attempt 4/20 (Elapsed: 3.3s)</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">Progress: 0/10 tasks completed</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">Attempt 5/20 (Elapsed: 4.3s)</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">Progress: 0/10 tasks completed</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">Task 39c3d7fb-e160-4f7f-a2d7-440ac86be039 completed </span><span style="font-weight:400;">in</span><span style="font-weight:400;"> 5 seconds!</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">Result: {</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">"message"</span><span style="font-weight:400;">: </span><span style="font-weight:400;">"Task 39c3d7fb-e160-4f7f-a2d7-440ac86be039 processed successfully"</span><span style="font-weight:400;">,</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">"processed_at"</span><span style="font-weight:400;">: </span><span style="font-weight:400;">"2025-06-06 12:49:01"</span><span style="font-weight:400;">,</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">"processing_time"</span><span style="font-weight:400;">: 5,</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">"status"</span><span style="font-weight:400;">: </span><span style="font-weight:400;">"completed"</span><span style="font-weight:400;">,</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">"task_id"</span><span style="font-weight:400;">: </span><span style="font-weight:400;">"39c3d7fb-e160-4f7f-a2d7-440ac86be039"</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">}</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">Task 0f482a2c-4b47-4493-b65f-cbdbc9f876bf completed </span><span style="font-weight:400;">in</span><span style="font-weight:400;"> 5 seconds!</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">Result: {</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">"message"</span><span style="font-weight:400;">: </span><span style="font-weight:400;">"Task 0f482a2c-4b47-4493-b65f-cbdbc9f876bf processed successfully"</span><span style="font-weight:400;">,</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">"processed_at"</span><span style="font-weight:400;">: </span><span style="font-weight:400;">"2025-06-06 12:49:01"</span><span style="font-weight:400;">,</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">"processing_time"</span><span style="font-weight:400;">: 5,</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">"status"</span><span style="font-weight:400;">: </span><span style="font-weight:400;">"completed"</span><span style="font-weight:400;">,</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">"task_id"</span><span style="font-weight:400;">: </span><span style="font-weight:400;">"0f482a2c-4b47-4493-b65f-cbdbc9f876bf"</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">}</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">…</span></span>
<span style="background-color:#fff0cf;"><span style="font-weight:400;"><br /></span><span style="font-weight:400;">Attempt 15/20 (Elapsed: 14.8s)</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">Progress: 10/10 tasks completed</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">Processing Time Summary:</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">Task 39c3d7fb-e160-4f7f-a2d7-440ac86be039: 5 seconds</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">Task 0f482a2c-4b47-4493-b65f-cbdbc9f876bf: 5 seconds</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">Task 31005379-1e93-4bbc-a589-fd62c58e20ce: 6 seconds</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">Task afa02ae1-8d3c-457e-b56b-66fcdea4e779: 6 seconds</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">Task 95181dc3-ee32-45c5-a05a-64937e8c0583: 7 seconds</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">Task 3bdc5223-8146-4343-b7d7-fe4b393b7e02: 9 seconds</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">Task 7b5ff995-0f80-4f30-a698-15d1dc9604eb: 9 seconds</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">Task 4d6d61cc-2192-4dac-a401-4fa54d63efcd: 10 seconds</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">Task c9a4bb82-8a7c-4907-8865-2d368f3b4540: 5 seconds</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">Task 9d8e1b1f-59a0-4c2d-b8dd-56b348da3607: 9 seconds</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">Average processing time: 7.1 seconds</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">All tasks completed!</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">===========================================================================</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">Demo completed successfully!</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">===========================================================================</span></span>
Add OpenTelemetry Dependencies to the Project
Instrumenting the Flask application and the Celery task queue provides a rich source of data that can be used in debugging and optimization. Let's walk through how to use OpenTelemetry and the OpenTelemetry Protocol (OTLP) to capture telemetry—traces, metrics, and logs—for export to SolarWinds Observability SaaS.
Start by setting up the necessary dependencies. The core OpenTelemetry packages are <span style="background-color:#fff0cf;"><a style="background-color:#fff0cf;" href="https://pypi.org/project/opentelemetry-api/">opentelemetry-api</a></span>
and <a style="background-color:#fff0cf;" href="https://pypi.org/project/opentelemetry-sdk/">opentelemetry-sdk</a>
, which provide the fundamental functionality for creating and managing telemetry data. For our specific application stack, we'll also need <span style="background-color:#fff0cf;"><a style="background-color:#fff0cf;" href="https://pypi.org/project/opentelemetry-instrumentation-flask/">opentelemetry-instrumentation-flask</a></span>
and <span style="background-color:#fff0cf;"><a style="background-color:#fff0cf;" href="https://pypi.org/project/opentelemetry-instrumentation-celery/">opentelemetry-instrumentation-celery</a></span>
to monitor our background tasks. To send the telemetry data to SolarWinds Observability SaaS, we'll use <span style="background-color:#fff0cf;"><a style="background-color:#fff0cf;" href="https://pypi.org/project/opentelemetry-exporter-otlp/">opentelemetry-exporter-otlp</a></span>
(OpenTelemetry Protocol). We'll also include packages for requests, logging, and helping to ensure we follow standard naming conventions for our telemetry data.
Add these dependencies to requirements.txt with specific versions to help ensure compatibility and stability.
<span style="background-color:#fff0cf;"><span style="font-weight:400;">flask==</span><span style="font-weight:400;">3.0</span><span style="font-weight:400;">.2</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">celery==</span><span style="font-weight:400;">5.3</span><span style="font-weight:400;">.6</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">redis==</span><span style="font-weight:400;">5.0</span><span style="font-weight:400;">.1</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">python-dotenv==</span><span style="font-weight:400;">1.0</span><span style="font-weight:400;">.1</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">requests==</span><span style="font-weight:400;">2.31</span><span style="font-weight:400;">.0</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"># OpenTelemetry </span><span style="font-weight:400;">dependencies</span><span style="font-weight:400;"> with specific versions to avoid conflicts</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">importlib-metadata>=</span><span style="font-weight:400;">6.0</span><span style="font-weight:400;">,<</span><span style="font-weight:400;">7.0</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">protobuf>=</span><span style="font-weight:400;">3.19</span><span style="font-weight:400;">,<</span><span style="font-weight:400;">5.0</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">opentelemetry-api==</span><span style="font-weight:400;">1.23</span><span style="font-weight:400;">.0</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">opentelemetry-sdk==</span><span style="font-weight:400;">1.23</span><span style="font-weight:400;">.0</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">opentelemetry-exporter-otlp==</span><span style="font-weight:400;">1.23</span><span style="font-weight:400;">.0</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">opentelemetry-instrumentation-flask==</span><span style="font-weight:400;">0.44b0</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">opentelemetry-instrumentation-celery==</span><span style="font-weight:400;">0.44b0</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">opentelemetry-instrumentation-requests==</span><span style="font-weight:400;">0.44b0</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">opentelemetry-instrumentation-logging==</span><span style="font-weight:400;">0.44b0</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">opentelemetry-semantic-conventions==</span><span style="font-weight:400;">0.44b0</span></span>
Once the dependencies are defined, create a Python virtual environment to isolate and install your project dependencies using <span style="background-color:#fff0cf;">pip</span>
. This setup provides you with all the necessary tools to start collecting and exporting telemetry data from the application to SolarWinds Observability SaaS.
Configure the Environment
To manage your OpenTelemetry configuration securely and flexibly, use environment variables with <span style="background-color:#fff0cf;"><a style="background-color:#fff0cf;" href="https://pypi.org/project/python-dotenv/">python-dotenv</a></span>
. This approach allows you to keep sensitive information like API keys out of the codebase while making it easy to configure different environments.
OTLP endpoint and ingestion token
To send telemetry data to SolarWinds Observability SaaS, you will need to find your OpenTelemetry endpoint and create an API token for data ingestion. Follow these steps.
Log in to SolarWinds Observability SaaS. The URL, after you log in, may look similar to this:
<span style="background-color:#fff0cf;font-weight:400;">https://my.na-01.cloud.solarwinds.com/</span>
The xx-yy
part of the URL (<span style="background-color:#fff0cf;">na-0</span>
1 in this example) will depend on the data center used for your SolarWinds Observability SaaS account and organization. Take note of this.
Navigate to Settings > API Tokens.

On the API Tokens page, click Create API Token.

Specify a name for your new API token. Select Ingestion as the token type. Click Create API Token.

Copy the resulting token value.

Store Information in an .env
File
Create an .env
file in the project root with the following essential variables:
<span style="font-weight:400;"># OpenTelemetry Configuration</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">OTEL_SERVICE_NAME</span><span style="font-weight:400;">=python-celery-demo</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">OTEL_ENVIRONMENT</span><span style="font-weight:400;">=development</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">OTEL_EXPORTER_OTLP_ENDPOINT</span><span style="font-weight:400;">=https://otel.collector.</span><b>XX-YY</b><span style="font-weight:400;">.cloud.solarwinds.com:</span><span style="font-weight:400;">443</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">OTEL_EXPORTER_OTLP_API_KEY</span><span style="font-weight:400;">=</span><b>replace-with-solarwinds-observability-token</b><span style="font-weight:400;"><br /></span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"># Celery Configuration (optional, defaults shown)</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">CELERY_BROKER_URL</span><span style="font-weight:400;">=redis://localhost:</span><span style="font-weight:400;">6379</span><span style="font-weight:400;">/</span><span style="font-weight:400;">0</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">CELERY_RESULT_BACKEND</span><span style="font-weight:400;">=redis://localhost:</span><span style="font-weight:400;">6379</span><span style="font-weight:400;">/</span><span style="font-weight:400;">0</span>
The application already uses python-dotenv
(as seen in app.py
and tasks.py
), which automatically loads these variables when the application starts. The <span style="background-color:#fff0cf;">.env</span>
file should be added to <span style="background-color:#fff0cf;">.gitignore</span>
to prevent accidental commits of sensitive information. For production deployments, these environment variables would be set through the deployment platform's configuration management system.
Configure OpenTelemetry
To centralize the OpenTelemetry configuration, create an otel_config.py file that sets up all three pillars of observability: traces, metrics, and logs. The resulting file looks like this:
<span style="font-weight:400;">import</span><span style="font-weight:400;"> os</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">import</span><span style="font-weight:400;"> json</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">from</span><span style="font-weight:400;"> opentelemetry </span><span style="font-weight:400;">import</span><span style="font-weight:400;"> trace</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">from</span><span style="font-weight:400;"> opentelemetry.sdk.trace </span><span style="font-weight:400;">import</span><span style="font-weight:400;"> TracerProvider</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">from</span><span style="font-weight:400;"> opentelemetry.sdk.trace.export </span><span style="font-weight:400;">import</span><span style="font-weight:400;"> BatchSpanProcessor</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">from</span><span style="font-weight:400;"> opentelemetry.exporter.otlp.proto.grpc.trace_exporter </span><span style="font-weight:400;">import</span><span style="font-weight:400;"> OTLPSpanExporter</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">from</span><span style="font-weight:400;"> opentelemetry.sdk.resources </span><span style="font-weight:400;">import</span><span style="font-weight:400;"> Resource</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">from</span><span style="font-weight:400;"> opentelemetry.sdk.metrics </span><span style="font-weight:400;">import</span><span style="font-weight:400;"> MeterProvider</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">from</span><span style="font-weight:400;"> opentelemetry.exporter.otlp.proto.grpc.metric_exporter </span><span style="font-weight:400;">import</span><span style="font-weight:400;"> OTLPMetricExporter</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">from</span><span style="font-weight:400;"> opentelemetry.sdk.metrics.export </span><span style="font-weight:400;">import</span><span style="font-weight:400;"> PeriodicExportingMetricReader</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">from</span><span style="font-weight:400;"> opentelemetry.instrumentation.flask </span><span style="font-weight:400;">import</span><span style="font-weight:400;"> FlaskInstrumentor</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">from</span><span style="font-weight:400;"> opentelemetry.instrumentation.celery </span><span style="font-weight:400;">import</span><span style="font-weight:400;"> CeleryInstrumentor</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">from</span><span style="font-weight:400;"> opentelemetry.instrumentation.requests </span><span style="font-weight:400;">import</span><span style="font-weight:400;"> RequestsInstrumentor</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">from</span><span style="font-weight:400;"> opentelemetry.instrumentation.logging </span><span style="font-weight:400;">import</span><span style="font-weight:400;"> LoggingInstrumentor</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">from</span><span style="font-weight:400;"> opentelemetry.semconv.resource </span><span style="font-weight:400;">import</span><span style="font-weight:400;"> ResourceAttributes</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">from</span><span style="font-weight:400;"> opentelemetry.exporter.otlp.proto.grpc._log_exporter </span><span style="font-weight:400;">import</span><span style="font-weight:400;"> OTLPLogExporter</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">from</span><span style="font-weight:400;"> opentelemetry.sdk._logs </span><span style="font-weight:400;">import</span><span style="font-weight:400;"> LoggerProvider, LoggingHandler</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">from</span><span style="font-weight:400;"> opentelemetry.sdk._logs.export </span><span style="font-weight:400;">import</span><span style="font-weight:400;"> BatchLogRecordProcessor</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">from</span><span style="font-weight:400;"> opentelemetry._logs </span><span style="font-weight:400;">import</span><span style="font-weight:400;"> set_logger_provider</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">from</span><span style="font-weight:400;"> flask </span><span style="font-weight:400;">import</span><span style="font-weight:400;"> request</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">import</span><span style="font-weight:400;"> logging</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">import</span><span style="font-weight:400;"> grpc</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">def</span> <span style="font-weight:400;">setup_otel</span><span style="font-weight:400;">():</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">"""Configure OpenTelemetry with OTLP exporter."""</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">try</span><span style="font-weight:400;">:</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;"># Get configuration from environment variables</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> endpoint = os.getenv(</span><span style="font-weight:400;">'OTEL_EXPORTER_OTLP_ENDPOINT'</span><span style="font-weight:400;">)</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> api_key = os.getenv(</span><span style="font-weight:400;">'OTEL_EXPORTER_OTLP_API_KEY'</span><span style="font-weight:400;">)</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> service_name = os.getenv(</span><span style="font-weight:400;">'OTEL_SERVICE_NAME'</span><span style="font-weight:400;">, </span><span style="font-weight:400;">'python-celery-demo'</span><span style="font-weight:400;">)</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">if</span> <span style="font-weight:400;">not</span><span style="font-weight:400;"> endpoint </span><span style="font-weight:400;">or</span> <span style="font-weight:400;">not</span><span style="font-weight:400;"> api_key:</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">raise</span><span style="font-weight:400;"> ValueError(</span><span style="font-weight:400;">"OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_EXPORTER_OTLP_API_KEY must be set"</span><span style="font-weight:400;">)</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;"># Create gRPC metadata with API key</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> metadata = [</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> (</span><span style="font-weight:400;">'authorization'</span><span style="font-weight:400;">, </span><span style="font-weight:400;">f'Bearer </span><span style="font-weight:400;">{api_key.strip()}</span><span style="font-weight:400;">'</span><span style="font-weight:400;">)</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> ]</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;"># Create a resource with service information</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> resource = Resource.create({</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> ResourceAttributes.SERVICE_NAME: service_name,</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> ResourceAttributes.SERVICE_VERSION: </span><span style="font-weight:400;">"1.0.0"</span><span style="font-weight:400;">,</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> ResourceAttributes.DEPLOYMENT_ENVIRONMENT: os.getenv(</span><span style="font-weight:400;">'OTEL_ENVIRONMENT'</span><span style="font-weight:400;">, </span><span style="font-weight:400;">'development'</span><span style="font-weight:400;">)</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> })</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;"># Create channel credentials</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> channel_credentials = grpc.ssl_channel_credentials()</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;"># Configure trace provider</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> trace_provider = TracerProvider(resource=resource)</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> otlp_exporter = OTLPSpanExporter(</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> endpoint=endpoint,</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> credentials=channel_credentials,</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> headers=metadata,</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> timeout=</span><span style="font-weight:400;">30</span><span style="font-weight:400;">,</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> compression=grpc.Compression.Gzip,</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> )</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> span_processor = BatchSpanProcessor(otlp_exporter)</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> trace_provider.add_span_processor(span_processor)</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> trace.set_tracer_provider(trace_provider)</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;"># Configure metrics provider</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> metric_reader = PeriodicExportingMetricReader(</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> OTLPMetricExporter(</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> endpoint=endpoint,</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> credentials=channel_credentials,</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> headers=metadata,</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> timeout=</span><span style="font-weight:400;">30</span><span style="font-weight:400;">,</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> compression=grpc.Compression.Gzip,</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> )</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> )</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> meter_provider = MeterProvider(resource=resource, metric_readers=[metric_reader])</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;"># Configure logging provider</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> log_exporter = OTLPLogExporter(</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> endpoint=endpoint,</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> credentials=channel_credentials,</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> headers=metadata,</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> timeout=</span><span style="font-weight:400;">30</span><span style="font-weight:400;">,</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> compression=grpc.Compression.Gzip,</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> )</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> logger_provider = LoggerProvider(resource=resource) logger_provider.add_log_record_processor(BatchLogRecordProcessor(log_exporter))</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> set_logger_provider(logger_provider)</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;"># Get the root logger</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> root_logger = logging.getLogger()</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;"># Remove any existing handlers to avoid duplicates</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">for</span><span style="font-weight:400;"> handler </span><span style="font-weight:400;">in</span><span style="font-weight:400;"> root_logger.handlers[:]:</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> root_logger.removeHandler(handler)</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;"># Add OTLP handler to root logger</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> otlp_handler = LoggingHandler(logger_provider=logger_provider)</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> otlp_handler.setLevel(logging.INFO)</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> root_logger.addHandler(otlp_handler)</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;"># Add a console handler for local debugging</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> console_handler = logging.StreamHandler()</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> console_handler.setLevel(logging.INFO)</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> console_formatter = logging.Formatter(</span><span style="font-weight:400;">'%(asctime)s %(levelname)s [%(name)s] - %(message)s'</span><span style="font-weight:400;">)</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> console_handler.setFormatter(console_formatter)</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> root_logger.addHandler(console_handler)</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;"># Set the root logger level</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> root_logger.setLevel(logging.INFO)</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;"># Log startup message</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> root_logger.info(</span><span style="font-weight:400;">"OpenTelemetry initialized successfully"</span><span style="font-weight:400;">)</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">return</span><span style="font-weight:400;"> trace_provider, meter_provider</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">except</span><span style="font-weight:400;"> Exception </span><span style="font-weight:400;">as</span><span style="font-weight:400;"> e:</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> logging.error(</span><span style="font-weight:400;">f"Failed to initialize OpenTelemetry: </span><span style="font-weight:400;">{str(e)}</span><span style="font-weight:400;">"</span><span style="font-weight:400;">, exc_info=</span><span style="font-weight:400;">True</span><span style="font-weight:400;">)</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">raise</span>
The configuration in the above code snippet involves several key steps:
- Resource configuration: Define a resource (
Resource.create
) that identifies the service with attributes like service name, version, and environment. This metadata helps identify and filter telemetry data in SolarWinds Observability SaaS.
- Provider setup: Configure the
<span style="background-color:#fff0cf;">TracerProvider</span>
with an OTLP exporter that sends spans to SolarWinds Observability SaaS. The configuration includes endpoint information and authentication credentials. Similarly, configure the MeterProvider
, LoggerProvider
, and LoggingHandler
.
The configuration is initialized early in the application lifecycle, helping ensure that all telemetry data is properly collected and exported. In addition, the centralized approach makes it easy to maintain and modify your observability setup, while ensuring consistent telemetry collection across both your Flash we application and Celery workers.
Instrument the Application
With your OpenTelemetry configuration in place, you can instrument the application as needed.
Example Trace Implementation
For example, implement a trace in tasks.py
by modifying process_task
to look like this:
<span style="font-weight:400;">@celery_app.task(name='process_task')</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">def process_task(task_id, min_delay=3, max_delay=10):</span><span style="font-weight:400;"><br /><br /></span>
<span style="font-weight:400;"> …</span>
<span style="font-weight:400;"> # Get current span context for correlation</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> current_span = trace.get_current_span()</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> span_context = current_span.get_span_context() if current_span else None</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> … </span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><b>with tracer.start_as_current_span("process_task") as span:</b><b><br /></b><b> # Set span attributes</b><b><br /></b><b> span.set_attribute("task_id", task_id)</b><b><br /></b><b> span.set_attribute("min_delay", min_delay)</b><b><br /></b><b> span.set_attribute("max_delay", max_delay)</b><b><br /></b><b> </b><b><br /></b><b> # Generate random delay</b><b><br /></b><b> delay = random.uniform(min_delay, max_delay)</b><b><br /></b><b> span.set_attribute("actual_delay", delay)</b><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> # Simulate processing</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> time.sleep(delay)</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> # Prepare result without including the message</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> result = {</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> "task_id": task_id,</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> "status": "completed",</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> "processing_time": delay,</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> "min_delay": min_delay,</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> "max_delay": max_delay</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> }</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> return result</span>
Example Metrics and Logs Implementation
To instrument the application to collect various metrics, modify otel_config.py
to implement various auto-instrumentation tools. In the calls below, Flask and Celery are automatically instrumented, as are API requests. In addition, the configuration sets up a logging middleware for use within the Flask app.
<span style="font-weight:400;">def</span> <span style="font-weight:400;">instrument_app</span><span style="font-weight:400;">(app):</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">"""Instrument the Flask application with OpenTelemetry."""</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> FlaskInstrumentor().instrument_app(app)</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> RequestsInstrumentor().instrument()</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> CeleryInstrumentor().instrument()</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;"># Add a simple request logging middleware</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> @app.before_request</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">def</span> <span style="font-weight:400;">before_request</span><span style="font-weight:400;">():</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> current_span = trace.get_current_span()</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">if</span><span style="font-weight:400;"> current_span:</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;"># Log request details with trace context</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> logging.info(</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">"FLASK: Request received"</span><span style="font-weight:400;">,</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> extra={</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">"otel.service.name"</span><span style="font-weight:400;">: os.getenv(</span><span style="font-weight:400;">'OTEL_SERVICE_NAME'</span><span style="font-weight:400;">),</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">"otel.environment"</span><span style="font-weight:400;">: os.getenv(</span><span style="font-weight:400;">'OTEL_ENVIRONMENT'</span><span style="font-weight:400;">),</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">"otel.span_id"</span><span style="font-weight:400;">: format(current_span.get_span_context().span_id, </span><span style="font-weight:400;">"016x"</span><span style="font-weight:400;">),</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">"otel.trace_id"</span><span style="font-weight:400;">: format(current_span.get_span_context().trace_id, </span><span style="font-weight:400;">"032x"</span><span style="font-weight:400;">),</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">"http.method"</span><span style="font-weight:400;">: request.method,</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">"http.url"</span><span style="font-weight:400;">: request.url,</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">"http.route"</span><span style="font-weight:400;">: request.endpoint</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> }</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> )<br /></span>
Make sure, within app.py
, to call instrument_app
.
<span style="font-weight:400;">from</span><span style="font-weight:400;"> flask </span><span style="font-weight:400;">import</span><span style="font-weight:400;"> Flask, jsonify, request</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">import</span><span style="font-weight:400;"> uuid</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">from</span><span style="font-weight:400;"> tasks </span><span style="font-weight:400;">import</span><span style="font-weight:400;"> process_task</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">import</span><span style="font-weight:400;"> logging</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">from</span><span style="font-weight:400;"> dotenv </span><span style="font-weight:400;">import</span><span style="font-weight:400;"> load_dotenv</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">import</span><span style="font-weight:400;"> os</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">from</span><span style="font-weight:400;"> otel_config </span><span style="font-weight:400;">import</span><span style="font-weight:400;"> setup_otel, instrument_app</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"># Load environment variables from .env file</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">load_dotenv()</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"># Log environment variables (without sensitive data)</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">logging.info(</span><span style="font-weight:400;">f"OTEL_SERVICE_NAME: </span><span style="font-weight:400;">{os.getenv(</span><span style="font-weight:400;">'OTEL_SERVICE_NAME'</span><span style="font-weight:400;">)}</span><span style="font-weight:400;">"</span><span style="font-weight:400;">)</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">logging.info(</span><span style="font-weight:400;">f"OTEL_ENVIRONMENT: </span><span style="font-weight:400;">{os.getenv(</span><span style="font-weight:400;">'OTEL_ENVIRONMENT'</span><span style="font-weight:400;">)}</span><span style="font-weight:400;">"</span><span style="font-weight:400;">)</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">logging.info(</span><span style="font-weight:400;">f"OTEL_EXPORTER_OTLP_ENDPOINT: </span><span style="font-weight:400;">{os.getenv(</span><span style="font-weight:400;">'OTEL_EXPORTER_OTLP_ENDPOINT'</span><span style="font-weight:400;">)}</span><span style="font-weight:400;">"</span><span style="font-weight:400;">)</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">logging.info(</span><span style="font-weight:400;">f"OTEL_EXPORTER_OTLP_HEADERS present: </span><span style="font-weight:400;">{</span><span style="font-weight:400;">'Yes'</span> <span style="font-weight:400;">if</span><span style="font-weight:400;"> os.getenv(</span><span style="font-weight:400;">'OTEL_EXPORTER_OTLP_HEADERS'</span><span style="font-weight:400;">) </span><span style="font-weight:400;">else</span> <span style="font-weight:400;">'No'</span><span style="font-weight:400;">}</span><span style="font-weight:400;">"</span><span style="font-weight:400;">)</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"># Initialize OpenTelemetry</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">try</span><span style="font-weight:400;">:</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> trace_provider, meter_provider = setup_otel()</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> logging.info(</span><span style="font-weight:400;">"OpenTelemetry initialized successfully"</span><span style="font-weight:400;">)</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">except</span><span style="font-weight:400;"> Exception </span><span style="font-weight:400;">as</span><span style="font-weight:400;"> e:</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> logging.error(</span><span style="font-weight:400;">f"Failed to initialize OpenTelemetry: </span><span style="font-weight:400;">{str(e)}</span><span style="font-weight:400;">"</span><span style="font-weight:400;">)</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"> </span><span style="font-weight:400;">raise</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">app = Flask(__name__)</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"><br /></span><span style="font-weight:400;"># Instrument the Flask application</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">instrument_app(app)</span><span style="font-weight:400;"><br /></span><span style="font-weight:400;">...</span>
Verify Telemetry Data Export
With OpenTelemetry configured to export telemetry data to SolarWinds Observability SaaS, and the application properly instrumented, start up the application and the Celery task queue process. Then, run the demo script.
After that has run, log in to SolarWinds Observability SaaS to verify the proper ingestion of telemetry data. Navigate to APM. There, you'll see your application listed.
<span style="font-weight:400;"></span>
<span style="font-weight:400;"><img style="max-height:615px;max-width:820px;" src="https://us.v-cdn.net/6038570/uploads/communityserver-blogs-components-weblogfiles/00/00/00/00/69/pastedimage1751386582679v5.png" alt=" " /></span>
Clicking on the listed application will show the presence of captured traces, metrics, and logs, available in the sub-navigation of tabs at the top of the page.
</code></p><p><span style="background-color:#fff0cf;"><code><span style="font-weight:400;"></span>
Click on the Traces tab. API request traces are shown across the various runs of the demo script.

Clicking on the details icon for any individual trace shows a breakdown of durations spent with each custom span.

Return to the APM details page. Then, navigate to Metrics. Metrics captured alongside the auto-instrumented traces are displayed.

Individual metric visualizations are available, but you can also create custom dashboards that overlay metrics or play them side by side for analysis and correlation.

The Logs tab shows every log event emitted by the application and sent to SolarWinds Observability SaaS.

This is helpful for application debugging as well as monitoring resources and transactions.
Conclusion
By instrumenting your Python (Flask/Celery) application with OpenTelemetry, you can transform your background tasks from black boxes into fully observable components of your system. The combination of automatic instrumentation for Flask, Celery, and HTTP requests, along with custom spans and structured logging, helps you create a complete picture of application behavior. Now, you can track requests from the initial web call through the entire task lifecycle, including queue time, processing duration, and any external service calls.
In SolarWinds Observability, this telemetry data comes together to provide powerful insights. You can visualize the complete request flow across your distributed system, set up alerts for task failures or performance degradation, and use the correlated logs to diagnose issues quickly. Seeing real-time task queue lengths, processing times, and error rates helps you proactively manage your application's health and performance.
Whether debugging a specific task failure or optimizing your system's overall performance, the observability data you've set up provides the context to make informed decisions and maintain a reliable application.
To learn more about OpenTelemetry with SolarWinds Observability SaaS, try it for free for 30 days.
<span style="font-weight:400;"></span>
<span style="font-weight:400;"></span>
<span style="font-weight:400;"></span>
<span style="font-weight:400;"></span>
<span style="background-color:#fff0cf;font-weight:400;"></span>
<span style="background-color:#fff0cf;"><span style="font-weight:400;"></span></span>
<span style="background-color:#fff0cf;"><span style="font-weight:400;"></span></span>
<span style="background-color:#fff0cf;"><span style="font-weight:400;"></span></span>
<span style="background-color:#fff0cf;"><span style="font-weight:400;"></span></span>
<span style="background-color:#fff0cf;"><span style="font-weight:400;"></span></span>
<span style="background-color:#fff0cf;"><span style="font-weight:400;"></span></span>
<span style="background-color:#fff0cf;"><span style="font-weight:400;"></span></span>
<span style="font-weight:400;"></span>