Skip to content

Commit 20a21e7

Browse files
committed
Remove framework-specific route detection from collector middleware
Sadly, with the benefit of hindsight, this wasn't a good idea. There are two reasons we're dropping this: - It doesn't play nicely with libraries like `Rack::Builder`, which dispatches requests to different Rack apps based on a path prefix in a way that isn't visible to middleware. For example, when using `Rack::Builder`, `sinatra.route` only contains the parts of the path after the prefix that `Rack::Builder` used to dispatch to the specific app, and doesn't leave any information in the request environment to indicate which prefix it dispatched to. - It turns out framework-specific route info isn't always formatted in a way that looks good in a Prometheus label. For example, when using regex-based route-matching in Sinatra, you can end up with labels that look like `\\/vapes\\/([0-9]+)\\/?`. For a really detailed dive into those two issues, see this GitHub comment: #249 (comment)
1 parent 134c1fd commit 20a21e7

File tree

2 files changed

+5
-78
lines changed

2 files changed

+5
-78
lines changed

lib/prometheus/middleware/collector.rb

Lines changed: 5 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -72,12 +72,12 @@ def record(env, code, duration)
7272
counter_labels = {
7373
code: code,
7474
method: env['REQUEST_METHOD'].downcase,
75-
path: strip_ids_from_path(path),
75+
path: path,
7676
}
7777

7878
duration_labels = {
7979
method: env['REQUEST_METHOD'].downcase,
80-
path: strip_ids_from_path(path),
80+
path: path,
8181
}
8282

8383
@requests.increment(labels: counter_labels)
@@ -87,52 +87,10 @@ def record(env, code, duration)
8787
nil
8888
end
8989

90-
# While `PATH_INFO` is framework agnostic, and works for any Rack app, some Ruby web
91-
# frameworks pass a more useful piece of information into the request env - the
92-
# route that the request matched.
93-
#
94-
# This means that rather than using our generic `:id` and `:uuid` replacements in
95-
# the `path` label for any path segments that look like dynamic IDs, we can put the
96-
# actual route that matched in there, with correctly named parameters. For example,
97-
# if a Sinatra app defined a route like:
98-
#
99-
# get "/foo/:bar" do
100-
# ...
101-
# end
102-
#
103-
# instead of containing `/foo/:id`, the `path` label would contain `/foo/:bar`.
104-
#
105-
# Sadly, Rails is a notable exception, and (as far as I can tell at the time of
106-
# writing) doesn't provide this info in the request env.
10790
def generate_path(env)
108-
if env['sinatra.route']
109-
route = env['sinatra.route'].partition(' ').last
110-
elsif env['grape.routing_args']
111-
# We are deep in the weeds of an object that Grape passes into the request env,
112-
# but don't document any explicit guarantees about. Let's have a fallback in
113-
# case they change it down the line.
114-
#
115-
# This code would be neater with the safe navigation operator (`&.`) here rather
116-
# than the much more verbose `respond_to?` calls, but unlike Rails' `try`
117-
# method, it still raises an error if the object is non-nil, but doesn't respond
118-
# to the method being called on it.
119-
route = nil
120-
121-
route_info = env.dig('grape.routing_args', :route_info)
122-
if route_info.respond_to?(:pattern)
123-
pattern = route_info.pattern
124-
if pattern.respond_to?(:origin)
125-
route = pattern.origin
126-
end
127-
end
128-
129-
# Fall back to PATH_INFO if Grape change the structure of `grape.routing_args`
130-
route ||= env['PATH_INFO']
131-
else
132-
route = env['PATH_INFO']
133-
end
134-
135-
[env['SCRIPT_NAME'], route].join
91+
full_path = [env['SCRIPT_NAME'], env['PATH_INFO']].join
92+
93+
strip_ids_from_path(full_path)
13694
end
13795

13896
def strip_ids_from_path(path)

spec/prometheus/middleware/collector_spec.rb

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -123,37 +123,6 @@
123123
expect(registry.get(metric).get(labels: labels)).to include("0.1" => 0, "0.5" => 1)
124124
end
125125

126-
it 'prefers sinatra.route to PATH_INFO' do
127-
metric = :http_server_requests_total
128-
129-
env('sinatra.route', 'GET /foo/:named_param')
130-
get '/foo/7'
131-
env('sinatra.route', nil)
132-
expect(registry.get(metric).values.keys.last[:path]).to eql("/foo/:named_param")
133-
end
134-
135-
it 'prefers grape.routing_args to PATH_INFO' do
136-
metric = :http_server_requests_total
137-
138-
# request.env["grape.routing_args"][:route_info].pattern.origin
139-
#
140-
# Yes, this is the object you have to traverse to get the path.
141-
#
142-
# No, I'm not happy about it either.
143-
grape_routing_args = {
144-
route_info: double(:route_info,
145-
pattern: double(:pattern,
146-
origin: '/foo/:named_param'
147-
)
148-
)
149-
}
150-
151-
env('grape.routing_args', grape_routing_args)
152-
get '/foo/7'
153-
env('grape.routing_args', nil)
154-
expect(registry.get(metric).values.keys.last[:path]).to eql("/foo/:named_param")
155-
end
156-
157126
it "falls back to PATH_INFO if the structure of grape.routing_args changes" do
158127
metric = :http_server_requests_total
159128

0 commit comments

Comments
 (0)