logstruct 0.1.12 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +139 -1
- data/README.md +76 -68
- data/lib/log_struct/concerns/configuration.rb +139 -91
- data/lib/log_struct/concerns/error_handling.rb +36 -6
- data/lib/log_struct/config_struct/error_handling_modes.rb +3 -1
- data/lib/log_struct/config_struct/filters.rb +116 -33
- data/lib/log_struct/config_struct/integrations.rb +0 -4
- data/lib/log_struct/config_struct/limits.rb +29 -0
- data/lib/log_struct/configuration.rb +2 -0
- data/lib/log_struct/enums/event.rb +3 -1
- data/lib/log_struct/enums/log_field.rb +5 -0
- data/lib/log_struct/formatter.rb +304 -56
- data/lib/log_struct/hash_utils.rb +33 -4
- data/lib/log_struct/integrations/action_mailer/error_handling.rb +78 -20
- data/lib/log_struct/integrations/action_mailer/fallback_error_handlers.rb +81 -0
- data/lib/log_struct/integrations/action_mailer/metadata_collection.rb +0 -20
- data/lib/log_struct/integrations/action_mailer.rb +22 -17
- data/lib/log_struct/integrations/active_job/log_subscriber.rb +177 -25
- data/lib/log_struct/integrations/active_job.rb +2 -7
- data/lib/log_struct/integrations/active_model_serializers.rb +7 -1
- data/lib/log_struct/integrations/active_record.rb +39 -17
- data/lib/log_struct/integrations/active_storage.rb +31 -31
- data/lib/log_struct/integrations/ahoy.rb +19 -15
- data/lib/log_struct/integrations/carrierwave.rb +55 -51
- data/lib/log_struct/integrations/dotenv.rb +22 -32
- data/lib/log_struct/integrations/good_job/logger.rb +30 -24
- data/lib/log_struct/integrations/good_job.rb +111 -40
- data/lib/log_struct/integrations/host_authorization.rb +47 -36
- data/lib/log_struct/integrations/lograge.rb +80 -4
- data/lib/log_struct/integrations/puma.rb +74 -34
- data/lib/log_struct/integrations/rack_error_handler/debug_exceptions_logging.rb +35 -0
- data/lib/log_struct/integrations/rack_error_handler/error_lines.rb +160 -0
- data/lib/log_struct/integrations/rack_error_handler/middleware.rb +14 -169
- data/lib/log_struct/integrations/rack_error_handler/parse_error_logging.rb +36 -0
- data/lib/log_struct/integrations/rack_error_handler.rb +20 -4
- data/lib/log_struct/integrations/rack_middleware.rb +23 -0
- data/lib/log_struct/integrations/rails_log_subscribers.rb +32 -0
- data/lib/log_struct/integrations/request_context/middleware.rb +4 -9
- data/lib/log_struct/integrations/shrine.rb +48 -73
- data/lib/log_struct/integrations/sidekiq/job_context_middleware.rb +32 -0
- data/lib/log_struct/integrations/sidekiq/logger.rb +17 -9
- data/lib/log_struct/integrations/sidekiq.rb +98 -7
- data/lib/log_struct/integrations/sorbet.rb +10 -46
- data/lib/log_struct/integrations.rb +3 -2
- data/lib/log_struct/log/{good_job/enqueue.rb → active_job/discard.rb} +13 -13
- data/lib/log_struct/log/active_job/enqueue.rb +0 -4
- data/lib/log_struct/log/{good_job/schedule.rb → active_job/error.rb} +15 -15
- data/lib/log_struct/log/{good_job/start.rb → active_job/retry.rb} +13 -13
- data/lib/log_struct/log/{good_job/finish.rb → active_job/retry_stopped.rb} +13 -17
- data/lib/log_struct/log/active_job.rb +4 -0
- data/lib/log_struct/log/active_storage/delete.rb +6 -2
- data/lib/log_struct/log/active_storage/download.rb +2 -4
- data/lib/log_struct/log/active_storage/exist.rb +2 -2
- data/lib/log_struct/log/active_storage/metadata.rb +4 -4
- data/lib/log_struct/log/active_storage/upload.rb +2 -10
- data/lib/log_struct/log/active_storage/url.rb +2 -6
- data/lib/log_struct/log/active_storage.rb +2 -3
- data/lib/log_struct/log/error.rb +24 -1
- data/lib/log_struct/log/good_job.rb +0 -4
- data/lib/log_struct/log/puma/error.rb +63 -0
- data/lib/log_struct/log/puma.rb +1 -0
- data/lib/log_struct/log/security/blocked_host.rb +0 -4
- data/lib/log_struct/log/shrine/exist.rb +0 -2
- data/lib/log_struct/log/shrine/metadata.rb +2 -4
- data/lib/log_struct/log/shrine/upload.rb +4 -0
- data/lib/log_struct/log/sidekiq/error.rb +63 -0
- data/lib/log_struct/log/{active_storage/stream.rb → sidekiq/log.rb} +17 -11
- data/lib/log_struct/log/sidekiq.rb +3 -45
- data/lib/log_struct/log.rb +4 -2
- data/lib/log_struct/logged_errors.rb +25 -0
- data/lib/log_struct/monkey_patches/active_support/tagged_logging/formatter.rb +2 -3
- data/lib/log_struct/param_filters.rb +51 -38
- data/lib/log_struct/params_errors.rb +78 -0
- data/lib/log_struct/rails_boot_banner_silencer.rb +7 -2
- data/lib/log_struct/railtie.rb +29 -14
- data/lib/log_struct/semantic_logger/color_formatter.rb +60 -53
- data/lib/log_struct/semantic_logger/concerns/log_methods.rb +65 -57
- data/lib/log_struct/semantic_logger/formatter.rb +79 -57
- data/lib/log_struct/semantic_logger/logger.rb +29 -53
- data/lib/log_struct/semantic_logger/reopen_after_fork.rb +40 -0
- data/lib/log_struct/semantic_logger/setup.rb +115 -112
- data/lib/log_struct/shared/serialize_common.rb +2 -7
- data/lib/log_struct/string_scrubber.rb +229 -20
- data/lib/log_struct/version.rb +1 -1
- data/lib/log_struct.rb +6 -4
- data/logstruct.gemspec +19 -10
- metadata +63 -11
- data/lib/log_struct/builders/active_job.rb +0 -87
- data/lib/log_struct/integrations/good_job/log_subscriber.rb +0 -189
- data/lib/log_struct/integrations/rack.rb +0 -31
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 23182266ea1c9f53e0b824a8b806207b92255ed6a6d2755cf67c8f3d35680d19
|
|
4
|
+
data.tar.gz: 4d684c903461a10aa875314f6dac3fca9978ac190ce0616c3f6d34076c3b8ba7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4a3dc3b56efac2d68703b77210006852059188425e612f537be307e7740da79781600dc4147d955c52c0e65a39978d476fefd0a796b07b7fe5ee170d2ce5b07a
|
|
7
|
+
data.tar.gz: bb328f655eda7dbcfb2ace155586966bfc47a4a60df88dcb7aa2e355b5213f0e76623d52e83906063a5aaff63039c8937c59bdd373c783faa7013a7c860acfac
|
data/CHANGELOG.md
CHANGED
|
@@ -5,10 +5,148 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [
|
|
8
|
+
## [0.2.1] - 2026-10-18
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **Lograge**: With `config.exceptions_app = routes`, a request whose action raises has one request line, the action's: the error page's controller no longer logs a second line as `GET /500`. When the error comes before any action (a routing error) or after it (in a middleware), the error page's line has the request's own method and path instead of `GET /404` (#28)
|
|
13
|
+
|
|
14
|
+
## [0.2.0] - 2026-10-18
|
|
9
15
|
|
|
10
16
|
### Changed
|
|
11
17
|
|
|
18
|
+
- **Breaking**: A Hash passed to the logger (e.g. `Rails.logger.info({evt: "signup", user_id: 1})`) is written with its keys at the top level of the line, instead of nested under `payload`. A Hash can set its own `src` and `evt`; `ts` and `lvl` are reserved and are reported as an error and dropped
|
|
19
|
+
- **Breaking**: `Log::Error`'s `error_class` is any `Exception` class, not only `StandardError` classes, since ActiveJob and GoodJob pass any `Exception` that a job lets through
|
|
20
|
+
- **Breaking**: LogStruct depends on `railties`, `actionpack`, `activesupport`, `globalid` and `lograge` instead of `rails`, so apps no longer install every Rails framework to use it (#9, thanks @langalex)
|
|
21
|
+
- **Rails**: Integrations are set up after `config/initializers` run, so `config.integrations.*` settings and filters added in `config/initializers/logstruct.rb` take effect. `enabled` and `enabled_environments` can be set in an initializer; `LOGSTRUCT_ENABLED` overrides both
|
|
22
|
+
- **Rails**: Because integrations are set up after the app's initializers, LogStruct's loggers and handlers replace ones an initializer set (for example a custom Sidekiq logger or lograge settings). Configure them through LogStruct instead
|
|
23
|
+
- **Sidekiq**: A failed job is logged once, as a structured warn line with its `jid`, class, queue and retry count, instead of Sidekiq's default handler's three lines with the job's arguments in a JSON string. ActiveJob jobs' failures are left to the ActiveJob integration
|
|
24
|
+
- **Puma**: Request errors Puma handles itself (its "Rack app (...)" lines) are structured error lines with the method and path, without the query string
|
|
25
|
+
- **Breaking**: Production (any environment outside `config.local_environments`) logs to STDOUT only. LogStruct added a `log/<env>.log` file appender, which nothing rotated, unless `RAILS_LOG_TO_STDOUT` was set, and it overrode `config.logger = STDOUT`. Development logs to STDOUT and its log file, test to its log file; with `RAILS_LOG_TO_STDOUT` set, only to STDOUT
|
|
26
|
+
- **Filtering**: Rails' `filter_parameters` are matched the way Rails matches them: a Symbol or String filters every key that contains it (`:secret` filters `clientSecret`), case-insensitively, and Regexps and blocks are applied as Rails applies them. Rails' partial matches don't filter LogStruct's own top-level fields (Rails' default `:_key` would filter GoodJob's `cron_key`); exact names in the list and LogStruct's own filters still do
|
|
27
|
+
- **Filtering**: Blocks in `filter_parameters` get copies of the key and value, so a block that changes its value in place doesn't change the app's data
|
|
28
|
+
- **ActiveJob**: For `ActionMailer::MailDeliveryJob` (every `deliver_later`) and its subclasses, job lines log only the mailer, the mail and the delivery method as `arguments`, not the mail's own arguments. Devise passes its confirmation, password reset and invitation tokens there as plain strings, which key-based filtering can't find, so apps using Devise with `deliver_later` logged live tokens. Other jobs' arguments are logged as before (when the job's `log_arguments` is on). With the ActiveJob integration off, Rails' own job logs still include them
|
|
29
|
+
- **Breaking**: Hashed values (email addresses, `filter_keys_with_hashes`) are an HMAC-SHA256 keyed with `hash_salt`, or by default with a key derived from the app's `secret_key_base`, and are 16 characters long. They were a SHA256 with a salt shared by every install, 12 characters long, which a list of candidate emails could reverse. Every hash value changes; set `hash_salt` to keep hashes the same across apps or after a `secret_key_base` change. Hashes are pseudonyms, not anonymous data
|
|
30
|
+
- **Breaking**: Sensitive keys are matched by their words, whatever their case or separators, so `apiKey`, `X-Api-Key`, `HTTP_AUTHORIZATION`, `Set-Cookie` and plurals like `passwords` are filtered by default, and so are `cookie`, `session`, `jwt`, `bearer`, `otp` and `passphrase`. `filter_keys` entries match by their words too. Whole words keep `author`, `keyboard` and `signature_field_id`
|
|
31
|
+
- **Breaking**: A filtered String is logged as `{_filtered: {_class: "String"}}`, without its length (`_bytes`)
|
|
32
|
+
- **Breaking**: `Log::Sidekiq` is now `Log::Sidekiq::Log`. Sidekiq's job error line (`Log::Sidekiq::Error`), Puma's request error line (`Log::Puma::Error`) and ActiveJob's error line (`Log::ActiveJob::Error`) have typed fields instead of `additional_data`, with a new `error_context` field for what Sidekiq or Puma was doing. Error lines have the request's `path`, `method`, `source_ip`, `user_agent` and `referer` as fields
|
|
33
|
+
- **Breaking**: `error_handling_modes.type_checking_errors` defaults to `ReportProduction`: Sorbet type errors in the app and its gems are raised in development and test, and logged and reported in production. They were only logged
|
|
34
|
+
- **Breaking**: ActiveStorage `url` lines don't have the `url` field: the signed URL (S3 signatures, Disk tokens) lets anyone read the file until it expires. `url` and `exist` lines are debug lines, as Rails logs them
|
|
35
|
+
- **Breaking**: Lines have size limits (`config.limits`): Hashes write their first 100 keys and a `_more_keys` count, backtrace-like Arrays (an error line's `backtrace` too) 20 lines, other Arrays 10 items, and strings are cut at 32 KB after they're scrubbed (a URL password or `Basic` credentials that the cut runs through are replaced too). Error lines' backtraces were cut to 5 lines
|
|
36
|
+
- **Breaking**: The IP spoofing check no longer runs on every request and answers 403. Rails raises `IpSpoofAttackError` only where `remote_ip` is read, and responds as it would without LogStruct; LogStruct logs the error there (and reports it, in the `security_errors` mode)
|
|
37
|
+
- **Errors**: A message logged with an exception (`logger.error("Import failed", e)`) is an error line with the message in `msg` and the exception's in the new `error_message` field
|
|
38
|
+
- **Errors**: A failure building an integration's own line (ActiveModelSerializers, Dotenv, Puma, Ahoy, host authorization, GoodJob) is a LogStruct error (`logstruct_errors`: logged in production, raised in development and test), not one of the app's (`standard_errors`, raised everywhere), so it can't fail a render, a request or the boot in production
|
|
39
|
+
- **Host authorization**: Blocked hosts get the app's own `response_app` if it set one (LogStruct's 403 page otherwise). LogStruct replaced it
|
|
40
|
+
- **Puma**: The TERM and INT traps log the shutdown and then run the handler they replaced (or the signal's default action). The Puma patches, the boot banner replacement and the Puma lifecycle lines are off when LogStruct is disabled
|
|
41
|
+
- **Ahoy**: The tracking wrapper passes Ahoy's arguments on as given, so Ahoy's default properties still apply
|
|
42
|
+
- **Breaking**: LogStruct needs Ruby 3.3 or newer, the oldest version its CI tests
|
|
43
|
+
- **Breaking**: Keys in `filter_keys_with_hashes` (default `email` and `email_address`) are filtered, with a `_hash` of the value so lines about the same address can be found. They were only hashed when another filter matched them too
|
|
44
|
+
- **Breaking**: An email address has one hash however it's capitalized, and addresses in any script (`müller@example.de`) are scrubbed
|
|
45
|
+
- **Breaking**: Every time in a struct line (`scheduled_at`, `started_at`, `finished_at`, ...) is written with milliseconds, like `ts`
|
|
46
|
+
- **Breaking**: Removed fields that were never written: ActiveJob's `retries` on enqueue lines, the blocked host line's `allowed_hosts` and `allow_ip_hosts`, Shrine's `metadata` and `location` on metadata lines and `exist` on exists lines (Shrine's events don't carry them), and ActiveStorage's `filename`, `mime_type`, `size` and `metadata` on upload and download lines (Rails' events have only the blob's key). ActiveStorage metadata lines have the `mime_type` instead of `metadata`
|
|
47
|
+
- **Breaking**: `Log::ActiveStorage::Stream` and the `stream` event are removed: Rails sends no `service_stream` event. Streaming downloads are `download` lines
|
|
48
|
+
- **Requests**: Request lines have the client's IP (`source_ip`) and the `host`, as Rails' own request line does. A `lograge_custom_options` that added them can drop them
|
|
49
|
+
- **Tags**: Tags are written: `logger.tagged("x")` and `config.log_tags` give a `tags` array, and named tags go in `ctx` (`request_id` is written as `req_id`)
|
|
50
|
+
- **ActionMailer**: Mailers keep their logger, so `logger.info` in a mailer is written. LogStruct detaches Rails' ActionMailer log subscriber instead, which wrote the text lines its own mail lines replace
|
|
51
|
+
- **Breaking**: **ActionMailer**: `log_and_report_error` always reports the error and never raises it, as its name says. It went through `error_handling_modes.standard_errors`, so in `Raise` mode (the default) it raised the error without reporting it, and in the `Log` and `Ignore` modes it didn't report it. `log_and_reraise_error` still raises every error, and reports it only in the `Report` modes
|
|
52
|
+
- **ActionMailer**: The error line says "Email delivery failed" instead of "Email delivery error, will retry" (nothing retries a mail by default), and names the mail's recipients (scrubbed) when the error doesn't say which recipients failed
|
|
53
|
+
- **ActiveJob**: A job the backend couldn't enqueue (`ActiveJob::EnqueueError`, or another exception) writes an `error` line instead of an `enqueue` line, and a job a `before_enqueue` callback stopped writes no line. A job a `before_perform` callback stopped has no `finish` line
|
|
54
|
+
- **ActiveJob**: A finish line's `ts` is when the job finished, not when it started
|
|
55
|
+
- **CarrierWave**: An `upload` line is written when an uploader stores a new file, with its duration, and not when a record is saved with files that are already stored. Loading a stored file (`retrieve_from_store!`) logs nothing
|
|
56
|
+
- **Sidekiq**: Lines logged outside a job have no `ctx` (it was `{}`)
|
|
57
|
+
- **Logger settings**: `prefer_json_in_development`, `enable_color_output`, `color_map`, `filter_noisy_loggers` and `local_environments` set in `config/initializers` apply: LogStruct adds its appenders again after the initializers run. Appenders the app added to SemanticLogger there are kept
|
|
58
|
+
- **Development**: `color_map` changes only the colors it names; the others keep their defaults
|
|
59
|
+
|
|
60
|
+
### Added
|
|
61
|
+
|
|
62
|
+
- **ActiveJob**: `retry`, `retry_stopped` and `discard` lines, with the exception that `retry_on` or `discard_on` caught (and the wait before the retry). The exception was in no line. `perform_all_later` writes a line per job; with an adapter that reports only how many jobs it enqueued (Sidekiq's), a line per job when it enqueued them all, one warn line with the counts when it didn't, and one error line when it raised
|
|
63
|
+
- **ActiveStorage**: Deleting files by prefix (a blob's variants, when an image is purged) is a `delete` line with `prefix`. It raised a type error inside LogStruct, and wrote no line
|
|
64
|
+
- **CarrierWave**: `download` lines (a remote file fetched with `download!`) and `delete` lines (`remove!`)
|
|
65
|
+
- **Shrine**: Upload lines have the file's `mime_type` and `size`, and metadata lines how long extracting the metadata took. They leave out the file's original name, which can name a person, and the file names in a `content_disposition` upload option
|
|
66
|
+
- **Security**: Blocked host lines have `blocked_hosts`, every host Rails blocked (the Host header, and X-Forwarded-Host)
|
|
67
|
+
- **Filtering**: `Bearer` tokens and `Basic` credentials in strings are scrubbed (`filters.authorization_credentials`, on by default)
|
|
68
|
+
- **Filtering**: The default sensitive key words include `passcode`, `apikey`, `social_security`, `credit_card` and `card_number`
|
|
69
|
+
- **Filtering**: US phone numbers are scrubbed in their other common formats (`(555) 123-4567`, `555.123.4567`, with or without `+1`). The opt-in scrubbers also match MAC addresses written with dashes (`filters.mac_addresses`) and IPv6 addresses (`filters.ip_addresses`)
|
|
70
|
+
- **ActionMailer**: Where LogStruct is off (development, consoles, Rake tasks, job runners it doesn't recognize), mailers still have `log_and_ignore_error`, `log_and_report_error` and `log_and_reraise_error`, which log a text line and report or raise. A `rescue_from` naming one of them raised `NameError` there
|
|
71
|
+
- **Docs**: Pages for each job, mail, SQL and security integration, every log field, when LogStruct runs, upgrading to 0.2.0 and troubleshooting
|
|
72
|
+
- **Error handling**: `LogStruct.handle_exception` takes `log:` (default `true`). Pass `log: false` when your own structured line already logged the error, so that it's only reported (or raised) according to the mode. If your tests stub `handle_exception` with a lambda, accept `log:` too
|
|
73
|
+
- **Sidekiq**: Every line logged during a Sidekiq job includes the job's `jid` and class in `ctx`. A Hash logged with its own `ctx` is merged with the job's
|
|
74
|
+
- **Filtering**: `FilterMatcher` takes `matches_logstruct_fields:` (default `true`), and `ParamFilters.should_filter_key?` takes `logstruct_field:` and `path:`, for matchers that shouldn't apply to LogStruct's own field names and for nested keys
|
|
75
|
+
- **Filtering**: Rails' dotted `filter_parameters` (`"card.number"`, `/\Acard\.number\z/`) match nested keys by their path, and three-argument filter blocks get the parameters around the key, as in Rails. New `filters.path_matchers` setting (a matcher that doesn't read the parameters sets `uses_root: false`, so they aren't copied for it)
|
|
76
|
+
- **Filtering**: Sensitive query params in URLs are filtered (`?token=`, `?apiKey=`, `user[password]=`, OAuth's `?code=`), checked like keys. New `filters.url_query_params` setting, on by default
|
|
77
|
+
- **Config**: `config.limits` (`max_hash_keys`, `max_array_items`, `max_backtrace_lines`, `max_string_bytes`)
|
|
78
|
+
- **Forking servers**: SemanticLogger's appenders are reopened in every forked process (a `Process._fork` hook), so Unicorn, Passenger and Pitchfork workers, Resque jobs and other forked children write their lines. It replaces the Puma cluster worker patch
|
|
79
|
+
- **Repository**: `SECURITY.md` (report vulnerabilities privately) and weekly Dependabot updates
|
|
80
|
+
|
|
81
|
+
### Removed
|
|
82
|
+
|
|
83
|
+
- **GoodJob**: `LogStruct::Integrations::GoodJob::LogSubscriber`, which listened for events GoodJob doesn't send (`enqueue`, `start`, `finish`, `error`, `schedule`), so it never logged anything. GoodJob's jobs are logged by the ActiveJob integration
|
|
84
|
+
- **GoodJob**: the `Log::GoodJob::Enqueue`, `Start`, `Finish` and `Schedule` log types, which only that subscriber built. The Terraform provider's catalog lists only `error` and `log` for GoodJob; ActiveJob's job lines have the same `src` and `evt` as the removed types
|
|
85
|
+
- **ActionMailer**: The mailer metadata that was meant to add the current tags and job ID to a mailer's lines. It asked for methods neither TaggedLogging nor ActiveJob has, so it never added anything
|
|
86
|
+
- **Config**: `config.integrations.enable_semantic_logger`, which did nothing: LogStruct always writes through SemanticLogger. Setting it raises `NoMethodError`
|
|
87
|
+
- **Rack**: `LogStruct::Integrations::Rack`, which nothing loaded and which required a file that doesn't exist
|
|
88
|
+
|
|
89
|
+
### Fixed
|
|
90
|
+
|
|
91
|
+
- **Lograge**: The Hash a `lograge_custom_options` proc returns is merged into the request line, as Lograge does
|
|
92
|
+
- **Errors**: An error the app didn't handle (Rails' 500 page, and errors Rails turns into a 404 or 422) is an error line with `src` `rails`, the error's class and message, the trace Rails logs, the request's path, method and IP, the response's `status`, and the error's cause (`cause_class`, `cause_message`), instead of Rails' block of text. It's written at `config.action_dispatch.debug_exception_log_level`, and `config.action_dispatch.log_rescued_responses = false` still leaves out the 404s and 422s. A CSRF or IP spoofing error has only its security line
|
|
93
|
+
- **Sorbet**: LogStruct installs Sorbet's four runtime error handlers once. It also installed one that the next replaced
|
|
94
|
+
- **Security**: LogStruct no longer empties Rails' `filter_parameters` or loads ActiveRecord and the other frameworks before the app's initializers run, so `filter_attributes` and Rails' own parameter filtering keep working (GHSA-cpp6-p4hf-383g)
|
|
95
|
+
- **Security**: Values nested deeper than the depth limit are summarized instead of logged raw, skipping filtering and scrubbing (#15)
|
|
96
|
+
- **Security**: When Rails can't parse a request's body (malformed JSON), its debug line gives the body's size and type. Rails logged the whole body there, so a password in it was logged unfiltered
|
|
97
|
+
- **Security**: The error line for params Rails can't parse (malformed JSON, a bad %-encoding) leaves out the input the parser's message quotes: it has no `cause_message`, and Rails' "Invalid request parameters: ..." stops before the input. So do the lines for such an error that the app logs itself (`LogStruct.handle_exception`, `Rails.logger.error(error)`, in JSON and in color). A password where the parser stopped was logged unfiltered
|
|
98
|
+
- **Security**: Structs, `ActionController::Parameters` and other objects in a log line are filtered and scrubbed like Hashes, instead of being serialized unfiltered
|
|
99
|
+
- **Fix**: A value that refers back to itself (a Hash or Array containing its own parent) is logged once instead of being expanded up to the depth limit, which took seconds and produced very large lines
|
|
100
|
+
- **ActiveRecord**: Verbose query logs (the "↳ caller" lines) are turned off as intended. The integration set them on `ActiveRecord::Base`, which loaded it early and never matched, because the setting is on the `ActiveRecord` module
|
|
101
|
+
- **Sidekiq**: Lines logged during a Sidekiq job are written once instead of twice (#13)
|
|
102
|
+
- **Sidekiq**: The logger logs a copy of `Sidekiq::Context` instead of the live hash, which could mark successful jobs as failed (#14)
|
|
103
|
+
- **Development**: Colored lines filter named tags as JSON lines filter `ctx`: `logger.tagged(password: "...")` wrote the password. Colored Hash and LogStruct lines are the JSON line, pretty-printed, with the message, tags and named tags it has, and without a blank line after them
|
|
104
|
+
- **Errors**: An `error_reporting_handler` that raises (a timeout calling its service) no longer changes what happens to the error it was reporting: `handle_exception` in the Report modes and the mailers' handlers carry on (or raise the original error), and the handler's error is a `logstruct` error line
|
|
105
|
+
- **Logger settings**: `filter_noisy_loggers` leaves out ActionView's, routing errors' and schema migrations' lines. It kept only those lines and left out every other one
|
|
106
|
+
- **GoodJob**: The GoodJob integration now sets GoodJob's logger and logs GoodJob's thread errors. It referred to its own module instead of `::GoodJob`, so neither was set up before
|
|
107
|
+
- **GoodJob**: LogStruct turns itself on in GoodJob's executable (`good_job start`), like Puma and Sidekiq, and stops GoodJob adding a plain text STDOUT logger beside it, so GoodJob workers log JSON, each line once
|
|
108
|
+
- **GoodJob**: Thread errors are logged whenever the app sets its own handler (`config.good_job.on_thread_error`, which GoodJob applies after LogStruct's setup, or `GoodJob.on_thread_error =` at any time). LogStruct logs the error, then calls the app's handler, which keeps reporting it as before. This needs GoodJob 3.0 or later
|
|
109
|
+
- **GoodJob**: A failed job is logged once. GoodJob passes every unhandled job exception to `on_thread_error`, and the ActiveJob integration has already logged it with the job's ID and class, so the thread error line skips it. Exceptions that aren't `StandardError`s (GoodJob's `handled_exceptions` can let them through) are logged and passed on too; outside production they raised a `TypeError` and skipped the app's handler
|
|
110
|
+
- **GoodJob**: GoodJob's own lines during a job have the job's ID, class, queue, execution count, scheduled time and priority. The logger read them from a thread variable GoodJob never sets
|
|
111
|
+
- **GoodJob**: A thread error is logged once, as LogStruct's structured line. GoodJob's own log subscriber also wrote it as a plain text "GoodJob error: ..." line with the whole backtrace. GoodJob still logs the notifier's connection errors, which it passes to `on_thread_error` only once they repeat, without the backtrace. Loggers an app adds to `GoodJob::LogSubscriber.loggers` no longer get job errors; they're in `GoodJob.logger`'s structured line
|
|
112
|
+
- **GoodJob**: LogStruct's GoodJob logger logs LogStruct log entries and Hashes as they are. It only took strings, so the thread error handler's entry raised a `TypeError`
|
|
113
|
+
- **Output**: STDOUT is written without buffering. Without it, a process whose STDOUT is a pipe (a container) held its lines back until the buffer filled; GoodJob's executable sets `$stdout.sync` only for its own logger, which LogStruct turns off
|
|
114
|
+
- **Sidekiq**: Sidekiq's logger is replaced as soon as Rails' logger is, so lines Sidekiq logs while the app's initializers run (connecting to Redis, setting up a fetcher) are JSON too
|
|
115
|
+
- **Puma**: Removed a debug line (`[logstruct] puma lifecycle init`) that Puma servers wrote to STDERR at boot
|
|
116
|
+
- **ActionMailer**: `log_and_reraise_error`, `log_and_ignore_error` and `log_and_report_error` also work when `ActionMailer::MailDeliveryJob` passes an error to the mailer class. Every email that failed to send in a delivery job raised `NameError: undefined method 'log_and_reraise_error' for class` instead of the real error
|
|
117
|
+
- **ActionMailer**: The mailer logs a failed delivery once. It logged it twice, and a third time when the error reached the mailer class (in `Raise` mode, the default for standard errors, `handle_exception` raised before the error was marked as logged); in `Log` and `Report` modes `handle_exception` logged it again too. A delivery job also has the ActiveJob integration's job error line
|
|
118
|
+
- **ActionMailer**: In `Raise` mode, a `deliver_later` email whose mailer handles the error with `log_and_report_error` fails its job, as `deliver_now` raises. The job succeeded and the error was never reported
|
|
119
|
+
- **Shrine**: Shrine's own log subscriber no longer logs the events LogStruct logs
|
|
120
|
+
- **ActiveModelSerializers**: AMS's own text log subscriber is detached, so renders are logged once
|
|
121
|
+
- **ActiveJob**: Integer `provider_job_id` values (SolidQueue) are logged as strings instead of raising a TypeError (#7, thanks @justinko)
|
|
122
|
+
- **Security**: Strings and Hash keys are written as valid UTF-8. A byte like `\xFF` in a header (Puma passes headers as binary strings) stopped the whole line from being written as JSON and SemanticLogger dropped it, so anyone could hide their request line and the IP spoofing and CSRF lines. A line that still can't be written is replaced by one that says so
|
|
123
|
+
- **Security**: The Referer is logged without its query and fragment (request, blocked host, CSRF and IP spoofing lines, and the context sent to error reporters), which can carry password reset, confirmation and magic link tokens or OAuth codes
|
|
124
|
+
- **Security**: Error reporters (Sentry, Bugsnag, Rollbar, Honeybadger and custom handlers) get the context filtered and scrubbed like a log line
|
|
125
|
+
- **Security**: The request ID, program name and Sidekiq job context are filtered and scrubbed like the rest of the line. They were added after filtering, so an `X-Request-Id` header could carry a card number into the logs
|
|
126
|
+
- **Security**: With `sql_log_bind_params` on, bind values are filtered by their column names, like request params. Only values that looked like tokens were filtered, so a `password` bind was logged, and values were filtered for containing a word like `key`, `auth` or `password` (`monkey`, `Author`, "Forgot password"). Values shaped like tokens still are. Binary values are logged as their size
|
|
127
|
+
- **Security**: `Sidekiq.logger` passes Hashes on as structured data, so their keys are filtered. It turned them into Strings first, so `{password: "hunter2"}` was logged as it was
|
|
128
|
+
- **Security**: Card numbers of 13 to 19 digits are scrubbed (written together, in fours, or in American Express and Diners Club groups), and numbers that fail the Luhn check are left alone. URL passwords are scrubbed whole (`user:pass:word`, `redis://:pw@host`, lowercase `%3a`/`%40`). Hash keys that aren't plain identifiers (an email address as a key), a filtered Hash's `_keys`, Symbol values and GlobalIDs are scrubbed
|
|
129
|
+
- **Security**: The color formatter (development, opt-in) and loggers added with `broadcast_to` get scrubbed messages
|
|
130
|
+
- **Security**: Formatting is bounded (see `config.limits`): one huge value held up the thread that writes every line, and memory grew
|
|
131
|
+
- **CSRF**: CSRF lines are logged. They never appeared: the middleware sat outside ShowExceptions, which turns `InvalidAuthenticityToken` into a 422 first. LogStruct registers a DebugExceptions interceptor instead, and Rails responds as before
|
|
132
|
+
- **Errors**: `logger.error(exception)` writes an error line with the class, message and backtrace (it wrote an empty line), and the message logged beside a Hash (`logger.info("Saved", id: 1)`) is kept. The log methods take SemanticLogger's exception argument, which raised an `ArgumentError`
|
|
133
|
+
- **Errors**: An object whose `as_json` refers back to itself is summarized instead of raising `SystemStackError` from the formatter
|
|
134
|
+
- **Sidekiq**: An ActiveJob job's error is logged by the Sidekiq integration unless the ActiveJob integration logged it. An error raised before ActiveJob ran the job (a job class a deploy removed) was in no line
|
|
135
|
+
- **Puma**: Requiring LogStruct with `server` in `ARGV` and no Puma installed raised `LoadError`, so the app didn't boot
|
|
136
|
+
- **GoodJob**: The app's `on_thread_error` handler runs even if LogStruct's line fails
|
|
137
|
+
- **Ahoy**: Symbol event names and properties that aren't a Hash (they can come from the browser) are logged without raising; a String of properties raised a type error and failed the request in production
|
|
138
|
+
- **TypeScript**: The generated types give `error_class` as a string, not `any`
|
|
139
|
+
- **Gem**: The gem packages only files committed to git
|
|
140
|
+
- **Logging**: `rails server` in development printed every message twice: Rails' console logger wrote it as text beside LogStruct's line. `broadcast_to` ignores a logger that writes to STDOUT or STDERR when SemanticLogger already writes there
|
|
141
|
+
- **Logging**: Loggers added with `broadcast_to` got each `logger << "..."` line twice
|
|
142
|
+
- **Notifications**: `log.logstruct` subscribers get an entry after its line is queued to be written, so a subscriber that raises no longer stops the line being written. Its error is handled like the app's other errors (`standard_errors`, which raises it from the app's log call by default)
|
|
143
|
+
- **ActionMailer**: The entry a mailer's own `rescue_from` handler logs has level `error`, like its line. It said `info`, which `log.logstruct` subscribers saw
|
|
144
|
+
- **ActiveJob**: With the inline adapter, a failed `perform_later` writes one error line. The error that raised out of `perform_later` was logged again
|
|
145
|
+
- **Sorbet**: Type error lines and reports have a backtrace that starts at the call that failed. Method signature errors had none, and the others started inside sorbet-runtime
|
|
146
|
+
- **Rails 8.2**: Apps on Rails main boot with LogStruct. Rails' ActiveStorage, ActionMailer and ActiveRecord log subscribers read `Rails.event` there, and detaching them raised `NoMethodError`
|
|
147
|
+
- **Rails 8.2**: Requests on Rails main log only the request line, without Rails' "Processing by" and "Completed 200 OK" text lines, which lograge detaches only on earlier Rails versions
|
|
148
|
+
- **Filtering**: US phone numbers with a leading `1` (`1-555-123-4567`) are scrubbed whole, SSN-shaped parts of longer numbers (`123-45-6789-0`) are left alone, and an IPv6 address's zone (`fe80::1%en0`) is scrubbed with it
|
|
149
|
+
|
|
12
150
|
## [0.1.12] - 2026-01-30
|
|
13
151
|
|
|
14
152
|
### Added
|
data/README.md
CHANGED
|
@@ -1,95 +1,103 @@
|
|
|
1
1
|
# LogStruct
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
JSON structured logging for Rails apps (Rails 7.1 or newer, Ruby 3.3 or newer). Add the gem, and in production your
|
|
4
|
+
app writes one JSON line per log entry to STDOUT: requests, jobs, mail, errors, security events and your own logs. The
|
|
5
|
+
lines are easy to search, and you can turn them into metrics, alerts and dashboards in CloudWatch, Grafana, Datadog or
|
|
6
|
+
any other log platform. Sensitive values are filtered and scrubbed before a line is written.
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
```json
|
|
9
|
+
{"src":"rails","evt":"request","lvl":"info","ts":"2026-10-17T03:09:30.839+00:00","path":"/orders","method":"GET","source_ip":"203.0.113.7","user_agent":"Mozilla/5.0","referer":"https://app.example.com/","format":"html","controller":"OrdersController","action":"index","status":200,"duration_ms":42.1,"view":30.2,"db":8.4,"params":{"page":"2"},"host":"app.example.com","accept":"text/html","prog":"Rails","req_id":"6f1c2b7e-0d4a-4c1e-9a55-3b8e2f7d9c10"}
|
|
10
|
+
```
|
|
6
11
|
|
|
7
12
|
## Features
|
|
8
13
|
|
|
9
|
-
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
-
|
|
17
|
-
|
|
18
|
-
-
|
|
19
|
-
|
|
20
|
-
-
|
|
14
|
+
- **On where it matters.** In production LogStruct switches on for the processes it recognizes: Puma, Unicorn,
|
|
15
|
+
Thin, Falcon, `rails server`, a Sidekiq server and `good_job start`. In test it's on for every process except the
|
|
16
|
+
console. It's off in development, in consoles and in production Rake tasks, unless you set `LOGSTRUCT_ENABLED=true`.
|
|
17
|
+
See [When LogStruct runs](https://logstruct.com/docs/when-logstruct-runs).
|
|
18
|
+
- **Integrations** for Lograge (request lines), ActionMailer, ActiveJob, Sidekiq, GoodJob, Puma, ActiveStorage,
|
|
19
|
+
Shrine, CarrierWave, Ahoy, ActiveModelSerializers, Dotenv, host authorization, CSRF and IP spoofing errors, and
|
|
20
|
+
Sorbet's runtime type errors. SQL query logging is there too, off by default.
|
|
21
|
+
- **Filtering by key name.** Values under keys like `password`, `token`, `api_key`, `authorization` and `cookie` are
|
|
22
|
+
replaced by a summary of the value, in request params, job arguments and anything else you log. Keys are matched by
|
|
23
|
+
their words, so `apiKey`, `X-Api-Key` and `HTTP_AUTHORIZATION` are filtered too. The keys in Rails'
|
|
24
|
+
`config.filter_parameters` are filtered the way Rails filters them.
|
|
25
|
+
- **Scrubbing inside strings** (inspired by [Logstop](https://github.com/ankane/logstop)): email addresses become a
|
|
26
|
+
keyed hash (`[EMAIL:5f0c1e2a9b3d4e6f]`), so you can follow one person through your logs without logging the
|
|
27
|
+
address. Card numbers that pass the Luhn check, US phone numbers, SSNs written as 123-45-6789, URL passwords,
|
|
28
|
+
`Bearer` and `Basic` credentials and sensitive URL query params (`?token=`) are replaced too. IP and MAC address
|
|
29
|
+
scrubbing are opt-in.
|
|
30
|
+
- **Limits.** Huge values are cut down (100 keys per Hash, 10 items per Array, 20 lines per backtrace, 32 KB per
|
|
31
|
+
String by default), so one large request body can't slow down your logging.
|
|
32
|
+
- **Error handling.** A mode for each kind of error decides whether it's raised, logged or reported, and whether
|
|
33
|
+
that differs between development and production. Reports go to Sentry, Bugsnag, Rollbar, Honeybadger or your own
|
|
34
|
+
reporter.
|
|
35
|
+
- **Sorbet types.** Every built-in line is a typed struct, and you can define your own typed log lines.
|
|
36
|
+
- **STDOUT only in production**, with no log file to rotate or fill the disk.
|
|
21
37
|
|
|
22
38
|
## Installation
|
|
23
39
|
|
|
24
40
|
Add this line to your application's Gemfile:
|
|
25
41
|
|
|
26
42
|
```ruby
|
|
27
|
-
gem
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
And then execute:
|
|
31
|
-
|
|
32
|
-
```bash
|
|
33
|
-
$ bundle install
|
|
43
|
+
gem "logstruct"
|
|
34
44
|
```
|
|
35
45
|
|
|
36
|
-
|
|
46
|
+
And then run:
|
|
37
47
|
|
|
38
48
|
```bash
|
|
39
|
-
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
## Configuration and Initialization
|
|
43
|
-
|
|
44
|
-
LogStruct is designed to be highly opinionated and work out of the box with minimal configuration.
|
|
45
|
-
|
|
46
|
-
Please see the [documentation](https://logstruct.com/docs/configuration/) for configuration options.
|
|
47
|
-
|
|
48
|
-
### Important Notes on Integration
|
|
49
|
-
|
|
50
|
-
Once initialized (and enabled), the gem automatically includes its modules into the appropriate base classes:
|
|
51
|
-
|
|
52
|
-
- `ActiveSupport::TaggedLogging` is patched to support both Hashes and Strings (only when LogStruct is enabled)
|
|
53
|
-
- `ActionMailer::Base` includes error handling and event logging modules
|
|
54
|
-
- We configure `Lograge` for request logging
|
|
55
|
-
- Lograge request logs include request metadata (request_id, source_ip, user_agent, referer, host, content_type, accept) and custom fields from `lograge_custom_options`
|
|
56
|
-
- A Rack middleware is inserted to catch and log errors, including security violations (IP spoofing, CSRF, blocked hosts, etc.)
|
|
57
|
-
- Structured logging is set up for ActiveJob, Sidekiq, Shrine, etc.
|
|
58
|
-
- Rails `config.filter_parameters` are merged into LogStruct's filters and then cleared (to avoid double filtering). Configure sensitive keys via `LogStruct.config.filters`.
|
|
59
|
-
- When `RAILS_LOG_TO_STDOUT` is set, we log to STDOUT only. Otherwise, we log to STDOUT by default without adding a file appender to avoid duplicate logs.
|
|
60
|
-
|
|
61
|
-
### Default behavior by process type
|
|
62
|
-
|
|
63
|
-
- **Server processes** (`rails server`): JSON logging is enabled by default in production and test environments
|
|
64
|
-
- **Test runs** (`rails test`): JSON logging is enabled by default in test environment to ensure tests catch production bugs
|
|
65
|
-
- **Console** (`rails console`): JSON logging is disabled by default in all environments, providing human-readable logs instead
|
|
66
|
-
- **Other Rake tasks** (`rake db:migrate`, etc.): JSON logging is disabled by default in production, providing human-readable logs instead
|
|
67
|
-
- **Development environment**: Disabled by default for all process types. Enable explicitly via `LOGSTRUCT_ENABLED=true` or `LogStruct.configure { |c| c.enabled = true }`.
|
|
68
|
-
|
|
69
|
-
When enabled in development, LogStruct defaults to production‑style JSON output so you can preview exactly what will be shipped in prod. You can opt back into the colorful human formatter with:
|
|
70
|
-
|
|
71
|
-
```ruby
|
|
72
|
-
LogStruct.configure do |c|
|
|
73
|
-
c.prefer_json_in_development = false
|
|
74
|
-
c.enable_color_output = true
|
|
75
|
-
end
|
|
49
|
+
bundle install
|
|
76
50
|
```
|
|
77
51
|
|
|
78
|
-
|
|
52
|
+
That's all you need: there's nothing to generate. To configure LogStruct, add an initializer
|
|
53
|
+
(`config/initializers/logstruct.rb`). The [configuration docs](https://logstruct.com/docs/configuration) list every
|
|
54
|
+
setting.
|
|
55
|
+
|
|
56
|
+
To see the JSON lines in development, start your server with `LOGSTRUCT_ENABLED=true rails server`.
|
|
57
|
+
|
|
58
|
+
## What LogStruct changes in your app
|
|
59
|
+
|
|
60
|
+
When LogStruct is on in a process:
|
|
61
|
+
|
|
62
|
+
- `Rails.logger` and the framework loggers (ActiveRecord, ActionController, ActionMailer, ActiveJob, ActionView,
|
|
63
|
+
ActionCable) are replaced by LogStruct's SemanticLogger logger. A logger you set with `config.logger` only applies
|
|
64
|
+
while LogStruct is off.
|
|
65
|
+
- Lograge is turned on and writes the request lines. Request lines include `req_id`, `source_ip`, `host`,
|
|
66
|
+
`user_agent`, `referer` (without its query string), `content_type`, `accept` and the fields your
|
|
67
|
+
`lograge_custom_options` proc adds.
|
|
68
|
+
- Every line logged during a request gets its `req_id`. Tags from `logger.tagged(...)` and `config.log_tags` are
|
|
69
|
+
written in `tags`.
|
|
70
|
+
- Rails' own text lines for requests, mail and jobs are replaced by LogStruct's JSON lines. An error your app didn't
|
|
71
|
+
handle is one error line with the request's fields and the response's status, instead of Rails' block of text.
|
|
72
|
+
- Mailers get a `rescue_from StandardError` that logs a failed delivery before re-raising it, and the
|
|
73
|
+
`log_and_ignore_error`, `log_and_report_error` and `log_and_reraise_error` methods for your own `rescue_from`
|
|
74
|
+
handlers (those methods exist where LogStruct is off too).
|
|
75
|
+
- CSRF and IP spoofing errors are logged as security lines and reported (`error_handling_modes.security_errors`), and
|
|
76
|
+
Rails still responds to them as usual. Blocked hosts are logged too.
|
|
77
|
+
- Rails' `filter_parameters` list is left in place, so `request.filtered_parameters`,
|
|
78
|
+
`ActiveRecord::Base.filter_attributes` and other Rails features keep filtering. Values Rails has already replaced
|
|
79
|
+
are logged as `[FILTERED]`.
|
|
80
|
+
- Loggers you build yourself with `ActiveSupport::TaggedLogging.new(...)` write LogStruct's JSON lines too.
|
|
81
|
+
- In production LogStruct logs to STDOUT only. In development it logs to STDOUT and Rails' log file, and in test to
|
|
82
|
+
the log file only. With `RAILS_LOG_TO_STDOUT` set, both log to STDOUT only.
|
|
79
83
|
|
|
80
84
|
## Documentation
|
|
81
85
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
### Custom Typed Logs
|
|
86
|
+
The full documentation is at [logstruct.com/docs](https://logstruct.com/docs). Its Ruby examples are tested in this
|
|
87
|
+
repository's test suite.
|
|
85
88
|
|
|
86
|
-
|
|
89
|
+
- [Getting started](https://logstruct.com/docs/getting-started)
|
|
90
|
+
- [Configuration](https://logstruct.com/docs/configuration)
|
|
91
|
+
- [Filtering sensitive data](https://logstruct.com/docs/filtering-sensitive-data)
|
|
92
|
+
- [Log fields](https://logstruct.com/docs/log-fields)
|
|
93
|
+
- [Custom typed logs](https://logstruct.com/docs/sorbet-types#custom-log-classes)
|
|
94
|
+
- [Upgrading to 0.2.0](https://logstruct.com/docs/upgrading) (many breaking changes)
|
|
95
|
+
- [Troubleshooting](https://logstruct.com/docs/troubleshooting)
|
|
96
|
+
- [Changelog](CHANGELOG.md)
|
|
87
97
|
|
|
88
|
-
|
|
89
|
-
- Fix your `source` to a constant (e.g., return the string `"payments"`), and restrict `event` with a `T::Enum` (e.g., `processed|failed|refunded`).
|
|
90
|
-
- The `LogStruct.info` signature accepts either the internal `CommonFields` (for built‑ins) or your public custom type, so you keep type safety at the call site.
|
|
98
|
+
## Security
|
|
91
99
|
|
|
92
|
-
|
|
100
|
+
To report a vulnerability, see [SECURITY.md](SECURITY.md).
|
|
93
101
|
|
|
94
102
|
## License
|
|
95
103
|
|