Skill 46 · Setting Up CloudWatch Observability
Subchapter 46.15
references/cloudwatch-omni/instrumentation/ec2-python.mdMarkdown9 KBView on GitHub
Install the ADOT Python distro on an EC2 instance and start the application through the opentelemetry-instrument wrapper, by editing the instance’s UserData (or the systemd unit that starts the app).
Read instrumentation.md first — it defines what is out of scope (OTLP endpoints, Application Signals). Deploying a collector is a separate, optional step — collector-ec2.md.
Do NOT:
OTEL_TRACES_SAMPLER=xray, or any localhost:4316 / localhost:2000 endpoint — these stay forbidden in all casesOTEL_EXPORTER_OTLP_* — unless you are also deploying a collector (collector-ec2.md), which sets OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_EXPORTER_OTLP_PROTOCOL deliberatelyOTEL_AWS_APPLICATION_SIGNALS_*, OTEL_AWS_SERVICE_EVENTS_*, or OTEL_AWS_DYNAMIC_INSTRUMENTATION_*CloudWatchAgentServerPolicy or AWSXRayDaemonWriteAccess to the instance role — unless you are also deploying a collector (collector-ec2.md), which requires CloudWatchAgentServerPolicy on the instance rolecdk deploy / terraform apply, or modify a running instance in place.py filesOn IAM: instrumentation on its own needs no permissions, so this change adds none. The instance may later need permission to reach wherever telemetry is sent — that belongs with the destination, not this guide. If you deploy a collector instead (collector-ec2.md), the collector is what gets the IAM — the workload still gets none.
If you cannot determine a value from the IaC, ask the user. Do not guess.
docker run / docker start → Docker. python, flask run, gunicorn, uwsgi, manage.py runserver → runs directly on the instance. If it is unclear, ask.<APP_DIR> — where the application code lives on the instance.<ENTRY_POINT> — the module or file that starts the app, and the exact startup command (it may be a WSGI/ASGI server rather than python).<SERVICE_NAME> — the application or stack name; becomes OTEL_SERVICE_NAME.<VENV>/bin/pip) so opentelemetry-instrument resolves on the same interpreter.yum, Amazon Linux 2023 uses dnf, Ubuntu/Debian uses apt.Terraform heredoc warning: when adding lines to a user_data heredoc, match the exact leading whitespace of the existing lines. <<-EOF only strips indentation when it is consistent; inconsistent indentation leaves spaces before #!/bin/bash and cloud-init fails.
instance.userData.addCommands(
'# Install the ADOT Python distro (provides the opentelemetry-instrument wrapper)',
'pip install aws-opentelemetry-distro',
);If the application runs from a virtual environment, install into it instead so the wrapper and the app share an interpreter:
instance.userData.addCommands(
'<VENV>/bin/pip install aws-opentelemetry-distro',
);Place this after the application’s own dependency install.
Find the existing startup command and prefix it with opentelemetry-instrument, leaving the rest of the command exactly as it was:
instance.userData.addCommands(
'export OTEL_SERVICE_NAME=<SERVICE_NAME>',
'',
'cd <APP_DIR>',
'opentelemetry-instrument python <ENTRY_POINT>',
);The wrapper goes in front of whatever the app actually uses:
opentelemetry-instrument flask run
opentelemetry-instrument gunicorn -c gunicorn.conf.py app:app
opentelemetry-instrument uwsgi --ini uwsgi.ini
opentelemetry-instrument python manage.py runserver 0.0.0.0:<PORT> --noreloadFor Django’s dev server, --noreload is required — the autoreloader re-executes the process and the instrumentation is lost.
An export in UserData does not reach a process started by a .service unit — ExecStart is a fresh process that does not inherit the UserData shell’s environment. Put the variable on the unit and wrap ExecStart:
# /etc/systemd/system/<SERVICE_NAME>.service (add to the [Service] section)
[Service]
WorkingDirectory=<APP_DIR>
Environment=OTEL_SERVICE_NAME=<SERVICE_NAME>
ExecStart=/usr/local/bin/opentelemetry-instrument /usr/bin/python <ENTRY_POINT>ExecStart requires an absolute path for the first argument. Confirm where opentelemetry-instrument was installed (/usr/local/bin/, or <VENV>/bin/ for a venv install) rather than assuming. Then add systemctl daemon-reload and systemctl restart <SERVICE_NAME> to UserData.
The distro has to be installed inside the container — a pip install in UserData installs on the host, where the containerized interpreter cannot see it. Unlike the other languages, the ADOT Python SDK is a Python package resolved by the container’s own interpreter, so the image has to be rebuilt.
Add the install after the existing dependency install in the Dockerfile, and wrap CMD:
# After the existing pip install / pip install -r requirements.txt
RUN pip install --no-cache-dir aws-opentelemetry-distro
# Wrap the existing CMD — keep its arguments unchanged
# Before: CMD ["python", "app.py"]
CMD ["opentelemetry-instrument", "python", "app.py"]
# Other shapes:
# CMD ["opentelemetry-instrument", "flask", "run"]
# CMD ["opentelemetry-instrument", "gunicorn", "-c", "gunicorn.conf.py", "djangoapp.wsgi:application"]Then add the service name to the existing docker run, keeping every flag it already had:
instance.userData.addCommands(
`docker run -d --name <APP_NAME> \\`,
` -e OTEL_SERVICE_NAME=<SERVICE_NAME> \\`,
` <IMAGE_URI>`,
);Tell the user this path requires rebuilding and republishing the image.
Gunicorn with its default sync workers and no --preload needs no special handling — instrumentation loads and produces both server spans and nested client spans normally.
The configurations that more often need attention are --preload and async worker classes (gevent, eventlet). If the workload uses one of those and no telemetry appears, the usual remedy is a Gunicorn post_fork hook that re-initializes the SDK — flag that to the user rather than changing the server configuration silently.
After the user deploys and the instance boots:
journalctl -u <SERVICE_NAME> -o cat for a systemd service. ADOT Python prints no line containing “opentelemetry” — grep for the real one: Configuration of aws_configurator not loaded, configurator already loaded. The -o cat matters: under the wrapper the syslog identifier is opentelemetry-instrument[<pid>], so a plain journalctl | grep -i opentelemetry matches every line. /var/log/cloud-init-output.log shows whether the pip install succeeded.docker logs <APP_NAME>.Until a receiver exists at the default OTLP endpoint, exporter connection errors are expected — that is the next step, not a failure of instrumentation.
Tell the user:
“I’ve wired ADOT Python auto-instrumentation into your EC2 deployment.
Changes:
aws-opentelemetry-distroopentelemetry-instrument and set OTEL_SERVICE_NAMEEnvironment= and wrapped ExecStart (an export in UserData would not reach it)Not changed: your application source, the instance role’s IAM policies, and the instance’s software — no CloudWatch Agent or collector was installed.
Next steps:
localhost:4318). Two ways to fix that: deploy an OTel Collector alongside it and export to that (collector-ec2.md), or point OTEL_EXPORTER_OTLP_ENDPOINT at an OTLP endpoint you already have.Let me know if you’d like adjustments before you deploy.”