SOC 2 and Your Logs

SOC 2 auditors look at how you record what happens in your systems and how you protect the data in those records. LogStruct can help with part of that: it writes consistent JSON lines, and it filters common secrets and personal data out of them. It doesn't make an app compliant, and this page isn't audit advice. It says what LogStruct does, where it stops, and what your app still has to do.

What LogStruct Does

Consistent lines

  • In production, every line LogStruct writes is one JSON object with the same keys for the same things: src and evt say what happened, lvl and ts its level and time, and req_id ties a request's lines together (see Log Fields). You can search and alert on them in your log platform without parsing text.
  • LogStruct's own lines (requests, jobs, mail, security events, errors) are built from Sorbet-typed structs. A field with the wrong type raises an error in development and test, so a test that runs that code fails. In production the type error is logged and reported (the type_checking_errors mode, ReportProduction by default), and the line is still written with the value it was given. Hashes and strings you log, the message of a Log::Plain and additional_data aren't type-checked.
  • Security events have their own lines (src security): requests to hosts Rails blocks, invalid CSRF tokens and IP spoofing errors (see Security).
  • Request lines don't say which user made a request unless you add it, for example a user_id with custom Lograge options.

Filtering and scrubbing

LogStruct filters each line before it writes it, by key and by pattern (details in Filtering Sensitive Data):

  • Keys. A value under a sensitive key name (passwords, tokens, secrets, API keys, cookies, sessions, authorization headers, card numbers, social security numbers and the like), or a key in Rails' filter_parameters or your filter_keys, is replaced by a summary: its class, plus a Hash's key count and first 10 keys or an Array's item count. email and email_address values get a hash of the address too.
  • Strings. In every string, LogStruct replaces email addresses with [EMAIL:<hash>], and scrubs passwords in URLs, sensitive URL query params (?token=, OAuth's ?code=), the credentials after Bearer and Basic, card numbers that pass the Luhn check, US phone numbers and social security numbers written like 123-45-6789 or 123 45 6789. IP and MAC addresses are scrubbed only if you turn that on.

For example, this line:

Rails.logger.info(
  msg: "Password reset requested",
  email: "Jo@Example.com",
  note: "Reset link sent to jo@example.com",
  credentials: {username: "jo", password: "hunter2"},
  reset_url: "https://app.example.com/reset?token=abc123&step=2",
  card: "4111 1111 1111 1111",
  name: "Jo Smith"
)

is written as:

{
  "msg": "Password reset requested",
  "email": {
    "_filtered": {
      "_class": "String",
      "_hash": "0d68e00d48aadbd7"
    }
  },
  "note": "Reset link sent to [EMAIL:0d68e00d48aadbd7]",
  "credentials": {
    "_filtered": {
      "_class": "Hash",
      "_keys_count": 2,
      "_keys": [
        "username",
        "password"
      ]
    }
  },
  "reset_url": "https://app.example.com/reset?token=[FILTERED]&step=2",
  "card": "[CREDIT_CARD]",
  "name": "Jo Smith",
  "src": "app",
  "evt": "log",
  "ts": "2026-10-17T05:20:38.341+00:00",
  "lvl": "info",
  "prog": "Rails"
}

The name is logged as it is: nothing matches it.

Email hashes

The hash is an HMAC-SHA256, keyed with config.filters.hash_salt or, by default, with a key derived from your app's secret_key_base. An address gets the same hash however it's capitalized, under a key or inside a string, so you can follow one person's lines without logging their address. The hashes change when the key changes. Anyone with the key and a list of addresses can match them to the hashes, so a hashed address is still personal data (a pseudonym), and the key must stay secret.

Where It Stops

  • Only where LogStruct is on. By default it's on in production only in the web servers and job runners it recognizes, and never in a console. Rake tasks, rails runner and job runners it doesn't recognize (Solid Queue's bin/jobs, for example) use Rails' own logger, with no filtering, unless you set LOGSTRUCT_ENABLED=true for them (see When LogStruct Runs).
  • Only the lines LogStruct writes. Filtering happens in LogStruct's formatter. An appender you add to SemanticLogger with another formatter gets the values unfiltered, and so does a subscriber to LogStruct's log.logstruct notifications. Output that doesn't go through Rails' logger (puts, a gem with its own logger) isn't filtered either.
  • Only what its keys and patterns match. Everything else is logged in full: names, postal addresses, dates of birth, free text, phone numbers outside the US formats, secrets in formats it doesn't know (an API key in a sentence), and numbers. A card number logged as an Integer isn't scrubbed. A _filtered summary shows that one value was filtered, not that nothing else got through.
  • Error reports. When LogStruct reports an error, it filters the context it adds, but the exception goes to Sentry, Bugsnag, Rollbar, Honeybadger or your handler as it is, message and backtrace included. Errors your error tracker catches itself don't go through LogStruct at all.
  • Storage and alerts. LogStruct writes lines to STDOUT in production. It doesn't store, ship, rotate, sign or delete them. Apart from the errors it reports to your error tracker, it doesn't alert anyone.

What Your App Still Has to Do

  • Ship the lines from STDOUT to a log platform, and set its retention, access control and protection against changes or deletion there (see Logging to STDOUT).
  • Set up alerts in that platform, for example on src security or lvl error lines.
  • Turn LogStruct on in every kind of process you run in production, including Rake tasks and job runners it doesn't recognize.
  • Read the lines your app writes for its main flows, with LogStruct on, and add what the defaults miss: your own keys to filter_keys, a string_scrubbing_handler for your own formats, and the IP address scrubber if you mustn't keep IP addresses (see Configuration).
  • Configure your error tracker's own data scrubbing for exception messages.
  • Keep secret_key_base (or hash_salt) secret, and treat hashed addresses as personal data in your retention and deletion policies.
Questions about LogStruct and compliance are welcome on GitHub.