Skip to content

feat: implement Epic 07 Phase 5: versioned cost accounting on usage_events, OTel metric instruments, and span-helper wiring - #170

Merged
pateatlau merged 1 commit into
mainfrom
feat/v2-epic-07-observability-and-evaluation-phase-05
Aug 8, 2026
Merged

feat: implement Epic 07 Phase 5: versioned cost accounting on usage_events, OTel metric instruments, and span-helper wiring#170
pateatlau merged 1 commit into
mainfrom
feat/v2-epic-07-observability-and-evaluation-phase-05

Conversation

@pateatlau

@pateatlau pateatlau commented Aug 8, 2026

Copy link
Copy Markdown
Owner

Summary

Implements Epic 07 Phase 5 — Token & Cost Metrics: approximate, versioned dollar cost on usage_events, plus the OTel counters/histograms Epic 06 pre-declared (and LLM/tool/agent equivalents).

  • ModelPricingTable loads git-tracked config/model_pricing.yaml with startup version lock (observability_cost_pricing_version)
  • CostCalculator prices ProviderUsage inside SqlUsageStore.record() only — unknown models → NULL, never blocking
  • Migration 0008_observability_usage_cost adds nullable cost_usd / pricing_version and index (provider, model, created_at)
  • Metric instruments with bounded label keys/values via normalize_metric_label(); wired through span helpers / TracingLLMProvider / workflow manager+executor (independent of trace sampling)
  • Flag-off behaviour unchanged: no metrics, cost fields stay NULL

Test plan

  • uv run pytest tests/ai/observability/ -q (78 passed)
  • pre-commit run --all-files
  • uv run alembic upgrade head on target DB before deploy

Migration

cd backend-python && uv run alembic upgrade head

Summary by CodeRabbit

  • New Features
    • Added cost tracking for AI usage, including model pricing, token-based estimates, and pricing versions.
    • Added observability metrics for LLM requests, tools, agents, workflows, retries, checkpoints, approvals, and parallel execution.
    • Added support for OpenAI, Gemini, Groq, and Anthropic model pricing.
  • Improvements
    • Usage costs are saved with usage records without interrupting normal persistence when pricing is unavailable.
    • Metric labels are validated and normalized for consistent reporting.

…vents, OTel metric instruments, and span-helper wiring
@vercel

vercel Bot commented Aug 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
fullstack-ai-platform Ready Ready Preview Aug 8, 2026 8:37am

@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Observability accounting

Layer / File(s) Summary
Pricing and cost calculation
backend-python/app/ai/observability/cost/*, backend-python/app/core/config.py, backend-python/config/model_pricing.yaml, backend-python/.env.example
Adds validated YAML pricing, token-based cost calculation, registry management, configuration fields, and public exports.
Usage cost persistence
backend-python/app/db/models.py, backend-python/app/db/usage.py, backend-python/alembic/versions/0008_observability_usage_cost.py, backend-python/app/main.py
Stores nullable usage costs and pricing versions, adds a query index and migration, and initializes cost tracking during startup.
Metric labels and instruments
backend-python/app/ai/observability/metrics/*, backend-python/tests/ai/observability/test_metrics_instruments.py
Adds bounded label registries, normalization, OpenTelemetry instruments, guarded recorders, and validation tests.
Runtime metric integration
backend-python/app/ai/observability/tracing/*, backend-python/app/ai/tools/executor.py, backend-python/app/ai/workflow/*
Records LLM, tool, agent, workflow, retry, checkpoint, approval, and parallel-branch metrics across execution paths.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Application
  participant CostRegistry
  participant PricingTable
  participant UsageStore
  participant UsageEvent
  Application->>CostRegistry: initialize settings
  CostRegistry->>PricingTable: load and validate pricing
  PricingTable-->>CostRegistry: return pricing table
  UsageStore->>CostRegistry: calculate usage cost
  CostRegistry-->>UsageStore: return cost and pricing version
  UsageStore->>UsageEvent: persist usage and cost
Loading
sequenceDiagram
  participant LLMOrWorkflow
  participant Tracing
  participant MetricInstruments
  participant OpenTelemetry
  LLMOrWorkflow->>Tracing: complete request or workflow action
  Tracing->>MetricInstruments: record metric data
  MetricInstruments->>OpenTelemetry: emit normalized attributes and measurement
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: versioned cost accounting, OTel metric instruments, and span-helper integration.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/v2-epic-07-observability-and-evaluation-phase-05

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend-python/app/ai/observability/cost/calculator.py`:
- Around line 34-46: Update CostCalculator.price() to return (None, None) when
usage.prompt_tokens or usage.completion_tokens is negative, before looking up
pricing or calculating cost. Preserve the existing handling for missing usage,
missing token values, and unavailable pricing entries.

In `@backend-python/app/ai/observability/cost/pricing.py`:
- Around line 121-128: Update the pricing validation in the model-loading logic
around the existing type and range checks to also reject non-finite numeric
values by applying math.isfinite(value). Preserve the current errors for invalid
and negative rates, and add loader tests covering both NaN and positive/negative
infinity inputs.

In `@backend-python/app/ai/observability/tracing/provider_wrapper.py`:
- Around line 34-39: Update the provider wrapper around complete_chat,
complete_chat_with_tools, and stream_chat so exceptions from each provider call
or stream iteration record record_llm_request_metrics with succeeded=False and
no usage before re-raising the original error; retain the existing successful
metrics path with usage.

In `@backend-python/app/ai/workflow/manager.py`:
- Around line 525-526: Update cancel_run to
record_workflow_approval_pending_delta(-1) after the cancellation checkpoint
succeeds when the run’s prior status is WAITING_APPROVAL, while preserving
behavior for other statuses. Add a cancellation test covering a WAITING_APPROVAL
run and verifying the pending-approval count is decremented.

In `@backend-python/app/db/usage.py`:
- Around line 57-62: Move the record_llm_cost_metric call from before
self._session.flush() to after persistence succeeds, ensuring duplicate usage
events cannot emit a cost metric. In the transaction-owned path, defer emission
until the transaction commits; preserve the existing provider, model, and
cost_usd values.

In `@backend-python/config/model_pricing.yaml`:
- Around line 11-14: Update the gemini-3.1-flash-lite pricing entry in
model_pricing.yaml to use Google’s standard rates: $0.00025 input per 1K tokens
and $0.0015 output per 1K tokens.

In `@backend-python/tests/ai/observability/test_metrics_instruments.py`:
- Around line 51-56: Update the metric_reader fixture around
MetricInstruments.initialize so the OpenTelemetry test MeterProvider is
installed only once at module or session scope, rather than replacing the global
provider per fixture instance. Ensure each test’s InMemoryMetricReader still
observes emitted metrics, either by sharing the scoped provider/reader or by
injecting the provider through MeterRegistry.

In `@backend-python/tests/ai/observability/test_usage_cost_persistence.py`:
- Around line 188-194: Update the pricing setup around ModelPricingTable.load
and CostRegistry._calculator to load a temporary pricing file with a new pricing
version and modified rate instead of reloading _CANONICAL_PRICING unchanged.
Assert that second uses the updated cost and pricing version, while first
retains the original cost and version.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a44141ed-1793-4f3a-82f7-d7231c27034d

📥 Commits

Reviewing files that changed from the base of the PR and between 1df7f92 and 33447b6.

📒 Files selected for processing (22)
  • backend-python/.env.example
  • backend-python/alembic/versions/0008_observability_usage_cost.py
  • backend-python/app/ai/observability/__init__.py
  • backend-python/app/ai/observability/cost/__init__.py
  • backend-python/app/ai/observability/cost/calculator.py
  • backend-python/app/ai/observability/cost/pricing.py
  • backend-python/app/ai/observability/metrics/instruments.py
  • backend-python/app/ai/observability/metrics/labels.py
  • backend-python/app/ai/observability/tracing/provider_wrapper.py
  • backend-python/app/ai/observability/tracing/spans.py
  • backend-python/app/ai/tools/executor.py
  • backend-python/app/ai/workflow/engine/executor.py
  • backend-python/app/ai/workflow/manager.py
  • backend-python/app/core/config.py
  • backend-python/app/db/models.py
  • backend-python/app/db/usage.py
  • backend-python/app/main.py
  • backend-python/config/model_pricing.yaml
  • backend-python/tests/ai/observability/test_cost_calculator.py
  • backend-python/tests/ai/observability/test_metrics_instruments.py
  • backend-python/tests/ai/observability/test_package_init.py
  • backend-python/tests/ai/observability/test_usage_cost_persistence.py

Comment on lines +34 to +46
if usage is None:
return None, None
if usage.prompt_tokens is None or usage.completion_tokens is None:
return None, None

entry = self._table.lookup(provider, model)
if entry is None:
return None, None

cost = (usage.prompt_tokens / 1000.0) * entry.input_usd_per_1k + (
usage.completion_tokens / 1000.0
) * entry.output_usd_per_1k
return cost, self._table.pricing_version

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline backend-python/app/providers/base.py --match ProviderUsage --view expanded
rg -n -C 5 'class ProviderUsage|prompt_tokens|completion_tokens' backend-python/app/providers/base.py

Repository: pateatlau/fullstack-ai-platform

Length of output: 1060


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Calculator and SqlUsageStore references:"
rg -n -C 4 'calculate.*cost|record\(|record_usage|cost.*usage|ProviderUsage|prompt_tokens|completion_tokens' backend-python/app/ai backend-python/app | head -n 240

echo
echo "Candidate files:"
git ls-files backend-python/app | rg '(calculator|usage|store|base|provider)'

Repository: pateatlau/fullstack-ai-platform

Length of output: 20778


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "calculator.py"
cat -n backend-python/app/ai/observability/cost/calculator.py

echo
echo "usage_service.py"
cat -n backend-python/app/services/usage_service.py

echo
echo "db usage models"
fd -a 'usage.py|tables.py|schema.py|models.py' backend-python/app/db | while read -r f; do
  echo "--- $f"
  wc -l "$f"
  rg -n -C 4 'class .*Usage|Usage|input_tokens|completion_tokens|prompt_tokens|price|cost|record' "$f" || true
done

echo
echo "observability usages"
rg -n -C 5 'calculate|record_usage|SqlUsageStore|UsageStore|ProviderUsage' backend-python/app/services backend-python/app/ai backend-python/app/db

Repository: pateatlau/fullstack-ai-platform

Length of output: 35777


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "ChatService usage recording slices:"
sed -n '792,850p' backend-python/app/services/chat_service.py
sed -n '1040,1105p' backend-python/app/services/chat_service.py
sed -n '170,225p' backend-python/app/ai/agent/adapters/chat_adapter.py

echo
echo "SqlUsageStore.record bounded slice:"
sed -n '28,95p' backend-python/app/db/usage.py

echo
echo "Behavioral probe of current bounds and calculation:"
python3 - <<'PY'
from dataclasses import dataclass
from typing import Any

`@dataclass`(frozen=True)
class PricingEntry:
    input_usd_per_1k: float = 0.0
    output_usd_per_1k: float = 0.0

`@dataclass`(frozen=True)
class ModelPricingTable:
    data: dict[tuple[str, str], PricingEntry]
    pricing_version = "probe"

    def lookup(self, provider: str, model: str) -> PricingEntry | None:
        return self.data.get((provider, model))

class CostCalculator:
    def __init__(self, table: ModelPricingTable) -> None:
        self._table = table

    def price(self, provider: str, model: str, usage: Any | None) -> tuple[float | None, str | None]:
        if usage is None:
            return None, None
        if usage.prompt_tokens is None or usage.completion_tokens is None:
            return None, None
        entry = self._table.lookup(provider, model)
        if entry is None:
            return None, None
        cost = (usage.prompt_tokens / 1000.0) * entry.input_usd_per_1k + (
            usage.completion_tokens / 1000.0
        ) * entry.output_usd_per_1k
        return cost, self._table.pricing_version

ProviderUsage = type("ProviderUsage", (), {})
ProviderUsage = dataclass(frozen=True)(type(
    "ProviderUsage", (), {
        "__annotations__": {"prompt_tokens": "int | None", "completion_tokens": "int | None", "total_tokens": "int | None"}
    }
))
ProviderUsage.prompt_tokens = None
ProviderUsage.completion_tokens = None
ProviderUsage.total_tokens = None

calculator = CostCalculator(ModelPricingTable({("p", "m"): PricingEntry(1.0, 2.0)}))
for tokens in [
    (10, 5),
    (0, 0),
    (-1, 5),
    (10, -2),
    (-3, -4),
]:
    usage = ProviderUsage(prompt_tokens=tokens[0], completion_tokens=tokens[1], total_tokens=None)
    print(tokens, calculator.price("p", "m", usage))
PY

Repository: pateatlau/fullstack-ai-platform

Length of output: 9296


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "ChatService surrounding error handling:"
sed -n '830,915p' backend-python/app/services/chat_service.py
sed -n '220,345p' backend-python/app/ai/agent/adapters/chat_adapter.py

echo
echo "Simple arithmetic probe:"
python3 - <<'PY'
def current_price(prompt_tokens, completion_tokens, in_price, out_price):
    if prompt_tokens is None or completion_tokens is None:
        return None
    return (prompt_tokens / 1000.0) * in_price + (completion_tokens / 1000.0) * out_price

for prompt_tokens, completion_tokens in [
    (10, 5),
    (0, 0),
    (-1, 5),
    (10, -2),
    (-3, -4),
]:
    print(f"input={prompt_tokens}, completion={completion_tokens} -> cost={current_price(prompt_tokens, completion_tokens, 1.0, 2.0)}")
PY

Repository: pateatlau/fullstack-ai-platform

Length of output: 6014


Reject negative usage before calculating cost.

ProviderUsage accepts negative integer token counts, and SqlUsageStore.record() persists those inputs into usage_events; valid pricing rows would then produce a negative cost_usd. Reject prompt_tokens < 0 or completion_tokens < 0 in CostCalculator.price(), or return (None, None).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend-python/app/ai/observability/cost/calculator.py` around lines 34 - 46,
Update CostCalculator.price() to return (None, None) when usage.prompt_tokens or
usage.completion_tokens is negative, before looking up pricing or calculating
cost. Preserve the existing handling for missing usage, missing token values,
and unavailable pricing entries.

Comment on lines +121 to +128
if not isinstance(value, (int, float)) or isinstance(value, bool):
raise ObservabilityConfigError(
f"models[{index}].{field_name} must be a finite number >= 0."
)
if value < 0:
raise ObservabilityConfigError(
f"models[{index}].{field_name} must be >= 0."
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reject non-finite pricing rates.

.nan and .inf pass these checks because both are float values and neither is less than zero. The loader then accepts invalid rates despite the error text requiring finite values. This can persist invalid cost_usd values and emit invalid cost metrics.

Use math.isfinite(value) in this condition. Add .nan and .inf loader test cases.

Proposed fix
+import math
+
-            if not isinstance(value, (int, float)) or isinstance(value, bool):
+            if (
+                not isinstance(value, (int, float))
+                or isinstance(value, bool)
+                or not math.isfinite(value)
+            ):
                 raise ObservabilityConfigError(
                     f"models[{index}].{field_name} must be a finite number >= 0."
                 )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if not isinstance(value, (int, float)) or isinstance(value, bool):
raise ObservabilityConfigError(
f"models[{index}].{field_name} must be a finite number >= 0."
)
if value < 0:
raise ObservabilityConfigError(
f"models[{index}].{field_name} must be >= 0."
)
import math
if (
not isinstance(value, (int, float))
or isinstance(value, bool)
or not math.isfinite(value)
):
raise ObservabilityConfigError(
f"models[{index}].{field_name} must be a finite number >= 0."
)
if value < 0:
raise ObservabilityConfigError(
f"models[{index}].{field_name} must be >= 0."
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend-python/app/ai/observability/cost/pricing.py` around lines 121 - 128,
Update the pricing validation in the model-loading logic around the existing
type and range checks to also reject non-finite numeric values by applying
math.isfinite(value). Preserve the current errors for invalid and negative
rates, and add loader tests covering both NaN and positive/negative infinity
inputs.

Comment on lines +34 to +39
record_llm_request_metrics(
provider=provider,
model=model,
succeeded=succeeded,
total_tokens=usage.total_tokens if usage is not None else None,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Record failed provider requests.

Line 34 runs only after the inner provider completes. If complete_chat, complete_chat_with_tools, or stream_chat raises, the code emits no failed llm_requests_total metric. Catch provider errors around each inner call or iteration, record succeeded=False with no usage, then re-raise the error.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend-python/app/ai/observability/tracing/provider_wrapper.py` around lines
34 - 39, Update the provider wrapper around complete_chat,
complete_chat_with_tools, and stream_chat so exceptions from each provider call
or stream iteration record record_llm_request_metrics with succeeded=False and
no usage before re-raising the original error; retain the existing successful
metrics path with usage.

Comment on lines +525 to +526
record_workflow_approval_pending_delta(-1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Decrement pending approvals on cancellation.

Line 525 handles approval decisions only. cancel_run can change a WAITING_APPROVAL run to CANCELLED, but it does not record record_workflow_approval_pending_delta(-1). Each such cancellation leaves workflow_approval_pending_count elevated. Decrement after the cancellation checkpoint succeeds when the prior status was WAITING_APPROVAL, and add a cancellation test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend-python/app/ai/workflow/manager.py` around lines 525 - 526, Update
cancel_run to record_workflow_approval_pending_delta(-1) after the cancellation
checkpoint succeeds when the run’s prior status is WAITING_APPROVAL, while
preserving behavior for other statuses. Add a cancellation test covering a
WAITING_APPROVAL run and verifying the pending-approval count is decremented.

Comment on lines +57 to +62
if cost_usd is not None:
record_llm_cost_metric(
provider=provider,
model=model,
cost_usd=cost_usd,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Emit the cost metric only after successful persistence.

record_llm_cost_metric runs before await self._session.flush(). If a retry supplies an existing request_id, the unique usage-event constraint rejects the row after this metric has already been emitted. This overcounts LLM cost while the database correctly prevents duplicate usage events.

Move metric emission after a successful flush. If the caller owns the transaction, emit after commit through the transaction lifecycle.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend-python/app/db/usage.py` around lines 57 - 62, Move the
record_llm_cost_metric call from before self._session.flush() to after
persistence succeeds, ensuring duplicate usage events cannot emit a cost metric.
In the transaction-owned path, defer emission until the transaction commits;
preserve the existing provider, model, and cost_usd values.

Comment on lines +11 to +14
- provider: gemini
model: gemini-3.1-flash-lite
input_usd_per_1k: 0.000075
output_usd_per_1k: 0.0003

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Correct the Gemini 3.1 Flash-Lite standard rates.

These values equal $0.075 input and $0.30 output per 1M tokens. Google lists standard rates of $0.25 input and $1.50 output per 1M tokens. The current values understate persisted and emitted cost by 3.33× for input and 5× for output. (ai.google.dev)

Proposed fix
-    input_usd_per_1k: 0.000075
-    output_usd_per_1k: 0.0003
+    input_usd_per_1k: 0.00025
+    output_usd_per_1k: 0.0015
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- provider: gemini
model: gemini-3.1-flash-lite
input_usd_per_1k: 0.000075
output_usd_per_1k: 0.0003
- provider: gemini
model: gemini-3.1-flash-lite
input_usd_per_1k: 0.00025
output_usd_per_1k: 0.0015
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend-python/config/model_pricing.yaml` around lines 11 - 14, Update the
gemini-3.1-flash-lite pricing entry in model_pricing.yaml to use Google’s
standard rates: $0.00025 input per 1K tokens and $0.0015 output per 1K tokens.

Comment on lines +51 to +56
reader = InMemoryMetricReader()
provider = MeterProvider(metric_readers=[reader])
metrics.set_meter_provider(provider)
MeterRegistry._initialized = True
MeterRegistry._enabled = True
MetricInstruments.initialize()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate files =="
git ls-files | rg '(^backend-python/tests/ai/observability/test_metrics_instruments\.py$|metrics|meter|instruments)'

echo "== diff/stat =="
git diff --stat || true
git diff -- backend-python/tests/ai/observability/test_metrics_instruments.py | sed -n '1,220p' || true

echo "== target file outline/section =="
wc -l backend-python/tests/ai/observability/test_metrics_instruments.py || true
sed -n '1,140p' backend-python/tests/ai/observability/test_metrics_instruments.py

echo "== MeterRegistry/initialize references =="
rg -n "class MeterRegistry|def initialize|MeterRegistry|metrics\.set_meter_provider|MeterProvider|InMemoryMetricReader|set_meter_provider" backend-python -S

Repository: pateatlau/fullstack-ai-platform

Length of output: 11638


🌐 Web query:

OpenTelemetry Python metrics set_meter_provider accepts first global provider

💡 Result:

In OpenTelemetry Python, the set_meter_provider function is designed to set the global MeterProvider only once [1][2]. If you attempt to call it multiple times, the library will log a warning indicating that overriding the current MeterProvider is not allowed [1][2][3]. Internally, this mechanism is enforced using a "set-once" lock or check [1]. When set_meter_provider is called for the first time, the provided MeterProvider is stored as the global instance [1]. Any subsequent calls are ignored in terms of changing the global provider, and the library issues a warning [1][2][3]. This ensures that the global configuration remains consistent throughout the application lifecycle.

Citations:


🌐 Web query:

OpenTelemetry Python metrics NoOpMeterProvider instruments get_meter

💡 Result:

In the OpenTelemetry Python API, the NoOpMeterProvider is the default implementation provided when no other meter provider is configured [1][2]. Its purpose is to allow applications to interact with the OpenTelemetry metrics API without requiring a fully operational SDK, thereby avoiding telemetry production and minimizing computational overhead [1][3][4]. When you call get_meter(name, version=None, schema_url=None, attributes=None) on a NoOpMeterProvider, it returns a NoOpMeter [1][2]. The NoOpMeter, in turn, provides "no-op" versions of instruments (such as NoOpCounter, NoOpHistogram, etc.) when its create_* methods are called [3][5]. These instrument objects perform no operations, hold no state, and do not aggregate or emit any telemetry data [5][4]. Key characteristics of this behavior include: 1. No Validation: Consistent with the OpenTelemetry specification, the NoOpMeterProvider accepts the provided parameters (name, version, schema_url, attributes) but does not validate them [4]. 2. No State: Neither the NoOpMeterProvider nor the instruments it creates hold any configuration or operational state, ensuring a minimal memory footprint [4]. 3. Silence: To prevent interference with application logic, the NoOpMeterProvider and its associated objects do not produce errors or log messages during operations [4]. In many production scenarios, the global default provider is actually a _ProxyMeterProvider, which buffers instrumentation calls until a concrete SDK implementation is loaded and set via set_meter_provider, at which point it upgrades the buffered instruments to real, operational ones [5]. The NoOpMeterProvider is typically reserved for instances where no telemetry processing is desired or explicitly configured [1][5].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== pyproject dependency versions =="
for f in backend-python/pyproject.toml pyproject.toml backend-python/Pipfile backend-python/Pipfile.lock; do
  [ -f "$f" ] && { echo "-- $f"; sed -n '/opentelemetry/p;/otel/p;/metrics/p' "$f" || true; }
done

echo "== instrument/meter implementations =="
wc -l backend-python/app/ai/observability/metrics/instruments.py backend-python/app/ai/observability/metrics/meter.py
sed -n '1,180p' backend-python/app/ai/observability/metrics/meter.py
sed -n '1,160p' backend-python/app/ai/observability/metrics/instruments.py

Repository: pateatlau/fullstack-ai-platform

Length of output: 8081


🌐 Web query:

"proxy meter provider" "overriding" "set_meter_provider" OpenTelemetry Python metrics

💡 Result:

In OpenTelemetry Python, the set_meter_provider function is designed to set the global MeterProvider only once [1][2]. If you attempt to call set_meter_provider again, it will log a warning and ignore the subsequent attempt [1][3]. When you call set_meter_provider for the first time, it interacts with a internal _ProxyMeterProvider [1][4]. The proxy mechanism exists to allow instrumentation libraries to create meters before a concrete SDK provider has been configured [5]. Here is how the override process and proxy interaction work: 1. Initial State: Before set_meter_provider is called, get_meter_provider returns a _ProxyMeterProvider [1][4]. Any meters or instruments created at this stage are "proxy" objects [4][5]. 2. Setting the Provider: When you call set_meter_provider with a concrete SDK MeterProvider, the _ProxyMeterProvider is notified via its on_set_meter_provider method [1][4]. 3. Resolving Proxies: Upon receiving the concrete provider, the proxy informs its existing _ProxyMeters and proxy instruments to switch to the "real" backing objects provided by the SDK [1][4]. This ensures that telemetry recorded via initial proxies is correctly routed to the configured SDK [5]. 4. Overriding Constraints: Because the API intentionally enforces a single-assignment policy for the global provider, you cannot programmatically override the MeterProvider once it has been set [1][2]. Attempts to do so result in a warning being logged [1][3]. If you find yourself needing to change the provider, this is generally considered an anti-pattern in the current OpenTelemetry Python design, as seen in ongoing community discussions regarding the inability to reset global providers [6]. If your application architecture requires dynamic configuration, you should configure the provider before any global telemetry initialization occurs, or use a custom implementation that handles configuration changes internally (such as a wrapper provider) rather than relying on the OpenTelemetry global API for runtime re-configuration.

Citations:


Install the test meter provider once.

metrics.set_meter_provider sets the global provider only once. Replacing it in each metric_reader fixture makes later tests continue to use the first provider’s meters, so their InMemoryMetricReader does not receive emitted metrics. Use one module- or session-scoped provider, or inject the provider through MeterRegistry.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend-python/tests/ai/observability/test_metrics_instruments.py` around
lines 51 - 56, Update the metric_reader fixture around
MetricInstruments.initialize so the OpenTelemetry test MeterProvider is
installed only once at module or session scope, rather than replacing the global
provider per fixture instance. Ensure each test’s InMemoryMetricReader still
observes emitted metrics, either by sharing the scoped provider/reader or by
injecting the provider through MeterRegistry.

Comment on lines +188 to +194
settings = Settings(
openai_api_key="test-key",
observability_enabled=True,
observability_cost_pricing_version="2026-08",
)
table_v2 = ModelPricingTable.load(settings, pricing_file=_CANONICAL_PRICING)
CostRegistry._calculator = CostCalculator(table_v2)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Exercise an actual pricing-table change.

This reloads the same file with the same required version, 2026-08. table_v2 therefore has the same prices and pricing version as the first calculator.

Load a temporary pricing file with a new version and changed rate. Assert that second has the new cost and version while first retains its original values.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend-python/tests/ai/observability/test_usage_cost_persistence.py` around
lines 188 - 194, Update the pricing setup around ModelPricingTable.load and
CostRegistry._calculator to load a temporary pricing file with a new pricing
version and modified rate instead of reloading _CANONICAL_PRICING unchanged.
Assert that second uses the updated cost and pricing version, while first
retains the original cost and version.

@pateatlau
pateatlau merged commit 22f636c into main Aug 8, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant