Skip to content

Commit 78f584d

Browse files
authored
Merge pull request #57 from tma1-ai/greptime-batch-export-local
feat(batch-export): local-volume backend so object storage is fully optional (#48)
2 parents 20d0a8d + 63319f2 commit 78f584d

20 files changed

Lines changed: 579 additions & 66 deletions

File tree

.env.dev.example

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,12 @@ EMAIL_FROM_ADDRESS="" # Defines the email address to use as the from address.
9595
SMTP_CONNECTION_URL="" # Defines the connection url for smtp server.
9696
CLOUD_CRM_EMAIL="" # Optional BCC address for usage threshold emails (e.g., for CRM integration like HubSpot)
9797

98-
# S3 Batch Exports
98+
# Batch Exports
99+
## Backend: "local" writes the export to a shared volume and serves it via a signed web download
100+
## URL (no object storage needed); "s3" uploads to a bucket and hands out a presigned URL.
101+
# LANGFUSE_BATCH_EXPORT_STORAGE_BACKEND=local
102+
# LANGFUSE_BATCH_EXPORT_LOCAL_PATH=/tmp/langfuse/batch-exports
103+
# S3 Batch Exports (only when LANGFUSE_BATCH_EXPORT_STORAGE_BACKEND=s3)
99104
LANGFUSE_S3_BATCH_EXPORT_ENABLED=true
100105
LANGFUSE_S3_BATCH_EXPORT_BUCKET=langfuse
101106
LANGFUSE_S3_BATCH_EXPORT_ACCESS_KEY_ID=minio

.env.prod.example

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,13 @@ OTEL_SERVICE_NAME="langfuse"
161161
# Defines the connection url for smtp server.
162162
# SMTP_CONNECTION_URL=
163163

164-
# S3 Batch Exports
164+
# Batch Exports
165+
# Backend: "local" writes the export to a shared volume and serves it via a signed web download URL
166+
# (no object storage needed; the web download URL is built from NEXTAUTH_URL); "s3" uploads to a
167+
# bucket and hands out a presigned URL. The local path must be shared between web and worker.
168+
# LANGFUSE_BATCH_EXPORT_STORAGE_BACKEND=local
169+
# LANGFUSE_BATCH_EXPORT_LOCAL_PATH=/langfuse_batch_export_data
170+
# S3 Batch Exports (only when LANGFUSE_BATCH_EXPORT_STORAGE_BACKEND=s3)
165171
# LANGFUSE_S3_BATCH_EXPORT_ENABLED=
166172
# LANGFUSE_S3_BATCH_EXPORT_BUCKET=
167173
# LANGFUSE_S3_BATCH_EXPORT_ACCESS_KEY_ID=

Dockerfile.standalone

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,8 @@ COPY --chown=openfuse:openfuse ./scripts/standalone-entrypoint.sh ./standalone-e
132132
RUN chmod +x ./standalone-entrypoint.sh
133133

134134
# Default local blob-store mounts owned by the app user (see web/Dockerfile).
135-
RUN mkdir -p /langfuse_media_data /langfuse_event_data && \
136-
chown openfuse:openfuse /langfuse_media_data /langfuse_event_data
135+
RUN mkdir -p /langfuse_media_data /langfuse_event_data /langfuse_batch_export_data && \
136+
chown openfuse:openfuse /langfuse_media_data /langfuse_event_data /langfuse_batch_export_data
137137

138138
USER openfuse
139139

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ Full instructions for standalone, split web/worker images, and tag policy: [depl
112112

113113
## Architecture
114114

115-
Postgres holds application and config data (users, projects, prompts, dataset definitions, API keys), unchanged from upstream Langfuse. GreptimeDB is the analytics event store: an append-only `raw_events` table as the source of truth, plus merged projection tables and indexed EAV side-tables that back metadata, tag, and tool filtering. Redis runs the BullMQ queues. Object storage (S3/MinIO) is optional for the default stack: media uploads, the OTel carrier, and the eval blob store default to local filesystem paths. Opt-in batch/blob exports still need an S3-compatible bucket.
115+
Postgres holds application and config data (users, projects, prompts, dataset definitions, API keys), unchanged from upstream Langfuse. GreptimeDB is the analytics event store: an append-only `raw_events` table as the source of truth, plus merged projection tables and indexed EAV side-tables that back metadata, tag, and tool filtering. Redis runs the BullMQ queues. Object storage (S3/MinIO) is optional for the default stack: media uploads, the OTel carrier, the eval blob store, and batch exports all default to local filesystem paths, so a stock deployment needs no bucket at all.
116116

117117
Full write-up: [architecture](docs/architecture.md).
118118

docker-compose.standalone.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ services:
3838
volumes:
3939
- langfuse_media_data:/langfuse_media_data
4040
- langfuse_event_data:/langfuse_event_data
41+
- langfuse_batch_export_data:/langfuse_batch_export_data
4142
environment:
4243
# --- core ---
4344
NEXTAUTH_URL: ${NEXTAUTH_URL:-http://localhost:3000}
@@ -64,6 +65,8 @@ services:
6465
LANGFUSE_MEDIA_LOCAL_PATH: ${LANGFUSE_MEDIA_LOCAL_PATH:-/langfuse_media_data}
6566
LANGFUSE_EVENT_STORAGE_BACKEND: ${LANGFUSE_EVENT_STORAGE_BACKEND:-local}
6667
LANGFUSE_EVENT_LOCAL_PATH: ${LANGFUSE_EVENT_LOCAL_PATH:-/langfuse_event_data}
68+
LANGFUSE_BATCH_EXPORT_STORAGE_BACKEND: ${LANGFUSE_BATCH_EXPORT_STORAGE_BACKEND:-local}
69+
LANGFUSE_BATCH_EXPORT_LOCAL_PATH: ${LANGFUSE_BATCH_EXPORT_LOCAL_PATH:-/langfuse_batch_export_data}
6770
# --- optional initial bootstrap org/project/user (web only) ---
6871
LANGFUSE_INIT_ORG_ID: ${LANGFUSE_INIT_ORG_ID:-}
6972
LANGFUSE_INIT_ORG_NAME: ${LANGFUSE_INIT_ORG_NAME:-}
@@ -144,5 +147,7 @@ volumes:
144147
driver: local
145148
langfuse_event_data:
146149
driver: local
150+
langfuse_batch_export_data:
151+
driver: local
147152
langfuse_redis_data:
148153
driver: local

docker-compose.yml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,12 @@ services:
2828
volumes: &langfuse-blob-volumes
2929
- langfuse_media_data:/langfuse_media_data
3030
- langfuse_event_data:/langfuse_event_data
31+
- langfuse_batch_export_data:/langfuse_batch_export_data
3132
environment: &langfuse-worker-env
3233
NEXTAUTH_URL: ${NEXTAUTH_URL:-http://localhost:3000}
34+
# Shared with web so the worker can mint signed local batch-export download
35+
# tokens that the web download route verifies with the same secret.
36+
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET:-mysecret} # CHANGEME
3337
DATABASE_URL: ${DATABASE_URL:-postgresql://postgres:postgres@postgres:5432/postgres} # CHANGEME
3438
SALT: ${SALT:-mysalt} # CHANGEME
3539
ENCRYPTION_KEY: ${ENCRYPTION_KEY:-0000000000000000000000000000000000000000000000000000000000000000} # CHANGEME: generate via `openssl rand -hex 32`
@@ -68,6 +72,11 @@ services:
6872
LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT: ${LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT:-http://localhost:9090}
6973
LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE: ${LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE:-true}
7074
LANGFUSE_S3_MEDIA_UPLOAD_PREFIX: ${LANGFUSE_S3_MEDIA_UPLOAD_PREFIX:-media/}
75+
# Batch exports default to the local-file backend (shared volume + signed
76+
# web download URL), so no bucket is needed. Set to "s3" + start the minio
77+
# profile (or your own bucket) to use object storage instead.
78+
LANGFUSE_BATCH_EXPORT_STORAGE_BACKEND: ${LANGFUSE_BATCH_EXPORT_STORAGE_BACKEND:-local}
79+
LANGFUSE_BATCH_EXPORT_LOCAL_PATH: ${LANGFUSE_BATCH_EXPORT_LOCAL_PATH:-/langfuse_batch_export_data}
7180
LANGFUSE_S3_BATCH_EXPORT_ENABLED: ${LANGFUSE_S3_BATCH_EXPORT_ENABLED:-false}
7281
LANGFUSE_S3_BATCH_EXPORT_BUCKET: ${LANGFUSE_S3_BATCH_EXPORT_BUCKET:-langfuse}
7382
LANGFUSE_S3_BATCH_EXPORT_PREFIX: ${LANGFUSE_S3_BATCH_EXPORT_PREFIX:-exports/}
@@ -104,7 +113,6 @@ services:
104113
volumes: *langfuse-blob-volumes
105114
environment:
106115
<<: *langfuse-worker-env
107-
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET:-mysecret} # CHANGEME
108116
LANGFUSE_INIT_ORG_ID: ${LANGFUSE_INIT_ORG_ID:-}
109117
LANGFUSE_INIT_ORG_NAME: ${LANGFUSE_INIT_ORG_NAME:-}
110118
LANGFUSE_INIT_PROJECT_ID: ${LANGFUSE_INIT_PROJECT_ID:-}
@@ -212,6 +220,8 @@ volumes:
212220
driver: local
213221
langfuse_event_data:
214222
driver: local
223+
langfuse_batch_export_data:
224+
driver: local
215225
langfuse_minio_data:
216226
driver: local
217227
langfuse_redis_data:

docs/architecture.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ BullMQ job queues for ingestion, evals, exports, and reconciliation. Unchanged f
4949

5050
### Object storage (optional)
5151

52-
With the event store in GreptimeDB, S3 or MinIO is no longer required to ingest. Media uploads, the OTel ingestion carrier, and the eval blob store support a local-filesystem backend, and the bundled Compose files default them to it. Opt-in batch/blob _exports_ still need an S3-compatible bucket.
52+
With the event store in GreptimeDB, S3 or MinIO is no longer required. Media uploads, the OTel ingestion carrier, the eval blob store, and batch exports all support a local-filesystem backend, and the bundled Compose files default them to it. Batch exports on the local backend stream from a shared volume via a signed, time-limited download URL instead of an S3 presigned URL, so a stock deployment needs no object storage at all.
5353

5454
## Write path: source of truth and replay
5555

docs/deployment.md

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -51,31 +51,33 @@ The fork ships a small server config at `docker/greptimedb/config.toml`, mounted
5151

5252
### Object storage is optional
5353

54-
Ingestion and eval-generated scores persist to GreptimeDB `raw_events`, not to a blob store. The remaining object-storage consumers support a local-file backend, so a stock deployment needs **no** MinIO/S3:
54+
Ingestion and eval-generated scores persist to GreptimeDB `raw_events`, not to a blob store. Every remaining object-storage consumer supports a local-file backend, so a stock deployment needs **no** MinIO/S3:
5555

56-
| Variable | App default | Bundled Compose | Local backend |
57-
| -------------------------------- | ----------- | --------------- | ------------------------------------- |
58-
| `LANGFUSE_MEDIA_STORAGE_BACKEND` | `s3` | `local` | `local` + `LANGFUSE_MEDIA_LOCAL_PATH` |
59-
| `LANGFUSE_EVENT_STORAGE_BACKEND` | `s3` | `local` | `local` + `LANGFUSE_EVENT_LOCAL_PATH` |
56+
| Variable | App default | Bundled Compose | Local backend |
57+
| --------------------------------------- | ----------- | --------------- | -------------------------------------------- |
58+
| `LANGFUSE_MEDIA_STORAGE_BACKEND` | `s3` | `local` | `local` + `LANGFUSE_MEDIA_LOCAL_PATH` |
59+
| `LANGFUSE_EVENT_STORAGE_BACKEND` | `s3` | `local` | `local` + `LANGFUSE_EVENT_LOCAL_PATH` |
60+
| `LANGFUSE_BATCH_EXPORT_STORAGE_BACKEND` | `s3` | `local` | `local` + `LANGFUSE_BATCH_EXPORT_LOCAL_PATH` |
6061

61-
The application default for these variables is `s3`, but this repo's `docker-compose.yml` overrides both to `local` (`${...:-local}`), so the bundled stack starts with no object store. `LANGFUSE_EVENT_STORAGE_BACKEND` covers both the OTel carrier and the eval blob store; with `local` they share a filesystem volume, so web and worker must mount the same `LANGFUSE_EVENT_LOCAL_PATH` (the Compose files wire a shared `langfuse_event_data` volume). Only opt-in batch/blob **exports** still require an S3-compatible bucket. The Compose files default both backends to `local` and put MinIO behind a `s3` profile (`docker compose --profile s3 up`), so the default stack starts no object store.
62+
The application default for these variables is `s3`, but this repo's `docker-compose.yml` overrides them to `local` (`${...:-local}`), so the bundled stack starts with no object store. Each `local` backend shares a filesystem volume between web and worker (the Compose files wire `langfuse_event_data` and `langfuse_batch_export_data`); `LANGFUSE_EVENT_STORAGE_BACKEND` covers both the OTel carrier and the eval blob store. Batch exports on the `local` backend are served by an authenticated web download route (signed, time-limited token, re-validated against the export row) instead of an S3 presigned URL, so the link in the export email and the exports page works without a bucket. The Compose files put MinIO behind a `s3` profile (`docker compose --profile s3 up`), so the default stack starts no object store.
6263

6364
### Data directories and persistent volumes
6465

6566
The Compose files use Docker named volumes for every stateful path. Treat these as the deployment's data directory set:
6667

67-
| Volume | Mounted path | Stores |
68-
| ---------------------------- | ----------------------------- | ---------------------------------------------------------------------- |
69-
| `langfuse_postgres_data` | `/var/lib/postgresql/data` | Postgres application state: users, projects, API keys, prompts, config |
70-
| `langfuse_greptimedb_data` | `/greptimedb_data` | GreptimeDB analytics store: traces, observations, scores, dashboards |
71-
| `langfuse_redis_data` | `/data` | Redis queue state |
72-
| `langfuse_media_data` | `/langfuse_media_data` | Local media uploads when `LANGFUSE_MEDIA_STORAGE_BACKEND=local` |
68+
| Volume | Mounted path | Stores |
69+
| ---------------------------- | ----------------------------- | ----------------------------------------------------------------------------- |
70+
| `langfuse_postgres_data` | `/var/lib/postgresql/data` | Postgres application state: users, projects, API keys, prompts, config |
71+
| `langfuse_greptimedb_data` | `/greptimedb_data` | GreptimeDB analytics store: traces, observations, scores, dashboards |
72+
| `langfuse_redis_data` | `/data` | Redis queue state |
73+
| `langfuse_media_data` | `/langfuse_media_data` | Local media uploads when `LANGFUSE_MEDIA_STORAGE_BACKEND=local` |
7374
| `langfuse_event_data` | `/langfuse_event_data` | Local OTel carrier and eval blobs when `LANGFUSE_EVENT_STORAGE_BACKEND=local` |
74-
| `langfuse_minio_data` | `/data` in `minio` | Optional MinIO bucket data when the `s3` profile is enabled |
75+
| `langfuse_batch_export_data` | `/langfuse_batch_export_data` | Local batch-export files when `LANGFUSE_BATCH_EXPORT_STORAGE_BACKEND=local` |
76+
| `langfuse_minio_data` | `/data` in `minio` | Optional MinIO bucket data when the `s3` profile is enabled |
7577

7678
Runtime logs are not written to a separate application log directory by default. Web, worker, the standalone supervisor, Postgres, Redis, and GreptimeDB all write to container stdout/stderr; collect them through `docker compose logs` or your Docker logging driver. If you enable GreptimeDB file logging or replace Docker named volumes with bind mounts, keep the log directory outside the container's writable layer and include it in the same backup/retention plan as `langfuse_greptimedb_data`.
7779

78-
Do not run `docker compose down -v` on a real deployment unless you intentionally want to delete the service data. `docker compose down` removes containers and networks but keeps the named volumes; `down -v` removes them. If you replace the named volumes with bind mounts, keep the same container paths above and make sure the app containers can write to the media/event paths. In the split topology, both `langfuse-web` and `langfuse-worker` must mount the same `langfuse_media_data` and `langfuse_event_data` storage.
80+
Do not run `docker compose down -v` on a real deployment unless you intentionally want to delete the service data. `docker compose down` removes containers and networks but keeps the named volumes; `down -v` removes them. If you replace the named volumes with bind mounts, keep the same container paths above and make sure the app containers can write to the media/event paths. In the split topology, both `langfuse-web` and `langfuse-worker` must mount the same `langfuse_media_data`, `langfuse_event_data`, and `langfuse_batch_export_data` storage.
7981

8082
## 2. Migrations run automatically on startup
8183

docs/greptimedb-migration/07-deployment.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,14 @@ section.
4040

4141
Ingestion and eval-generated scores persist to GreptimeDB `raw_events`, not to a
4242
blob store. The remaining object-storage consumers — media uploads, the OTel
43-
ingestion carrier, and the eval observation blob store — all support a local-file
44-
backend, so a stock deployment needs **no** MinIO/S3 (only opt-in batch/blob
45-
exports still require an S3-compatible bucket):
46-
47-
| Variable | Default | Set to `local` for no object storage |
48-
| -------------------------------- | ------- | ------------------------------------- |
49-
| `LANGFUSE_MEDIA_STORAGE_BACKEND` | `s3` | `local` + `LANGFUSE_MEDIA_LOCAL_PATH` |
50-
| `LANGFUSE_EVENT_STORAGE_BACKEND` | `s3` | `local` + `LANGFUSE_EVENT_LOCAL_PATH` |
43+
ingestion carrier, the eval observation blob store, and batch exports — all
44+
support a local-file backend, so a stock deployment needs **no** MinIO/S3:
45+
46+
| Variable | Default | Set to `local` for no object storage |
47+
| --------------------------------------- | ------- | -------------------------------------------- |
48+
| `LANGFUSE_MEDIA_STORAGE_BACKEND` | `s3` | `local` + `LANGFUSE_MEDIA_LOCAL_PATH` |
49+
| `LANGFUSE_EVENT_STORAGE_BACKEND` | `s3` | `local` + `LANGFUSE_EVENT_LOCAL_PATH` |
50+
| `LANGFUSE_BATCH_EXPORT_STORAGE_BACKEND` | `s3` | `local` + `LANGFUSE_BATCH_EXPORT_LOCAL_PATH` |
5151

5252
`LANGFUSE_EVENT_STORAGE_BACKEND` covers both the OTel carrier and the eval blob
5353
store; with `local` they share a filesystem volume, so the API and worker must

docs/known-limitations.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ The web and standalone entrypoints apply the GreptimeDB schema on startup (gated
1818

1919
### Object storage is optional, not gone
2020

21-
Ingestion needs no object store — traces, observations, and scores persist to GreptimeDB `raw_events`. Media uploads, the OTel carrier, and the eval blob store default to local filesystem volumes in the bundled Compose files. But opt-in batch/blob **exports** still require an S3-compatible bucket. See [deployment](deployment.md).
21+
Ingestion needs no object store — traces, observations, and scores persist to GreptimeDB `raw_events`. Media uploads, the OTel carrier, the eval blob store, and batch exports all default to local filesystem volumes in the bundled Compose files, so a stock deployment runs with no S3/MinIO at all. Object storage stays fully supported (set the `*_STORAGE_BACKEND` variables to `s3`) for multi-node deployments, where a shared object store is the simpler way to share blobs across replicas. See [deployment](deployment.md).
2222

2323
## Source-of-truth exception
2424

0 commit comments

Comments
 (0)