diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/Dockerfile b/instrumentation/opentelemetry-instrumentation-fastapi/examples/rolldice/Dockerfile
new file mode 100644
index 0000000000..338d654d38
--- /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 0000000000..c685899307
--- /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 0000000000..b0a6f42841
--- /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 0000000000..b0b3bd5e22
--- /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 0000000000..ca3c7a03de
--- /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 0000000000..b0a6f42841
--- /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 0000000000..e54435749d
--- /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 0000000000..6c97ee6b84
--- /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 0000000000..b40fdaabc9
--- /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 0000000000..b0a6f42841
--- /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 0000000000..b732b5ff0a
--- /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 0000000000..b0a6f42841
--- /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 0000000000..48b5084c7b
--- /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)