← Field Notes
Configuration management · Field note

A change feed is not a desired state

R Rob Flanagan · · 11 min read

An audit log tells you who changed a setting and when. Google Workspace, Linear, and Jamf Pro each leave a different gap between that record and the baseline you actually need.

A SaaS audit log can tell you who changed a setting and when. Some logs also record the old value and the new one. Google Workspace does that for supported admin setting changes.

That still is not the same thing as knowing what the value was supposed to be.

Those are different questions. The gap between them is where most SaaS config drift actually lives.

An audit log is a record of events. A desired state is a decision someone made, reviewed, and agreed to keep. Nothing about logging an event, no matter how completely, proves that the event was the right one. The two get treated as the same problem because the audit log is the only artifact most teams have.

So be precise about what each platform gives you. The gap looks different everywhere you check.

Google Workspace: the logging part, done properly

Google Workspace is the closest thing to a clean example. For supported admin console setting changes, it records the setting name, the old value, and the new value as distinct fields. No prose parsing required.

The same structured records show up in Google's investigation tool. The Reports API returns OLD_VALUE and NEW_VALUE as named event parameters.

The retention is plain: admin log events are kept six months. A second Google page backs that up. The Reports API documents a maximum report window of the last 180 days. Two Google surfaces give the same limit, so the point does not rest on one page's wording.

If you need longer retention, the documented path is export. Google walks through exporting to BigQuery. BigQuery's default export expiration is 60 days unless an admin changes it. That matters because export does not automatically buy you more time. It moves the retention choice into the destination, and that destination's default is shorter than Google's admin console window. Exporting to Google SecOps for insider-risk monitoring has a separate 12-month default retention figure, and it only captures events created after the connection is turned on. There is no retroactive backfill.

So the honest description of Google Workspace is this: real old/new values for supported admin setting changes, a structured API, and a published retention number. Keeping any of it past six months requires export.

Linear: a shorter audit window and an Enterprise gate

Linear's audit log is narrower in the published docs. It covers the last 90 days of workspace events. It is documented as an Enterprise-plan feature and is queryable through GraphQL rather than REST. Linear documents actor, email, IP address, date range, event type, and other filter metadata. Linear's public audit-log documentation reviewed for this post does not document the same old-value/new-value pair that Google documents for supported admin setting changes.

Linear also documents streaming to a webhook or a SIEM. Its docs do not say that streaming extends the native 90-day retention. Treat it as a forwarding path unless your own archive proves otherwise.

Ninety days is short next to retention floors in nearby compliance frameworks, even though neither framework governs Linear specifically. PCI DSS v4.0.1, requirement 10.5.1, sets a 12-month audit-log-history floor for payment-card environments. The most recent three months have to be ready for analysis.

SEC Rule 2-06 (17 CFR 210.2-06), the rule implementing Sarbanes-Oxley section 802, requires an accountant to retain records relevant to an issuer's audit or review for seven years. Neither rule says anything about a project-management tool's audit log.

Both are useful as a sense check. A 90-day product window is a fraction of either floor. If your obligations are closer to either number, Linear's native log was never going to be the record you keep.

Jamf Pro: configurable retention and thin history records

Jamf Pro breaks the pattern in a different direction. Google and Linear each publish a fixed native audit window for the records discussed here. Jamf's current Log Flushing API exposes admin-set retention fields: retentionPeriod and retentionPeriodUnit. Jamf's examples and public docs show week, month, and year units. The API field description also gives day as an example. The reviewed public docs did not publish a vendor default or prove every runtime limit a tenant accepts.

That is the wrong place to stop anyway. The retention window only matters after the record contains the thing you need.

Jamf Pro exposes two separate change-tracking surfaces in the public docs. The first is Change Management, including the Log Stream output. The second is the per-object History API surface. In the current published Jamf Pro API index, 41 unique GET ...history... endpoints appear, and 38 of them return the same object-history schema. The other three return local admin password history, which is a different thing.

The schema behind the Script History endpoint, ObjectHistory, has exactly five properties: id, username, date, note, and details. There is no structured field for what a setting used to be.

Jamf's own API example shows a note reading "Sso settings update." The details field says "Is SSO Enabled false\nSelected SSO Provider": one current-looking value, no prior value, and free text rather than structured data.

The separate Log Stream documentation has an even sharper example. It includes a Change Management sample of type [UPDATE] [User] whose entire detail body is UNKNOWN. The same page says the format and type of additional information for a resource can change without notice.

That last point is worth sitting with. That is not a stable parsing contract. Do not make it the sole evidence source for a compliance control without normalizing and retaining your own record.

The coverage gap adds to the retention story. The published History API surface covers infrastructure objects: scripts, packages, buildings, SSO config, SMTP settings, extension attributes, and similar.

The current published Jamf Pro API and Classic API endpoint indexes do not document History endpoints for policies, configuration profiles, smart or static groups, or Jamf Pro administrator accounts. The Classic API's computerhistory and mobiledevicehistory resources are device-history resources, not admin object-change history.

The export path Jamf documents for a cloud tenant is Jamf Log Stream. It sends Change Management and access-log data to Splunk, Amazon S3, Datadog, or an HTTPS endpoint. Current Jamf docs say the feature is available only to Jamf Cloud Premium customers. On-premise environments have a different option: logging those changes to a file or a syslog server. Jamf says that option "is only available for on-premise environments. If your environment is hosted in Jamf Cloud, changes are automatically displayed in the Change Management settings and cannot be exported."

Put together: Jamf's public docs show configurable retention and no structured old-value field in the per-object History schema. The reviewed indexes do not publish History endpoints for several high-value object classes. The documented Log Stream export path is gated behind Cloud Premium.

Retention length was never the whole constraint. The record itself was not built as a before-and-after.

The gap isn't only about time. Some of it is buried in the schema.

Retention windows are the visible version of this problem. There is a quieter version that shows up even inside the window, when the schema itself hides what actually changed behind a field that looks complete but isn't.

Linear's GraphQL schema answers introspection queries with no credential required. That means this next claim is checkable by anyone with curl and no account. Querying the input type used to update organization security settings returns 12 named fields. All 12 are role gates for who can create teams, invite members, or mint API keys.

Querying the readable Organization type returns more than 80 fields. Not one is a typed field containing "Role" in its name. The 12 settable gates write into securitySettings, a single opaque JSONObject scalar. A scheduled diff will still catch that object changing as a whole. Introspection cannot expose the per-gate values inside it.

Linear's own schema partially confirms what is inside that object: three legacy boolean fields carry deprecation notices pointing at three of the 12 gates by name (invitationsRole, teamCreationRole, labelManagementRole). The other nine are a reasonable inference from shared naming, not a confirmed fact, because confirming them would require an authenticated Enterprise-tier credential that was not available during this research.

Jamf's version of the same problem needs no live query, because it is visible in the published schema. In the Classic API's macOS config profile object, sibling fields are typed, several of them enumerated: name, description, distribution_method, user_removable, level, uuid, and redeploy_on_update. The field carrying the actual payload, including FileVault, firewall rules, password policy, and every other managed setting the profile enforces, is declared "payloads": {"type": "string"}. One string holds the enforced security posture of every Mac the profile touches. A scheduled diff can catch that string changing. It cannot tell you which setting inside it moved without a parser and a stable payload format.

The pattern across both platforms: a diff can tell you when the collected object changes. Whether it can tell you what changed depends on whether the platform's schema names the thing you care about, or buries it inside a scalar field.

What actually closes the gap

None of this is an argument against audit logs. It is an argument against treating one as sufficient. The fix does not require a platform capability that does not exist yet.

Start with the unglamorous version of the job: pull config through the API on a schedule, store what comes back, and diff it against the last run.

That gives you three things a retained change log, on its own, cannot:

  • A previous observed value and current observed value for fields your collector actually captures, even after the vendor's native log window has closed.
  • A short list, every morning, of collected fields that moved without anyone announcing it.
  • Somewhere to hang an approved baseline, once a person commits to declaring one.

The morning list is where checking two independent sources earns its keep. A diff by itself only tells you that the collected object changed.

Read it next to the platform's own log events for the same window. The interesting result is the setting that changed with no corresponding vendor log event in the data you collected for that window.

The third item is the one worth being careful about. A snapshot records what is currently set. It does not record what should be set. It only becomes a standard the moment a person reviews it and says, deliberately, this is the shape we want this to hold.

Almost nobody schedules that review. That leaves many organizations with a strong record of drift and no baseline to measure the drift against. Building the pipeline is the easy half.

And be honest about coverage while you're at it. Some event logs, Google Workspace among them, give you structured old/new fields. For configuration snapshots, say separately which API fields your collector actually covers. Others give you an opaque blob that a diff can flag as changed but not decode. Say which is which when you report on it, rather than letting a clean-looking dashboard imply uniform coverage it does not have.

None of this is a new idea. Terraform structures infrastructure work this way: desired state declared in config, state tracking what was last applied, and a plan step that compares the two before anything changes.

The review the third bullet depends on is at least possible in that model, because the plan is a readable artifact that exists before anything changes. Teams can gate that artifact before apply. Terraform does not force the review, and terraform apply -auto-approve will skip the confirmation entirely, but the plan output gives a reviewer something concrete to approve. The audit-log surfaces discussed here are not that artifact.

The pipeline this post describes is narrower. Pull config on a schedule, store it, and compare current state with a reviewed baseline. It catches drift after it has already happened instead of gating a change before it lands. That is a real limit. It is still better than discovering last quarter's config by reading last quarter's tickets.

Clicking a toggle in an admin console is still a production change to whatever that setting controls. It does not come with a diff, a review, or an approval trail unless somebody builds one. The honest test for your most critical SaaS platform is simple: produce last quarter's actual config, not last quarter's tickets about it.

The same distinction appears in the macOS 27 declarative-management model, where a declared target still has to be compared with what the device reports.

Sources

Primary sources, retrieved 2026-08-24 through 2026-08-26, re-verified live on 2026-08-26. GraphQL introspection queries are reproducible by anyone with curl and no account:

← Back to Field Notes