Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

FROM python:3.14-slim

WORKDIR /app

# Copy the project definition first so pip can install dependencies.
# Layering this before the source copy lets Docker cache the dependency
# layer when only application code changes.
COPY pyproject.toml .
RUN pip install --no-cache-dir .

# Copy application source and configuration after dependencies are installed.
COPY otel-config.yaml .
COPY app/ app/
COPY library/ library/

EXPOSE 8080

# Key environment variables (override with -e / --env):
# OTEL_EXPORTER_OTLP_ENDPOINT — OTLP HTTP endpoint (default: http://localhost:4318)
# OTEL_RESOURCE_ATTRIBUTES — extra resource attributes, e.g. deployment.environment=prod
# APPLICATION_PORT — listening port (default: 8080)
CMD ["python", "-m", "app.main"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
Rolldice — OpenTelemetry Reference Application (FastAPI)
=========================================================

This is the Python/FastAPI implementation of the
`OpenTelemetry reference application specification <https://opentelemetry.io/docs/getting-started/reference-application-specification/>`_.

It demonstrates all four OpenTelemetry signal types in a single service:

* **Traces** — automatic HTTP spans via ``FastAPIInstrumentor``, plus manual
child spans for the dice-rolling business logic.
* **Metrics** — a call counter, an outcome histogram, and an observable gauge,
defined in the library using only the OTel API.
* **Logs** — structured log records emitted through Python's ``logging`` module
and forwarded to OpenTelemetry via ``LoggingHandler`` (the log bridge).
* **Resources** — process and environment metadata attached automatically via
resource detectors.

Architecture
------------

The code is split into two modules to illustrate the recommended library/application boundary:

``library/rolldice.py``
Dice logic. Imports only from ``opentelemetry`` (the API package) — **no**
SDK imports. This makes the library usable in any application regardless
of which SDK implementation is chosen.

``otel-config.yaml``
Declarative configuration file. Defines the trace, metric, and log
pipelines (exporters, processors, samplers, resource detectors) in a YAML
file following the `OpenTelemetry Configuration Schema
<https://github.com/open-telemetry/opentelemetry-configuration>`_. This
replaces programmatic SDK initialization and allows operators to change
telemetry behavior without modifying code.

``app/telemetry.py``
SDK initialization. Loads ``otel-config.yaml`` using the SDK's file
configuration API and installs the configured providers globally. Also
sets up the Python logging bridge (``LoggingHandler``), which is not
covered by the config schema.

``app/main.py``
FastAPI application. Imports ``app.telemetry`` first, then creates the
``FastAPI`` app, instruments it, and defines the ``/rolldice`` endpoint.

Prerequisites
-------------

* Python ≥ 3.12
* ``pip`` or ``uv``

Install
-------

.. code-block:: bash

cd instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice
pip install .

Run
---

.. code-block:: bash

uvicorn app.main:app --host 0.0.0.0 --port 8080

Or directly:

.. code-block:: bash

python -m app.main

The server listens on port 8080 by default. Set ``APPLICATION_PORT`` to use a
different port.

Test the API
------------

.. code-block:: bash

# Single roll (default) — returns a number, e.g. "4"
curl "http://localhost:8080/rolldice"

# Five rolls — returns a JSON array, e.g. [3, 5, 2, 6, 1]
curl "http://localhost:8080/rolldice?rolls=5"

# With a player name (shown in DEBUG logs)
curl "http://localhost:8080/rolldice?rolls=3&player=Alice"

# Invalid input — returns HTTP 400 with error JSON
curl "http://localhost:8080/rolldice?rolls=abc"

# Non-positive rolls — returns HTTP 500 with empty body
curl "http://localhost:8080/rolldice?rolls=-1"

Telemetry output
~~~~~~~~~~~~~~~~

Span and metric data is printed to stdout (via the console exporters) even
without a running collector, so you can verify instrumentation locally.

Configuration
-------------

Telemetry behavior is defined in ``otel-config.yaml``. You can edit this file
to add or remove exporters, change sampling strategies, or adjust processor
settings — no code changes required.

Environment variables
---------------------

+-----------------------------------+----------------------------------------------+-------------------------+
| Variable | Description | Default |
+===================================+==============================================+=========================+
| ``OTEL_EXPORTER_OTLP_ENDPOINT`` | OTLP HTTP endpoint for the backend | ``http://localhost:4318``|
+-----------------------------------+----------------------------------------------+-------------------------+
| ``OTEL_RESOURCE_ATTRIBUTES`` | Extra resource attributes (``key=value,...``)| *(none)* |
+-----------------------------------+----------------------------------------------+-------------------------+
| ``APPLICATION_PORT`` | Listening port | ``8080`` |
+-----------------------------------+----------------------------------------------+-------------------------+

.. note::

``OTEL_SERVICE_NAME`` is set to ``rolldice`` in ``otel-config.yaml``. To
override it, edit the config file or add ``service.name`` to
``OTEL_RESOURCE_ATTRIBUTES``.

Run with an OTLP backend
------------------------

Start the `OpenTelemetry Collector <https://opentelemetry.io/docs/collector/>`_ or
another OTLP-compatible backend, then:

.. code-block:: bash

OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 \
uvicorn app.main:app --host 0.0.0.0 --port 8080

Docker
------

Build:

.. code-block:: bash

docker build -t rolldice .

Run (without a collector — console output only):

.. code-block:: bash

docker run -p 8080:8080 rolldice

Run (with a collector on the host):

.. code-block:: bash

docker run -p 8080:8080 \
-e OTEL_EXPORTER_OTLP_ENDPOINT=http://host.docker.internal:4318 \
rolldice
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""FastAPI application entry point for the rolldice reference application.

Start-up order matters for OpenTelemetry:
1. ``app.telemetry`` must be imported first — it installs the SDK providers
globally before anything else creates tracers or meters.
2. FastAPIInstrumentor must be called after the app is created but still at
import time (not inside a request handler) so it can patch the ASGI
middleware correctly.
3. Library objects (tracer, meter, metric instruments) are created at module
import time in ``library.rolldice``; because telemetry is already
configured by then, those calls immediately resolve to real SDK objects.
"""

import logging
import os

import uvicorn
from fastapi import FastAPI, Query, Response
from fastapi.responses import JSONResponse, PlainTextResponse

# Importing telemetry first ensures the SDK is initialized before the
# FastAPIInstrumentor (below) registers its ASGI middleware, and before
# library/rolldice.py creates its tracer and meter at module load time.
import app.telemetry # noqa: F401

from library import rolldice

# FastAPIInstrumentor automatically wraps every FastAPI route with an HTTP
# server span. It reads the ASGI scope to populate standard HTTP semantic
# convention attributes such as:
# • http.request.method (GET, POST, …)
# • http.response.status_code
# • url.path
# • server.address / server.port
# It also propagates the W3C TraceContext from incoming request headers so
# that distributed traces connect across service boundaries.
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor

logger = logging.getLogger(__name__)

app = FastAPI(
title="Rolldice",
description="OpenTelemetry reference application — rolls a six-sided die.",
version="0.1.0",
)

# instrument_app() wraps the FastAPI ASGI app with OTel middleware. This must
# be called after the app object is created and before the first request is
# served. The instrumentation is additive: manual spans created inside route
# handlers automatically become children of the HTTP span created here.
FastAPIInstrumentor.instrument_app(app)


@app.get("/rolldice")
@app.post("/rolldice")
async def rolldice_endpoint(
rolls: str | None = Query(default=None),
player: str | None = Query(default=None),
) -> Response:
"""Roll a six-sided die one or more times.

Query parameters:
- ``rolls``: number of dice to roll (default: 1, must be a positive integer)
- ``player``: optional player name included in debug log output

Returns a single number (1-6) when rolls is 1, or a JSON array of numbers
when rolls > 1.
"""
# Default to 1 roll when the parameter is omitted.
rolls_raw = rolls if rolls is not None else "1"

# Validate that the value is numeric before passing it to the library.
# Non-numeric input is a client error (HTTP 400).
try:
rolls_int = int(rolls_raw)
except ValueError:
logger.warning(
"HTTP 400: invalid rolls parameter %r (must be an integer)",
rolls_raw,
)
return JSONResponse(
status_code=400,
content={
"status": "error",
"message": "Parameter rolls must be a positive integer",
},
)

# Delegate dice logic to the library module. The library validates that
# rolls > 0 and raises ValueError for non-positive values, which we map
# to HTTP 500 per the reference application specification.
try:
results = rolldice.roll_dice(rolls_int, player)
except ValueError as exc:
logger.error(
"HTTP 500: library raised ValueError for rolls=%d — %s",
rolls_int,
exc,
)
return PlainTextResponse(status_code=500, content="")

logger.info(
"HTTP 200: %s rolled %d die/dice → %s",
player or "anonymous player",
rolls_int,
results,
)

# Return a single number when only one die was rolled, otherwise an array.
if rolls_int == 1:
return PlainTextResponse(content=str(results[0]))
return JSONResponse(content=results)


if __name__ == "__main__":
# APPLICATION_PORT lets operators change the listening port without
# modifying code, e.g. when running inside a container.
port = int(os.environ.get("APPLICATION_PORT", 8080))
uvicorn.run(app, host="0.0.0.0", port=port)
Loading