Stream runtime logs from any Fly.io app to BunnyLogs — either directly from your Python app using the bunnylogs handler, or at the infrastructure level using Fly.io's native log drain.
Install bunnylogs and attach it as a logging handler. Logs are shipped in a background thread so your app is never blocked.
pip install bunnylogs
import logging
from bunnylogs import BunnyLogsHandler
logger = logging.getLogger()
logger.addHandler(BunnyLogsHandler("<uuid>"))
logger.info("app started")
logger.error("something went wrong")
For Django or Flask apps on Fly.io, add the handler in your settings / app factory so it is wired up at startup:
# Django — settings.py
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"handlers": {
"bunnylogs": {
"class": "bunnylogs.BunnyLogsHandler",
"uuid": "<uuid>",
}
},
"root": {"handlers": ["bunnylogs"], "level": "INFO"},
}
Store the UUID as a Fly.io secret rather than hard-coding it:
fly secrets set BUNNYLOGS_UUID=<uuid> --app <app-name>
import os, logging
from bunnylogs import BunnyLogsHandler
logging.getLogger().addHandler(BunnyLogsHandler(os.environ["BUNNYLOGS_UUID"]))
Fly.io can forward all stdout/stderr from your app to an external HTTP endpoint. Each log line is POSTed as JSON. BunnyLogs accepts JSON POSTs natively — no adapter or forwarder required.
https://bunnylogs.com/live/<uuid>).fly logs ship create https://bunnylogs.com/live/<uuid> --app <app-name>
Fly.io will immediately start POSTing log entries to BunnyLogs as JSON. The message and timestamp fields are mapped automatically. The program field defaults to api — you can override it in your app's log output or by using a Vector transform (see below).
To verify the drain is active:
fly logs ship list --app <app-name>
To remove it:
fly logs ship delete <drain-id> --app <app-name>
If you want to enrich log entries with the Fly.io app name, region, or instance ID before they reach BunnyLogs, deploy the fly-log-shipper — Fly.io's official Vector-based log forwarder. Configure an http sink in your vector.toml:
[sinks.bunnylogs]
type = "http"
inputs = ["my_transform"]
uri = "https://bunnylogs.com/live/<uuid>"
method = "post"
encoding.codec = "json"
[sinks.bunnylogs.request.headers]
Content-Type = "application/json"
In the transform step, remap fields to BunnyLogs' schema:
[transforms.my_transform]
type = "remap"
inputs = ["fly_logs"]
source = '''
.message = .message
.level = upcase(string!(.log.level)) ?? "INFO"
.program = .fly.app.name
.timestamp = .timestamp
'''
To notify BunnyLogs whenever a new version is deployed, add a release_command to fly.toml:
[deploy]
release_command = "curl -s -d 'message=Deploying $FLY_APP_NAME' -d 'level=INFO' -d 'program=fly/$FLY_APP_NAME' https://bunnylogs.com/live/<uuid>"
Or use a Dockerfile entrypoint script that POSTs a startup message before handing off to the main process:
#!/bin/sh
curl -s \
-d "message=Started: $FLY_APP_NAME in $FLY_REGION" \
-d "level=INFO" \
-d "program=fly/$FLY_APP_NAME" \
https://bunnylogs.com/live/$BUNNYLOGS_UUID
exec "$@"
| Variable | Value |
|---|---|
$FLY_APP_NAME | Name of the Fly.io app |
$FLY_REGION | Three-letter region code, e.g. iad, ams |
$FLY_ALLOC_ID | Unique ID for this VM instance |
$FLY_IMAGE_REF | Docker image reference for the running build |
$FLY_PROCESS_GROUP | Process group name (e.g. app, worker) |
fly secret — never hard-code it in fly.toml or commit it to your repository.level=ERROR and program contains fly/ to get notified on errors via Slack, Telegram, or Discord.program field.