Skip to content

Commit 47d9699

Browse files
committed
Add user agent to telemetry
1 parent 6cab54a commit 47d9699

File tree

7 files changed

+188
-5
lines changed

7 files changed

+188
-5
lines changed

lib/cloud_controller/telemetry_logger.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ def emit(event_name, entries, raw_entries={})
3131
INTEGER_FIELDS.include?(k)
3232
end.transform_values(&:to_i))
3333

34+
# Add user-agent if available
35+
user_agent = ::VCAP::Request.user_agent
36+
converted_entries['user-agent'] = user_agent if user_agent.present?
37+
3438
resp = {
3539
'telemetry-source' => 'cloud_controller_ng',
3640
'telemetry-time' => Time.now.to_datetime.rfc3339,

lib/vcap/request.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,14 @@ def b3_span_id
5656
Thread.current[:b3_span_id]
5757
end
5858

59+
def user_agent=(user_agent)
60+
Thread.current[:user_agent] = user_agent
61+
end
62+
63+
def user_agent
64+
Thread.current[:user_agent]
65+
end
66+
5967
def db_query_metrics
6068
init_db_query_metrics
6169

middleware/vcap_request_id.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@ def initialize(app)
1111
def call(env)
1212
env['cf.request_id'] = external_request_id(env) || internal_request_id
1313
::VCAP::Request.current_id = env['cf.request_id']
14+
::VCAP::Request.user_agent = env['HTTP_USER_AGENT']
1415

1516
begin
1617
status, headers, body = @app.call(env)
1718
ensure
1819
::VCAP::Request.current_id = nil
20+
::VCAP::Request.user_agent = nil
1921
end
2022

2123
headers['X-VCAP-Request-ID'] = env['cf.request_id']
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
require 'spec_helper'
2+
3+
module VCAP::CloudController
4+
RSpec.describe 'VCAP Request ID Middleware - User Agent' do
5+
let(:captured_values) { {} }
6+
7+
before do
8+
allow_any_instance_of(CloudFoundry::Middleware::VcapRequestId).to receive(:call).and_wrap_original do |original_method, *args|
9+
env = args[0]
10+
result = original_method.call(*args)
11+
captured_values[:user_agent_during_request] = env['HTTP_USER_AGENT']
12+
captured_values[:request_id_during_request] = env['cf.request_id']
13+
14+
result
15+
end
16+
end
17+
18+
describe 'user agentand request id handling' do
19+
let(:space) { VCAP::CloudController::Space.make }
20+
let(:user) { make_developer_for_space(space) }
21+
let(:request_id) { 'test-request-123' }
22+
let(:user_agent) { 'cf/8.7.0 (go1.21.4; amd64 linux)' }
23+
let(:user_headers) do
24+
headers_for(user, user_name: 'roto').merge('HTTP_USER_AGENT' => user_agent)
25+
end
26+
27+
context 'when User-Agent header is provided' do
28+
it 'sets VCAP::Request.user_agent during the request' do
29+
get '/v3/spaces', nil, user_headers
30+
expect(last_response.status).to eq(200)
31+
expect(captured_values[:user_agent_during_request]).to eq(user_agent)
32+
expect(captured_values[:request_id_during_request]).to be_present
33+
# After the request completes, user_agent should be nil
34+
expect(::VCAP::Request.user_agent).to be_nil
35+
end
36+
end
37+
38+
context 'when User-Agent header and HTTP_X_VCAP_REQUEST_ID are provided' do
39+
it 'sets VCAP::Request.user_agent and HTTP_X_VCAP_REQUEST_ID during the request' do
40+
get '/v3/spaces', nil, user_headers.merge('HTTP_X_VCAP_REQUEST_ID' => request_id)
41+
expect(last_response.status).to eq(200)
42+
expect(captured_values[:user_agent_during_request]).to eq(user_agent)
43+
expect(captured_values[:request_id_during_request]).to include(request_id)
44+
expect(last_response.headers['X-VCAP-Request-ID']).to include(request_id)
45+
# After the request completes, user_agent and current_id should be nil
46+
expect(::VCAP::Request.user_agent).to be_nil
47+
expect(::VCAP::Request.current_id).to be_nil
48+
end
49+
end
50+
end
51+
end
52+
end

spec/unit/lib/cloud_controller/telemetry_logger_spec.rb

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,5 +127,88 @@ class DisabledTelemetryLogger < TelemetryLogger; end
127127
})
128128
end
129129
end
130+
131+
describe 'user-agent tracking' do
132+
context 'when user-agent is set' do
133+
before do
134+
::VCAP::Request.user_agent = 'cf/8.7.0 (go1.21.4; amd64 linux)'
135+
end
136+
137+
after do
138+
::VCAP::Request.user_agent = nil
139+
end
140+
141+
it 'includes user-agent in v3 telemetry events' do
142+
TelemetryLogger.v3_emit(
143+
'some-event',
144+
{ 'key' => 'value' }
145+
)
146+
147+
expect(Oj.load(file.read)).to match({
148+
'telemetry-source' => 'cloud_controller_ng',
149+
'telemetry-time' => rfc3339,
150+
'some-event' => {
151+
'key' => OpenSSL::Digest.hexdigest('SHA256', 'value'),
152+
'api-version' => 'v3',
153+
'user-agent' => 'cf/8.7.0 (go1.21.4; amd64 linux)'
154+
}
155+
})
156+
end
157+
158+
it 'includes user-agent in v2 telemetry events' do
159+
TelemetryLogger.v2_emit(
160+
'some-event',
161+
{ 'key' => 'value' }
162+
)
163+
164+
expect(Oj.load(file.read)).to match({
165+
'telemetry-source' => 'cloud_controller_ng',
166+
'telemetry-time' => rfc3339,
167+
'some-event' => {
168+
'key' => OpenSSL::Digest.hexdigest('SHA256', 'value'),
169+
'api-version' => 'v2',
170+
'user-agent' => 'cf/8.7.0 (go1.21.4; amd64 linux)'
171+
}
172+
})
173+
end
174+
175+
it 'includes user-agent in internal telemetry events' do
176+
TelemetryLogger.internal_emit(
177+
'some-event',
178+
{ 'key' => 'value' }
179+
)
180+
181+
expect(Oj.load(file.read)).to match({
182+
'telemetry-source' => 'cloud_controller_ng',
183+
'telemetry-time' => rfc3339,
184+
'some-event' => {
185+
'key' => OpenSSL::Digest.hexdigest('SHA256', 'value'),
186+
'api-version' => 'internal',
187+
'user-agent' => 'cf/8.7.0 (go1.21.4; amd64 linux)'
188+
}
189+
})
190+
end
191+
192+
it 'includes user-agent with other raw entries' do
193+
TelemetryLogger.v3_emit(
194+
'some-event',
195+
{ 'anonymize_key' => 'anonymize_value' },
196+
{ 'safe_key' => 'safe-value', 'memory-in-mb' => '2048' }
197+
)
198+
199+
expect(Oj.load(file.read)).to match({
200+
'telemetry-source' => 'cloud_controller_ng',
201+
'telemetry-time' => rfc3339,
202+
'some-event' => {
203+
'anonymize_key' => OpenSSL::Digest.hexdigest('SHA256', 'anonymize_value'),
204+
'safe_key' => 'safe-value',
205+
'memory-in-mb' => 2048,
206+
'api-version' => 'v3',
207+
'user-agent' => 'cf/8.7.0 (go1.21.4; amd64 linux)'
208+
}
209+
})
210+
end
211+
end
212+
end
130213
end
131214
end

spec/unit/lib/vcap/request_spec.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,5 +131,25 @@ module VCAP
131131
expect(Steno.config.context.data.key?('user_guid')).to be false
132132
end
133133
end
134+
135+
describe '.user_agent' do
136+
after do
137+
Request.user_agent = nil
138+
end
139+
140+
let(:user_agent) { 'cf/99.0.0-dev-961e7e2+build1070100 (go1.25.2; arm64 darwin)' }
141+
142+
it 'sets the new user_agent' do
143+
Request.user_agent = user_agent
144+
145+
expect(Request.user_agent).to eq user_agent
146+
end
147+
148+
it 'uses the :user_agent thread local' do
149+
Request.user_agent = user_agent
150+
151+
expect(Thread.current[:user_agent]).to eq(user_agent)
152+
end
153+
end
134154
end
135155
end

spec/unit/middleware/vcap_request_id_spec.rb

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@ module Middleware
1010
let(:uuid_regex) { '\w+-\w+-\w+-\w+-\w+' }
1111

1212
class VcapRequestId::FakeApp
13-
attr_accessor :last_request_id, :last_env_input
13+
attr_accessor :last_request_id, :last_env_input, :last_user_agent
1414

1515
def call(env)
1616
@last_request_id = ::VCAP::Request.current_id
17+
@last_user_agent = ::VCAP::Request.user_agent
1718
@last_env_input = env
1819
[200, {}, 'a body']
1920
end
@@ -22,13 +23,21 @@ def call(env)
2223
describe 'handling the request' do
2324
context 'setting the request_id in the logger' do
2425
it 'has assigned it before passing the request' do
25-
middleware.call('HTTP_X_VCAP_REQUEST_ID' => 'specific-request-id')
26+
middleware.call(
27+
'HTTP_X_VCAP_REQUEST_ID' => 'specific-request-id',
28+
'HTTP_USER_AGENT' => 'cf/8.7.0 (go1.21.4; amd64 linux)'
29+
)
2630
expect(app.last_request_id).to match(/^specific-request-id::#{uuid_regex}$/)
31+
expect(app.last_user_agent).to eq('cf/8.7.0 (go1.21.4; amd64 linux)')
2732
end
2833

2934
it 'nils it out after the request has been processed' do
30-
middleware.call('HTTP_X_VCAP_REQUEST_ID' => 'specific-request-id')
35+
middleware.call(
36+
'HTTP_X_VCAP_REQUEST_ID' => 'specific-request-id',
37+
'HTTP_USER_AGENT' => 'cf/8.7.0 (go1.21.4; amd64 linux)'
38+
)
3139
expect(::VCAP::Request.current_id).to be_nil
40+
expect(::VCAP::Request.user_agent).to be_nil
3241
end
3342
end
3443

@@ -41,15 +50,20 @@ def call(env)
4150
end
4251

4352
it 'sets the request id to nil' do
44-
expect { middleware.call('HTTP_X_VCAP_REQUEST_ID' => 'specific-request-id') }.to raise_error(error)
53+
expect { middleware.call('HTTP_X_VCAP_REQUEST_ID' => 'specific-request-id', 'HTTP_USER_AGENT' => 'cf/8.7.0 (go1.21.4; amd64 linux)') }.to raise_error(error)
4554
expect(::VCAP::Request.current_id).to be_nil
55+
expect(::VCAP::Request.user_agent).to be_nil
4656
end
4757
end
4858

4959
context 'and no error is raised when the request is passed' do
5060
it 'sets the request id to nil' do
51-
middleware.call('HTTP_X_VCAP_REQUEST_ID' => 'specific-request-id')
61+
middleware.call(
62+
'HTTP_X_VCAP_REQUEST_ID' => 'specific-request-id',
63+
'HTTP_USER_AGENT' => 'cf/8.7.0 (go1.21.4; amd64 linux)'
64+
)
5265
expect(::VCAP::Request.current_id).to be_nil
66+
expect(::VCAP::Request.user_agent).to be_nil
5367
end
5468
end
5569
end

0 commit comments

Comments
 (0)