Skip to content

Commit 996fa53

Browse files
committed
Align structured logging grammar
1 parent 40144e2 commit 996fa53

5 files changed

Lines changed: 115 additions & 36 deletions

File tree

app/web/config/environment_validator.rb

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,17 @@ def auto_source_enabled?
4949

5050
def set_development_key
5151
ENV['HTML2RSS_SECRET_KEY'] = 'development-default-key-not-for-production'
52-
puts '⚠️ WARNING: Using default secret key for development/testing only!'
53-
puts ' Set HTML2RSS_SECRET_KEY environment variable for production use.'
52+
log_development_default_secret_key_warning
53+
warn_lines(
54+
'WARNING: Using default secret key for development/testing only!',
55+
'Set HTML2RSS_SECRET_KEY environment variable for production use.'
56+
)
57+
nil
5458
end
5559

5660
def show_production_error
57-
puts production_error_message
61+
SecurityLogger.log_config_validation_failure('secret_key', 'Missing required secret key')
62+
warn_lines(*production_error_message.lines(chomp: true))
5863
exit 1
5964
end
6065

@@ -79,9 +84,11 @@ def validate_secret_key!
7984
return unless secret == 'your-generated-secret-key-here' || secret.length < 32
8085

8186
SecurityLogger.log_config_validation_failure('secret_key', 'Invalid or weak secret key')
82-
puts '❌ CRITICAL: Invalid secret key for production deployment!'
83-
puts ' Secret key must be at least 32 characters and not the default placeholder.'
84-
puts ' Generate a secure key: openssl rand -hex 32'
87+
warn_lines(
88+
'CRITICAL: Invalid secret key for production deployment!',
89+
'Secret key must be at least 32 characters and not the default placeholder.',
90+
'Generate a secure key: openssl rand -hex 32'
91+
)
8592
exit 1
8693
end
8794

@@ -90,11 +97,35 @@ def validate_account_configuration!
9097
weak_tokens = accounts.select { |acc| acc[:token].length < 16 }
9198
return unless weak_tokens.any?
9299

100+
handle_weak_account_tokens!(weak_tokens)
101+
end
102+
103+
# @param lines [Array<String>]
104+
# @return [void]
105+
def warn_lines(*lines)
106+
lines.each { |line| Kernel.warn(line) }
107+
nil
108+
end
109+
110+
# @return [void]
111+
def log_development_default_secret_key_warning
112+
SecurityLogger.log_config_validation_failure(
113+
'secret_key',
114+
'Using development default secret key',
115+
severity: :warn
116+
)
117+
end
118+
119+
# @param weak_tokens [Array<Hash{Symbol=>String}>]
120+
# @return [void]
121+
def handle_weak_account_tokens!(weak_tokens)
93122
weak_usernames = weak_tokens.map { |acc| acc[:username] }.join(', ')
94123
SecurityLogger.log_config_validation_failure('account_tokens', "Weak tokens for users: #{weak_usernames}")
95-
puts '❌ CRITICAL: Weak authentication tokens detected in production!'
96-
puts ' All tokens must be at least 16 characters long.'
97-
puts " Weak tokens found for users: #{weak_usernames}"
124+
warn_lines(
125+
'CRITICAL: Weak authentication tokens detected in production!',
126+
'All tokens must be at least 16 characters long.',
127+
"Weak tokens found for users: #{weak_usernames}"
128+
)
98129
exit 1
99130
end
100131
end

app/web/security/security_logger.rb

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# frozen_string_literal: true
22

33
require 'digest'
4-
require 'time'
54
module Html2rss
65
module Web
76
##
@@ -103,12 +102,13 @@ def log_blocked_request(ip, reason, endpoint)
103102
# Log configuration validation failure
104103
# @param component [String] component that failed validation
105104
# @param details [String] validation failure details
105+
# @param severity [Symbol]
106106
# @return [void]
107-
def log_config_validation_failure(component, details)
107+
def log_config_validation_failure(component, details, severity: :error)
108108
log_event('config_validation_failure', {
109109
component: component,
110110
details: details
111-
}, severity: :error)
111+
}, severity: severity)
112112
end
113113

114114
# Log lifecycle events for in-memory config/cache snapshots
@@ -135,7 +135,7 @@ def log_event(event_type, data, severity: :warn)
135135
level: severity,
136136
payload: {
137137
security_event: event_type,
138-
**data
138+
details: data
139139
}
140140
)
141141
rescue StandardError => error
@@ -148,8 +148,8 @@ def log_event(event_type, data, severity: :warn)
148148
# @param event_type [String] type of security event
149149
# @param data [Hash] event data
150150
def handle_logging_error(error, event_type, data)
151-
Kernel.warn("Security logging error: #{error.message}")
152-
Kernel.warn("Security event: #{event_type} - #{data}")
151+
Kernel.warn("Structured logging fallback: #{error.class}: #{error.message}")
152+
Kernel.warn("component=security_logger security_event=#{event_type} details=#{data}")
153153
end
154154
end
155155
end

app/web/telemetry/observability.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ def emit(event_name:, outcome:, details: {}, level: :info)
2626
# @param outcome [String]
2727
# @return [void]
2828
def handle_emit_error(error, event_name, outcome)
29-
Kernel.warn("Observability emit error: #{error.message}")
30-
Kernel.warn("event_name=#{event_name} outcome=#{outcome}")
29+
Kernel.warn("Structured logging fallback: #{error.class}: #{error.message}")
30+
Kernel.warn("component=observability event_name=#{event_name} outcome=#{outcome}")
3131
end
3232

3333
# @param event_name [String]

spec/html2rss/web/environment_validator_spec.rb

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,60 @@
44
require 'climate_control'
55

66
require_relative '../../../app/web/config/environment_validator'
7+
require_relative '../../../app/web/config/flags'
8+
require_relative '../../../app/web/security/security_logger'
79

810
RSpec.describe Html2rss::Web::EnvironmentValidator do
11+
describe '.validate_environment!' do
12+
it 'sets a development default secret key without exiting' do
13+
allow(Html2rss::Web::SecurityLogger).to receive(:log_config_validation_failure)
14+
allow(Kernel).to receive(:warn)
15+
16+
ClimateControl.modify('RACK_ENV' => 'development', 'HTML2RSS_SECRET_KEY' => nil) do
17+
described_class.validate_environment!
18+
expect(ENV.fetch('HTML2RSS_SECRET_KEY')).to eq('development-default-key-not-for-production')
19+
end
20+
end
21+
22+
it 'logs development default secret key warnings' do
23+
allow(Html2rss::Web::SecurityLogger).to receive(:log_config_validation_failure)
24+
allow(Kernel).to receive(:warn)
25+
26+
ClimateControl.modify('RACK_ENV' => 'development', 'HTML2RSS_SECRET_KEY' => nil) do
27+
described_class.validate_environment!
28+
end
29+
30+
expect(Html2rss::Web::SecurityLogger).to have_received(:log_config_validation_failure)
31+
.with('secret_key', 'Using development default secret key', severity: :warn)
32+
end
33+
34+
it 'logs missing production secret key failures before exiting' do
35+
allow(Html2rss::Web::SecurityLogger).to receive(:log_config_validation_failure)
36+
allow(Kernel).to receive(:warn)
37+
38+
ClimateControl.modify('RACK_ENV' => 'production', 'HTML2RSS_SECRET_KEY' => nil) do
39+
expect { described_class.validate_environment! }.to raise_error(SystemExit)
40+
end
41+
42+
expect(Html2rss::Web::SecurityLogger).to have_received(:log_config_validation_failure)
43+
.with('secret_key', 'Missing required secret key')
44+
end
45+
end
46+
47+
describe '.validate_production_security!' do
48+
it 'logs weak production secret keys before exiting' do
49+
allow(Html2rss::Web::SecurityLogger).to receive(:log_config_validation_failure)
50+
allow(Kernel).to receive(:warn)
51+
52+
ClimateControl.modify('RACK_ENV' => 'production', 'HTML2RSS_SECRET_KEY' => 'short-secret') do
53+
expect { described_class.validate_production_security! }.to raise_error(SystemExit)
54+
end
55+
56+
expect(Html2rss::Web::SecurityLogger).to have_received(:log_config_validation_failure)
57+
.with('secret_key', 'Invalid or weak secret key')
58+
end
59+
end
60+
961
describe '.auto_source_enabled?' do
1062
context 'when in development' do
1163
it 'defaults to enabled when flag is not set' do

spec/html2rss/web/log_sanitizer_spec.rb

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@
1313
RSpec.describe Html2rss::Web::LogSanitizer do
1414
let(:io) { StringIO.new }
1515
let(:logger) { Logger.new(io).tap { |log| log.formatter = Html2rss::Web::AppLogger.send(:method, :format_entry) } }
16+
let(:sanitized_url) do
17+
{
18+
host: 'news.ycombinator.com',
19+
scheme: 'https',
20+
hash: Digest::SHA256.hexdigest('https://news.ycombinator.com')[0..11]
21+
}
22+
end
1623
let(:context) do
1724
Html2rss::Web::RequestContext::Context.new(
1825
request_id: 'req-123',
@@ -45,13 +52,7 @@
4552
end
4653

4754
it 'replaces logged urls with hashed host metadata' do
48-
expected_url = {
49-
host: 'news.ycombinator.com',
50-
scheme: 'https',
51-
hash: url_hash('https://news.ycombinator.com')
52-
}
53-
54-
expect(described_class.sanitize_details(url: 'https://news.ycombinator.com')).to eq(url: expected_url)
55+
expect(described_class.sanitize_details(url: 'https://news.ycombinator.com')).to eq(url: sanitized_url)
5556
end
5657

5758
it 'falls back to a hash for malformed urls' do
@@ -72,14 +73,13 @@
7273
Html2rss::Web::SecurityLogger.log_token_usage('very-secret-token', 'https://news.ycombinator.com', true)
7374
payload = JSON.parse(io.string.lines.last, symbolize_names: true)
7475

75-
expect(payload.slice(:path, :url, :token_hash)).to eq(
76+
expect(payload.slice(:path, :details)).to eq(
7677
path: '/api/v1/feeds/[REDACTED]',
77-
url: {
78-
host: 'news.ycombinator.com',
79-
scheme: 'https',
80-
hash: url_hash('https://news.ycombinator.com')
81-
},
82-
token_hash: Digest::SHA256.hexdigest('very-secret-token')[0..7]
78+
details: {
79+
url: sanitized_url,
80+
token_hash: Digest::SHA256.hexdigest('very-secret-token')[0..7],
81+
success: true
82+
}
8383
)
8484
end
8585

@@ -93,11 +93,7 @@
9393
lines = io.string.lines.map { |line| JSON.parse(line, symbolize_names: true) }
9494
observability_payload = lines.first
9595

96-
expect(observability_payload.dig(:details, :url)).to eq(
97-
host: 'news.ycombinator.com',
98-
scheme: 'https',
99-
hash: url_hash('https://news.ycombinator.com')
100-
)
96+
expect(observability_payload.dig(:details, :url)).to eq(sanitized_url)
10197
end
10298

10399
it 'formats rack-timeout logfmt as json' do

0 commit comments

Comments
 (0)