From 2b97f1e01ea47ce66baedbd64a4deae4cdb67ab1 Mon Sep 17 00:00:00 2001 From: Reggie Dugard Date: Thu, 21 May 2026 08:50:37 -0700 Subject: [PATCH 1/3] Add rolldice reference application (FastAPI) Implements the OpenTelemetry reference application specification [1] with both uninstrumented and instrumented versions of a FastAPI dice-rolling service. The instrumented version uses declarative (file-based) YAML configuration for the telemetry pipeline with OTLP HTTP and console exporters for all three signal types (traces, metrics, logs). [1] https://opentelemetry.io/docs/getting-started/reference-application-specification/ Assisted-by: Claude Opus 4.6 --- .../examples/rolldice/Dockerfile | 36 ++++ .../examples/rolldice/README.rst | 160 +++++++++++++++ .../examples/rolldice/app/__init__.py | 13 ++ .../examples/rolldice/app/main.py | 133 +++++++++++++ .../examples/rolldice/app/telemetry.py | 184 ++++++++++++++++++ .../examples/rolldice/library/__init__.py | 13 ++ .../examples/rolldice/library/rolldice.py | 163 ++++++++++++++++ .../examples/rolldice/otel-config.yaml | 72 +++++++ .../examples/rolldice/pyproject.toml | 38 ++++ .../rolldice/uninstrumented/app/__init__.py | 13 ++ .../rolldice/uninstrumented/app/main.py | 100 ++++++++++ .../uninstrumented/library/__init__.py | 13 ++ .../uninstrumented/library/rolldice.py | 54 +++++ 13 files changed, 992 insertions(+) create mode 100644 instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/Dockerfile create mode 100644 instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/README.rst create mode 100644 instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/app/__init__.py create mode 100644 instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/app/main.py create mode 100644 instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/app/telemetry.py create mode 100644 instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/library/__init__.py create mode 100644 instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/library/rolldice.py create mode 100644 instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/otel-config.yaml create mode 100644 instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/pyproject.toml create mode 100644 instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/uninstrumented/app/__init__.py create mode 100644 instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/uninstrumented/app/main.py create mode 100644 instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/uninstrumented/library/__init__.py create mode 100644 instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/uninstrumented/library/rolldice.py diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/Dockerfile b/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/Dockerfile new file mode 100644 index 00000000000..338d654d383 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/Dockerfile @@ -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"] diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/README.rst b/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/README.rst new file mode 100644 index 00000000000..c685899307e --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/README.rst @@ -0,0 +1,160 @@ +Rolldice — OpenTelemetry Reference Application (FastAPI) +========================================================= + +This is the Python/FastAPI implementation of the +`OpenTelemetry 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 + `_. 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 `_ 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 diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/app/__init__.py b/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/app/__init__.py new file mode 100644 index 00000000000..b0a6f428417 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/app/__init__.py @@ -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. diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/app/main.py b/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/app/main.py new file mode 100644 index 00000000000..b0b3bd5e229 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/app/main.py @@ -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) diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/app/telemetry.py b/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/app/telemetry.py new file mode 100644 index 00000000000..ca3c7a03ded --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/app/telemetry.py @@ -0,0 +1,184 @@ +# 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. + +"""OpenTelemetry SDK initialization via declarative (file-based) configuration. + +This module loads ``otel-config.yaml`` and uses it as the single source of +truth for the telemetry pipeline configuration. + +────────────────────────────────────────────────────────────────────────────── +STATUS: The SDK's file configuration module (opentelemetry-sdk >= 1.41) can +parse and validate the YAML, but the released version does not yet wire up +end-to-end application. Specifically: + + • load_config_file() returns an OpenTelemetryConfiguration dataclass whose + nested fields (resource, tracer_provider, etc.) are still raw dicts rather + than typed model instances. + + • configure_logger_provider() has not been released yet (merged to main in + PR #4990, 2026-04-13). + + • A top-level configure_sdk(config) orchestrator is proposed in issue #5126 + but not yet implemented. + +Once the SDK ships configure_sdk() (or the individual configure_* functions +work with the loader output), this entire file can be reduced to: + + from opentelemetry.sdk._configuration.file import configure_sdk, load_config_file + config = load_config_file(str(_CONFIG_PATH)) + configure_sdk(config) + +Until then, this module parses the YAML for validation and endpoint extraction, +then constructs providers programmatically to match the declared config. + +The Python logging bridge (LoggingHandler) is not part of the declarative +config schema and will always require a small programmatic setup step. +────────────────────────────────────────────────────────────────────────────── + +Importing this module activates the SDK globally. +""" + +import logging +from pathlib import Path + +from opentelemetry import _logs, metrics, trace +from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter +from opentelemetry.exporter.otlp.proto.http.metric_exporter import ( + OTLPMetricExporter, +) +from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter +from opentelemetry.instrumentation.logging.handler import LoggingHandler +from opentelemetry.sdk._configuration.file import load_config_file +from opentelemetry.sdk._logs import LoggerProvider +from opentelemetry.sdk._logs.export import BatchLogRecordProcessor, ConsoleLogExporter +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.metrics.export import ( + ConsoleMetricExporter, + PeriodicExportingMetricReader, +) +from opentelemetry.sdk.resources import ( + OTELResourceDetector, + ProcessResourceDetector, + Resource, + SERVICE_NAME, +) +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import ( + BatchSpanProcessor, + ConsoleSpanExporter, + SimpleSpanProcessor, +) + +_CONFIG_PATH = Path(__file__).resolve().parent.parent / "otel-config.yaml" + + +# ────────────────────────────────────────────────────────────────────────────── +# Helpers for extracting values from the parsed config (raw dicts). +# These can be removed once configure_sdk() lands in the SDK. +# ────────────────────────────────────────────────────────────────────────────── + + +def _build_resource(config) -> Resource: + """Build a Resource from the parsed config's resource section.""" + service_name = "unknown_service" + if config.resource and isinstance(config.resource, dict): + for attr in config.resource.get("attributes", []): + if attr.get("name") == "service.name": + service_name = attr["value"] + + base = Resource.create({SERVICE_NAME: service_name}) + process_resource = ProcessResourceDetector().detect() + otel_resource = OTELResourceDetector().detect() + return base.merge(process_resource).merge(otel_resource) + + +def _get_otlp_endpoint(provider_dict, signal_path) -> str | None: + """Extract the OTLP HTTP endpoint from a provider config dict.""" + if not isinstance(provider_dict, dict): + return None + items = provider_dict.get("processors") or provider_dict.get("readers") or [] + for item in items: + if not isinstance(item, dict): + continue + for processor_type in ("batch", "simple", "periodic"): + proc = item.get(processor_type) + if not isinstance(proc, dict): + continue + exporter = proc.get("exporter", {}) + if isinstance(exporter, dict) and "otlp_http" in exporter: + otlp = exporter["otlp_http"] + if isinstance(otlp, dict): + return otlp.get("endpoint") + return None + + +# ────────────────────────────────────────────────────────────────────────────── + + +def configure_opentelemetry() -> None: + """Load otel-config.yaml and install the configured SDK globally. + + Parses and validates the YAML via load_config_file(), then constructs + providers programmatically using endpoints from the parsed config. + """ + config = load_config_file(str(_CONFIG_PATH)) + resource = _build_resource(config) + + # ── Traces ── + tracer_provider = TracerProvider(resource=resource) + trace.set_tracer_provider(tracer_provider) + + endpoint = _get_otlp_endpoint(config.tracer_provider, "traces") + tracer_provider.add_span_processor( + BatchSpanProcessor(OTLPSpanExporter(endpoint=endpoint)) + ) + tracer_provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter())) + + # ── Metrics ── + endpoint = _get_otlp_endpoint(config.meter_provider, "metrics") + metric_readers = [ + PeriodicExportingMetricReader(OTLPMetricExporter(endpoint=endpoint)), + PeriodicExportingMetricReader(ConsoleMetricExporter()), + ] + metrics.set_meter_provider( + MeterProvider(resource=resource, metric_readers=metric_readers) + ) + + # ── Logs ── + logger_provider = LoggerProvider(resource=resource) + _logs.set_logger_provider(logger_provider) + + endpoint = _get_otlp_endpoint(config.logger_provider, "logs") + logger_provider.add_log_record_processor( + BatchLogRecordProcessor(OTLPLogExporter(endpoint=endpoint)) + ) + logger_provider.add_log_record_processor( + BatchLogRecordProcessor(ConsoleLogExporter()) + ) + + # ── Log bridge (always programmatic — not covered by the config schema) ── + otel_handler = LoggingHandler(logger_provider=logger_provider) + + console_handler = logging.StreamHandler() + console_handler.setFormatter( + logging.Formatter("%(levelname)s %(name)s - %(message)s") + ) + + root_logger = logging.getLogger() + root_logger.addHandler(otel_handler) + root_logger.addHandler(console_handler) + root_logger.setLevel(logging.DEBUG) + + +configure_opentelemetry() diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/library/__init__.py b/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/library/__init__.py new file mode 100644 index 00000000000..b0a6f428417 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/library/__init__.py @@ -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. diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/library/rolldice.py b/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/library/rolldice.py new file mode 100644 index 00000000000..e54435749d4 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/library/rolldice.py @@ -0,0 +1,163 @@ +# 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. + +"""Dice-rolling logic for the rolldice reference application. + +────────────────────────────────────────────────────────────────────────────── +IMPORTANT: This module imports ONLY from ``opentelemetry`` (the API package). +It does NOT import from ``opentelemetry.sdk`` or any SDK subpackage. + +This is intentional and reflects the recommended OTel library design: + + • Library code should depend only on the *API* so it can be used in + applications that choose any SDK implementation (or no SDK at all). + + • The *SDK* — which actually records spans, collects metrics, and exports + telemetry — is configured once by the *application* (in ``app/telemetry.py``). + + • When the application installs an SDK via ``set_tracer_provider()`` / + ``set_meter_provider()``, the API calls below automatically delegate to it. + Without an installed SDK the API calls are no-ops, so this library is safe + to use in any context. +────────────────────────────────────────────────────────────────────────────── +""" + +import logging +import random + +# opentelemetry.trace and opentelemetry.metrics are the *API* namespaces. +# They expose get_tracer() and get_meter(), which return a Tracer/Meter bound +# to whatever provider the application has installed (or a no-op if none). +from opentelemetry import metrics, trace + +# Observation is used to yield values from ObservableGauge callbacks. +from opentelemetry.metrics import Observation + +logger = logging.getLogger(__name__) + +# ── Tracer ─────────────────────────────────────────────────────────────────── +# get_tracer() returns a Tracer scoped to this instrumentation library. +# The name (__name__ = "library.rolldice") becomes the instrumentation scope +# name visible in trace backends; it is not the span name. +_tracer = trace.get_tracer(__name__) + +# ── Meter and instruments ──────────────────────────────────────────────────── +# get_meter() returns a Meter scoped to this library. Metric instruments +# (Counter, Histogram, ObservableGauge) are created once at module load time; +# creating them repeatedly would produce duplicate registrations. +_meter = metrics.get_meter(__name__) + +# Counter: monotonically increasing. Use it for things that are counted and +# never decrease, like the total number of dice rolls requested. +_calls_counter = _meter.create_counter( + "rolldice.calls", + description="Total number of roll_dice() calls", +) + +# Histogram: records a distribution of values. Use it to understand the +# spread of measurements — here, which dice faces (1-6) are landed on most. +_outcome_histogram = _meter.create_histogram( + "rolldice.outcome", + description="Distribution of individual dice outcomes (1–6)", +) + +# ObservableGauge: reports the current value of something at collection time +# via a callback. Prefer it over an UpDownCounter when you care about the +# instantaneous value, not the cumulative change. Here it tracks the most +# recent ``rolls`` argument so operators can see the last workload size. +_last_rolls_value: int = 0 + + +def _observe_last_rolls(options: metrics.CallbackOptions): + """Callback invoked by the SDK at each metric collection interval.""" + yield Observation(_last_rolls_value) + + +_last_rolls_gauge = _meter.create_observable_gauge( + "rolldice.last_rolls", + callbacks=[_observe_last_rolls], + description="Most recent value of the rolls parameter", +) + + +def roll_dice(rolls: int, player: str | None = None) -> list[int]: + """Roll a six-sided die ``rolls`` times and return the results. + + Args: + rolls: Number of dice to roll. Must be a positive integer. + player: Optional display name for the player (used in log output). + + Returns: + A list of ``rolls`` integers, each in the range [1, 6]. + + Raises: + ValueError: If ``rolls`` is not a positive integer. + """ + # with_span() is a context manager that: + # 1. Creates a new span named "roll_dice" as a child of the current + # active span (the HTTP request span created by FastAPIInstrumentor). + # 2. Sets it as the active span for the duration of the block. + # 3. Ends the span (and records any exception) when the block exits. + with _tracer.start_as_current_span("roll_dice") as span: + # Semantic convention attributes describe the code location that + # created the span. code.function and code.filepath are part of the + # "code" namespace in the OTel semantic conventions. + span.set_attribute("code.function", "roll_dice") + span.set_attribute("code.filepath", __file__) + + # Application-specific attribute: how many rolls were requested. + # Recording it on the span lets you filter traces by workload size. + span.set_attribute("rolls", rolls) + + if rolls <= 0: + # record_exception() attaches the exception details as a span event + # with the standard "exception.*" attributes, then we re-raise so + # the caller (the HTTP handler) can return a 500 response. + exc = ValueError(f"rolls must be a positive integer, got {rolls}") + span.record_exception(exc) + raise exc + + results = [_do_roll() for _ in range(rolls)] + + # Update the module-level variable observed by the gauge callback. + global _last_rolls_value + _last_rolls_value = rolls + + # Counter: increment by 1 for this call, regardless of roll count. + _calls_counter.add(1) + + # Histogram: record each individual outcome so the distribution of + # dice faces is captured. + for value in results: + _outcome_histogram.record(value) + + player_label = player or "anonymous player" + logger.debug("%s rolled %s → %s", player_label, rolls, results) + + return results + + +def _do_roll() -> int: + """Roll a single six-sided die and return the result. + + This inner function has its own span so the trace shows both the + aggregate ``roll_dice`` operation and each individual roll, making + it easy to see exactly how long each random number generation took. + """ + with _tracer.start_as_current_span("_do_roll") as span: + value = random.randint(1, 6) + # Record the generated value on the span so you can inspect individual + # roll results in a trace backend without needing to look at logs. + span.set_attribute("roll.value", value) + return value diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/otel-config.yaml b/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/otel-config.yaml new file mode 100644 index 00000000000..6c97ee6b84a --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/otel-config.yaml @@ -0,0 +1,72 @@ +# 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. + +# Declarative configuration for the rolldice reference application. +# +# NOTE: With the exception of env var substitution syntax, +# SDKs ignore environment variables when interpreting config files. +# +# Schema docs: https://github.com/open-telemetry/opentelemetry-configuration/blob/main/schema-docs.md + +file_format: "1.0" +disabled: false + +resource: + attributes: + - name: service.name + value: rolldice + attributes_list: ${OTEL_RESOURCE_ATTRIBUTES:-} + detection/development: + detectors: + - service: + - process: + +propagator: + composite: + - tracecontext: + - baggage: + +tracer_provider: + processors: + - batch: + exporter: + otlp_http: + endpoint: ${OTEL_EXPORTER_OTLP_ENDPOINT:-http://localhost:4318}/v1/traces + - simple: + exporter: + console: {} + sampler: + parent_based: + root: + always_on: + +meter_provider: + readers: + - periodic: + exporter: + otlp_http: + endpoint: ${OTEL_EXPORTER_OTLP_ENDPOINT:-http://localhost:4318}/v1/metrics + - periodic: + exporter: + console: {} + +logger_provider: + processors: + - batch: + exporter: + otlp_http: + endpoint: ${OTEL_EXPORTER_OTLP_ENDPOINT:-http://localhost:4318}/v1/logs + - batch: + exporter: + console: {} diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/pyproject.toml b/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/pyproject.toml new file mode 100644 index 00000000000..b40fdaabc95 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/pyproject.toml @@ -0,0 +1,38 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "opentelemetry-reference-application-rolldice" +version = "0.1.0" +description = "OpenTelemetry reference application — rolldice (FastAPI)" +license = "Apache-2.0" +requires-python = ">=3.12" +dependencies = [ + # Web framework and ASGI server. + "fastapi >= 0.100", + "uvicorn[standard] >= 0.20", + + # OpenTelemetry API — the stable, vendor-neutral interface used by library + # code. Does not include any SDK or exporters. + "opentelemetry-api ~= 1.36", + + # OpenTelemetry SDK with file-configuration support — enables declarative + # setup from otel-config.yaml (brings in pyyaml and jsonschema). + "opentelemetry-sdk[file-configuration] ~= 1.36", + + # FastAPI instrumentation — auto-instruments FastAPI apps with HTTP server + # spans and metrics following the OTel HTTP semantic conventions. + "opentelemetry-instrumentation-fastapi ~= 0.57b0", + + # Python logging bridge — provides LoggingHandler, a stdlib logging.Handler + # that forwards Python log records to the OTel LoggerProvider. + "opentelemetry-instrumentation-logging ~= 0.57b0", + + # OTLP HTTP exporter — sends spans, metrics, and log records to any + # OTLP-compatible backend (OTel Collector, Jaeger, Tempo, etc.) over HTTP. + "opentelemetry-exporter-otlp-proto-http ~= 1.36", +] + +[tool.hatch.build.targets.wheel] +packages = ["app", "library"] diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/uninstrumented/app/__init__.py b/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/uninstrumented/app/__init__.py new file mode 100644 index 00000000000..b0a6f428417 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/uninstrumented/app/__init__.py @@ -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. diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/uninstrumented/app/main.py b/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/uninstrumented/app/main.py new file mode 100644 index 00000000000..b732b5ff0aa --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/uninstrumented/app/main.py @@ -0,0 +1,100 @@ +# 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 (uninstrumented). + +This is the plain version of the rolldice application without any +OpenTelemetry instrumentation. Compare with the instrumented version +in the parent directory to see what OTel adds. +""" + +import logging +import os + +import uvicorn +from fastapi import FastAPI, Query, Response +from fastapi.responses import JSONResponse, PlainTextResponse + +from library import rolldice + +logging.basicConfig( + level=logging.DEBUG, + format="%(levelname)s %(name)s - %(message)s", +) +logger = logging.getLogger(__name__) + +app = FastAPI( + title="Rolldice", + description="OpenTelemetry reference application — rolls a six-sided die.", + version="0.1.0", +) + + +@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. + """ + rolls_raw = rolls if rolls is not None else "1" + + 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", + }, + ) + + 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, + ) + + if rolls_int == 1: + return PlainTextResponse(content=str(results[0])) + return JSONResponse(content=results) + + +if __name__ == "__main__": + port = int(os.environ.get("APPLICATION_PORT", 8080)) + uvicorn.run(app, host="0.0.0.0", port=port) diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/uninstrumented/library/__init__.py b/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/uninstrumented/library/__init__.py new file mode 100644 index 00000000000..b0a6f428417 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/uninstrumented/library/__init__.py @@ -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. diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/uninstrumented/library/rolldice.py b/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/uninstrumented/library/rolldice.py new file mode 100644 index 00000000000..48b5084c7b7 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/uninstrumented/library/rolldice.py @@ -0,0 +1,54 @@ +# 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. + +"""Dice-rolling logic (uninstrumented). + +This is the plain version of the rolldice library without any OpenTelemetry +instrumentation. Compare with the instrumented version in the parent +directory to see what OTel adds. +""" + +import logging +import random + +logger = logging.getLogger(__name__) + + +def roll_dice(rolls: int, player: str | None = None) -> list[int]: + """Roll a six-sided die ``rolls`` times and return the results. + + Args: + rolls: Number of dice to roll. Must be a positive integer. + player: Optional display name for the player (used in log output). + + Returns: + A list of ``rolls`` integers, each in the range [1, 6]. + + Raises: + ValueError: If ``rolls`` is not a positive integer. + """ + if rolls <= 0: + raise ValueError(f"rolls must be a positive integer, got {rolls}") + + results = [_do_roll() for _ in range(rolls)] + + player_label = player or "anonymous player" + logger.debug("%s rolled %s → %s", player_label, rolls, results) + + return results + + +def _do_roll() -> int: + """Roll a single six-sided die and return the result.""" + return random.randint(1, 6) From d871490569c5bdc51475450d64f54ebaf4ef1626 Mon Sep 17 00:00:00 2001 From: Reggie Dugard Date: Thu, 28 May 2026 09:20:26 -0700 Subject: [PATCH 2/3] Address reference app specification compliance gaps Add OS resource detector, OTEL_LOG_LEVEL diagnostic verbosity control, dynamic OTEL_SERVICE_NAME support via env var substitution, and a CI workflow that smoke-tests both the instrumented and uninstrumented variants of the rolldice application. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/reference-app-rolldice.yml | 102 ++++++++++++++++++ .../examples/rolldice/README.rst | 14 ++- .../examples/rolldice/app/telemetry.py | 16 ++- .../examples/rolldice/otel-config.yaml | 4 +- 4 files changed, 124 insertions(+), 12 deletions(-) create mode 100644 .github/workflows/reference-app-rolldice.yml diff --git a/.github/workflows/reference-app-rolldice.yml b/.github/workflows/reference-app-rolldice.yml new file mode 100644 index 00000000000..a922c069568 --- /dev/null +++ b/.github/workflows/reference-app-rolldice.yml @@ -0,0 +1,102 @@ +name: "Reference App: rolldice" + +on: + push: + branches: [main] + paths: + - "instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/**" + pull_request: + paths: + - "instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/**" + +permissions: + contents: read + +jobs: + smoke-test: + strategy: + matrix: + variant: [instrumented, uninstrumented] + runs-on: ubuntu-latest + timeout-minutes: 10 + defaults: + run: + working-directory: instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install instrumented version + if: matrix.variant == 'instrumented' + run: pip install . + + - name: Install uninstrumented version + if: matrix.variant == 'uninstrumented' + run: pip install fastapi "uvicorn[standard]>=0.20" + + - name: Start server (instrumented) + if: matrix.variant == 'instrumented' + run: | + python -m app.main & + echo "SERVER_PID=$!" >> "$GITHUB_ENV" + + - name: Start server (uninstrumented) + if: matrix.variant == 'uninstrumented' + run: | + cd uninstrumented + python -m app.main & + echo "SERVER_PID=$!" >> "$GITHUB_ENV" + + - name: Wait for server to be ready + run: | + for i in $(seq 1 30); do + if curl -s -o /dev/null -w '%{http_code}' http://localhost:8080/rolldice | grep -q 200; then + echo "Server ready" + exit 0 + fi + sleep 1 + done + echo "Server failed to start" + exit 1 + + - name: "Smoke test: default roll (rolls omitted)" + run: | + status=$(curl -s -o /tmp/response.txt -w '%{http_code}' http://localhost:8080/rolldice) + echo "Status: $status, Body: $(cat /tmp/response.txt)" + test "$status" = "200" + # Body should be a single digit 1-6 + grep -qE '^[1-6]$' /tmp/response.txt + + - name: "Smoke test: multiple rolls" + run: | + status=$(curl -s -o /tmp/response.txt -w '%{http_code}' 'http://localhost:8080/rolldice?rolls=3') + echo "Status: $status, Body: $(cat /tmp/response.txt)" + test "$status" = "200" + # Body should be a JSON array of 3 numbers + python -c "import json; d=json.load(open('/tmp/response.txt')); assert len(d)==3 and all(1<=v<=6 for v in d)" + + - name: "Smoke test: invalid rolls (non-numeric) → 400" + run: | + status=$(curl -s -o /tmp/response.txt -w '%{http_code}' 'http://localhost:8080/rolldice?rolls=abc') + echo "Status: $status, Body: $(cat /tmp/response.txt)" + test "$status" = "400" + python -c "import json; d=json.load(open('/tmp/response.txt')); assert d['message']=='Parameter rolls must be a positive integer'" + + - name: "Smoke test: zero rolls → 500" + run: | + status=$(curl -s -o /tmp/response.txt -w '%{http_code}' 'http://localhost:8080/rolldice?rolls=0') + echo "Status: $status, Body: $(cat /tmp/response.txt)" + test "$status" = "500" + + - name: "Smoke test: player parameter" + run: | + status=$(curl -s -o /tmp/response.txt -w '%{http_code}' 'http://localhost:8080/rolldice?rolls=1&player=Alice') + echo "Status: $status, Body: $(cat /tmp/response.txt)" + test "$status" = "200" + + - name: Stop server + if: always() + run: kill $SERVER_PID 2>/dev/null || true diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/README.rst b/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/README.rst index c685899307e..5ba115ee791 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/README.rst +++ b/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/README.rst @@ -12,8 +12,8 @@ It demonstrates all four OpenTelemetry signal types in a single service: 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. +* **Resources** — process, OS, and environment metadata attached automatically + via resource detectors. Architecture ------------ @@ -112,19 +112,17 @@ Environment variables +-----------------------------------+----------------------------------------------+-------------------------+ | Variable | Description | Default | +===================================+==============================================+=========================+ +| ``OTEL_SERVICE_NAME`` | Service name reported in telemetry | ``rolldice`` | ++-----------------------------------+----------------------------------------------+-------------------------+ | ``OTEL_EXPORTER_OTLP_ENDPOINT`` | OTLP HTTP endpoint for the backend | ``http://localhost:4318``| +-----------------------------------+----------------------------------------------+-------------------------+ | ``OTEL_RESOURCE_ATTRIBUTES`` | Extra resource attributes (``key=value,...``)| *(none)* | +-----------------------------------+----------------------------------------------+-------------------------+ +| ``OTEL_LOG_LEVEL`` | Verbosity of OTel diagnostic output | ``info`` | ++-----------------------------------+----------------------------------------------+-------------------------+ | ``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 ------------------------ diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/app/telemetry.py b/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/app/telemetry.py index ca3c7a03ded..0a4df057589 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/app/telemetry.py +++ b/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/app/telemetry.py @@ -50,6 +50,7 @@ """ import logging +import os from pathlib import Path from opentelemetry import _logs, metrics, trace @@ -69,6 +70,7 @@ ) from opentelemetry.sdk.resources import ( OTELResourceDetector, + OsResourceDetector, ProcessResourceDetector, Resource, SERVICE_NAME, @@ -91,16 +93,19 @@ def _build_resource(config) -> Resource: """Build a Resource from the parsed config's resource section.""" - service_name = "unknown_service" + service_name = os.environ.get("OTEL_SERVICE_NAME", "rolldice") if config.resource and isinstance(config.resource, dict): for attr in config.resource.get("attributes", []): if attr.get("name") == "service.name": - service_name = attr["value"] + value = attr["value"] + if not value.startswith("${"): + service_name = value base = Resource.create({SERVICE_NAME: service_name}) process_resource = ProcessResourceDetector().detect() + os_resource = OsResourceDetector().detect() otel_resource = OTELResourceDetector().detect() - return base.merge(process_resource).merge(otel_resource) + return base.merge(process_resource).merge(os_resource).merge(otel_resource) def _get_otlp_endpoint(provider_dict, signal_path) -> str | None: @@ -180,5 +185,10 @@ def configure_opentelemetry() -> None: root_logger.addHandler(console_handler) root_logger.setLevel(logging.DEBUG) + # ── OTEL_LOG_LEVEL: control verbosity of OTel's own diagnostic output ── + otel_log_level = os.environ.get("OTEL_LOG_LEVEL", "info").upper() + otel_level = getattr(logging, otel_log_level, logging.INFO) + logging.getLogger("opentelemetry").setLevel(otel_level) + configure_opentelemetry() diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/otel-config.yaml b/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/otel-config.yaml index 6c97ee6b84a..85d23c4d55b 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/otel-config.yaml +++ b/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/otel-config.yaml @@ -25,12 +25,14 @@ disabled: false resource: attributes: - name: service.name - value: rolldice + value: ${OTEL_SERVICE_NAME:-rolldice} attributes_list: ${OTEL_RESOURCE_ATTRIBUTES:-} detection/development: detectors: - service: - process: + - container: + - host: propagator: composite: From 4456610bca820a8c5cfa0a86aa643e172b8f2eb6 Mon Sep 17 00:00:00 2001 From: Reggie Dugard Date: Thu, 28 May 2026 09:44:20 -0700 Subject: [PATCH 3/3] Fix style compliance for rolldice reference application Update all license headers to the SPDX two-line format, fix import sorting and formatting per ruff, and add a towncrier changelog fragment. --- .changelog/5244.added | 1 + .../examples/rolldice/Dockerfile | 13 +------ .../examples/rolldice/app/__init__.py | 13 +------ .../examples/rolldice/app/main.py | 17 ++------- .../examples/rolldice/app/telemetry.py | 38 +++++++++---------- .../examples/rolldice/library/__init__.py | 13 +------ .../examples/rolldice/library/rolldice.py | 13 +------ .../examples/rolldice/otel-config.yaml | 13 +------ .../rolldice/uninstrumented/app/__init__.py | 13 +------ .../rolldice/uninstrumented/app/main.py | 14 +------ .../uninstrumented/library/__init__.py | 13 +------ .../uninstrumented/library/rolldice.py | 13 +------ 12 files changed, 32 insertions(+), 142 deletions(-) create mode 100644 .changelog/5244.added diff --git a/.changelog/5244.added b/.changelog/5244.added new file mode 100644 index 00000000000..6e6ae142a9c --- /dev/null +++ b/.changelog/5244.added @@ -0,0 +1 @@ +`opentelemetry-instrumentation-fastapi`: add rolldice reference application example diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/Dockerfile b/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/Dockerfile index 338d654d383..818a1564ce0 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/Dockerfile +++ b/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/Dockerfile @@ -1,16 +1,5 @@ # 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. +# SPDX-License-Identifier: Apache-2.0 FROM python:3.14-slim diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/app/__init__.py b/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/app/__init__.py index b0a6f428417..e57cf4aba95 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/app/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/app/__init__.py @@ -1,13 +1,2 @@ # 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. +# SPDX-License-Identifier: Apache-2.0 diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/app/main.py b/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/app/main.py index b0b3bd5e229..33b1e6c1eeb 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/app/main.py +++ b/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/app/main.py @@ -1,16 +1,5 @@ # 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. +# SPDX-License-Identifier: Apache-2.0 """FastAPI application entry point for the rolldice reference application. @@ -35,9 +24,9 @@ # 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 +import app.telemetry # noqa: F401, E402 # isort: skip -from library import rolldice +from library import rolldice # noqa: E402 # FastAPIInstrumentor automatically wraps every FastAPI route with an HTTP # server span. It reads the ASGI scope to populate standard HTTP semantic diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/app/telemetry.py b/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/app/telemetry.py index 0a4df057589..0a5dff6d658 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/app/telemetry.py +++ b/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/app/telemetry.py @@ -1,16 +1,5 @@ # 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. +# SPDX-License-Identifier: Apache-2.0 """OpenTelemetry SDK initialization via declarative (file-based) configuration. @@ -54,26 +43,33 @@ from pathlib import Path from opentelemetry import _logs, metrics, trace -from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter +from opentelemetry.exporter.otlp.proto.http._log_exporter import ( + OTLPLogExporter, +) from opentelemetry.exporter.otlp.proto.http.metric_exporter import ( OTLPMetricExporter, ) -from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter +from opentelemetry.exporter.otlp.proto.http.trace_exporter import ( + OTLPSpanExporter, +) from opentelemetry.instrumentation.logging.handler import LoggingHandler from opentelemetry.sdk._configuration.file import load_config_file from opentelemetry.sdk._logs import LoggerProvider -from opentelemetry.sdk._logs.export import BatchLogRecordProcessor, ConsoleLogExporter +from opentelemetry.sdk._logs.export import ( + BatchLogRecordProcessor, + ConsoleLogExporter, +) from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import ( ConsoleMetricExporter, PeriodicExportingMetricReader, ) from opentelemetry.sdk.resources import ( - OTELResourceDetector, + SERVICE_NAME, OsResourceDetector, + OTELResourceDetector, ProcessResourceDetector, Resource, - SERVICE_NAME, ) from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( @@ -112,7 +108,9 @@ def _get_otlp_endpoint(provider_dict, signal_path) -> str | None: """Extract the OTLP HTTP endpoint from a provider config dict.""" if not isinstance(provider_dict, dict): return None - items = provider_dict.get("processors") or provider_dict.get("readers") or [] + items = ( + provider_dict.get("processors") or provider_dict.get("readers") or [] + ) for item in items: if not isinstance(item, dict): continue @@ -148,7 +146,9 @@ def configure_opentelemetry() -> None: tracer_provider.add_span_processor( BatchSpanProcessor(OTLPSpanExporter(endpoint=endpoint)) ) - tracer_provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter())) + tracer_provider.add_span_processor( + SimpleSpanProcessor(ConsoleSpanExporter()) + ) # ── Metrics ── endpoint = _get_otlp_endpoint(config.meter_provider, "metrics") diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/library/__init__.py b/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/library/__init__.py index b0a6f428417..e57cf4aba95 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/library/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/library/__init__.py @@ -1,13 +1,2 @@ # 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. +# SPDX-License-Identifier: Apache-2.0 diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/library/rolldice.py b/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/library/rolldice.py index e54435749d4..248046a5159 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/library/rolldice.py +++ b/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/library/rolldice.py @@ -1,16 +1,5 @@ # 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. +# SPDX-License-Identifier: Apache-2.0 """Dice-rolling logic for the rolldice reference application. diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/otel-config.yaml b/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/otel-config.yaml index 85d23c4d55b..cb337e11ac7 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/otel-config.yaml +++ b/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/otel-config.yaml @@ -1,16 +1,5 @@ # 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. +# SPDX-License-Identifier: Apache-2.0 # Declarative configuration for the rolldice reference application. # diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/uninstrumented/app/__init__.py b/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/uninstrumented/app/__init__.py index b0a6f428417..e57cf4aba95 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/uninstrumented/app/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/uninstrumented/app/__init__.py @@ -1,13 +1,2 @@ # 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. +# SPDX-License-Identifier: Apache-2.0 diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/uninstrumented/app/main.py b/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/uninstrumented/app/main.py index b732b5ff0aa..ccd47c18349 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/uninstrumented/app/main.py +++ b/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/uninstrumented/app/main.py @@ -1,16 +1,5 @@ # 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. +# SPDX-License-Identifier: Apache-2.0 """FastAPI application entry point (uninstrumented). @@ -25,7 +14,6 @@ import uvicorn from fastapi import FastAPI, Query, Response from fastapi.responses import JSONResponse, PlainTextResponse - from library import rolldice logging.basicConfig( diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/uninstrumented/library/__init__.py b/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/uninstrumented/library/__init__.py index b0a6f428417..e57cf4aba95 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/uninstrumented/library/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/uninstrumented/library/__init__.py @@ -1,13 +1,2 @@ # 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. +# SPDX-License-Identifier: Apache-2.0 diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/uninstrumented/library/rolldice.py b/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/uninstrumented/library/rolldice.py index 48b5084c7b7..37a0b7c2cb6 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/uninstrumented/library/rolldice.py +++ b/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/uninstrumented/library/rolldice.py @@ -1,16 +1,5 @@ # 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. +# SPDX-License-Identifier: Apache-2.0 """Dice-rolling logic (uninstrumented).