Skip to content

Commit ef57f1a

Browse files
committed
fix(observability): harden sentry log bridge and docs
1 parent fd4bf7c commit ef57f1a

6 files changed

Lines changed: 57 additions & 12 deletions

File tree

README.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,14 @@ This trial run is intentionally minimal. Use Docker Compose for Browserless, aut
4949

5050
1. Generate a key: `openssl rand -hex 32`.
5151
2. Export `HTML2RSS_SECRET_KEY`, `HEALTH_CHECK_TOKEN`, and `BROWSERLESS_IO_API_TOKEN` in your shell or `.env`.
52-
3. Optionally export `SENTRY_DSN` to send application errors to Sentry.
53-
4. Optionally export `SENTRY_ENABLE_LOGS=true` to forward structured application logs to Sentry.
54-
5. Start: `docker-compose up`.
52+
3. Export build metadata required by `docker-compose.yml`:
53+
```bash
54+
export BUILD_TAG=local
55+
export GIT_SHA="$(git rev-parse --short HEAD 2>/dev/null || echo dev)"
56+
```
57+
4. Optionally export `SENTRY_DSN` to send application errors to Sentry.
58+
5. Optionally export `SENTRY_ENABLE_LOGS=true` to forward structured application logs to Sentry.
59+
6. Start: `docker-compose up`.
5560

5661
UI + API run on `http://localhost:4000`. The app exits if the secret key is missing.
5762

app/web/telemetry/app_logger.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,18 @@ def normalize_logfmt_value(raw_value)
101101
# @param payload [Hash{Symbol=>Object}]
102102
# @return [void]
103103
def emit_to_sentry(payload)
104+
return unless sentry_payload?(payload)
105+
104106
SentryLogs.emit(payload)
105107
rescue StandardError
106108
nil
107109
end
110+
111+
# @param payload [Hash{Symbol=>Object}]
112+
# @return [Boolean]
113+
def sentry_payload?(payload)
114+
payload.key?(:event_name) || payload.key?(:security_event)
115+
end
108116
end
109117
end
110118
end

app/web/telemetry/sentry_logs.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ module Web
77
# enabled for the current runtime.
88
module SentryLogs
99
OMIT = Object.new.freeze
10+
ALLOWED_LEVELS = %i[debug info warn error fatal].freeze
1011
SENSITIVE_ATTRIBUTE_KEYS = %w[actor email ip remote_ip user_agent username x_forwarded_for].freeze
1112

1213
class << self
@@ -40,7 +41,10 @@ def logger
4041
# @param payload [Hash{Symbol=>Object}]
4142
# @return [Symbol]
4243
def level(payload)
43-
payload.fetch(:level, 'INFO').to_s.downcase.to_sym
44+
requested_level = payload.fetch(:level, 'INFO').to_s.downcase.to_sym
45+
return requested_level if ALLOWED_LEVELS.include?(requested_level)
46+
47+
:info
4448
end
4549

4650
# @param payload [Hash{Symbol=>Object}]

public/openapi.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,7 @@ paths:
331331
- success
332332
- data
333333
type: object
334-
description: returns health status after production-style env scrubbing
334+
description: returns current health status when a valid bearer token is provided
335335
'401':
336336
content:
337337
application/json:

spec/html2rss/web/app_logger_spec.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,5 +35,16 @@
3535
end.not_to raise_error
3636
expect(io.string).to include('"event_name":"boot.test"')
3737
end
38+
39+
it 'does not forward plain string logs to the Sentry bridge' do
40+
allow(Logger).to receive(:new).and_return(test_logger)
41+
allow(Html2rss::Web::SentryLogs).to receive(:emit)
42+
43+
described_class.reset_logger!
44+
described_class.logger.info('plain-text log line with request details')
45+
46+
expect(Html2rss::Web::SentryLogs).not_to have_received(:emit)
47+
expect(io.string).to include('"message":"plain-text log line with request details"')
48+
end
3849
end
3950
end

spec/html2rss/web/sentry_logs_spec.rb

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,18 @@
66
require_relative '../../../app/web/telemetry/sentry_logs'
77

88
RSpec.describe Html2rss::Web::SentryLogs do
9+
let(:logger_class) do
10+
Struct.new(:captured_call) do
11+
%i[debug info warn error fatal].each do |log_level|
12+
define_method(log_level) do |message, **attributes|
13+
captured_call[:level] = log_level
14+
captured_call[:message] = message
15+
captured_call[:attributes] = attributes
16+
end
17+
end
18+
end
19+
end
20+
921
let(:captured_call) { {} }
1022
let(:sentry_logger) { build_sentry_logger }
1123
let(:fake_sentry) do
@@ -53,14 +65,18 @@
5365
expect(captured_call).to eq({})
5466
end
5567

56-
def build_sentry_logger
57-
logger_class = Struct.new(:captured_call) do
58-
def info(message, **attributes)
59-
captured_call[:message] = message
60-
captured_call[:attributes] = attributes
61-
end
62-
end
68+
it 'falls back to info when an unsupported level is requested', :aggregate_failures do
69+
stub_const('Sentry', fake_sentry)
70+
allow(Html2rss::Web::RuntimeEnv).to receive_messages(sentry_enabled?: true, sentry_logs_enabled?: true)
71+
allow(described_class).to receive(:logger).and_return(sentry_logger)
72+
73+
described_class.emit(raw_payload.merge(level: :unknown_method))
74+
75+
expect(captured_call).to include(:message, :attributes)
76+
expect(captured_call.fetch(:message)).to eq('auth.authenticate')
77+
end
6378

79+
def build_sentry_logger
6480
logger_class.new(captured_call)
6581
end
6682

@@ -71,6 +87,7 @@ def expect_forwarded_payload
7187
end
7288

7389
def expect_forwarded_message
90+
expect(captured_call.fetch(:level)).to eq(:info)
7491
expect(captured_call.fetch(:message)).to eq('auth.authenticate')
7592
end
7693

0 commit comments

Comments
 (0)