Skip to content

Add native Prometheus /internal/metrics endpoint#187

Closed
Puupuls wants to merge 2 commits into
thushan:mainfrom
Puupuls:feature/internal-prometheus-metrics
Closed

Add native Prometheus /internal/metrics endpoint#187
Puupuls wants to merge 2 commits into
thushan:mainfrom
Puupuls:feature/internal-prometheus-metrics

Conversation

@Puupuls

@Puupuls Puupuls commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds a built-in GET /internal/metrics endpoint that exposes the same operational data as /internal/status in Prometheus text format
  • Refactors status data gathering into a shared snapshot so JSON status and Prometheus metrics stay in sync
  • Registers the endpoint on the internal monitoring route table and advertises it via /version

Metrics exposed

System-level gauges/counters aligned with the monitoring docs:

  • olla_requests_total, olla_failures_total, olla_endpoints_healthy, olla_avg_latency_ms, olla_success_rate_percent, olla_active_connections, olla_uptime_seconds, olla_total_traffic_bytes, olla_security_violations_total
  • Per-endpoint series with endpoint and status labels (requests, connections, success rate, latency, traffic, priority, model count, up)
  • olla_info with version, commit, engine, profile, and balancer labels

Test plan

  • make ready
  • Unit tests for /internal/metrics handler and Prometheus formatting helpers
  • Manual scrape check: curl -s http://localhost:40114/internal/metrics | head
  • Prometheus scrape config pointing at /internal/metrics

Summary by CodeRabbit

  • New Features

    • Added an internal metrics endpoint and exposed it in the API response.
    • Included Prometheus and Grafana local development examples for easier observability setup.
    • Added a local compose workflow for starting the full monitoring stack.
  • Bug Fixes

    • Improved status reporting so endpoint, proxy, system, security, and connection data are assembled more consistently.
  • Tests

    • Added coverage for metrics output, formatting helpers, and status-related proxy data handling.

Expose the same operational data as /internal/status in Prometheus text format so scrapers can ingest Olla metrics without an external wrapper.
@Puupuls
Puupuls requested a review from thushan as a code owner June 25, 2026 09:58
@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Adds a Prometheus metrics endpoint, refactors status snapshot assembly, exposes the endpoint in route/version metadata, and adds local Prometheus/Grafana compose examples plus Makefile targets.

Changes

Prometheus metrics and local dev stack

Layer / File(s) Summary
Endpoint contract and route exposure
internal/core/constants/endpoint.go, internal/core/constants/content.go, internal/app/handlers/server_routes.go, internal/app/handlers/handler_version.go
Adds the metrics path and Prometheus content type constants, registers the GET route, and includes the metrics endpoint in version metadata.
Status snapshot assembly
internal/app/handlers/handler_status.go, internal/app/handlers/handler_status_endpoints_test.go
Adds a snapshot object for status data, rebuilds StatusResponse from that snapshot, and updates the proxy stats test double.
Metrics handler and overall render
internal/app/handlers/handler_metrics.go, internal/app/handlers/handler_metrics_test.go
Adds the metrics handler, overall Prometheus text rendering, shared numeric helpers, and tests for the endpoint response and metric values.
Endpoint gauges and formatting helpers
internal/app/handlers/handler_metrics.go, internal/app/handlers/handler_metrics_test.go
Adds per-endpoint Prometheus gauges, label escaping, float formatting, and tests for the formatting helpers.
Local observability compose stack
config/prometheus.local.yaml.example, docker-compose.local.yaml.example, docker-compose.yaml, makefile
Adds the Prometheus config example, compose overlay example, base compose comments, and Makefile targets for launching the local stack.

Estimated review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 matches the main change: adding a native Prometheus /internal/metrics endpoint.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

Provide example compose and Prometheus configs for local stacks that build Olla from source and scrape /internal/metrics.

@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: 2

🧹 Nitpick comments (2)
internal/app/handlers/handler_metrics.go (2)

51-52: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Avoid round-tripping the success rate through a formatted string.

parsePercentage re-parses response.System.SuccessRate, which buildSystemSummary produced via format.Percentage. Parsing back from display text is fragile (formatting changes silently break the metric). The raw value is available from snapshot.proxyStats; compute it directly for the gauge.

Also applies to: 171-177

🤖 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 `@internal/app/handlers/handler_metrics.go` around lines 51 - 52, The
success-rate metric is being derived by parsing display-formatted text, which is
fragile; update the gauge path in handler_metrics.go to compute the value
directly from the raw proxy statistics instead of calling parsePercentage on
response.System.SuccessRate. Use the existing raw data from snapshot.proxyStats
in buildSystemSummary and the related metric-writing logic (including the later
matching section around the other success-rate gauge) so the Prometheus gauge is
based on the original numeric value, not a formatted string.

106-138: 🚀 Performance & Scalability | 🔵 Trivial

Per-endpoint status label causes time-series churn.

Each health-state transition for an endpoint creates a new series because the string value of the status label changes (for example, from "healthy" to "unhealthy"). This churn leaves stale series in the TSDB, increases memory pressure and degrades query performance. The olla_endpoint_up gauge already expresses health numerically, so the status label is redundant for that metric. It is advisable to drop the status label from all gauges and key the series solely on the stable endpoint label.

🤖 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 `@internal/app/handlers/handler_metrics.go` around lines 106 - 138, The
per-endpoint status label is creating unstable metric series in
handler_metrics.go, so update the Prometheus gauge writes in the metrics loop to
remove the changing status label and keep only the stable endpoint label. Use
the existing writePrometheusLabeledGauge calls in this handler to drop status
from olla_endpoint_up and the other endpoint gauges, while preserving the health
signal via the numeric up value and the existing endpoint name identity.
🤖 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 `@docker-compose.local.yaml.example`:
- Around line 41-44: The Grafana admin credentials in the compose example are
hardcoded in the environment section, so update the
docker-compose.local.yaml.example entries for GF_SECURITY_ADMIN_USER and
GF_SECURITY_ADMIN_PASSWORD to use environment variable interpolation instead of
literal values. Keep GF_USERS_ALLOW_SIGN_UP as-is, and make sure the service
configuration still references the same environment block so users can supply
secrets externally when running Grafana.

In `@internal/app/handlers/handler_metrics.go`:
- Around line 142-151: The totalTrafficBytes helper is incorrectly basing
aggregation on connectionStats, which skips endpoints with traffic but no active
connections; update totalTrafficBytes to sum directly from endpointStats so the
ola_total_traffic_bytes metric stays accurate. Use the existing
totalTrafficBytes symbol in handler_metrics.go and align its logic with
buildSystemSummary by extracting a shared helper if needed to keep the API
response and metrics consistent.

---

Nitpick comments:
In `@internal/app/handlers/handler_metrics.go`:
- Around line 51-52: The success-rate metric is being derived by parsing
display-formatted text, which is fragile; update the gauge path in
handler_metrics.go to compute the value directly from the raw proxy statistics
instead of calling parsePercentage on response.System.SuccessRate. Use the
existing raw data from snapshot.proxyStats in buildSystemSummary and the related
metric-writing logic (including the later matching section around the other
success-rate gauge) so the Prometheus gauge is based on the original numeric
value, not a formatted string.
- Around line 106-138: The per-endpoint status label is creating unstable metric
series in handler_metrics.go, so update the Prometheus gauge writes in the
metrics loop to remove the changing status label and keep only the stable
endpoint label. Use the existing writePrometheusLabeledGauge calls in this
handler to drop status from olla_endpoint_up and the other endpoint gauges,
while preserving the health signal via the numeric up value and the existing
endpoint name identity.
🪄 Autofix (Beta)

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 233fee9a-9ad4-4e8b-96f9-026b015d6a2c

📥 Commits

Reviewing files that changed from the base of the PR and between a595ca7 and 2eb6be4.

📒 Files selected for processing (12)
  • config/prometheus.local.yaml.example
  • docker-compose.local.yaml.example
  • docker-compose.yaml
  • internal/app/handlers/handler_metrics.go
  • internal/app/handlers/handler_metrics_test.go
  • internal/app/handlers/handler_status.go
  • internal/app/handlers/handler_status_endpoints_test.go
  • internal/app/handlers/handler_version.go
  • internal/app/handlers/server_routes.go
  • internal/core/constants/content.go
  • internal/core/constants/endpoint.go
  • makefile

Comment on lines +41 to +44
environment:
- GF_SECURITY_ADMIN_USER=admin
- GF_SECURITY_ADMIN_PASSWORD=admin
- GF_USERS_ALLOW_SIGN_UP=false

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Avoid hardcoded Grafana admin credentials in the compose example.

Line 42 and Line 43 embed sensitive auth config directly in the file. Move these to environment variable interpolation so secrets are not committed or copied around.

As per coding guidelines: "Use environment variables for storing API keys and sensitive configuration."

Suggested patch
     environment:
-      - GF_SECURITY_ADMIN_USER=admin
-      - GF_SECURITY_ADMIN_PASSWORD=admin
+      - GF_SECURITY_ADMIN_USER=${GRAFANA_ADMIN_USER:-admin}
+      - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD}
       - GF_USERS_ALLOW_SIGN_UP=false
📝 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
environment:
- GF_SECURITY_ADMIN_USER=admin
- GF_SECURITY_ADMIN_PASSWORD=admin
- GF_USERS_ALLOW_SIGN_UP=false
environment:
- GF_SECURITY_ADMIN_USER=${GRAFANA_ADMIN_USER:-admin}
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD}
- GF_USERS_ALLOW_SIGN_UP=false
🤖 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 `@docker-compose.local.yaml.example` around lines 41 - 44, The Grafana admin
credentials in the compose example are hardcoded in the environment section, so
update the docker-compose.local.yaml.example entries for GF_SECURITY_ADMIN_USER
and GF_SECURITY_ADMIN_PASSWORD to use environment variable interpolation instead
of literal values. Keep GF_USERS_ALLOW_SIGN_UP as-is, and make sure the service
configuration still references the same environment block so users can supply
secrets externally when running Grafana.

Source: Coding guidelines

Comment on lines +142 to +151
func totalTrafficBytes(snapshot *statusSnapshot) int64 {
var total int64
for url, conn := range snapshot.connectionStats {
_ = conn
if stats, exists := snapshot.endpointStats[url]; exists {
total += stats.TotalBytes
}
}
return total
}

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP --type=go -C4 'func .*GetConnectionStats\s*\('

Repository: thushan/olla

Length of output: 150


totalTrafficBytes omits endpoints with no active connections.

The logic iterates over connectionStats, excluding any endpoint that has recorded traffic but zero active connections. Since endpointStats accumulates the definitive byte counts, the loop should aggregate directly from there to ensure the ola_total_traffic_bytes metric remains accurate even when connections drop.

This logic is identical to the redundant pattern in buildSystemSummary; consider extracting a shared helper to guarantee consistency between the API response and metrics.

♻️ Suggested fix
 func totalTrafficBytes(snapshot *statusSnapshot) int64 {
 	var total int64
-	for url, conn := range snapshot.connectionStats {
-		_ = conn
-		if stats, exists := snapshot.endpointStats[url]; exists {
-			total += stats.TotalBytes
-		}
-	}
+	for _, stats := range snapshot.endpointStats {
+		total += stats.TotalBytes
+	}
 	return total
 }
📝 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
func totalTrafficBytes(snapshot *statusSnapshot) int64 {
var total int64
for url, conn := range snapshot.connectionStats {
_ = conn
if stats, exists := snapshot.endpointStats[url]; exists {
total += stats.TotalBytes
}
}
return total
}
func totalTrafficBytes(snapshot *statusSnapshot) int64 {
var total int64
for _, stats := range snapshot.endpointStats {
total += stats.TotalBytes
}
return total
}
🤖 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 `@internal/app/handlers/handler_metrics.go` around lines 142 - 151, The
totalTrafficBytes helper is incorrectly basing aggregation on connectionStats,
which skips endpoints with traffic but no active connections; update
totalTrafficBytes to sum directly from endpointStats so the
ola_total_traffic_bytes metric stays accurate. Use the existing
totalTrafficBytes symbol in handler_metrics.go and align its logic with
buildSystemSummary by extracting a shared helper if needed to keep the API
response and metrics consistent.

@Puupuls Puupuls closed this by deleting the head repository Jun 25, 2026
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