PostHog / PostHog/posthog

feat(logs): parse JSON attributes during ingestion

Open
#101,583 0 comments 0 reactions 1 assignee View on GitHub

@jonmcwest is already working on this.

Since Sep 16, 2026.

Dominant language
Python
Stars
39.9k
Forks
3.4k
Avg merge
6h 51m
Merged PRs (30d)
232

Description

Follow-up to #100759, which shipped the JSON parse log attribute setting, and #101599, which added a logs-only, per-team sniff probe. The probe measures whether the selected value looks like JSON; it does not parse JSON or extract attributes. This issue adds actual extraction.

Depends on #101582, which carries the shared size helper and the bounded flattenJson this builds on. Land that first.

Current state

  • logs_settings.json_parse_logs_attribute_key is accepted by the project API, trimmed, and capped at 200 characters.
  • The settings UI exists behind the logs-json-attribute-parsing flag. UI visibility is separate from ingestion enablement.
  • #101599 reads the key for the sniff-only probe, gated by LOGS_JSON_ATTRIBUTE_PARSING_ENABLED_TEAMS and appSource === 'logs'. It does not alter stored records.

What to land

  • Reuse the existing rollout plumbing for LOGS_JSON_ATTRIBUTE_PARSING_ENABLED_TEAMS: empty disables, comma-separated team IDs enable selected projects, * enables all. Review the deployed allowlist before switching from measurement to extraction; probe enablement must not be treated as evidence that extraction is safe.
  • enrichBatchAttributeJsonAttributes in log-record-avro.ts, parsing the selected attribute and flattening object fields into up to 50 attributes prefixed with the selected key. Handles JSON objects and JSON wrapped in a string, since SDKs commonly stringify it. Arrays are terminal values, never traversed by index; see the contract below.
  • Consumer gating in logs-ingestion-consumer.ts: strip the key from logs_settings unless appSource === 'logs' and the team is allowlisted. The strip must stay upstream of bufferProcessingMode, so a non-allowlisted team cannot change processing mode by setting the field.
  • bufferProcessingMode returns decode_and_reencode when the key is set.
  • Reuse json_parse_logs_attribute_key on the existing LogsSettings type in nodejs/src/types.ts.
  • Collision precedence: sender-supplied attributes, then fields from the selected attribute, then fields from the JSON body.

Array behavior and compatibility

Flatten objects, not arrays. Positional fields such as feature_flags.0 have unstable meaning when elements are reordered and do not answer the useful question, "does this array contain this flag?" The index itself is not a high-cardinality value; changing contents can produce many values, and varying lengths add field names. Keeping an array together avoids positional expansion, but does not make its serialized value low-cardinality.

  • Nested arrays: emit one attribute at the array's object path, storing the complete array as a JSON string. Preserve order, element types, and empty arrays. Apply this to primitive, object, mixed, and nested arrays; do not recurse into their elements or create .0, .1, etc. Each retained array counts as one of the 50 extracted fields, and its full serialized size counts against the shared output budget.
  • Top-level arrays: if the selected value resolves to an array (including after unwrapping a JSON string), keep the original selected attribute unchanged and add no extracted fields. Do not create indexed fields or a duplicate normalized array attribute.
  • Original input: retain the selected source attribute unchanged for both object and array inputs. Invalid or over-budget input must not drop the original log; use the shared enrichment size-guard policy.
  • Compatibility: this contract applies only to the new selected-attribute extraction. Do not change existing json_parse_logs body-array behavior. If the bounded helper from #101582 is shared, make array handling explicit per caller and retain the body's existing default.
  • Probe: leave the deployed sniff-only counter and its outcomes unchanged. looks_like_json includes arrays and is not a count of records that will produce extracted fields.
  • Not included: native array storage, membership-query support, array-to-label expansion, and positional extraction controls. Those need separate product/API design; storing a JSON string alone does not provide membership filtering.

For selected key attributes containing {"user":{"id":"123"},"feature_flags":["checkout","search"]}, retain the original attributes value and add:

attributes.user.id = "123"
attributes.feature_flags = '["checkout","search"]'  (JSON string)

There must be no attributes.feature_flags.0 or .1 fields.

Acceptance coverage
  • Nested primitive arrays, arrays of objects, mixed/nested arrays, and empty arrays remain single serialized values without indexed descendants.
  • Top-level arrays and string-wrapped arrays add no fields and preserve the source attribute.
  • Reordered arrays retain their order without changing the set of extracted field names.
  • Ordinary nested objects still flatten; sender collisions retain the documented precedence.
  • A nested array counts as one field; wide objects respect the 50-field cap. Large arrays and Unicode values respect byte-based size limits without dropping or changing the source log when enrichment is skipped.
  • Existing body parsing, logs-only/allowlist gating, and sniff-only behavior have regression coverage.

Rollout risk

The main cost is not the JSON parsing. A team with json_parse_logs and pii_scrub_logs off and no stages currently runs passthrough, with no Avro decode at all. Setting the key moves them to full decode, transform and encode. That dominates everything else, so budget per-team enablement as "this team moves to the re-encode tier" rather than "this team parses a little JSON".

It is the same cost class that json_parse_logs and pii_scrub_logs already trigger, not a new one, and the per-team allowlist means it can be watched as it widens.

Also in scope

  • Docs. The ingestion sections were pulled from docs/published/docs/logs/logs-config.mdx in #100759 and belong here: the rollout variable, the shared 1 MiB output limit, collision precedence, and using attributes.sessionId / attributes.personId in Link to session and Link to person. The page currently ends by saying ingestion does not read the key yet, which needs removing.
  • In-app copy. LogsJsonParseAttributeSettings.tsx already describes the working behavior in the present tense. It becomes accurate when this lands, so it is worth a read-through rather than an edit.
  • Deployment config. PostHog/charts#15693 provides the rollout variable. Its default is empty, but environment overrides may already enable the probe; audit those before rolling out extraction.
  • Access control. A review finding on #100759 noted that a project admin with Logs set to Viewer can PATCH the key directly, because the team endpoint applies only a project-admin check. That is true of every existing key inside logs_settings and was declined there as pre-existing and inert. The impact claim becomes accurate once ingestion reads the key, so it is worth re-weighing here.
  • API schema. #100759 also reverted the typed OpenAPI declaration for logs_settings, which left the frontend on a handwritten LogsSettings interface. Restoring it here would let the frontend adopt the generated type, which is the documented convention. The tradeoff is that generated clients start validating retention_days and the attribute key length.

Recovering the code

The implementation is written and was reviewed on #100759 before being cut. That PR merged as a squash, so the pre-trim commit is reachable through the pull request ref rather than master:

git fetch origin refs/pull/100759/head:pr-100759
git checkout 15a043ccce2955385e76c0d27b21ac90a0e86262 -- nodejs/

This is historical reference code, not a patch to apply wholesale over the shipped probe. Adapt the feature pieces to the array contract above, preserve #101599's behavior until extraction is deliberately enabled, and leave the shared bounded-walk and size-helper work to #101582.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.