Module: Otto::LoggingHelpers
- Defined in:
- lib/otto/logging_helpers.rb
Overview
LoggingHelpers provides utility methods for consistent structured logging across the Otto framework. Centralizes common request context extraction to eliminate duplication while keeping logging calls simple and explicit.
Class Method Summary collapse
-
.detect_project_root ⇒ String
Detect project root directory for path sanitization.
-
.log_backtrace(error, context = {}) ⇒ Object
Log exception backtrace with correlation fields for debugging.
-
.log_timed_operation(level, message, env, **metadata) { ... } ⇒ Object
Log a timed operation with consistent timing and error handling.
-
.request_context(env) ⇒ Hash
Extract common request context for structured logging.
-
.sanitize_backtrace(backtrace, project_root: nil) ⇒ Array<String>
Sanitize an array of backtrace lines.
-
.sanitize_backtrace_line(line, project_root = nil) ⇒ String
Sanitize a single backtrace line to remove sensitive path information.
Class Method Details
.detect_project_root ⇒ String
Detect project root directory for path sanitization
123 124 125 126 127 128 129 130 131 |
# File 'lib/otto/logging_helpers.rb', line 123 def self.detect_project_root @project_root ||= begin if defined?(Bundler) Bundler.root.to_s else Dir.pwd end end end |
.log_backtrace(error, context = {}) ⇒ Object
Log exception backtrace with correlation fields for debugging. Always logs for unhandled errors at ERROR level with sanitized paths. Limits to first 20 lines for critical errors.
SECURITY: Paths are sanitized to prevent exposing sensitive system information:
-
Project files: Show relative paths only
-
Gem files: Show gem name and relative path within gem
-
Ruby stdlib: Show filename only
-
External files: Show filename only
Expects caller to provide correlation context (error_id, handler, etc). Does NOT duplicate error/error_class fields - those belong in main error log.
265 266 267 268 269 270 271 |
# File 'lib/otto/logging_helpers.rb', line 265 def self.log_backtrace(error, context = {}) raw_backtrace = error.backtrace&.first(20) || [] sanitized = sanitize_backtrace(raw_backtrace) Otto.structured_log(:error, 'Exception backtrace', context.merge(backtrace: sanitized)) end |
.log_timed_operation(level, message, env, **metadata) { ... } ⇒ Object
Log a timed operation with consistent timing and error handling
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 |
# File 'lib/otto/logging_helpers.rb', line 101 def self.log_timed_operation(level, , env, **) start_time = Otto::Utils.now_in_μs result = yield duration = Otto::Utils.now_in_μs - start_time Otto.structured_log(level, , request_context(env).merge().merge(duration: duration)) result rescue StandardError => e duration = Otto::Utils.now_in_μs - start_time Otto.structured_log(:error, "#{} failed", request_context(env).merge().merge( duration: duration, error: e., error_class: e.class.name )) raise end |
.request_context(env) ⇒ Hash
IP addresses are already masked by IPPrivacyMiddleware (public IPs only)
User agents are truncated to 100 chars to prevent log bloat
Extract common request context for structured logging
Returns a hash containing privacy-aware request metadata suitable for merging with event-specific data in Otto.structured_log calls.
75 76 77 78 79 80 81 82 83 |
# File 'lib/otto/logging_helpers.rb', line 75 def self.request_context(env) { method: env['REQUEST_METHOD'], path: env['PATH_INFO'], ip: env['REMOTE_ADDR'], # Already masked by IPPrivacyMiddleware for public IPs country: env['otto.geo_country'], user_agent: env['HTTP_USER_AGENT']&.slice(0, 100), # Already anonymized by IPPrivacyMiddleware }.compact end |
.sanitize_backtrace(backtrace, project_root: nil) ⇒ Array<String>
Sanitize an array of backtrace lines
229 230 231 232 233 234 |
# File 'lib/otto/logging_helpers.rb', line 229 def self.sanitize_backtrace(backtrace, project_root: nil) return [] if backtrace.nil? || backtrace.empty? project_root ||= detect_project_root backtrace.map { |line| sanitize_backtrace_line(line, project_root) } end |
.sanitize_backtrace_line(line, project_root = nil) ⇒ String
Sanitize a single backtrace line to remove sensitive path information
Transforms absolute paths into relative or categorized paths:
-
Project files: relative path from project root
-
Gem files: [GEM] gem-name-version/relative/path
-
Ruby stdlib: [RUBY] filename
-
Unknown: [EXTERNAL] filename
152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 |
# File 'lib/otto/logging_helpers.rb', line 152 def self.sanitize_backtrace_line(line, project_root = nil) return line if line.nil? || line.empty? project_root ||= detect_project_root = File.(project_root) # Extract file path from backtrace line (format: "path:line:in `method'" or "path:line") if line =~ /^(.+?):\d+(?::in `.+')?$/ file_path = ::Regexp.last_match(1) suffix = line[file_path.length..] begin = File.(file_path) rescue ArgumentError # Handle malformed paths (e.g., containing null bytes) # File.basename also raises ArgumentError for null bytes, so use simple string manipulation basename = file_path.split('/').last || file_path return "[EXTERNAL] #{basename}#{suffix}" end # Try project-relative path first if .start_with?( + File::SEPARATOR) relative_path = .delete_prefix( + File::SEPARATOR) return relative_path + suffix end # Check for bundler gems specifically (e.g., /bundler/gems/otto-abc123/lib/...) # Must check BEFORE regular gems pattern to handle /gems/3.4.0/bundler/gems/... if =~ %r{/bundler/gems/([^/]+)/(.+)$} gem_name = ::Regexp.last_match(1) gem_relative = ::Regexp.last_match(2) # Strip git hash suffix for cleaner output (otto-abc123def456 → otto) gem_name = gem_name.sub(/-[a-f0-9]{7,}$/, '') if gem_name =~ /-[a-f0-9]{7,}$/i return "[GEM] #{gem_name}/#{gem_relative}#{suffix}" end # Check for regular gem path (e.g., /path/to/gems/rack-3.1.8/lib/rack.rb) # Handle version directories: /gems/3.4.0/gems/rack-3.1.8/... by looking for last /gems/ if =~ %r{/gems/([^/]+)/(.+)$} gem_name = ::Regexp.last_match(1) gem_relative = ::Regexp.last_match(2) # Skip version-only directory names (e.g., 3.4.0) # Look deeper if gem_name is just a version number if gem_name =~ /^[\d.]+$/ && gem_relative =~ %r{^(?:bundler/)?gems/([^/]+)/(.+)$} # Found nested gem path, use that instead gem_name = ::Regexp.last_match(1) gem_relative = ::Regexp.last_match(2) end # Strip version suffix for cleaner output (rack-3.1.8 → rack) base_gem_name = gem_name.split('-')[0..-2].join('-') base_gem_name = gem_name if base_gem_name.empty? return "[GEM] #{base_gem_name}/#{gem_relative}#{suffix}" end # Check for Ruby stdlib (e.g., /path/to/ruby/3.4.0/logger.rb) if =~ %r{/ruby/[\d.]+/(.+)$} stdlib_file = ::Regexp.last_match(1) return "[RUBY] #{stdlib_file}#{suffix}" end # Unknown/external path - show filename only filename = File.basename(file_path) return "[EXTERNAL] #{filename}#{suffix}" end # Couldn't parse - return as-is (better than failing) line end |