Skip to content

Commit 1510698

Browse files
bruno-espinoclaude
andauthored
docs: add self-hosted Redis, bucket, and FusionFire database migration guidance (#2682)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent e6c13d1 commit 1510698

4 files changed

Lines changed: 312 additions & 1 deletion

File tree

‎docs/navigation.yml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1031,6 +1031,10 @@ navigation:
10311031
path: "reference/self-hosted/bucket-migration.md"
10321032
source: "plain"
10331033
slug: "deploy/self-hosted-deployment/bucket-migration"
1034+
- page: "FusionFire Database Migration"
1035+
path: "reference/self-hosted/ff-database-migration.md"
1036+
source: "plain"
1037+
slug: "deploy/self-hosted-deployment/ff-database-migration"
10341038
- page: "Usage Report"
10351039
path: "reference/self-hosted/usage-report.md"
10361040
source: "plain"
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
---
2+
title: "Logfire Self-Hosted: FusionFire Database Migration Guide"
3+
description: "How to move the FusionFire (ff) PostgreSQL database of a self-hosted Pydantic Logfire deployment to a new PostgreSQL instance."
4+
---
5+
# FusionFire Database Migration
6+
7+
This guide moves the `ff` PostgreSQL database of a self-hosted **Logfire** deployment to a new PostgreSQL instance. The object storage bucket stays where it is.
8+
9+
!!! warning
10+
The `ff` database records which objects in the bucket hold live data. When Logfire starts on the new instance, the new database must be an exact copy of the old database. A copy that is older than the bucket is not supported, and Logfire cannot repair it:
11+
12+
* Compaction deletes the files it replaces about 5 minutes after it writes the new files. An older copy still points at the deleted files, and queries over those files fail.
13+
* An older copy has no record of the files written after the copy was made. Queries do not return that data, and retention never deletes it.
14+
15+
For the same reason, do not point Logfire back at the old database after the FusionFire workloads have run against the new one.
16+
17+
---
18+
19+
## Overview
20+
21+
The migration consists of three steps:
22+
23+
1. **Stop** the FusionFire workloads that use the `ff` database.
24+
2. **Copy** the database to the new instance, with one of these methods:
25+
* **Dump and restore.** This method is the simplest. Queries are unavailable for as long as the dump and restore take.
26+
* **Promote a replica.** Queries are unavailable for a few minutes. This method needs a physical replica of the old instance, for example a read replica from your PostgreSQL provider.
27+
3. **Deploy** the Helm chart with the new `postgresFFDsn` and start the workloads again.
28+
29+
`logfire-ff-ingest` keeps running through all steps. It does not connect to PostgreSQL. It accepts telemetry and buffers it on disk until the other workloads start again. By default, it keeps retrying a batch for only 10 minutes, so Step 1 raises that limit for the length of the migration.
30+
31+
## Prerequisites
32+
33+
- Admin access to your **Kubernetes** cluster.
34+
- The [Helm](https://helm.sh) CLI installed.
35+
- `pg_dump`, `pg_restore`, and `psql` with the same major version as the new instance.
36+
- A new PostgreSQL instance that meets the [PostgreSQL requirements](installation.md#postgresql). For dump and restore, it must run the same or a newer PostgreSQL major version than the old instance. A physical replica must run the same major version.
37+
- An empty `ff` database on the new instance, owned by the user in the new `postgresFFDsn`. That user must be able to create the `intarray` and `btree_gist` extensions.
38+
- Enough disk on the `logfire-ff-ingest` volumes to buffer incoming data for the length of the migration.
39+
- An estimate of how long the migration takes. Use a value that is longer than that estimate for `FF_INGEST_REMOTE_RETRY_MAX_DURATION` in Step 1.
40+
41+
Use the same chart version and the same values before and after the migration, except for `postgresFFDsn`. Do not upgrade the chart in the same window. Record the installed chart version, and pass it to every `helm upgrade` in this guide:
42+
43+
```bash
44+
CHART_VERSION=$(helm list -n logfire -f '^logfire$' -o json | jq -r '.[0].chart | sub("^logfire-"; "")')
45+
```
46+
47+
The examples below assume the Helm release is named `logfire` and installed in the `logfire` namespace. Adjust the commands if your deployment uses different names.
48+
49+
---
50+
51+
## Step 1: Stop the FusionFire Workloads
52+
53+
Pause the autoscalers of these workloads first. Every `sizingPreset` turns on HPA for them, and your values can also turn on HPA or KEDA. Otherwise, Kubernetes scales the workloads back up during the migration.
54+
55+
Also raise how long `logfire-ff-ingest` retries a batch. After `FF_INGEST_REMOTE_RETRY_MAX_DURATION` (10 minutes by default), ingest writes the batch to the `_ingest_failover` prefix of the bucket, and queries do not return that data. If your values already set `logfire-ff-ingest.env`, copy the other entries into this file too, because Helm replaces the whole list. Leave out any existing `FF_INGEST_REMOTE_RETRY_MAX_DURATION` entry.
56+
57+
Create a temporary `migration-pause-ff.yaml` file:
58+
59+
```yaml
60+
logfire-ff-ingest:
61+
env:
62+
- name: FF_INGEST_REMOTE_RETRY_MAX_DURATION
63+
value: "6h"
64+
65+
logfire-ff-ingest-processor:
66+
autoscaling:
67+
hpa:
68+
enabled: false
69+
keda:
70+
enabled: false
71+
72+
logfire-ff-compaction-worker:
73+
autoscaling:
74+
hpa:
75+
enabled: false
76+
keda:
77+
enabled: false
78+
79+
logfire-ff-maintenance-worker:
80+
autoscaling:
81+
hpa:
82+
enabled: false
83+
keda:
84+
enabled: false
85+
86+
logfire-ff-maintenance-scheduler:
87+
autoscaling:
88+
hpa:
89+
enabled: false
90+
keda:
91+
enabled: false
92+
93+
logfire-ff-crud-api:
94+
autoscaling:
95+
hpa:
96+
enabled: false
97+
keda:
98+
enabled: false
99+
100+
logfire-ff-query-api:
101+
autoscaling:
102+
hpa:
103+
enabled: false
104+
keda:
105+
enabled: false
106+
107+
logfire-ff-query-worker:
108+
autoscaling:
109+
hpa:
110+
enabled: false
111+
keda:
112+
enabled: false
113+
```
114+
115+
Apply it with your current production values:
116+
117+
```bash
118+
helm upgrade logfire pydantic/logfire \
119+
--version "$CHART_VERSION" \
120+
-f values.yaml \
121+
-f migration-pause-ff.yaml \
122+
--namespace logfire
123+
```
124+
125+
Wait until every `logfire-ff-ingest` pod runs with the new retry limit. A pod that still has the 10-minute limit loses data once the workloads stop:
126+
127+
```bash
128+
kubectl -n logfire rollout status statefulset/logfire-ff-ingest
129+
```
130+
131+
Then scale the workloads down:
132+
133+
```bash
134+
kubectl -n logfire scale deployment --replicas=0 \
135+
-l 'app.kubernetes.io/component in (logfire-ff-ingest-processor,logfire-ff-compaction-worker,logfire-ff-maintenance-worker,logfire-ff-maintenance-scheduler,logfire-ff-crud-api,logfire-ff-query-api,logfire-ff-query-worker)'
136+
```
137+
138+
Make sure that no client is still connected to the old `ff` database:
139+
140+
```sql
141+
SELECT application_name, usename, client_addr, state
142+
FROM pg_stat_activity
143+
WHERE datname = 'ff' AND pid <> pg_backend_pid();
144+
```
145+
146+
The query must return no rows before you continue. If it returns rows, find and stop the clients that it shows.
147+
148+
!!! note
149+
Queries in the Logfire UI and the API fail until Step 3 is complete. `logfire-ff-ingest` buffers incoming data on disk for as long as `FF_INGEST_REMOTE_RETRY_MAX_DURATION` allows. Finish Step 3 before that time ends, and monitor the ingest disk usage. If the ingest disk fills, ingest writes new data to the `_ingest_failover` prefix, and queries do not return that data. If the ingest autoscaler removes a pod during the window, that pod's buffered data stays on its volume until the pod starts again.
150+
151+
---
152+
153+
## Step 2: Copy the Database
154+
155+
Use one of the two methods below.
156+
157+
### Option A: Dump and Restore
158+
159+
Dump the old database. The directory format lets `pg_dump` and `pg_restore` use parallel jobs:
160+
161+
```bash
162+
pg_dump \
163+
--dbname="$OLD_FF_DSN" \
164+
--format=directory \
165+
--jobs=4 \
166+
--no-owner \
167+
--no-privileges \
168+
--file=ff-dump
169+
```
170+
171+
Restore it into the empty `ff` database on the new instance. Connect as the user in the new `postgresFFDsn`, so that this user owns the restored tables:
172+
173+
```bash
174+
pg_restore \
175+
--dbname="$NEW_FF_DSN" \
176+
--jobs=4 \
177+
--no-owner \
178+
--no-privileges \
179+
--exit-on-error \
180+
ff-dump
181+
```
182+
183+
The restore does not copy planner statistics. Collect them before Logfire uses the new database:
184+
185+
```bash
186+
psql "$NEW_FF_DSN" -c 'ANALYZE;'
187+
```
188+
189+
### Option B: Promote a Replica
190+
191+
Create a physical replica of the old instance before Step 1, while Logfire is still running. Most managed PostgreSQL services call this a read replica. A physical replica copies every change, including the schema migration history, so the promoted instance is an exact copy.
192+
193+
This guide does not cover logical replication. Logical replication does not copy sequence values or schema changes.
194+
195+
After Step 1 is complete:
196+
197+
1. Wait until the replica has replayed every change from the old instance. Your provider shows this as a replication lag of zero. On a self-managed replica, run `SELECT pg_current_wal_lsn();` on the old instance after Step 1. Then wait until `SELECT pg_wal_lsn_diff(pg_last_wal_replay_lsn(), '<LSN>') >= 0;` returns `true` on the replica.
198+
2. Promote the replica to a standalone instance, with your provider's promote action or with `pg_promote()`.
199+
3. Make sure that the user in the new `postgresFFDsn` can sign in to the promoted instance and owns the `ff` database.
200+
201+
---
202+
203+
## Step 3: Deploy Updated Configuration and Scale Up
204+
205+
Update `postgresFFDsn` in your values, or the `postgresFFDsn` key of the Secret that `postgresSecret.name` references:
206+
207+
```yaml
208+
postgresFFDsn: postgresql://logfire_ff:PASSWORD@new-postgres.example.com:5432/ff
209+
```
210+
211+
Deploy the updated Helm chart without `migration-pause-ff.yaml`, so Helm restores the autoscaling, replica, and ingest retry settings from your production values:
212+
213+
```bash
214+
helm upgrade logfire pydantic/logfire \
215+
--version "$CHART_VERSION" \
216+
-f values.yaml \
217+
--namespace logfire
218+
```
219+
220+
The FusionFire migration job runs against the new database first. The chart version did not change, so the job has no migration to apply.
221+
222+
The chart restarts every pod that receives `postgresFFDsn`. That includes `logfire-ff-ingest`, which receives the value but does not use it. Its buffer is on a persistent volume, so the restart does not lose buffered data. If an external tool manages the Secret and the pods did not restart, restart the FusionFire deployments and the `logfire-ff-ingest` StatefulSet with `kubectl rollout restart`.
223+
224+
If the Helm upgrade does not restore the workloads, scale them back up manually to your previous replica counts.
225+
226+
---
227+
228+
## Verification
229+
230+
1. **Check pod status.** All pods must be running without restarts:
231+
232+
```bash
233+
kubectl -n logfire get pods
234+
```
235+
236+
2. **Check the database connections.** Run the `pg_stat_activity` query from Step 1 against both instances. The new instance shows FusionFire connections. The old instance shows none.
237+
238+
3. **Check the writer logs** for database errors:
239+
240+
```bash
241+
kubectl -n logfire logs -l app.kubernetes.io/component=logfire-ff-ingest-processor --tail=50
242+
```
243+
244+
4. **Query data in the Logfire UI.** Query recent data, data from before the migration, and data sent during the migration window.
245+
246+
Keep the old database until you have finished the checks. Do not point Logfire at it again.

‎docs/reference/self-hosted/installation.md‎

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Before installing the chart, decide how you will configure:
1313
* Image pull credentials.
1414
* PostgreSQL databases and credentials.
1515
* Object storage bucket/container and credentials.
16+
* Managed Redis endpoint and credentials.
1617
* Dex identity provider connector.
1718
* Production sizing preset: `standard`, `small`, or `tiny`.
1819
* Storage classes if your cluster does not have a default `StorageClass`.
@@ -43,6 +44,10 @@ objectStore:
4344
postgresDsn: postgresql://logfire_crud:PASSWORD@postgres.example.com:5432/crud
4445
postgresFFDsn: postgresql://logfire_ff:PASSWORD@postgres.example.com:5432/ff
4546

47+
logfire-redis:
48+
enabled: false
49+
redisDsn: rediss://USER:PASSWORD@redis.example.com:6379
50+
4651
logfire-dex:
4752
config:
4853
storage:
@@ -101,6 +106,10 @@ Create three databases. They can live on the same PostgreSQL instance:
101106
Telemetry payloads are stored in object storage, not PostgreSQL.
102107
Each database user needs owner permissions so chart migrations can create and update schemas.
103108

109+
An instance with 2 vCPU and 16 GB of memory is a good starting size. Resize it from the CPU and connection use that you see. FusionFire migrations set autovacuum settings on the `ff` tables that change most often, so do not override them.
110+
111+
Connect the Logfire services directly to PostgreSQL, or through a connection pooler in transaction mode. With a pooler, the migration jobs, the FusionFire maintenance scheduler, and Dex need a separate session-mode pool or direct connection. The [chart README](https://github.com/pydantic/logfire-helm-chart/tree/main/charts/logfire#connection-poolers) lists what the pooler must support, the session DSN values, and the connections that each workload opens.
112+
104113
You can provide the two Logfire database DSNs directly:
105114

106115
```yaml
@@ -165,7 +174,14 @@ objectStore:
165174
uri: s3://logfire-prod
166175
```
167176

168-
Do not enable bucket versioning. Logfire manages its own data lifecycle, and bucket versioning can increase cost and interfere with lifecycle behavior.
177+
Logfire deletes the objects it no longer needs. Compaction replaces small files with larger ones and deletes the old files a few minutes later. Retention deletes data older than each project's retention period. Configure the bucket so that it does not keep or move these objects:
178+
179+
* Turn off object versioning.
180+
* Turn off soft delete. Google Cloud Storage turns on a 7-day soft delete for new buckets, and Azure Storage can keep deleted blobs for a period. Compaction deletes objects all the time, so soft-deleted objects can become a large part of the storage bill.
181+
* On Amazon S3 and Google Cloud Storage, add a lifecycle rule that aborts incomplete multipart uploads after 1 day. A worker that stops during an upload leaves parts behind. These parts are billed, but they do not appear in object listings.
182+
* Do not add lifecycle rules that delete objects or change their storage class by age. An age-based delete removes files that the `ff` database still references, and queries over those files then fail.
183+
184+
On Google Cloud Storage, you can turn on Autoclass to lower the cost of data that is read less often.
169185

170186
For Amazon S3, prefer workload identity such as IRSA, EKS Pod Identity, or your platform's equivalent. If you need static credentials, load them from a Secret:
171187

@@ -213,6 +229,35 @@ objectStore:
213229
key: account-key
214230
```
215231

232+
## Redis
233+
234+
The chart's bundled Redis is for evaluation. For production, turn it off and connect a managed Redis:
235+
236+
* Use one Redis endpoint. It can be a standalone instance, or a primary with replicas behind one write endpoint. Logfire does not support Redis Cluster mode.
237+
* Redis 7.2 is the tested version.
238+
* Logfire accepts `redis://` and `rediss://` DSNs. Put the password, or the ACL user and password, in the DSN.
239+
* With `rediss://`, Logfire verifies the Redis server certificate. Memorystore with in-transit encryption, for example, signs it with a private CA for each instance. For a private CA, set `trustedCaBundle` as the [chart README](https://github.com/pydantic/logfire-helm-chart/tree/main/charts/logfire#private-ca-certificates) describes.
240+
241+
You can provide the DSN directly:
242+
243+
```yaml
244+
logfire-redis:
245+
enabled: false
246+
247+
redisDsn: rediss://USER:PASSWORD@redis.example.com:6379
248+
```
249+
250+
Or reference an existing Kubernetes Secret with the `sharedDsn`, `tailDsn`, `tokenDsn`, and `usageDsn` keys. All four keys can hold the same DSN:
251+
252+
```yaml
253+
logfire-redis:
254+
enabled: false
255+
256+
redisSecret:
257+
enabled: true
258+
name: logfire-redis-dsn
259+
```
260+
216261
## Hostnames and Exposure
217262

218263
Logfire needs a stable hostname so it can generate correct public URLs and CORS headers.

‎docs/reference/self-hosted/overview.md‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Use the [Logfire Helm chart README](https://github.com/pydantic/logfire-helm-cha
1818
* [Examples](./examples.md): configure SSO connectors and exchange instance-admin tokens.
1919
* [Troubleshooting](./troubleshooting.md): debug image pull, database, and runtime issues.
2020
* [Bucket migration](./bucket-migration.md): migrate telemetry object storage buckets.
21+
* [FusionFire database migration](./ff-database-migration.md): move the `ff` database to a new PostgreSQL instance.
2122
* [Usage report](./usage-report.md): generate usage reports for self-hosted deployments.
2223

2324
## Production Prerequisites
@@ -29,9 +30,24 @@ A production deployment needs:
2930
* PostgreSQL 16 or later.
3031
* Three PostgreSQL databases: one for Logfire application data, one for FusionFire metadata, and one for Dex.
3132
* Object storage using Amazon S3, Google Cloud Storage, Azure Storage, or an S3-compatible provider.
33+
* A managed Redis instance. Redis Cluster mode is not supported.
3234
* An identity provider supported by [Dex](https://dexidp.io/docs/connectors/).
3335
* Kubernetes storage for scratch and ingest volumes, either through a default `StorageClass` or explicit storage class settings in values.
3436

37+
## Upgrades
38+
39+
Each chart version has a [GitHub release](https://github.com/pydantic/logfire-helm-chart/releases) with **Upgrade notes** and **Breaking changes** sections. Before you upgrade, read these sections for every version after your current one, up to and including the target version. You can then upgrade to the target version in one `helm upgrade`.
40+
41+
The backend and FusionFire migrations run as Helm pre-upgrade jobs, before the new pods start. The running pods keep serving requests during the migrations, because the migrations keep blocking locks short:
42+
43+
* Index changes use `CONCURRENTLY`.
44+
* Validations of large tables use locks that do not block reads or writes.
45+
* A statement that needs a stronger lock sets a short lock timeout, usually under a second. If it cannot get the lock in that time, the migration fails and Kubernetes runs the job again.
46+
47+
## Moving From Logfire Cloud
48+
49+
There is no supported way to move existing data from Logfire Cloud to a self-hosted instance. Point your applications at the self-hosted instance, and keep access to Logfire Cloud until you no longer need the data there.
50+
3551
## Client Configuration
3652

3753
After deploying self-hosted Logfire, point the SDK at your Logfire base URL:

0 commit comments

Comments
 (0)