Skip to content

🔥 feat: Prometheus middleware#1834

Open
ReneWerner87 wants to merge 10 commits into
mainfrom
implement-v3-prometheus-contrib-module
Open

🔥 feat: Prometheus middleware#1834
ReneWerner87 wants to merge 10 commits into
mainfrom
implement-v3-prometheus-contrib-module

Conversation

@ReneWerner87
Copy link
Copy Markdown
Member

@ReneWerner87 ReneWerner87 commented Apr 25, 2026

Reopened from #1610 — original was closed due to a broken force-push that orphaned main's history (now restored).

All original changes preserved from the same branch.

Summary by CodeRabbit

  • New Features
    • Added Prometheus middleware for Fiber v3 to collect and expose request metrics including counts, latency, and payload sizes
    • Configurable metrics endpoint with support for custom labels, URI filtering, and status code filtering
    • Compatible with OpenMetrics output and custom Prometheus registries

Copilot AI review requested due to automatic review settings April 25, 2026 11:48
@ReneWerner87 ReneWerner87 requested a review from a team as a code owner April 25, 2026 11:48
@ReneWerner87 ReneWerner87 requested review from efectn, gaby and sixcolors and removed request for a team April 25, 2026 11:48
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request adds a Prometheus middleware for Fiber v3, enabling the tracking of HTTP request metrics, durations, and sizes with support for custom registries and OpenMetrics. The review feedback points out a critical error regarding the Go version in go.mod and identifies major performance bottlenecks in the route-tracking logic. Specifically, the reviewer recommends utilizing Fiber's built-in ctx.Route() instead of manual stack scanning and advises against expensive metric deletions on unmatched requests to ensure the middleware remains performant under high traffic.

Comment thread v3/prometheus/go.mod
@@ -0,0 +1,41 @@
module github.com/gofiber/contrib/v3/prometheus

go 1.25.0
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

critical

The Go version is set to 1.25.0, which is a future version that has not been released yet (the current stable versions are 1.23 and 1.24). This will prevent the module from being compiled or used by developers on current stable toolchains.

Suggested change
go 1.25.0
go 1.23.0

routePath := m.resolveRoutePath(ctx)
routeKey := method + " " + routePath

registered := m.refreshRoutes(ctx, routeKey)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

The refreshRoutes function is called on every request and performs an $O(N)$ scan of the entire Fiber route stack via stackSize (line 403). This introduces a significant performance bottleneck, especially for applications with many routes.

Furthermore, stackSize is an unreliable cache invalidation key because the total count of routes can remain the same even if routes are modified or replaced.

Since Fiber v3 populates ctx.Route() before executing middlewares, you can rely on ctx.Route() as the source of truth for whether a request matches a registered route and what its template path is. This eliminates the need for manual stack scanning, the registeredRoutes map, and the associated mutex locks.

Comment on lines +330 to +373
func (m *middleware) refreshRoutes(ctx fiber.Ctx, routeKey string) bool {
stack := ctx.App().Stack()
stackVersion := stackSize(stack)

m.routesMu.RLock()
currentVersion := m.routesVersion
_, registered := m.registeredRoutes[routeKey]
m.routesMu.RUnlock()

if registered && currentVersion == stackVersion {
return true
}

routes := make(map[string]struct{})
for i := range stack {
routesList := stack[i]
for j := range routesList {
r := routesList[j]
if r == nil {
continue
}

path := utils.CopyString(r.Path)
if path == "" {
path = "/"
} else if path != "/" {
path = normalizePath(path)
}

routes[r.Method+" "+path] = struct{}{}
if r.Method == fiber.MethodGet {
routes[fiber.MethodHead+" "+path] = struct{}{}
}
}
}

m.routesMu.Lock()
m.registeredRoutes = routes
m.routesVersion = stackVersion
_, registered = routes[routeKey]
m.routesMu.Unlock()

return registered
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

As noted in the performance feedback for the instrument function, this entire route caching and stack scanning logic is redundant in Fiber v3. You can remove this function and the corresponding fields (registeredRoutes, routesVersion, routesMu) from the middleware struct to improve performance and simplify the codebase.

Comment thread v3/prometheus/README.md
defer func() {
m.requestInFlight.WithLabelValues(method, inflightPath).Dec()
if deleteGauge {
m.requestInFlight.DeleteLabelValues(method, inflightPath)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Calling DeleteLabelValues on every unmatched request (e.g., 404s) is computationally expensive because it involves acquiring global locks within the Prometheus registry and cleaning up internal metric maps. Under high load or a 404-based DDoS attack, this will cause significant performance degradation.

Instead of using the raw ctx.Path() and then deleting the series, it is better to use a fixed placeholder label (like /__unmatched__) for the in-flight gauge when ctx.Route() is nil. This avoids the expensive cleanup and protects against cardinality explosion.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a new Prometheus instrumentation middleware module for Fiber v3, including configurable collectors/OpenMetrics options and a full test suite, and wires it into the repo (workspace + docs + CI).

Changes:

  • Introduce v3/prometheus middleware that instruments requests (counts, status class, durations, sizes, in-flight) and serves a Prometheus/OpenMetrics scrape endpoint.
  • Add comprehensive tests covering skip/ignore behavior, custom buckets/registry, HEAD behavior, OpenMetrics negotiation, and exemplars.
  • Register the new module in workspace/docs and add a dedicated GitHub Actions workflow.

Reviewed changes

Copilot reviewed 9 out of 10 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
v3/prometheus/prometheus.go Implements the middleware, metrics registration, route tracking, and scrape handling.
v3/prometheus/config.go Defines configuration options and defaulting/copying behavior.
v3/prometheus/prometheus_test.go Adds test coverage for metrics emission and configuration toggles.
v3/prometheus/README.md Documents usage and available metrics/options.
v3/prometheus/go.mod New module definition and direct dependencies.
v3/prometheus/go.sum New module dependency checksums.
.github/workflows/test-prometheus.yml Adds CI job for the new module.
go.work Adds ./v3/prometheus to the workspace.
README.md Adds Prometheus to the v3 module list.
v3/README.md Adds Prometheus to the v3 module list.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +243 to +246
m.requestInFlight.WithLabelValues(method, inflightPath).Inc()
deleteGauge := false
defer func() {
m.requestInFlight.WithLabelValues(method, inflightPath).Dec()
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

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

The in-flight gauge uses WithLabelValues() again inside the defer and may also delete the label values. If another request deletes these label values before this defer runs, the deferred WithLabelValues() will create a fresh gauge at 0 and then Dec() it to -1, producing incorrect negative in-flight counts. Consider capturing the child gauge returned by WithLabelValues() once (use it for both Inc/Dec) and avoid calling WithLabelValues() in the defer; deletion can still be done after Dec if desired.

Suggested change
m.requestInFlight.WithLabelValues(method, inflightPath).Inc()
deleteGauge := false
defer func() {
m.requestInFlight.WithLabelValues(method, inflightPath).Dec()
inflightGauge := m.requestInFlight.WithLabelValues(method, inflightPath)
inflightGauge.Inc()
deleteGauge := false
defer func() {
inflightGauge.Dec()

Copilot uses AI. Check for mistakes.
Comment on lines +261 to +264
if _, ok := m.skipURIs[routePath]; ok {
deleteGauge = true
return err
}
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

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

SkipURIs are checked after ctx.Next() and after the in-flight gauge has already been incremented. This means "skipped" routes can still show up temporarily in http_requests_in_progress during the request (and adds avoidable overhead). If SkipURIs is intended to exclude all instrumentation including in-flight tracking, check SkipURIs before touching requestInFlight and return ctx.Next() early.

Copilot uses AI. Check for mistakes.
Comment thread v3/prometheus/README.md
Comment on lines +44 to +49
app.Use("/metrics", fiberprometheus.New(fiberprometheus.Config{
Service: "my-service-name",
SkipURIs: []string{"/ping"},
IgnoreStatusCodes: []int{401, 403, 404},
}))

Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

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

The example mounts the middleware only at /metrics (app.Use("/metrics", ...)). With Fiber's path-based middleware, that would generally only run for /metrics requests, so it won't instrument other routes unless the handler is also registered as a global middleware. Consider updating the example to show registering the returned handler twice (global + metrics path), or clarify explicitly how to mount it so it both instruments traffic and serves the scrape endpoint.

Suggested change
app.Use("/metrics", fiberprometheus.New(fiberprometheus.Config{
Service: "my-service-name",
SkipURIs: []string{"/ping"},
IgnoreStatusCodes: []int{401, 403, 404},
}))
prometheus := fiberprometheus.New(fiberprometheus.Config{
Service: "my-service-name",
SkipURIs: []string{"/ping"},
IgnoreStatusCodes: []int{401, 403, 404},
})
app.Use(prometheus)
app.Use("/metrics", prometheus)

Copilot uses AI. Check for mistakes.
Comment on lines +9 to +13
- 'v3/prometheus/go.mod'
pull_request:
paths:
- 'v3/prometheus/**/*.go'
- 'v3/prometheus/go.mod'
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

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

The workflow only triggers on changes to Go files and go.mod, but dependency-only updates can be done by modifying go.sum. Consider including v3/prometheus/go.sum in the workflow paths filters so CI still runs when module dependencies change.

Suggested change
- 'v3/prometheus/go.mod'
pull_request:
paths:
- 'v3/prometheus/**/*.go'
- 'v3/prometheus/go.mod'
- 'v3/prometheus/go.mod'
- 'v3/prometheus/go.sum'
pull_request:
paths:
- 'v3/prometheus/**/*.go'
- 'v3/prometheus/go.mod'
- 'v3/prometheus/go.sum'

Copilot uses AI. Check for mistakes.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 25, 2026

Walkthrough

This pull request introduces a complete Prometheus middleware implementation for Fiber v3. It adds configuration structures, core middleware logic for collecting request metrics (counters, histograms, gauges), documentation, and a comprehensive test suite covering various configuration scenarios and OpenMetrics support.

Changes

Cohort / File(s) Summary
Documentation
README.md, v3/README.md, v3/prometheus/README.md
Inserts Prometheus middleware reference into READMEs and creates new documentation page detailing metrics, installation, configuration options, example usage, and dashboard integration.
Configuration
v3/prometheus/config.go
Defines Config struct with fields for Prometheus registry/gatherer, collector toggles, histogram bucket configurations, route filtering, OpenMetrics encoding, compression, and middleware chaining. Provides ConfigDefault with sensible defaults and implements safe configuration normalization with defensive copying.
Middleware Implementation
v3/prometheus/prometheus.go
Implements the Prometheus middleware handler: initializes collectors, instruments request/response metrics (counters, histograms, gauges), detects metrics endpoint routing, caches registered routes, and intercepts requests to collect telemetry while supporting configurable filtering and custom registries.
Test Suite
v3/prometheus/prometheus_test.go
Comprehensive test coverage validating metric collection, configuration behaviors (URI exclusion, status filtering, bucket customization), unmatched-route tracking, route refresh logic, custom registry support, OpenMetrics encoding with exemplars, compression toggling, and runtime collector controls.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Middleware as Prometheus Middleware
    participant MetricsRegistry as Prometheus Registry
    participant Handler as Next Handler/App
    participant MetricsEndpoint as /metrics Endpoint

    Client->>Middleware: HTTP Request
    Note over Middleware: Check if metrics endpoint request
    
    alt Is Metrics Endpoint Request
        Middleware->>MetricsRegistry: Query registered metrics
        MetricsRegistry-->>Middleware: Serialized metrics (Prometheus/OpenMetrics)
        Middleware-->>Client: Metrics output
    else Regular Request
        Middleware->>Middleware: Increment in-flight gauge
        Middleware->>Handler: Execute handler
        Handler-->>Middleware: Response
        
        Middleware->>MetricsRegistry: Record request counter
        Middleware->>MetricsRegistry: Record request duration histogram
        Middleware->>MetricsRegistry: Record request/response size histograms
        Middleware->>MetricsRegistry: Record status-class counter
        Middleware->>Middleware: Decrement in-flight gauge
        
        Middleware-->>Client: HTTP Response
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Suggested labels

✏️ Feature

Suggested reviewers

  • gaby
  • sixcolors
  • efectn

Poem

🐰 A metrics hop through Prometheus we go,
Counting requests with a cheerful glow!
Histograms tracked, gauges dance in place,
Fiber's telemetry sets the pace—
Registry registered, the endpoint's set free! 📊✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.13% 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 references Prometheus middleware, which aligns with the changeset that adds a complete Prometheus Fiber middleware implementation across multiple files (config, implementation, tests, and documentation).
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch implement-v3-prometheus-contrib-module

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.11.4)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


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 and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (4)
v3/prometheus/README.md (1)

41-64: Mixed tabs/spaces in the example block.

Lines 42, 50, 54, 58, 62 use 8-space indentation while lines 44–48 use tabs. Pick one (Go convention is tabs) so the rendered code block is uniform.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@v3/prometheus/README.md` around lines 41 - 64, The README example mixes
spaces and tabs; normalize the indentation to Go convention (tabs) so the code
block renders uniformly: update the func main() example and its handlers
(main(), app := fiber.New(), app.Use(fiberprometheus.Config{...}), app.Get("/",
...), app.Get("/ping", ...), app.Post("/some", ...), app.Listen(":3000")) to use
tabs for all leading indentation and remove the 8-space prefixes on the listed
lines so every line uses tabs consistently.
v3/prometheus/prometheus.go (2)

403-409: stackSize only counts routes — it can miss a swap.

The version is just the total route count. If a route is removed and another added (or a sub-app is mounted/replaced) without changing the count, currentVersion == stackVersion stays true and the cached registeredRoutes map silently goes stale. In practice Fiber rarely removes routes, but sub-app mounting via Use(prefix, subApp) can rewrite the stack.

A cheaper and more reliable signal would be app.HandlersCount() (already exposed by Fiber) or hashing r.Method+r.Path while iterating once. Optional — only worth doing if you expect dynamic mounts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@v3/prometheus/prometheus.go` around lines 403 - 409, stackSize currently only
sums route counts which can miss swaps and leave registeredRoutes stale when
stack composition changes; update stackSize (or the versioning check that uses
it) to use a more reliable signal such as Fiber's app.HandlersCount() or compute
a cheap hash while iterating over routes (e.g., incorporate r.Method and r.Path)
so swaps change the version; modify the logic that compares
stackVersion/currentVersion to use the new handlers count or the hash, ensuring
functions and variables like stackSize, registeredRoutes, stackVersion,
currentVersion, and any code that mounts sub-apps via Use(prefix, subApp)
reflect the new version source.

58-64: Service silently overrides Labels["service"].

If a caller sets both cfg.Labels["service"] and cfg.Service, the latter wins without any warning. That's probably fine (Service is the dedicated knob), but worth documenting in the Service field doc in config.go so users aren't surprised when their custom label disappears.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@v3/prometheus/prometheus.go` around lines 58 - 64, The code in prometheus.go
silently lets cfg.Service override any existing cfg.Labels["service"] when
building labels (see the labels map creation and the cfg.Service assignment);
update the Service field's doc comment in the config struct (the Service field
in config.go) to explicitly state that Service takes precedence and will
overwrite any Labels["service"] value so users are not surprised, and mention
that they should set Service only if they want to override the label or omit the
key from Labels if they want the label retained.
v3/prometheus/prometheus_test.go (1)

712-721: Exemplar bucket-label assertion depends on OpenMetrics float formatting.

The check requires le="256.0" (OpenMetrics float) rather than le="256" (Prometheus text). That's correct given the Accept: application/openmetrics-text header, but the brittle multi-Contains chain will be hard to debug if client_golang ever updates its sample/exemplar serialization. Consider either parsing the line via strings.Fields and asserting on a structured form, or relaxing the bucket boundary to a regex (e.g., le="256(\.0)?"). Optional.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@v3/prometheus/prometheus_test.go` around lines 712 - 721, The test's exemplar
bucket-label checks in prometheus_test.go use brittle strings.Contains chains
against metrics and require the OpenMetrics float format le="256.0"; update the
assertions around requestLineWithExemplar and responseLineWithExemplar to be
robust by matching the bucket boundary with a regex that accepts either le="256"
or le="256.0" (e.g., le="256(\\.0)?") or by tokenizing each metric line via
strings.Fields and comparing structured label/key-value pairs instead of raw
substring matches; update the checks that currently call strings.Contains(line,
"...le=\"256.0\"...") to use the regex or parsed label comparison so they
tolerate formatting changes from client_golang while still asserting the
exemplar presence.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@v3/prometheus/prometheus.go`:
- Around line 242-278: The deferred cleanup in the middleware uses
m.requestInFlight.DeleteLabelValues with a deleteGauge flag (controlled in the
request path where deleteGauge is set) which races under concurrent requests and
can produce negative in-flight counts; remove the DeleteLabelValues call and the
deleteGauge plumbing (keep the Inc()/Dec() calls on
m.requestInFlight.WithLabelValues(method, inflightPath)) so labels remain with
value 0 instead of being removed, and update tests (e.g.,
TestIgnoreStatusCodesRemovesInFlightGauge) to expect a 0-valued metric rather
than absence.

In `@v3/prometheus/README.md`:
- Line 91: The sentence "All of the options default to `false`" is incorrect —
update the README text to accurately state that some options default to false
while others have non-false defaults; explicitly mention the exceptions by name
(UnmatchedRouteLabel defaults to `/__unmatched__`, and RequestDurationBuckets,
RequestSizeBuckets, ResponseSizeBuckets have non-false default bucket values) or
reword to "Most options default to `false`; the following have non-false
defaults: UnmatchedRouteLabel, RequestDurationBuckets, RequestSizeBuckets,
ResponseSizeBuckets" so the statement no longer contradicts the bullets.
- Around line 41-64: The README example mounts the Prometheus middleware only at
"/metrics" using app.Use("/metrics", ...), which prevents instrumentation of
other routes; change the example to register the middleware globally (call
app.Use(handler) with the fiberprometheus middleware returned by
fiberprometheus.New(...)) and then separately mount the metrics endpoint
(app.Use("/metrics", handler) or similar) so application routes ("/", "/ping",
"/some") are instrumented and metrics are exposed; also update the prose that
currently claims the middleware "continues to instrument all routed traffic" to
reflect that instrumentation only occurs when the middleware is registered
globally and correct the statement that "All of the options default to `false`"
to list the actual numeric defaults for RequestDurationBuckets,
RequestSizeBuckets, and ResponseSizeBuckets as shown in the config.

---

Nitpick comments:
In `@v3/prometheus/prometheus_test.go`:
- Around line 712-721: The test's exemplar bucket-label checks in
prometheus_test.go use brittle strings.Contains chains against metrics and
require the OpenMetrics float format le="256.0"; update the assertions around
requestLineWithExemplar and responseLineWithExemplar to be robust by matching
the bucket boundary with a regex that accepts either le="256" or le="256.0"
(e.g., le="256(\\.0)?") or by tokenizing each metric line via strings.Fields and
comparing structured label/key-value pairs instead of raw substring matches;
update the checks that currently call strings.Contains(line,
"...le=\"256.0\"...") to use the regex or parsed label comparison so they
tolerate formatting changes from client_golang while still asserting the
exemplar presence.

In `@v3/prometheus/prometheus.go`:
- Around line 403-409: stackSize currently only sums route counts which can miss
swaps and leave registeredRoutes stale when stack composition changes; update
stackSize (or the versioning check that uses it) to use a more reliable signal
such as Fiber's app.HandlersCount() or compute a cheap hash while iterating over
routes (e.g., incorporate r.Method and r.Path) so swaps change the version;
modify the logic that compares stackVersion/currentVersion to use the new
handlers count or the hash, ensuring functions and variables like stackSize,
registeredRoutes, stackVersion, currentVersion, and any code that mounts
sub-apps via Use(prefix, subApp) reflect the new version source.
- Around line 58-64: The code in prometheus.go silently lets cfg.Service
override any existing cfg.Labels["service"] when building labels (see the labels
map creation and the cfg.Service assignment); update the Service field's doc
comment in the config struct (the Service field in config.go) to explicitly
state that Service takes precedence and will overwrite any Labels["service"]
value so users are not surprised, and mention that they should set Service only
if they want to override the label or omit the key from Labels if they want the
label retained.

In `@v3/prometheus/README.md`:
- Around line 41-64: The README example mixes spaces and tabs; normalize the
indentation to Go convention (tabs) so the code block renders uniformly: update
the func main() example and its handlers (main(), app := fiber.New(),
app.Use(fiberprometheus.Config{...}), app.Get("/", ...), app.Get("/ping", ...),
app.Post("/some", ...), app.Listen(":3000")) to use tabs for all leading
indentation and remove the 8-space prefixes on the listed lines so every line
uses tabs consistently.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c33d0e7e-5d96-4b48-a332-f8555b2753ad

📥 Commits

Reviewing files that changed from the base of the PR and between 9937cda and 7fe5a88.

⛔ Files ignored due to path filters (4)
  • .github/workflows/test-prometheus.yml is excluded by !**/*.yml
  • go.work is excluded by !**/*.work, !**/*.work
  • v3/prometheus/go.mod is excluded by !**/*.mod
  • v3/prometheus/go.sum is excluded by !**/*.sum, !**/*.sum
📒 Files selected for processing (6)
  • README.md
  • v3/README.md
  • v3/prometheus/README.md
  • v3/prometheus/config.go
  • v3/prometheus/prometheus.go
  • v3/prometheus/prometheus_test.go
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: lint / lint (v3/prometheus)
  • GitHub Check: lint / lint (v3/otel/example)
  • GitHub Check: lint / lint (v3/newrelic)
  • GitHub Check: lint / lint (v3/loadshed)
  • GitHub Check: lint / lint (v3/otel)
  • GitHub Check: lint / lint (v3/opa)
  • GitHub Check: lint / lint (v3/jwt)
  • GitHub Check: lint / lint (v3/circuitbreaker)
  • GitHub Check: lint / lint (v3/hcaptcha)
  • GitHub Check: lint / lint (v3/i18n)
  • GitHub Check: Agent
🧰 Additional context used
🪛 LanguageTool
v3/prometheus/README.md

[style] ~90-~90: Consider removing “of” to be more concise
Context: ...62144 524288 1048576 2097152 5242880] All of the options default tofalse` and can be e...

(ALL_OF_THE)

🔇 Additional comments (5)
README.md (1)

36-36: LGTM.

Index entry follows the established pattern for link target and workflow badge.

v3/README.md (1)

34-34: LGTM.

Mirrors the top-level README index update with consistent link/badge format.

v3/prometheus/config.go (1)

133-187: Defaults & deep-copy logic looks good.

The dual-path defaulting (zero-config vs. caller-supplied) and the deep-copies of Labels, RequestDurationBuckets, RequestSizeBuckets, ResponseSizeBuckets, SkipURIs, and IgnoreStatusCodes correctly insulate the package-level defaults from later mutation and prevent callers from observing shared backing arrays. strings.Clone on UnmatchedRouteLabel is a nice touch.

One small consideration (not a defect): a caller-supplied non-nil but empty bucket slice ([]float64{}) bypasses the nil check and would produce a histogram with only the implicit +Inf bucket. The doc says "Provide nil to use the defaults," so this is by design — flagging just so it's a conscious choice.

v3/prometheus/prometheus_test.go (1)

56-729: Test coverage is thorough.

Strong end-to-end coverage of metrics emission, configuration knobs, OpenMetrics negotiation/exemplars, runtime collector toggles, custom registries, and HEAD handling. The defer recover() pattern in TestRegistererWithoutGathererPanics correctly asserts the documented panic path.

Note: if you adopt the suggested fix for the DeleteLabelValues race in prometheus.go, TestIgnoreStatusCodesRemovesInFlightGauge (lines 200–218) will need to change from "metric absent" to "metric present with value 0," since dropping the delete is the correctness-preserving choice.

v3/prometheus/prometheus.go (1)

212-226: Verify what route.Path returns for app.Use(prefix, handler) registrations in Fiber v3-rc.3.

The isMetricsRequest method performs an exact-string comparison between route.Path and ctx.Path() after normalizing trailing slashes. In Fiber v3, middleware registered via app.Use("/metrics", handler) may have route.Path set to "/metrics/*" (with a wildcard suffix for prefix matching) rather than "/metrics", which would cause the equality check to fail and prevent metrics endpoint detection.

The tests register the handler twice—once globally (app.Use(handler)) and once with a prefix (app.Use(metricsPath, handler))—and the tests pass, but this dual registration means the global handler always catches the metrics request, masking whether the prefix-specific detection actually works. Confirm the actual value of ctx.Route().Path for prefix-mounted middleware and adjust the comparison logic to handle potential wildcard suffixes if needed (e.g., use prefix matching, strip wildcards, or store the configured metrics path at construction time).

Comment on lines +242 to +278
inflightPath := routePath
m.requestInFlight.WithLabelValues(method, inflightPath).Inc()
deleteGauge := false
defer func() {
m.requestInFlight.WithLabelValues(method, inflightPath).Dec()
if deleteGauge {
m.requestInFlight.DeleteLabelValues(method, inflightPath)
}
}()

start := time.Now()

err := ctx.Next()

if !registered && !trackUnmatched {
deleteGauge = true
return err
}

if _, ok := m.skipURIs[routePath]; ok {
deleteGauge = true
return err
}

status := fiber.StatusInternalServerError
if err != nil {
if e, ok := err.(*fiber.Error); ok {
status = e.Code
}
} else {
status = ctx.Response().StatusCode()
}

if _, ok := m.ignoreStatusCode[status]; ok {
deleteGauge = true
return err
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

DeleteLabelValues in the deferred cleanup creates a race that yields negative in-flight values.

Under concurrent load with two requests sharing the same (method, inflightPath) label set, where one is destined for skip/ignore (deleteGauge=true):

T1: Inc          -> 1
T2: Inc          -> 2
T1: Dec          -> 1     (T2 still in flight)
T1: Delete       -> entry removed
T2: Dec          -> WithLabelValues recreates at 0, then Dec -> -1

The in-flight gauge will start emitting negative numbers, and the cleanup also corrupts T2's count. The same hazard applies on lines 256–264 and 275–278 anywhere deleteGauge is set while another goroutine has the matching label set in flight.

The simplest correct fix is to drop the deletion entirely — letting the gauge sit at 0 between requests is harmless and matches what the duration/size histograms already do. If lifecycle cleanup of stale labels is desired, it needs to be coordinated (e.g., a guarded counter per label that only deletes on the 0→0 transition, or periodic reaping outside the request path).

🔒 Suggested fix: stop deleting labels in the request path
 	inflightPath := routePath
 	m.requestInFlight.WithLabelValues(method, inflightPath).Inc()
-	deleteGauge := false
 	defer func() {
 		m.requestInFlight.WithLabelValues(method, inflightPath).Dec()
-		if deleteGauge {
-			m.requestInFlight.DeleteLabelValues(method, inflightPath)
-		}
 	}()
@@
 	if !registered && !trackUnmatched {
-		deleteGauge = true
 		return err
 	}

 	if _, ok := m.skipURIs[routePath]; ok {
-		deleteGauge = true
 		return err
 	}
@@
 	if _, ok := m.ignoreStatusCode[status]; ok {
-		deleteGauge = true
 		return err
 	}

Note: TestIgnoreStatusCodesRemovesInFlightGauge will need to be updated to assert … 0 instead of complete absence (since the metric will now persist with value 0), or the test should remove that strict expectation.

📝 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
inflightPath := routePath
m.requestInFlight.WithLabelValues(method, inflightPath).Inc()
deleteGauge := false
defer func() {
m.requestInFlight.WithLabelValues(method, inflightPath).Dec()
if deleteGauge {
m.requestInFlight.DeleteLabelValues(method, inflightPath)
}
}()
start := time.Now()
err := ctx.Next()
if !registered && !trackUnmatched {
deleteGauge = true
return err
}
if _, ok := m.skipURIs[routePath]; ok {
deleteGauge = true
return err
}
status := fiber.StatusInternalServerError
if err != nil {
if e, ok := err.(*fiber.Error); ok {
status = e.Code
}
} else {
status = ctx.Response().StatusCode()
}
if _, ok := m.ignoreStatusCode[status]; ok {
deleteGauge = true
return err
}
inflightPath := routePath
m.requestInFlight.WithLabelValues(method, inflightPath).Inc()
defer func() {
m.requestInFlight.WithLabelValues(method, inflightPath).Dec()
}()
start := time.Now()
err := ctx.Next()
if !registered && !trackUnmatched {
return err
}
if _, ok := m.skipURIs[routePath]; ok {
return err
}
status := fiber.StatusInternalServerError
if err != nil {
if e, ok := err.(*fiber.Error); ok {
status = e.Code
}
} else {
status = ctx.Response().StatusCode()
}
if _, ok := m.ignoreStatusCode[status]; ok {
return err
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@v3/prometheus/prometheus.go` around lines 242 - 278, The deferred cleanup in
the middleware uses m.requestInFlight.DeleteLabelValues with a deleteGauge flag
(controlled in the request path where deleteGauge is set) which races under
concurrent requests and can produce negative in-flight counts; remove the
DeleteLabelValues call and the deleteGauge plumbing (keep the Inc()/Dec() calls
on m.requestInFlight.WithLabelValues(method, inflightPath)) so labels remain
with value 0 instead of being removed, and update tests (e.g.,
TestIgnoreStatusCodesRemovesInFlightGauge) to expect a 0-valued metric rather
than absence.

Comment thread v3/prometheus/README.md
Comment on lines +41 to +64
func main() {
app := fiber.New()

app.Use("/metrics", fiberprometheus.New(fiberprometheus.Config{
Service: "my-service-name",
SkipURIs: []string{"/ping"},
IgnoreStatusCodes: []int{401, 403, 404},
}))

app.Get("/", func(c fiber.Ctx) error {
return c.SendString("Hello World")
})

app.Get("/ping", func(c fiber.Ctx) error {
return c.SendString("pong")
})

app.Post("/some", func(c fiber.Ctx) error {
return c.SendString("Welcome!")
})

app.Listen(":3000")
}
```
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In Fiber v3, does app.Use("/prefix", handler) execute the handler for requests outside that prefix?

💡 Result:

No, in Fiber v3, app.Use("/prefix", handler) does not execute the handler for requests outside that prefix. The prefix limits the middleware/handler to paths that begin with the specified prefix, with stricter matching requiring an exact match or slash boundary (e.g., "/api" matches "/api" and "/api/users" but not "/apiv2").

Citations:


🏁 Script executed:

# Find the Prometheus test files to verify the double-mount pattern claim
find . -path "*/prometheus/*" -name "*.go" -o -name "*.md" | head -20

Repository: gofiber/contrib

Length of output: 522


🏁 Script executed:

# Also check the README.md file location and content
find . -path "*/prometheus/README.md"

Repository: gofiber/contrib

Length of output: 85


🏁 Script executed:

# Search for newAppWithMiddleware to verify the double-mount pattern
rg "newAppWithMiddleware" --type go

Repository: gofiber/contrib

Length of output: 2137


🏁 Script executed:

# Extract the newAppWithMiddleware function implementation
sed -n '/^func newAppWithMiddleware/,/^}/p' v3/prometheus/prometheus_test.go

Repository: gofiber/contrib

Length of output: 315


🏁 Script executed:

# Now check the README.md file around line 91 and the example code
cat -n v3/prometheus/README.md | sed -n '85,100p'

Repository: gofiber/contrib

Length of output: 1053


🏁 Script executed:

# Check the indentation of the README example (lines 41-64)
cat -n v3/prometheus/README.md | sed -n '41,65p'

Repository: gofiber/contrib

Length of output: 836


Example only mounts at /metrics — other routes won't be instrumented.

The example uses app.Use("/metrics", handler) which, in Fiber v3, limits the middleware to requests under the /metrics prefix only. Requests to /, /ping, and /some never reach the middleware, so:

  • No counters/histograms/in-flight metrics are recorded for any application route.
  • Service, SkipURIs, and IgnoreStatusCodes in the example are effectively dead options.
  • /metrics would expose only the Go/process collectors.

The test suite's newAppWithMiddleware (prometheus_test.go) registers the handler twice: app.Use(handler) for global instrumentation, then app.Use(metricsPath, handler) for the metrics endpoint. The README must follow the same pattern.

Additionally, lines 93–96 claim the middleware "continues to instrument all routed traffic," which is false given the single prefix-mount behavior. Line 91 also incorrectly states "All of the options default to false" when RequestDurationBuckets, RequestSizeBuckets, and ResponseSizeBuckets have explicit numeric array defaults (lines 86–89).

📝 Suggested example fix
 func main() {
         app := fiber.New()

-	app.Use("/metrics", fiberprometheus.New(fiberprometheus.Config{
+	prom := fiberprometheus.New(fiberprometheus.Config{
 		Service:           "my-service-name",
 		SkipURIs:          []string{"/ping"},
 		IgnoreStatusCodes: []int{401, 403, 404},
-	}))
+	})
+
+	// Instrument every request.
+	app.Use(prom)
+	// Expose the metrics endpoint at /metrics.
+	app.Use("/metrics", prom)
📝 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 main() {
app := fiber.New()
app.Use("/metrics", fiberprometheus.New(fiberprometheus.Config{
Service: "my-service-name",
SkipURIs: []string{"/ping"},
IgnoreStatusCodes: []int{401, 403, 404},
}))
app.Get("/", func(c fiber.Ctx) error {
return c.SendString("Hello World")
})
app.Get("/ping", func(c fiber.Ctx) error {
return c.SendString("pong")
})
app.Post("/some", func(c fiber.Ctx) error {
return c.SendString("Welcome!")
})
app.Listen(":3000")
}
```
func main() {
app := fiber.New()
prom := fiberprometheus.New(fiberprometheus.Config{
Service: "my-service-name",
SkipURIs: []string{"/ping"},
IgnoreStatusCodes: []int{401, 403, 404},
})
// Instrument every request.
app.Use(prom)
// Expose the metrics endpoint at /metrics.
app.Use("/metrics", prom)
app.Get("/", func(c fiber.Ctx) error {
return c.SendString("Hello World")
})
app.Get("/ping", func(c fiber.Ctx) error {
return c.SendString("pong")
})
app.Post("/some", func(c fiber.Ctx) error {
return c.SendString("Welcome!")
})
app.Listen(":3000")
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@v3/prometheus/README.md` around lines 41 - 64, The README example mounts the
Prometheus middleware only at "/metrics" using app.Use("/metrics", ...), which
prevents instrumentation of other routes; change the example to register the
middleware globally (call app.Use(handler) with the fiberprometheus middleware
returned by fiberprometheus.New(...)) and then separately mount the metrics
endpoint (app.Use("/metrics", handler) or similar) so application routes ("/",
"/ping", "/some") are instrumented and metrics are exposed; also update the
prose that currently claims the middleware "continues to instrument all routed
traffic" to reflect that instrumentation only occurs when the middleware is
registered globally and correct the statement that "All of the options default
to `false`" to list the actual numeric defaults for RequestDurationBuckets,
RequestSizeBuckets, and ResponseSizeBuckets as shown in the config.

Comment thread v3/prometheus/README.md
- Request size: `[256 512 1024 2048 4096 8192 16384 32768 65536 131072 262144 524288 1048576 2097152 5242880]`
- Response size: `[256 512 1024 2048 4096 8192 16384 32768 65536 131072 262144 524288 1048576 2097152 5242880]`

All of the options default to `false` and can be enabled or disabled individually as needed.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Inaccurate defaults statement.

The bullets immediately above this line include UnmatchedRouteLabel (defaults to /__unmatched__), RequestDurationBuckets, RequestSizeBuckets, and ResponseSizeBuckets (all non-false defaults), so "All of the options default to false" is incorrect and contradicts the surrounding text.

📝 Proposed wording
-All of the options default to `false` and can be enabled or disabled individually as needed.
+All boolean toggles default to `false` and can be enabled individually as needed.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@v3/prometheus/README.md` at line 91, The sentence "All of the options default
to `false`" is incorrect — update the README text to accurately state that some
options default to false while others have non-false defaults; explicitly
mention the exceptions by name (UnmatchedRouteLabel defaults to
`/__unmatched__`, and RequestDurationBuckets, RequestSizeBuckets,
ResponseSizeBuckets have non-false default bucket values) or reword to "Most
options default to `false`; the following have non-false defaults:
UnmatchedRouteLabel, RequestDurationBuckets, RequestSizeBuckets,
ResponseSizeBuckets" so the statement no longer contradicts the bullets.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants