OnlyLogs

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 429 is honoured: the logger waits for Retry-After and keeps the batch. Any other 4xx (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 SocketLogger only 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 the HttpLogger instead.

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::BroadcastLogger with 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:

  • POST a plain-text body to your ONLYLOGS_DRAIN_URL. It is appended verbatim to the project's drain.log, plus a trailing newline when the body has none. Nothing is parsed or reformatted.
  • Bodies compressed with Content-Encoding: gzip are accepted.
  • 200 means stored. 403 means the project is paused (you can pause and resume a project from its page; paused lines are discarded). 404 means the drain code is unknown.
  • drain.log is rotated at 10 GB into drain.log.1, drain.log.2 and drain.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.