|
| 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. |
0 commit comments