-
Notifications
You must be signed in to change notification settings - Fork 57
docs(immutable-history): restructure the hub and add a page for querying historical data #10334
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: stable
Are you sure you want to change the base?
Changes from 2 commits
55f0bb9
4ffdf4a
5aaca81
fd1a3a0
1b04534
a5f17f4
c41619d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,65 +2,95 @@ | |
| title: Immutable history | ||
| --- | ||
|
|
||
| # Understanding immutable history in Infrahub | ||
| Infrahub keeps every state your infrastructure data has held. Query the graph as it was at any | ||
| point in the past — yesterday's routes, last week's VLAN definitions, the ACL a rollout replaced — | ||
| using the same queries you run against current data. | ||
|
|
||
| At its foundation, Infrahub implements data immutability—a principle where information in the database cannot be deleted or modified in place. Instead, every change creates a new version while preserving all previous states. This approach mirrors version control systems like Git, providing a robust history of all infrastructure changes. | ||
| ## Why it matters | ||
|
|
||
| This architectural decision provides several critical benefits for infrastructure management: | ||
| Debugging a change means comparing what is true now with what was true before it. A data model | ||
| that stores only current values answers the first half and loses the second: once a rollout has | ||
| overwritten the configuration that worked, the comparison you need is gone. | ||
|
|
||
| - **Complete audit trail**: Every modification is permanently recorded with who, what, and when, providing full traceability | ||
| - **Time travel queries**: Access the exact state of your infrastructure at any point in history to understand past configurations | ||
| - **Risk-free rollbacks**: Return to any previous state without data loss when issues are detected | ||
| - **Compliance and forensics**: Meet regulatory requirements with immutable change history for audits | ||
| - **Parallel workflows**: Enable multiple teams to work on infrastructure changes simultaneously using branches | ||
| - **Change verification**: Review proposed changes before committing them to production environments | ||
| That gap turns up in ordinary work: | ||
|
|
||
| ## Use cases | ||
| - A deployment breaks connectivity, and you need the exact configuration it replaced rather than | ||
| an approximation of it. | ||
| - Something has drifted from design intent, and the useful question is when it diverged. | ||
| - An audit asks which interfaces changed between two dates. | ||
| - A schema change is coming, and you want to know what it does to data that already exists. | ||
| - An automation behaved correctly in testing and incorrectly in production, and you want to run it | ||
| against the data as it stood at the time. | ||
|
|
||
| Immutable history in Infrahub supports several crucial infrastructure management scenarios: | ||
| Because Infrahub records the state of the graph at every point in time, each of these is a query | ||
| rather than an investigation. | ||
|
|
||
| - **Historical analysis**: View how your infrastructure looked at a specific point in time to troubleshoot issues or understand past decisions | ||
| - **Compliance auditing**: Extract all changes performed within a specific time frame for regulatory compliance | ||
| - **Change impact assessment**: Compare infrastructure states before and after significant changes | ||
| - **Security investigation**: Trace unauthorized or unexpected changes to their source | ||
| - **Knowledge preservation**: Understand why specific configuration decisions were made, even as team members change | ||
| ## How Infrahub stores history | ||
|
|
||
| ## Core concepts | ||
| Nothing in the database is modified in place. Updating an attribute adds a new value with its own | ||
| timestamp; the previous value stays where it is, still carrying the timestamps that say when it | ||
| was current. The same applies to relationships, so both the objects and the connections between | ||
| them are timestamped. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. would be good to mention that this also applies to schema. so an historical query to before an attribute was added/removed would include the correct schema for that point in time |
||
|
|
||
| ### Timestamps and commits | ||
| A graph recorded this way is a **temporal graph**: it holds not only how objects relate, but when | ||
| each of those relationships was valid. Naming a time in a query selects the values that were | ||
| current then, which is what makes "the topology before Tuesday's pipeline run" a question the | ||
| database can answer directly. | ||
|
|
||
| Every change in Infrahub is organized into commits, each with an immutable timestamp. These commits capture: | ||
| Two properties follow from storing history this way: | ||
|
|
||
| - The specific data that changed | ||
| - Who made the change | ||
| - When the change occurred | ||
| - **Change tracking is per attribute, not per object.** An update stores the value that changed | ||
| rather than a fresh copy of the whole object, and a diff can say which field moved rather than | ||
| only that the object was touched. | ||
| - **History cannot be rewritten.** Because values are added rather than replaced, a past state | ||
| cannot be edited after the fact, and the record of who changed what remains attached to it. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. there's a slight exception here (maybe too technical for most readers, but not sure). when a branch is rebased all of its timestamps before the rebase are moved up to the rebase time. so if Object A is created on a branch 10 minutes ago and then I rebase that branch right now, Object A's new |
||
|
|
||
| This approach ensures a complete and coherent historical record that can never be rewritten or deleted. | ||
| ## Temporal history and change logs | ||
|
|
||
| ### Temporal queries | ||
| A change log records events: what changed, when, and by whom. Reconstructing a past state from one | ||
| means finding an earlier snapshot and replaying the events on top of it, and the result is only as | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we offer this "replaying the events" an an Infrahub feature and it would probably be difficult to do manually |
||
| complete as the log. | ||
|
|
||
| Infrahub's temporal query system allows you to retrieve data from any point in time. When you query the database, you're not just accessing current data—you're accessing a specific moment in the database's history. By default, queries return the latest state, but you can specify any timestamp to see exactly how your infrastructure looked at that moment. | ||
| Infrahub stores the resulting state at every point in time instead of the operations that produced | ||
| it. Reading the past is therefore a query against data that is already there, and it returns | ||
| objects in the same shape as a query against current data — the same fields, the same filters, the | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe this is the right place to mention that the schema also has temporal support |
||
| same client code. | ||
|
|
||
| This capability extends across all interfaces: | ||
| Both are useful for different questions. Use the [activity | ||
| log](../deploy-manage/run-observe/activity-log) to see the sequence of operations someone | ||
| performed; query a past time to see the state those operations produced. | ||
|
|
||
| - **Web UI**: Time navigation controls in the interface | ||
| - **GraphQL API**: Timestamp parameters for historical queries | ||
| - **REST API**: Temporal query support for retrieving historical data | ||
| - **Python SDK**: Time-aware methods to access past states | ||
| ## Branches are parallel timelines | ||
|
|
||
| ### Attribute-level change tracking | ||
| A branch is a second timeline over the same history. Creating one stores a pointer to the moment | ||
| it diverged rather than a copy of the data, so the branch starts with the full history of its | ||
| origin available to it and records only the values you change on it. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the "origin" is always the default branch. we don't want to imply that you can branch off of a branch. maybe someday, but not yet |
||
|
|
||
| Unlike systems that capture entire object snapshots, Infrahub's immutable history operates at the attribute level. This approach offers several advantages: | ||
| That is why a query names both a branch and a time: together they identify one state out of every | ||
| state the graph has recorded. A branch created last week can be read as it was on Monday, and the | ||
| default branch can be read as it was before that branch merged into it. | ||
|
|
||
| - **Storage efficiency**: Only changed values are stored, not entire object copies | ||
| - **Change clarity**: Easier identification of exactly what changed in each commit | ||
| - **Performance optimization**: Faster queries and better scalability with large datasets | ||
| ## Where you can use it | ||
|
|
||
| ## Implementation details | ||
| | Surface | Access | | ||
| |---|---| | ||
| | Web interface | The time selector beside the branch selector; every object you open reflects the selected time | | ||
| | GraphQL API | `at` on the endpoint — see [Query historical data](./query-historical-data.mdx) | | ||
| | REST API | `at` on stored queries, artifacts, and transformations | | ||
|
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
|
||
| | Python SDK | `at` on `all()`, `get()`, `filters()`, and `execute_graphql()` | | ||
|
|
||
| There is a timestamp associated with every change in the database. This timestamp is immutable and cannot be changed or deleted. Every query to the database can be associated with a timestamp, allowing you to see the state of the database at that specific point in time. | ||
| To compare two moments rather than read one, query `DiffTree` with a start and end time. A | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. probably deserves its own topic if it does not have one yet, but you need to send a |
||
| [Proposed Change](../proposed-changes/overview.mdx) does the same comparison scoped to a branch, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you can also view (and refresh) the diff for a branch in its Branch view page even if a proposed change does not exist for the branch |
||
| which is the right tool when the two states you care about are a branch and its base. | ||
|
|
||
| ## Related topics | ||
|
|
||
| - [Proposed Changes](../proposed-changes/overview.mdx) | ||
| - [Branches](../branches/overview.mdx) | ||
| - [Branches](../branches/overview.mdx) — how branches isolate changes, and what merging does to | ||
| the timeline | ||
| - [Proposed Changes](../proposed-changes/overview.mdx) — review and validate a set of changes | ||
| before they merge | ||
|
|
||
| ## In this section | ||
|
|
||
| - [Query historical data](./query-historical-data.mdx) — Read the graph as it stood at a past | ||
| time from any interface, and compare two moments to see what changed | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,211 @@ | ||
| --- | ||
| title: Query historical data | ||
| --- | ||
|
|
||
| import Tabs from '@theme/Tabs'; | ||
| import TabItem from '@theme/TabItem'; | ||
|
|
||
| Attach a time to any read and Infrahub returns the data as it stood then. The query is the one you | ||
| already use — same fields, same filters, same client code — with a point in time added to it. | ||
|
|
||
| This is the tool for questions that current state cannot answer: which interfaces changed between | ||
| two dates, what the topology looked like before a pipeline ran, or what a device's configuration | ||
| was at the moment an alert fired. [Immutable history](./overview.mdx) covers why those states are | ||
| still available to query; this guide covers how to ask for them. | ||
|
|
||
| Three things carry most of the work: choosing the point in time, knowing how far back the branch | ||
|
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
|
||
| you are querying can be read, and picking the interface that fits what you are doing. | ||
|
|
||
| ## Choose a point in time | ||
|
|
||
| Two kinds of question need two kinds of timestamp, and `at` accepts both. | ||
|
|
||
| While you are working backwards from a problem, the useful reference is now — "what did this look | ||
| like an hour ago" — and a relative offset says that without arithmetic. An audit question, a | ||
| post-incident writeup, or anything two people need to agree on requires a fixed moment that still | ||
| means the same thing next week, which is an absolute time. | ||
|
|
||
| | Form | Example | Resolves to | | ||
| |---|---|---| | ||
| | ISO 8601 with a zone or offset | `2026-03-09T14:00:00Z`, `2026-03-09T15:00:00+01:00` | The instant you name | | ||
| | ISO 8601 without a zone | `2026-03-09T14:00:00` | The same wall-clock time, read as UTC | | ||
| | Date only | `2026-03-09` | 12:00 UTC on that day | | ||
| | Offset from now | `30s`, `45m`, `6h`, `2h30m` | That much time before now | | ||
|
|
||
| Two limits are worth knowing before they surprise you. A date on its own resolves to midday rather | ||
| than midnight, so an audit question about "the 9th" is not the same request as `2026-03-09` — write | ||
| the time out when the boundary of a day matters. And offsets stop at hours: seconds, minutes, and | ||
| hours combine in one string, but there is no day or week form, so `7d` returns a | ||
| `TimestampFormatError`. For anything further back than a few hours, pass an absolute time. | ||
|
|
||
| ## How far back a branch can be read | ||
|
|
||
| A branch's history starts where the branch does. Creating a branch stores a pointer to the moment | ||
| it diverged rather than a copy of the data, so what a branch can be read from is the history it | ||
| inherited, plus the changes you have made on it since. | ||
|
|
||
| That inheritance is what puts the boundary where it is. On the default branch and the global | ||
| branch, the earliest readable time is that branch's own creation. On any other branch, the boundary | ||
| is the creation time of the branch it came from, because everything before the divergence point is | ||
| the origin branch's history and that is where the branch reads it from. | ||
|
|
||
| Ask for anything earlier and Infrahub rejects the query rather than returning a partial answer: | ||
|
|
||
| ```text | ||
| Requested time '2026-01-05T00:00:00Z' is before branch 'main' was created at '2026-02-01T09:14:22.481000Z'. | ||
| ``` | ||
|
|
||
| If you need history from before a branch existed, query the branch it came from. | ||
|
|
||
| ## Read data at a past time | ||
|
|
||
| Each interface suits a different moment. Use the web interface while you are still working out | ||
| what changed, when moving the time and re-reading objects is the fastest way to find it. Use | ||
| GraphQL for a specific question you can express as a query. Use the REST API when something | ||
| scheduled or external needs the answer. Use the Python SDK when you are comparing states in code | ||
| rather than reading them. | ||
|
|
||
| The examples below all return the state of `ord1-edge1` at the same moment. | ||
|
|
||
| <Tabs groupId="method" queryString> | ||
| <TabItem value="web" label="Web interface" default> | ||
|
|
||
| Select the time selector — the calendar and clock icon beside the branch selector — and choose a | ||
| date and time in UTC. The control carries no text label until you set a time, at which point the | ||
| bar beside it reads **Current view time** with your selection. | ||
|
|
||
| The selected time then applies to everything you open, so you can move through related objects | ||
| without setting it again. Select the **×** beside the displayed time to return to the present. | ||
|
|
||
| Because the picker offers past times only, it is the one interface that cannot be pointed at a | ||
| future timestamp. | ||
|
|
||
|  | ||
|
|
||
| </TabItem> | ||
|
|
||
| <TabItem value="graphql" label="GraphQL"> | ||
|
|
||
| `at` is a query-string parameter on the endpoint, so the query itself is unchanged: | ||
|
|
||
| ```graphql # Read a device at a past time | ||
| # Endpoint : http://localhost:8000/graphql/main?at=2026-03-09T14:00:00Z | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The endpoint here is commented out and makes it hard to read in the docs. And in this case the endpoint containing the at URL parameter is exactly the IMPORTANT part. |
||
| query DeviceAtTime { | ||
| InfraDevice(name__value: "ord1-edge1") { | ||
| edges { | ||
| node { | ||
| name { value } | ||
| description { value } | ||
| status { value } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| Keeping the time out of the query body means a query you have already written and saved works | ||
| against any point in time — the endpoint you send it to decides which one. That is what makes a | ||
| stored query reusable for both monitoring current state and answering a question about the past. | ||
|
|
||
| </TabItem> | ||
|
|
||
| <TabItem value="rest" label="REST API"> | ||
|
|
||
| The REST API applies `at` to stored GraphQL queries, artifacts, and transformations rather than to | ||
| ad-hoc object reads. To read objects at a past time over REST, save the query as a | ||
| `CoreGraphQLQuery` object and run it by name: | ||
|
|
||
| ```bash | ||
| curl "http://localhost:8000/api/query/device-status?branch=main&at=2026-03-09T14:00:00Z" \ | ||
| -H "X-INFRAHUB-KEY: $INFRAHUB_API_TOKEN" | ||
| ``` | ||
|
|
||
| The same parameter works on `/api/artifact/{artifact_id}` and on the transformation endpoints, | ||
| which renders an artifact from the data as it stood rather than from current data — useful for | ||
| producing the configuration a device was given at a particular time and comparing it with what is | ||
| on the device now. | ||
|
|
||
| </TabItem> | ||
|
|
||
| <TabItem value="sdk" label="Python SDK"> | ||
|
|
||
| `all()`, `get()`, `filters()`, and `execute_graphql()` each take an `at` argument, so a script can | ||
| read two moments and compare them: | ||
|
|
||
| ```python | ||
| from infrahub_sdk import InfrahubClientSync | ||
| from infrahub_sdk.timestamp import Timestamp | ||
|
|
||
| client = InfrahubClientSync(address="http://localhost:8000") | ||
|
|
||
| before = client.get( | ||
| kind="InfraDevice", | ||
| name__value="ord1-edge1", | ||
| at=Timestamp("2026-03-09T14:00:00Z"), | ||
| ) | ||
| now = client.get(kind="InfraDevice", name__value="ord1-edge1") | ||
|
|
||
| print(before.description.value, "->", now.description.value) | ||
| ``` | ||
|
|
||
| `Timestamp` accepts every form in the table above, so `Timestamp("6h")` reads six hours back. | ||
| Pointing a script at a past state is also how you test an automation against the data it ran on, | ||
|
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
|
||
| rather than against data that has moved on since. | ||
|
|
||
| </TabItem> | ||
| </Tabs> | ||
|
|
||
| ## Compare two points in time | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is probably where the |
||
|
|
||
| Reading one state answers "what was it then". To answer "what changed between these two moments", | ||
| query `DiffTree` with `from_time` and `to_time`. The two times are yours to choose — they do not | ||
| have to line up with a branch point or a proposed change, which is what makes this usable for a | ||
| window like "the four hours around the incident". | ||
|
|
||
| ```graphql # What changed on main between two timestamps | ||
| # Endpoint : http://localhost:8000/graphql/main | ||
| query ChangesBetween { | ||
| DiffTree( | ||
| branch: "main" | ||
| from_time: "2026-03-09T00:00:00Z" | ||
| to_time: "2026-03-10T00:00:00Z" | ||
| ) { | ||
| num_added | ||
| num_updated | ||
| num_removed | ||
| nodes { | ||
| kind | ||
| label | ||
| status | ||
| attributes { | ||
| name | ||
| action | ||
| } | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| Omit `from_time` and the comparison starts at the branch's creation; omit `to_time` and it ends at | ||
| the current time. Add `filters` to restrict the result by kind, namespace, or status, and `limit` | ||
| and `offset` to page through a large result. For counts without the node detail, query | ||
| `DiffTreeSummary` with the same arguments. | ||
|
|
||
| Two things to expect from the result. The base of the comparison is always the default branch, so | ||
| naming a feature branch compares that branch against the default branch across the window, while | ||
| naming the default branch compares it against itself over time. And where no diff covering the | ||
| requested window is available, `DiffTree` returns `null` rather than an empty result — so treat a | ||
| null as "not calculated", not as "nothing changed". | ||
|
|
||
| When the two states you want to compare are a branch and its base, a | ||
| [Proposed Change](../proposed-changes/overview.mdx) gives you the same comparison with review and | ||
| validation attached. | ||
|
|
||
| ## Related | ||
|
|
||
| - [Immutable history](./overview.mdx) — how Infrahub stores every past state, and why reading one | ||
| is a query rather than a reconstruction | ||
| - [Branches](../branches/overview.mdx) — how branch creation and merging place the timestamps you | ||
| query against | ||
| - [Activity log](../deploy-manage/run-observe/activity-log.mdx) — the sequence of operations | ||
| someone performed, where a past state tells you what those operations produced | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is not exactly true. if there are changes on a branch and that branch is deleted, we hard-delete everything on that branch and it is not recoverable