Stream logs to onlylogs.io
If self-hosting your logs is not what you want, onlylogs.io has you covered. There are many reasons why you might not want to self-host, for example ephemeral disks on Heroku or deploio, or you simply want your logs to live in a space separated from your application.
This page explains all the options you have to stream your logs to onlylogs.io.
Create a new project on this website to receive an ONLYLOGS_DRAIN_URL, then configure your application using one of the methods below.
PaaS log drains
Most platform-as-a-service providers offer a built-in way to forward stdout to an HTTPS endpoint. Point it at your ONLYLOGS_DRAIN_URL and you're done.
Heroku
Heroku has first-class log drain support:
heroku drains:add <ONLYLOGS_DRAIN_URL>
Heroku sends each line wrapped in its syslog framing (<40>1 2026-04-17T12:34:56+00:00 host app web.1 - ...). onlylogs.io stores lines exactly as they arrive, so expect that prefix in front of every line.
Other Heroku-style PaaS
Providers that advertise Heroku-compatible log drains (Scalingo's scalingo log-drains-add, Clever Cloud's log drain configuration, etc.) expect the same HTTPS POST shape that onlylogs.io accepts. Consult your provider's drain documentation and point it at your ONLYLOGS_DRAIN_URL.
Vector
For platforms without native drains (Fly.io, Kubernetes, bare metal, Docker Compose), deploy a log shipper such as Vector or Fluent Bit and ship to your ONLYLOGS_DRAIN_URL. The endpoint accepts newline-delimited plain text over HTTPS POST.
Example Vector configuration:
[sinks.onlylogs]
type = "http"
inputs = ["your_source"]
uri = "${ONLYLOGS_DRAIN_URL}"
method = "post"
encoding.codec = "text"
framing.method = "newline_delimited"
Fly.io users can deploy the fly-log-shipper app and set its Vector sink to the snippet above.
Dokku
Dokku forwards logs via its built-in Vector plugin. Use the text codec so Rails lines arrive verbatim (rather than wrapped in Docker metadata). <URL_ENCODED_DRAIN_URL> is your drain URL passed through CGI.escape:
dokku logs:set <app-name> vector-sink "http://?uri=<URL_ENCODED_DRAIN_URL>&encoding[codec]=text&method=post"
Ruby on Rails
If your platform doesn't expose a log drain — or you want log shipping from every process type (Puma, GoodJob, Sidekiq, rake, migrations) — use the onlylogs gem, version 0.10.0 or later. It includes two loggers: HttpLogger (recommended) and SocketLogger (for Puma-only deployments).
Both loggers write every line to $stdout as well, so your platform's own log viewer keeps working while lines are streamed to onlylogs.io.
HttpLogger (recommended)
The HttpLogger ships logs directly via HTTP from any Ruby process: Puma (including cluster mode), GoodJob, Sidekiq, rake tasks, migrations. No sidecar or Puma plugin required.
# config/environments/production.rb
config.logger = Onlylogs::HttpLogger.new
Set the ONLYLOGS_DRAIN_URL environment variable and you're done. Without it, the logger logs locally only and prints a warning at boot.
Optional environment variables:
| Variable | Default | Description |
|---|---|---|
ONLYLOGS_DRAIN_URL |
— | The drain URL (required) |
ONLYLOGS_BATCH_SIZE |
100 |
Number of lines to batch before sending |
ONLYLOGS_FLUSH_INTERVAL |
0.5 |
Flush interval in seconds |
ONLYLOGS_MAX_QUEUE_SIZE |
10000 |
Lines held in memory before new ones are dropped |
ONLYLOGS_MAX_BATCH_BYTES |
1048576 |
Upper bound of a request body; longer lines are truncated and marked |
ONLYLOGS_OPEN_TIMEOUT |
0.5 |
Connect timeout in seconds |
ONLYLOGS_READ_TIMEOUT |
0.5 |
Response timeout in seconds |
ONLYLOGS_CIRCUIT_COOLDOWN |
30 |
Seconds to stop sending after three consecutive failures |
ONLYLOGS_KEEP_ALIVE_TIMEOUT |
30 |
Seconds an idle connection is kept for reuse |
ONLYLOGS_SPOOL_DIR |
tmp/onlylogs/spool |
Where undelivered batches are buffered; set it empty to disable the spool |
ONLYLOGS_SPOOL_MAX_BYTES |
134217728 |
Size of the on-disk spool (128 MB); oldest batches are evicted first |
Or pass them as keyword arguments:
config.logger = Onlylogs::HttpLogger.new(
drain_url: "https://sink-1.onlylogs.io/drain/your-token",
batch_size: 50,
flush_interval: 1.0
)
Invalid values never prevent the app from booting: a non-numeric or non-positive number falls back to its default with a warning, and a drain URL that is not http(s) falls back to local-only logging.
When onlylogs.io is unreachable
The logger is built so that a slow or dead drain never stalls your app and an outage never loses logs:
- Sending happens on a background thread. Log calls only enqueue a line, and the queue is bounded, so memory cannot grow without limit.
- Timeouts are short (half a second by default). After three consecutive failures the circuit opens and no request is attempted for about the cooldown period.
- Batches that could not be delivered are written to the on-disk spool and replayed once the drain answers again, and on the next boot. Delivery is at-least-once: a batch whose response was lost is replayed and shows up twice.
- A
429is honoured: the logger waits forRetry-Afterand keeps the batch. Any other4xx(unknown token, paused project, body too big) is dropped with a warning, since retrying cannot help.
Dropping noisy lines
Lines you never want to store, such as health checks or asset requests, can be filtered out before they are sent. Anything matching one of the patterns is discarded:
# config/environments/production.rb
config.logger = Onlylogs::HttpLogger.new
config.logger.formatter.denylist = [/health_check/, /\/assets\//]
Since storage is what you pay for, this is the cheapest way to keep your bill down.
SocketLogger + Puma sidecar
If you prefer the sidecar approach (lower latency, useful when Puma is the only process that needs log shipping):
# config/environments/production.rb
config.logger = Onlylogs::SocketLogger.new
This writes logs to ONLYLOGS_SIDECAR_SOCKET (default: tmp/sockets/onlylogs-sidecar.sock). From there, stream them to onlylogs.io using a Puma sidecar process:
# config/puma.rb
plugin :onlylogs_sidecar
Then configure the ONLYLOGS_DRAIN_URL environment variable.
You can also run the sidecar process separately:
bundle exec onlylogs_sidecar
Note: the
SocketLoggeronly works when the sidecar process is running. Background jobs and migrations that run outside of Puma will not have access to the socket. If you need log shipping from all process types, use theHttpLoggerinstead.
Keeping local files while streaming
Use HttpLogger's built-in local_fallback parameter to write to a local file and stream to onlylogs.io with a single logger:
# config/environments/production.rb
log_file = Logger::LogDevice.new(
Rails.root.join("log", "production.log"), shift_age: 5, shift_size: 100.megabytes
)
config.logger = Onlylogs::HttpLogger.new(local_fallback: log_file)
Every log line is written to the local file and forwarded to onlylogs.io. The LogDevice handles rotation (keep 5 files of 100 MB each in the example above). If the remote connection fails, logs continue to be written locally without interruption.
Warning — do not use
ActiveSupport::BroadcastLoggerwith two tagged loggers. This is a known Rails bug: rails/rails#56669.# DO NOT do this: local = Onlylogs::Logger.new("production.log", 5, 100.megabytes) remote = Onlylogs::HttpLogger.new config.logger = ActiveSupport::BroadcastLogger.new(local, remote)
Recommended log format
You don't need our gem to stream logs to onlylogs.io: any log drain (Heroku, Dokku, Vector, Fluent Bit, ...) can forward a plain-text stream. The backend accepts arbitrary lines, but here is a good default:
[2026-04-17T12:34:56+00:00] [I] Your log message here
That is: an ISO 8601 timestamp in square brackets, the severity letter (D, I, W, E, F) in square brackets, then the message.
In a Rails app you can produce this format with a one-line formatter — no gem required:
# config/environments/production.rb
config.log_formatter = proc do |severity, time, _progname, msg|
"[#{time.iso8601}] [#{severity[0]}] #{msg}\n"
end
This pairs well with config.log_tags = [:request_id] for per-request correlation, and works the same whether you're logging to $stdout (Heroku, Dokku) or to a file that Vector tails.
How the drain endpoint behaves
Whatever ships your logs, the endpoint is the same:
POSTa plain-text body to yourONLYLOGS_DRAIN_URL. It is appended verbatim to the project'sdrain.log, plus a trailing newline when the body has none. Nothing is parsed or reformatted.- Bodies compressed with
Content-Encoding: gzipare accepted. 200means stored.403means the project is paused (you can pause and resume a project from its page; paused lines are discarded).404means the drain code is unknown.drain.logis rotated at 10 GB intodrain.log.1,drain.log.2anddrain.log.3; older rotations are discarded. Every file can be downloaded or deleted from the project page.
Search your logs from an AI agent
onlylogs.io is also an MCP server, so an agent can search your logs the way you would. The endpoint is https://onlylogs.io/mcp, protected by OAuth 2.1: the first connection opens a browser window where you sign in with your onlylogs.io account, and the agent then sees exactly the projects you can see.
With Claude Code:
claude mcp add --transport http onlylogs https://onlylogs.io/mcp
Any client that supports Streamable HTTP with OAuth (dynamic client registration and PKCE) works the same way.
Two tools are exposed: list_projects returns your projects and their files with sizes and last-modified times, and search_logs greps a byte window of one file, literally or with a regular expression. Log files can be several gigabytes, so the tool descriptions teach the agent to bisect a file on the timestamps of its lines instead of reading it end to end.