Log Fields
Each line LogStruct writes is one JSON object. This page lists its keys: the ones every line has, the ones LogStruct adds from the logger's context, and the short names of each log type's fields. It ends with an example line for each built-in log type and the formats of the values.
Keys on Every Line
| Key | Value |
|---|---|
| src | Where the line came from: a LogStruct::Source value such as rails, app, job or mailer, or the src of a Hash you log. Lines without one get app. |
| evt | What happened: a LogStruct::Event value such as request, log or error, or the evt of a Hash you log. Lines without one get log. |
| lvl | The level the line was logged at (see Levels). |
| ts | When the line was logged (see Timestamps). |
| msg | The message, on lines that have one: a string you log, the message field of the log types that have one (see Short Names), or the msg of a Hash you log. |
| prog | The name of the logger that wrote the line: Rails for Rails.logger and the framework loggers LogStruct replaces, Sidekiq-Server or Sidekiq-Client for Sidekiq's own lines and GoodJob for GoodJob's. Lines written without a logger name have no prog. |
LogStruct's own log types write src, evt, lvl and ts first, then their fields. A Hash you log keeps its own keys first.
Keys from the Logger's Context
LogStruct adds these keys after the line's own fields. It filters and scrubs them like the rest of the line.
| Key | Value |
|---|---|
| req_id | The request's ID (Rails' request.request_id), on every line logged while Rails handles a request. Blocked host lines don't have it: Rails checks the host before it sets the request ID. A line with its own req_id keeps it. |
| tags | The logger's tags, as an array: logger.tagged("billing") and config.log_tags. The config/environments/production.rb that Rails generates sets config.log_tags = [:request_id], so the lines logged during a request in production have the request ID in tags too. A line with its own tags keeps them. |
| ctx | Named tags (logger.tagged(account_id: 42)) and, while a Sidekiq job runs, the job's jid and class. When a line has its own ctx Hash, its keys win and the other keys are added to it. |
Rails.logger.tagged("billing") do
Rails.logger.tagged(account_id: 42) do
Rails.logger.info("Charged card")
end
end{
"src": "app",
"evt": "log",
"lvl": "info",
"ts": "2026-10-17T09:30:15.123+00:00",
"msg": "Charged card",
"prog": "Rails",
"tags": [
"billing"
],
"req_id": "6f1c9a2e-7b4d-4e8a-9c61-2d5b8f0e4a17",
"ctx": {
"account_id": 42
}
}Logged Hashes
A Hash you log is written with its keys at the top level of the line. It can set its own src and evt. The timestamp and level come from the logger: a ts or lvl key in the Hash is dropped and handled as a LogStruct error (error_handling_modes.logstruct_errors: raised in local_environments, logged elsewhere).
Rails.logger.info(msg: "Invoice sent", invoice_id: 812, email: "Linda.Martinez@example.com"){
"msg": "Invoice sent",
"invoice_id": 812,
"email": {
"_filtered": {
"_class": "String",
"_hash": "a058d5090035d2e1"
}
},
"src": "app",
"evt": "log",
"ts": "2026-10-17T09:30:15.123+00:00",
"lvl": "info",
"prog": "Rails",
"req_id": "6f1c9a2e-7b4d-4e8a-9c61-2d5b8f0e4a17"
}additional_data
LogStruct's log types take an additional_data Hash for fields the type doesn't have. Its keys are written at the top level of the line, after the type's own fields, not under an additional_data key.
Rails.logger.info(
LogStruct::Log::Plain.new(
message: "Imported invoices",
additional_data: {account_id: 42, rows: 120}
)
){
"src": "app",
"evt": "log",
"lvl": "info",
"ts": "2026-10-17T09:30:15.123+00:00",
"msg": "Imported invoices",
"account_id": 42,
"rows": 120,
"prog": "Rails"
}- A key with the same name as one of the type's fields replaces that field: an
additional_dataof{msg: "..."}replaces the message. src,evt,lvlandtsare reserved. LogStruct drops them and handles theArgumentErroras a LogStruct error (raised inlocal_environments, logged elsewhere).
Short Names
LogStruct's log types name their fields with LogStruct::LogField, which maps each name to a short JSON key: MailerClass is written as mailer and Database as db. The table lists every field. Log::ActiveJob::* stands for every ActiveJob log type, and some fields aren't written by any built-in log type. At the top of a line, Rails' partial filter_parameters matches don't apply to these keys: Rails 8's default :_key would otherwise filter cron_key.
| Key | LogField | Written by |
|---|---|---|
| accept | Accept | Log::Request |
| action | Action | Log::Request |
| active_count | ActiveConnections | Log::SQL |
| adapter | Adapter | Log::ActiveModelSerializers, Log::SQL |
| addr | Address | No built-in log type |
| ahoy_event | AhoyEvent | Log::Ahoy |
| allow_ip_hosts | AllowIpHosts | No built-in log type |
| allowed_hosts | AllowedHosts | No built-in log type |
| arguments | Arguments | Log::ActiveJob::*, Log::GoodJob::* |
| attachments | AttachmentCount | Log::ActionMailer::* |
| attempt | Attempt | Log::ActiveJob::Start |
| backtrace | Backtrace | Log::ActionMailer::Error, Log::ActiveJob::Error, Log::Error, Log::GoodJob::Error, Log::Puma::Error, Log::Sidekiq::Error |
| bind_params | BindParams | Log::SQL |
| blocked_host | BlockedHost | Log::Security::BlockedHost |
| blocked_hosts | BlockedHosts | Log::Security::BlockedHost |
| cause_class | CauseClass | Log::Error |
| cause_message | CauseMessage | Log::Error |
| checksum | Checksum | Log::ActiveStorage::Upload |
| client_ip | ClientIp | Log::Security::IPSpoof |
| content_type | ContentType | Log::Request |
| controller | Controller | Log::Request |
| cron_key | CronKey | No built-in log type |
| ctx | Context | Log::Sidekiq::Log |
| data | Data | No built-in log type |
| db | Database | Log::Request |
| db_name | DatabaseName | Log::SQL |
| download_opts | DownloadOptions | Log::Shrine::Download |
| duration_ms | DurationMs | Log::ActiveJob::Finish, Log::ActiveModelSerializers, Log::ActiveStorage::Download, Log::ActiveStorage::Upload, Log::CarrierWave::Upload, Log::GoodJob::Error, Log::Request, Log::SQL, Log::Shrine::Metadata, Log::Shrine::Upload |
| enqueue_caller | EnqueueCaller | No built-in log type |
| environment | Environment | Log::Puma::Start |
| error_class | ErrorClass | Log::ActionMailer::Error, Log::ActiveJob::Discard, Log::ActiveJob::Error, Log::ActiveJob::Retry, Log::ActiveJob::RetryStopped, Log::Error, Log::GoodJob::Error, Log::Puma::Error, Log::Sidekiq::Error |
| error_context | ErrorContext | Log::Puma::Error, Log::Sidekiq::Error |
| error_message | ErrorMessage | Log::Error, Log::GoodJob::Error |
| evt | Event | Every log type |
| exception_executions | ExceptionExecutions | Log::GoodJob::Error |
| execution_time | ExecutionTime | No built-in log type |
| executions | Executions | Log::ActiveJob::*, Log::GoodJob::* |
| exist | Exist | Log::ActiveStorage::Exist |
| ext | Extension | Log::CarrierWave::* |
| file | File | Log::Dotenv::Load |
| file_id | FileId | Log::ActiveStorage::*, Log::CarrierWave::* |
| filename | Filename | Log::CarrierWave::Download, Log::CarrierWave::Upload |
| finished_at | FinishedAt | Log::ActiveJob::Finish |
| format | Format | Log::Request |
| from | From | Log::ActionMailer::* |
| host | Host | Log::Request |
| job_class | JobClass | Log::ActiveJob::*, Log::GoodJob::* |
| job_id | JobId | Log::ActiveJob::*, Log::GoodJob::* |
| listening_addresses | ListeningAddresses | Log::Puma::Start |
| location | Location | Log::Shrine::Delete, Log::Shrine::Download, Log::Shrine::Exist, Log::Shrine::Upload |
| lvl | Level | Every log type |
| mailer | MailerClass | Log::ActionMailer::* |
| mailer_action | MailerAction | Log::ActionMailer::* |
| max_threads | MaxThreads | Log::Puma::Start |
| metadata | Metadata | Log::CarrierWave::Upload |
| method | HttpMethod | Log::Error, Log::Puma::Error, Log::Request, Log::Security::* |
| mime_type | MimeType | Log::ActiveStorage::Metadata, Log::CarrierWave::Download, Log::CarrierWave::Upload, Log::Shrine::Upload |
| min_threads | MinThreads | Log::Puma::Start |
| mode | Mode | Log::Puma::Start |
| model | Model | Log::CarrierWave::* |
| mount_point | MountPoint | Log::CarrierWave::* |
| msg | Message | Log::ActionMailer::Error, Log::ActiveJob::Discard, Log::ActiveJob::Error, Log::ActiveJob::Retry, Log::ActiveJob::RetryStopped, Log::ActiveModelSerializers, Log::Ahoy, Log::Error, Log::GoodJob::Log, Log::Plain, Log::Puma::Error, Log::SQL, Log::Security::*, Log::Sidekiq::* |
| msg_id | MessageId | Log::ActionMailer::* |
| name | Name | Log::SQL |
| op | Operation | No built-in log type |
| op_type | OperationType | Log::SQL |
| opts | Options | Log::Shrine::Upload |
| params | Params | Log::Request |
| path | Path | Log::Error, Log::Puma::Error, Log::Request, Log::Security::* |
| pid | ProcessId | Log::GoodJob::*, Log::Puma::Shutdown, Log::Puma::Start, Log::Sidekiq::Log |
| pool_size | ConnectionPoolSize | Log::SQL |
| prefix | Prefix | Log::ActiveStorage::Delete |
| priority | Priority | Log::GoodJob::Log |
| properties | Properties | Log::Ahoy |
| provider_job_id | ProviderJobId | Log::ActiveJob::* |
| puma_codename | PumaCodename | Log::Puma::Start |
| puma_version | PumaVersion | Log::Puma::Start |
| queue_name | QueueName | Log::ActiveJob::*, Log::GoodJob::*, Log::Sidekiq::Error |
| range | Range | Log::ActiveStorage::Download |
| referer | Referer | Log::Error, Log::Request, Log::Security::* |
| req_id | RequestId | Added from the request (see above) |
| resource_class | ResourceClass | Log::ActiveModelSerializers |
| result | Result | No built-in log type |
| retries | Retries | No built-in log type |
| retry_count | RetryCount | Log::Sidekiq::Error |
| row_count | RowCount | Log::SQL |
| ruby_version | RubyVersion | Log::Puma::Start |
| run_time | RunTime | No built-in log type |
| scheduled_at | ScheduledAt | Log::ActiveJob::Schedule, Log::GoodJob::Log |
| serializer | Serializer | Log::ActiveModelSerializers |
| size | Size | Log::CarrierWave::Download, Log::CarrierWave::Upload, Log::Shrine::Upload |
| snapshot | Snapshot | Log::Dotenv::Save |
| source_ip | SourceIp | Log::Error, Log::Request, Log::Security::* |
| sql | Sql | Log::SQL |
| src | Source | Every log type |
| started_at | StartedAt | Log::ActiveJob::Start |
| status | Status | Log::Error, Log::Request |
| storage | Storage | Log::ActiveStorage::*, Log::CarrierWave::*, Log::Shrine::* |
| store_path | StorePath | Log::CarrierWave::* |
| subject | Subject | Log::ActionMailer::* |
| table_names | TableNames | Log::SQL |
| tid | ThreadId | Log::GoodJob::*, Log::Sidekiq::Log |
| to | To | Log::ActionMailer::* |
| ts | Timestamp | Every log type |
| upload_opts | UploadOptions | Log::Shrine::Upload |
| uploader | Uploader | Log::CarrierWave::*, Log::Shrine::Upload |
| url | Url | No built-in log type |
| user_agent | UserAgent | Log::Error, Log::Request, Log::Security::* |
| vars | Vars | Log::Dotenv::Restore, Log::Dotenv::Update |
| version | Version | Log::CarrierWave::* |
| view | View | Log::Request |
| wait_ms | WaitMs | Log::ActiveJob::Retry |
| wait_time | WaitTime | No built-in log type |
| x_forwarded_for | XForwardedFor | Log::Security::BlockedHost, Log::Security::IPSpoof |
Example Lines
One line for each built-in log type, as LogStruct's formatter writes it from the values its integration sets. The values are made up. Each line is shown over several lines here; LogStruct writes it on one. The fields of each type are listed in Built-In Log Classes.
Log::ActionMailer
Log::ActionMailer::Delivered src: mailer, evt: delivered
{
"src": "mailer",
"evt": "delivered",
"lvl": "info",
"ts": "2026-10-17T09:30:15.123+00:00",
"to": [
"[EMAIL:a058d5090035d2e1]"
],
"from": "[EMAIL:a6778e226026bac8]",
"subject": "Your October invoice",
"msg_id": "[EMAIL:ff61c70b949f216c]",
"mailer": "InvoiceMailer",
"mailer_action": "monthly",
"attachments": 1,
"account_id": 42,
"user_id": 1873,
"prog": "Rails",
"req_id": "6f1c9a2e-7b4d-4e8a-9c61-2d5b8f0e4a17"
}Log::ActionMailer::Delivery src: mailer, evt: delivery
{
"src": "mailer",
"evt": "delivery",
"lvl": "info",
"ts": "2026-10-17T09:30:15.123+00:00",
"to": [
"[EMAIL:a058d5090035d2e1]"
],
"from": "[EMAIL:a6778e226026bac8]",
"subject": "Your October invoice",
"mailer": "InvoiceMailer",
"mailer_action": "monthly",
"attachments": 1,
"account_id": 42,
"user_id": 1873,
"prog": "Rails",
"req_id": "6f1c9a2e-7b4d-4e8a-9c61-2d5b8f0e4a17"
}Log::ActionMailer::Error src: mailer, evt: error
{
"src": "mailer",
"evt": "error",
"lvl": "error",
"ts": "2026-10-17T09:30:15.123+00:00",
"to": [
"[EMAIL:a058d5090035d2e1]"
],
"from": "[EMAIL:a6778e226026bac8]",
"subject": "Your October invoice",
"mailer": "InvoiceMailer",
"mailer_action": "monthly",
"attachments": 1,
"error_class": "Net::SMTPAuthenticationError",
"msg": "Net::SMTPAuthenticationError: Email delivery failed. Recipients: [EMAIL:a6778e226026bac8]. Error message: 535 5.7.8 Authentication failed",
"backtrace": [
"app/mailers/invoice_mailer.rb:12:in 'InvoiceMailer#monthly'"
],
"account_id": 42,
"user_id": 1873,
"prog": "Rails",
"ctx": {
"jid": "b4a9c63e5f1d2a7e9c0f3b18",
"class": "ActionMailer::MailDeliveryJob"
}
}Log::ActiveJob
Log::ActiveJob::Discard src: job, evt: discard
{
"src": "job",
"evt": "discard",
"lvl": "error",
"ts": "2026-10-17T09:30:15.123+00:00",
"job_id": "c2b8e0f4-1d3a-4b6c-9e7f-5a1d2c3b4e5f",
"job_class": "InvoiceExportJob",
"queue_name": "default",
"arguments": [
"gid://my-app/Customer/1873",
{
"month": "2026-09",
"email": {
"_filtered": {
"_class": "String",
"_hash": "a058d5090035d2e1"
}
}
}
],
"executions": 1,
"provider_job_id": "b4a9c63e5f1d2a7e9c0f3b18",
"error_class": "ActiveJob::DeserializationError",
"msg": "Error while trying to deserialize arguments: Couldn't find Customer",
"prog": "Rails",
"ctx": {
"jid": "b4a9c63e5f1d2a7e9c0f3b18",
"class": "InvoiceExportJob"
}
}Log::ActiveJob::Enqueue src: job, evt: enqueue
{
"src": "job",
"evt": "enqueue",
"lvl": "info",
"ts": "2026-10-17T09:30:15.123+00:00",
"job_id": "c2b8e0f4-1d3a-4b6c-9e7f-5a1d2c3b4e5f",
"job_class": "InvoiceExportJob",
"queue_name": "default",
"arguments": [
"gid://my-app/Customer/1873",
{
"month": "2026-09",
"email": {
"_filtered": {
"_class": "String",
"_hash": "a058d5090035d2e1"
}
}
}
],
"executions": 0,
"provider_job_id": "b4a9c63e5f1d2a7e9c0f3b18",
"prog": "Rails",
"req_id": "6f1c9a2e-7b4d-4e8a-9c61-2d5b8f0e4a17"
}Log::ActiveJob::Error src: job, evt: error
{
"src": "job",
"evt": "error",
"lvl": "error",
"ts": "2026-10-17T09:30:15.123+00:00",
"job_id": "c2b8e0f4-1d3a-4b6c-9e7f-5a1d2c3b4e5f",
"job_class": "InvoiceExportJob",
"queue_name": "default",
"arguments": [
"gid://my-app/Customer/1873",
{
"month": "2026-09",
"email": {
"_filtered": {
"_class": "String",
"_hash": "a058d5090035d2e1"
}
}
}
],
"executions": 1,
"provider_job_id": "b4a9c63e5f1d2a7e9c0f3b18",
"error_class": "Net::ReadTimeout",
"msg": "Net::ReadTimeout with #<TCPSocket:(closed)>",
"backtrace": [
"app/services/invoice_export.rb:42:in 'InvoiceExport#upload'",
"app/services/invoice_export.rb:18:in 'InvoiceExport#call'",
"app/jobs/invoice_export_job.rb:9:in 'InvoiceExportJob#perform'"
],
"prog": "Rails",
"ctx": {
"jid": "b4a9c63e5f1d2a7e9c0f3b18",
"class": "InvoiceExportJob"
}
}Log::ActiveJob::Finish src: job, evt: finish
{
"src": "job",
"evt": "finish",
"lvl": "info",
"ts": "2026-10-17T09:30:15.935+00:00",
"job_id": "c2b8e0f4-1d3a-4b6c-9e7f-5a1d2c3b4e5f",
"job_class": "InvoiceExportJob",
"queue_name": "default",
"arguments": [
"gid://my-app/Customer/1873",
{
"month": "2026-09",
"email": {
"_filtered": {
"_class": "String",
"_hash": "a058d5090035d2e1"
}
}
}
],
"executions": 1,
"provider_job_id": "b4a9c63e5f1d2a7e9c0f3b18",
"duration_ms": 812.43,
"finished_at": "2026-10-17T09:30:15.935+00:00",
"prog": "Rails",
"ctx": {
"jid": "b4a9c63e5f1d2a7e9c0f3b18",
"class": "InvoiceExportJob"
}
}Log::ActiveJob::Retry src: job, evt: retry
{
"src": "job",
"evt": "retry",
"lvl": "info",
"ts": "2026-10-17T09:30:15.123+00:00",
"job_id": "c2b8e0f4-1d3a-4b6c-9e7f-5a1d2c3b4e5f",
"job_class": "InvoiceExportJob",
"queue_name": "default",
"arguments": [
"gid://my-app/Customer/1873",
{
"month": "2026-09",
"email": {
"_filtered": {
"_class": "String",
"_hash": "a058d5090035d2e1"
}
}
}
],
"executions": 1,
"provider_job_id": "b4a9c63e5f1d2a7e9c0f3b18",
"error_class": "Net::ReadTimeout",
"msg": "Net::ReadTimeout with #<TCPSocket:(closed)>",
"wait_ms": 3000,
"prog": "Rails",
"ctx": {
"jid": "b4a9c63e5f1d2a7e9c0f3b18",
"class": "InvoiceExportJob"
}
}Log::ActiveJob::RetryStopped src: job, evt: retry_stopped
{
"src": "job",
"evt": "retry_stopped",
"lvl": "error",
"ts": "2026-10-17T09:30:15.123+00:00",
"job_id": "c2b8e0f4-1d3a-4b6c-9e7f-5a1d2c3b4e5f",
"job_class": "InvoiceExportJob",
"queue_name": "default",
"arguments": [
"gid://my-app/Customer/1873",
{
"month": "2026-09",
"email": {
"_filtered": {
"_class": "String",
"_hash": "a058d5090035d2e1"
}
}
}
],
"executions": 5,
"provider_job_id": "b4a9c63e5f1d2a7e9c0f3b18",
"error_class": "Net::ReadTimeout",
"msg": "Net::ReadTimeout with #<TCPSocket:(closed)>",
"prog": "Rails",
"ctx": {
"jid": "b4a9c63e5f1d2a7e9c0f3b18",
"class": "InvoiceExportJob"
}
}Log::ActiveJob::Schedule src: job, evt: schedule
{
"src": "job",
"evt": "schedule",
"lvl": "info",
"ts": "2026-10-17T09:30:15.123+00:00",
"job_id": "c2b8e0f4-1d3a-4b6c-9e7f-5a1d2c3b4e5f",
"job_class": "InvoiceExportJob",
"queue_name": "default",
"arguments": [
"gid://my-app/Customer/1873",
{
"month": "2026-09",
"email": {
"_filtered": {
"_class": "String",
"_hash": "a058d5090035d2e1"
}
}
}
],
"executions": 0,
"provider_job_id": "b4a9c63e5f1d2a7e9c0f3b18",
"scheduled_at": "2026-10-17T10:30:15.123+00:00",
"prog": "Rails",
"req_id": "6f1c9a2e-7b4d-4e8a-9c61-2d5b8f0e4a17"
}Log::ActiveJob::Start src: job, evt: start
{
"src": "job",
"evt": "start",
"lvl": "info",
"ts": "2026-10-17T09:30:15.123+00:00",
"job_id": "c2b8e0f4-1d3a-4b6c-9e7f-5a1d2c3b4e5f",
"job_class": "InvoiceExportJob",
"queue_name": "default",
"arguments": [
"gid://my-app/Customer/1873",
{
"month": "2026-09",
"email": {
"_filtered": {
"_class": "String",
"_hash": "a058d5090035d2e1"
}
}
}
],
"executions": 1,
"provider_job_id": "b4a9c63e5f1d2a7e9c0f3b18",
"started_at": "2026-10-17T09:30:15.123+00:00",
"attempt": 1,
"prog": "Rails",
"ctx": {
"jid": "b4a9c63e5f1d2a7e9c0f3b18",
"class": "InvoiceExportJob"
}
}Log::ActiveModelSerializers
Log::ActiveModelSerializers src: rails, evt: generate
{
"src": "rails",
"evt": "generate",
"lvl": "info",
"ts": "2026-10-17T09:30:15.123+00:00",
"msg": "ams.render",
"serializer": "InvoiceSerializer",
"adapter": "ActiveModelSerializers::Adapter::Attributes",
"resource_class": "Invoice",
"duration_ms": 1.241,
"prog": "Rails",
"req_id": "6f1c9a2e-7b4d-4e8a-9c61-2d5b8f0e4a17"
}Log::ActiveStorage
Log::ActiveStorage::Delete src: storage, evt: delete
{
"src": "storage",
"evt": "delete",
"lvl": "info",
"ts": "2026-10-17T09:30:15.123+00:00",
"storage": "S3",
"file_id": "m3k9x2q7w1c8v4b6n0z5h2j7",
"prog": "Rails"
}Log::ActiveStorage::Download src: storage, evt: download
{
"src": "storage",
"evt": "download",
"lvl": "info",
"ts": "2026-10-17T09:30:15.123+00:00",
"storage": "S3",
"file_id": "m3k9x2q7w1c8v4b6n0z5h2j7",
"duration_ms": 96.12,
"prog": "Rails",
"req_id": "6f1c9a2e-7b4d-4e8a-9c61-2d5b8f0e4a17"
}Log::ActiveStorage::Exist src: storage, evt: exist
{
"src": "storage",
"evt": "exist",
"lvl": "debug",
"ts": "2026-10-17T09:30:15.123+00:00",
"storage": "S3",
"file_id": "m3k9x2q7w1c8v4b6n0z5h2j7",
"exist": true,
"prog": "Rails",
"req_id": "6f1c9a2e-7b4d-4e8a-9c61-2d5b8f0e4a17"
}Log::ActiveStorage::Metadata src: storage, evt: metadata
{
"src": "storage",
"evt": "metadata",
"lvl": "info",
"ts": "2026-10-17T09:30:15.123+00:00",
"storage": "S3",
"file_id": "m3k9x2q7w1c8v4b6n0z5h2j7",
"mime_type": "application/pdf",
"prog": "Rails"
}Log::ActiveStorage::Upload src: storage, evt: upload
{
"src": "storage",
"evt": "upload",
"lvl": "info",
"ts": "2026-10-17T09:30:15.123+00:00",
"storage": "S3",
"file_id": "m3k9x2q7w1c8v4b6n0z5h2j7",
"duration_ms": 184.3,
"checksum": "3Ys4l0pZ8wQ1mH7cE2uXrA==",
"prog": "Rails",
"req_id": "6f1c9a2e-7b4d-4e8a-9c61-2d5b8f0e4a17"
}Log::ActiveStorage::Url src: storage, evt: url
{
"src": "storage",
"evt": "url",
"lvl": "debug",
"ts": "2026-10-17T09:30:15.123+00:00",
"storage": "S3",
"file_id": "m3k9x2q7w1c8v4b6n0z5h2j7",
"prog": "Rails",
"req_id": "6f1c9a2e-7b4d-4e8a-9c61-2d5b8f0e4a17"
}Log::Ahoy
Log::Ahoy src: app, evt: log
{
"src": "app",
"evt": "log",
"lvl": "info",
"ts": "2026-10-17T09:30:15.123+00:00",
"msg": "ahoy.track",
"ahoy_event": "Viewed invoice",
"properties": {
"invoice_id": 812
},
"prog": "Rails",
"req_id": "6f1c9a2e-7b4d-4e8a-9c61-2d5b8f0e4a17"
}Log::CarrierWave
Log::CarrierWave::Delete src: carrierwave, evt: delete
{
"src": "carrierwave",
"evt": "delete",
"lvl": "info",
"ts": "2026-10-17T09:30:15.123+00:00",
"storage": "file",
"file_id": "avatar.png",
"uploader": "AvatarUploader",
"model": "User",
"mount_point": "avatar",
"version": "",
"store_path": "uploads/user/avatar/1873/avatar.png",
"ext": "png",
"prog": "Rails",
"req_id": "6f1c9a2e-7b4d-4e8a-9c61-2d5b8f0e4a17"
}Log::CarrierWave::Download src: carrierwave, evt: download
{
"src": "carrierwave",
"evt": "download",
"lvl": "info",
"ts": "2026-10-17T09:30:15.123+00:00",
"storage": "file",
"file_id": "avatar.png",
"uploader": "AvatarUploader",
"model": "User",
"mount_point": "avatar",
"version": "",
"store_path": "uploads/user/avatar/1873/avatar.png",
"ext": "png",
"filename": "avatar.png",
"mime_type": "image/png",
"size": 48213,
"prog": "Rails",
"req_id": "6f1c9a2e-7b4d-4e8a-9c61-2d5b8f0e4a17"
}Log::CarrierWave::Upload src: carrierwave, evt: upload
{
"src": "carrierwave",
"evt": "upload",
"lvl": "info",
"ts": "2026-10-17T09:30:15.123+00:00",
"storage": "file",
"file_id": "avatar.png",
"uploader": "AvatarUploader",
"model": "User",
"mount_point": "avatar",
"version": "",
"store_path": "uploads/user/avatar/1873/avatar.png",
"ext": "png",
"filename": "avatar.png",
"mime_type": "image/png",
"size": 48213,
"duration_ms": 12.7,
"prog": "Rails",
"req_id": "6f1c9a2e-7b4d-4e8a-9c61-2d5b8f0e4a17"
}Log::Dotenv
Log::Dotenv::Load src: dotenv, evt: load
{
"src": "dotenv",
"evt": "load",
"lvl": "info",
"ts": "2026-10-17T09:30:15.123+00:00",
"file": ".env",
"prog": "Rails"
}Log::Dotenv::Restore src: dotenv, evt: restore
{
"src": "dotenv",
"evt": "restore",
"lvl": "info",
"ts": "2026-10-17T09:30:15.123+00:00",
"vars": [
"DATABASE_URL",
"REDIS_URL"
],
"prog": "Rails"
}Log::Dotenv::Save src: dotenv, evt: save
{
"src": "dotenv",
"evt": "save",
"lvl": "info",
"ts": "2026-10-17T09:30:15.123+00:00",
"snapshot": true,
"prog": "Rails"
}Log::Dotenv::Update src: dotenv, evt: update
{
"src": "dotenv",
"evt": "update",
"lvl": "debug",
"ts": "2026-10-17T09:30:15.123+00:00",
"vars": [
"DATABASE_URL",
"REDIS_URL"
],
"prog": "Rails"
}Log::Error
Log::Error src: app, evt: error
{
"src": "app",
"evt": "error",
"lvl": "error",
"ts": "2026-10-17T09:30:15.123+00:00",
"error_class": "JSON::ParserError",
"msg": "Import failed",
"error_message": "unexpected end of input",
"backtrace": [
"app/services/invoice_import.rb:27:in 'InvoiceImport#parse'",
"app/controllers/imports_controller.rb:11:in 'ImportsController#create'"
],
"prog": "Rails",
"req_id": "6f1c9a2e-7b4d-4e8a-9c61-2d5b8f0e4a17"
}Log::GoodJob
Log::GoodJob::Error src: job, evt: error
{
"src": "job",
"evt": "error",
"lvl": "error",
"ts": "2026-10-17T09:30:15.123+00:00",
"error_class": "ActiveRecord::ConnectionNotEstablished",
"error_message": "connection to server at \"10.0.3.12\", port 5432 failed: Connection refused",
"pid": 4821,
"tid": "2ix4",
"backtrace": [
"good_job/notifier.rb:225:in 'GoodJob::Notifier#with_connection'"
],
"prog": "GoodJob"
}Log::GoodJob::Log src: job, evt: log
{
"src": "job",
"evt": "log",
"lvl": "info",
"ts": "2026-10-17T09:30:15.123+00:00",
"msg": "Notifier subscribed with LISTEN",
"pid": 4821,
"tid": "2ix4",
"prog": "GoodJob"
}Log::Plain
Log::Plain src: app, evt: log
{
"src": "app",
"evt": "log",
"lvl": "info",
"ts": "2026-10-17T09:30:15.123+00:00",
"msg": "Exported 120 invoices",
"prog": "Rails",
"req_id": "6f1c9a2e-7b4d-4e8a-9c61-2d5b8f0e4a17"
}Log::Puma
Log::Puma::Error src: puma, evt: error
{
"src": "puma",
"evt": "error",
"lvl": "error",
"ts": "2026-10-17T09:30:15.123+00:00",
"error_class": "NoMethodError",
"msg": "undefined method 'call' for nil",
"backtrace": [
"config/initializers/health_check.rb:8:in 'HealthCheck#call'"
],
"error_context": "Rack app",
"method": "GET",
"path": "/up",
"prog": "Rails"
}Log::Puma::Shutdown src: puma, evt: shutdown
{
"src": "puma",
"evt": "shutdown",
"lvl": "info",
"ts": "2026-10-17T09:30:15.123+00:00",
"pid": 1,
"prog": "Rails"
}Log::Puma::Start src: puma, evt: start
{
"src": "puma",
"evt": "start",
"lvl": "info",
"ts": "2026-10-17T09:30:15.123+00:00",
"mode": "single",
"puma_version": "6.6.1",
"puma_codename": "Return to Forever",
"ruby_version": "ruby 3.4.5 (2025-07-16 revision 20cda200d3) +PRISM [x86_64-linux]",
"min_threads": 3,
"max_threads": 3,
"environment": "production",
"pid": 1,
"listening_addresses": [
"http://0.0.0.0:3000"
],
"prog": "Rails"
}Log::Request
Log::Request src: rails, evt: request
{
"src": "rails",
"evt": "request",
"lvl": "info",
"ts": "2026-10-17T09:30:15.123+00:00",
"path": "/users/sign_in",
"method": "POST",
"source_ip": "198.51.100.23",
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:131.0) Gecko/20100101 Firefox/131.0",
"referer": "https://app.example.com/users/sign_in",
"format": "html",
"controller": "Users::SessionsController",
"action": "create",
"status": 302,
"duration_ms": 48.21,
"view": 0,
"db": 3.62,
"params": {
"authenticity_token": "[FILTERED]",
"user": {
"email": "[FILTERED]",
"password": "[FILTERED]",
"remember_me": "1"
},
"commit": "Log in"
},
"host": "app.example.com",
"content_type": "application/x-www-form-urlencoded",
"accept": "text/html,application/xhtml+xml",
"prog": "Rails",
"req_id": "6f1c9a2e-7b4d-4e8a-9c61-2d5b8f0e4a17"
}Log::Security
Log::Security::BlockedHost src: security, evt: blocked_host
{
"src": "security",
"evt": "blocked_host",
"lvl": "warn",
"ts": "2026-10-17T09:30:15.123+00:00",
"path": "/",
"method": "GET",
"source_ip": "203.0.113.7",
"user_agent": "curl/8.7.1",
"msg": "Blocked host detected: admin.example.net",
"blocked_host": "admin.example.net",
"blocked_hosts": [
"admin.example.net"
],
"prog": "Rails"
}Log::Security::CSRFViolation src: security, evt: csrf_violation
{
"src": "security",
"evt": "csrf_violation",
"lvl": "error",
"ts": "2026-10-17T09:30:15.123+00:00",
"path": "/invoices/812",
"method": "PATCH",
"source_ip": "198.51.100.23",
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:131.0) Gecko/20100101 Firefox/131.0",
"referer": "https://app.example.com/invoices/812/edit",
"msg": "Can't verify CSRF token authenticity.",
"prog": "Rails",
"req_id": "6f1c9a2e-7b4d-4e8a-9c61-2d5b8f0e4a17"
}Log::Security::IPSpoof src: security, evt: ip_spoof
{
"src": "security",
"evt": "ip_spoof",
"lvl": "warn",
"ts": "2026-10-17T09:30:15.123+00:00",
"path": "/login",
"method": "GET",
"source_ip": "10.0.1.15",
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:131.0) Gecko/20100101 Firefox/131.0",
"msg": "IP spoofing attack?! HTTP_CLIENT_IP=\"203.0.113.9\" HTTP_X_FORWARDED_FOR=\"198.51.100.4\"",
"client_ip": "203.0.113.9",
"x_forwarded_for": "198.51.100.4",
"prog": "Rails",
"req_id": "6f1c9a2e-7b4d-4e8a-9c61-2d5b8f0e4a17"
}Log::Shrine
Log::Shrine::Delete src: shrine, evt: delete
{
"src": "shrine",
"evt": "delete",
"lvl": "info",
"ts": "2026-10-17T09:30:15.123+00:00",
"storage": "store",
"location": "invoice/812/original-3f9a1c.pdf",
"prog": "Rails"
}Log::Shrine::Download src: shrine, evt: download
{
"src": "shrine",
"evt": "download",
"lvl": "info",
"ts": "2026-10-17T09:30:15.123+00:00",
"storage": "store",
"location": "invoice/812/original-3f9a1c.pdf",
"download_opts": {},
"prog": "Rails",
"req_id": "6f1c9a2e-7b4d-4e8a-9c61-2d5b8f0e4a17"
}Log::Shrine::Exist src: shrine, evt: exist
{
"src": "shrine",
"evt": "exist",
"lvl": "info",
"ts": "2026-10-17T09:30:15.123+00:00",
"storage": "store",
"location": "invoice/812/original-3f9a1c.pdf",
"prog": "Rails"
}Log::Shrine::Metadata src: shrine, evt: metadata
{
"src": "shrine",
"evt": "metadata",
"lvl": "info",
"ts": "2026-10-17T09:30:15.123+00:00",
"storage": "cache",
"prog": "Rails",
"req_id": "6f1c9a2e-7b4d-4e8a-9c61-2d5b8f0e4a17"
}Log::Shrine::Upload src: shrine, evt: upload
{
"src": "shrine",
"evt": "upload",
"lvl": "info",
"ts": "2026-10-17T09:30:15.123+00:00",
"storage": "store",
"location": "invoice/812/original-3f9a1c.pdf",
"upload_opts": {},
"opts": {},
"uploader": "DocumentUploader",
"duration_ms": 143.8,
"prog": "Rails",
"req_id": "6f1c9a2e-7b4d-4e8a-9c61-2d5b8f0e4a17"
}Log::Sidekiq
Log::Sidekiq::Error src: sidekiq, evt: error
{
"src": "sidekiq",
"evt": "error",
"lvl": "warn",
"ts": "2026-10-17T09:30:15.123+00:00",
"error_class": "Timeout::Error",
"msg": "execution expired",
"backtrace": [
"app/workers/invoice_sync_worker.rb:15:in 'InvoiceSyncWorker#perform'"
],
"error_context": "Job raised exception",
"queue_name": "default",
"retry_count": 2,
"prog": "Rails",
"ctx": {
"jid": "b4a9c63e5f1d2a7e9c0f3b18",
"class": "InvoiceSyncWorker"
}
}Log::Sidekiq::Log src: sidekiq, evt: log
{
"src": "sidekiq",
"evt": "log",
"lvl": "info",
"ts": "2026-10-17T09:30:15.123+00:00",
"msg": "start",
"ctx": {
"jid": "b4a9c63e5f1d2a7e9c0f3b18",
"class": "InvoiceSyncWorker"
},
"pid": 4821,
"tid": "1v8b",
"prog": "Sidekiq-Server"
}Log::SQL
Log::SQL src: app, evt: database
{
"src": "app",
"evt": "database",
"lvl": "info",
"ts": "2026-10-17T09:30:15.123+00:00",
"msg": "Invoice Load executed",
"sql": "SELECT \"invoices\".* FROM \"invoices\" WHERE \"invoices\".\"account_id\" = $1",
"name": "Invoice Load",
"duration_ms": 142.37,
"row_count": 120,
"adapter": "PostgreSQLAdapter",
"db_name": "app_production",
"pool_size": 5,
"active_count": 2,
"op_type": "SELECT",
"table_names": [
"invoices"
],
"prog": "Rails",
"req_id": "6f1c9a2e-7b4d-4e8a-9c61-2d5b8f0e4a17"
}Value Formats
Timestamps
ts is an ISO 8601 time with milliseconds and the UTC offset of the process's time zone: 2026-10-17T09:30:15.123+00:00 on a server whose time zone is UTC, 2026-10-17T22:30:15.123+13:00 on one in New Zealand. Rails' config.time_zone doesn't change it. Every other time in a line is written the same way: the times of the built-in log types, such as ActiveJob's scheduled_at, and the times in a Hash you log.
Levels
lvl is debug, info, warn, error, fatal or unknown. It comes from the logger method that wrote the line (logger.warn writes warn), not from the log type's own level. Any other severity is written as unknown.
Filtered Values
A value whose key is filtered is replaced by a summary under _filtered. Which keys are filtered is described in Filtering Sensitive Data.
Rails.logger.info(
msg: "Account updated",
email: "linda.martinez@example.com",
password: "correct horse battery staple",
api_tokens: ["tok_8c1f2a", "tok_93d0b7"],
credentials: {client_id: "my-app", client_secret: "abc123"},
note: "Call Linda on 555-201-7788"
){
"msg": "Account updated",
"email": {
"_filtered": {
"_class": "String",
"_hash": "a058d5090035d2e1"
}
},
"password": {
"_filtered": {
"_class": "String"
}
},
"api_tokens": {
"_filtered": {
"_class": "Array",
"_count": 2
}
},
"credentials": {
"_filtered": {
"_class": "Hash",
"_keys_count": 2,
"_keys": [
"client_id",
"client_secret"
]
}
},
"note": "Call Linda on [PHONE]",
"src": "app",
"evt": "log",
"ts": "2026-10-17T09:30:15.123+00:00",
"lvl": "info",
"prog": "Rails"
}- A String:
{"_class": "String"}, without its length. For keys infilter_keys_with_hashes(emailandemail_addressby default) the summary also has a_hashof the value: 16 hex characters of an HMAC-SHA256 keyed withfilters.hash_salt, or by default with a key derived from the app'ssecret_key_base. An email address has one hash however it's capitalized. - A Hash:
_class,_keys_countand its first 10 keys in_keys. An empty Hash is{"_class": "Hash", "_empty": true}. - An Array:
_classand_count. An empty Array is{"_class": "Array", "_empty": true}. - Any other value: its class, such as
{"_class": "Integer"}. - A value that Rails'
filter_parametersalready replaced, such as the params on request lines, is kept as Rails wrote it:"[FILTERED]". - Values nested more than 20 levels deep, and values that contain themselves, are written as a summary without
_keys.
Scrubbed strings keep their text, with the sensitive part replaced: [EMAIL:<hash>] (the same hash as _hash above), [PHONE], [SSN], [CREDIT_CARD], [PASSWORD] for a password in a URL, and [FILTERED] for the credentials after Bearer or Basic and sensitive query params. [IP] and [MAC] appear only when you turn those scrubbers on.
Truncation
config.limits sets how much of each value LogStruct writes:
max_hash_keys(default 100): a Hash writes its first 100 keys, then_more_keyswith the number left out. The line's own keys count too.max_array_items(default 10): an Array writes its first 10 items, then a last item"... and N more items".max_backtrace_lines(default 20): for an Array that looks like a backtrace (3 of its first 5 items start withfile:line), such as an error line'sbacktrace, the first 20 lines, then"... and N more lines".max_string_bytes(default 32768, 32 KB): a longer string is cut to that many bytes after it's scrubbed, and ends with"... (cut from N bytes)".
# Small limits, to show the markers (the defaults are 100 keys, 10 items and 32 KB).
# A line's own keys count too: this one has 4.
LogStruct.configure do |config|
config.limits.max_hash_keys = 4
config.limits.max_array_items = 3
config.limits.max_string_bytes = 24
end
Rails.logger.info(
msg: "Import finished",
columns: {first_name: "First name", last_name: "Last name", city: "City", country: "Country", zip: "Postcode"},
row_ids: [101, 102, 103, 104, 105, 106, 107, 108],
summary: "Rows 104 and 107 had no postcode"
){
"msg": "Import finished",
"columns": {
"first_name": "First name",
"last_name": "Last name",
"city": "City",
"country": "Country",
"_more_keys": 1
},
"row_ids": [
101,
102,
103,
"... and 5 more items"
],
"summary": "Rows 104 and 107 had no ... (cut from 32 bytes)",
"src": "app",
"evt": "log",
"ts": "2026-10-17T09:30:15.123+00:00",
"lvl": "info",
"prog": "Rails"
}