Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
883b496
feat(rails): forward Rails.logger to PostHog Logs via OpenTelemetry
johnnagro Jun 4, 2026
ef893c9
fix(rails): drop incorrect service.version OTel resource attribute
johnnagro Jun 4, 2026
33ba5e7
test(rails): cover all severity mappings in appender spec
johnnagro Jun 4, 2026
921777c
refactor(rails): capture init options for logs instead of exposing cl…
johnnagro Jun 10, 2026
c29ffe5
docs(rails): document Appender's lock-free thread-safety design
johnnagro Jun 10, 2026
fd95ed4
fix(rails): match self-log prefix with start_with? to avoid over-supp…
johnnagro Jun 10, 2026
ca3c155
feat(rails): cap PostHog Logs forwarding at a configurable records-pe…
johnnagro Jun 10, 2026
0b07c9b
feat(rails): add logs_before_send callback to scrub or drop PostHog L…
johnnagro Jun 10, 2026
532165a
Update posthog-rails/lib/posthog/rails/logs/appender.rb
johnnagro Jun 12, 2026
8b45826
fix: fallback to 'unknown_service' rather than 'rails'
johnnagro Jun 12, 2026
e382f3c
fix(rails): emit progname as logger.name to match OTel logger-name co…
johnnagro Jun 12, 2026
b5836cd
docs(rails): note automatic trace_id/span_id correlation for apps usi…
johnnagro Jun 12, 2026
318e7d3
refactor(rails): expose a single :severity enum in the logs_before_se…
johnnagro Jun 12, 2026
8329bf4
fix(rails): run logs_before_send before the rate cap so dropped recor…
johnnagro Jun 12, 2026
c29da56
fix(rails): use OTel semconv names (url.full, http.request.method, ur…
johnnagro Jun 12, 2026
0c8a179
test(rails): cover body UTF-8 normalization and before_send string sa…
johnnagro Jun 12, 2026
bf8d01e
fix(rails): warn when logs_before_send returns a non-Hash value inste…
johnnagro Jun 12, 2026
6f338b7
fix(rails): coerce numeric strings for logs rate cap instead of silen…
johnnagro Jun 12, 2026
be99a7c
docs(rails): recommend require: false for the optional OpenTelemetry …
johnnagro Jun 12, 2026
059d712
fix(rails): warn when logs are enabled but PostHog.init never ran
johnnagro Jun 12, 2026
100bdf5
fix(rails): bound the at_exit logs flush with a 2s timeout so it can'…
johnnagro Jun 12, 2026
a148692
refactor(rails): rename forward_rails_logger to logs_forward_rails_lo…
johnnagro Jun 12, 2026
84bfd61
Remove logs_resource_attributes config; resource attrs are now fixed …
johnnagro Jun 12, 2026
9bcd5ad
Remove unused Setup.force_flush; shutdown! already flushes buffered r…
johnnagro Jun 12, 2026
e7f1f83
Warn once on invalid logs_level instead of silently falling back to t…
johnnagro Jun 12, 2026
b5267ac
Keep forwarding threshold out of Logger#level so BroadcastLogger pred…
johnnagro Jun 12, 2026
4687427
Guard Appender#add against re-entrant logging and warn once when emit…
johnnagro Jun 12, 2026
9f527a3
Add fork-safety spec pinning BatchLogRecordProcessor's post-fork rest…
johnnagro Jun 12, 2026
87b49fc
feat(rails): add changeset and fork-safety spec for PostHog Logs forw…
johnnagro Jun 12, 2026
ce9ca0e
Remove unused Severity.for; name_for and for_name cover the split emi…
johnnagro Jun 12, 2026
1dfe9a8
Merge remote-tracking branch 'origin/main' into feat/posthog-rails-ot…
johnnagro Jun 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/rails-posthog-logs-forwarding.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"posthog-ruby": minor
---

Add opt-in PostHog Logs support to posthog-rails: set `config.logs_enabled = true` to forward `Rails.logger` output to PostHog Logs over OpenTelemetry (OTLP), automatically correlated with the request's PostHog distinct ID and session ID (and active trace/span when OpenTelemetry tracing is present). Includes a configurable severity filter (`logs_level`), a rate cap (`logs_max_records_per_minute`, default 6,000/min), and a `logs_before_send` callback for scrubbing or dropping records. Relies on the optional OpenTelemetry gems; when they are absent the feature warns once and no-ops.
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ group :development, :test do
gem 'activesupport', '~> 7.1'
gem 'commander', '~> 5.0'
gem 'oj', '~> 3.16.10'
# Soft dependency of posthog-rails' PostHog Logs feature; present here only
# so the fork-safety spec can exercise the real BatchLogRecordProcessor.
gem 'opentelemetry-logs-sdk', require: false

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried running this as is, and got the following error

OpenTelemetry error: unexpected error in OTLP::Exporter#encode -
undefined method 'event_name' for an instance of OpenTelemetry::SDK::Logs::LogRecordData

My lockfile resolved to:

  opentelemetry-logs-sdk              0.4.0
  opentelemetry-exporter-otlp-logs    0.5.1

and it looks like the exporter expects log_record_data.event_name to exist, but opentelemetry-logs-sdk v0.4.0 defines LogRecordData without an event_name field.

a fresh bundle with the following fixed it:

  gem 'opentelemetry-sdk', require: false                # resolved to 0.6.0
  gem 'opentelemetry-logs-sdk', require: false
  gem 'opentelemetry-exporter-otlp-logs', require: false # resolved to 0.5.1

maybe we use something like this?

Suggested change
gem 'opentelemetry-logs-sdk', require: false
gem 'opentelemetry-logs-sdk', '>= 0.6.0', require: false
gem 'opentelemetry-exporter-otlp-logs', require: false

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might be worth a test using real exports from these libs

gem 'prettier'
gem 'railties', '~> 7.1'
gem 'rake', '~> 13.2.1'
Expand Down
27 changes: 27 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,25 @@ GEM
oj (3.16.16)
bigdecimal (>= 3.0)
ostruct (>= 0.2)
opentelemetry-api (1.8.0)
logger
opentelemetry-common (0.23.0)
opentelemetry-api (~> 1.0)
opentelemetry-logs-api (0.2.0)
opentelemetry-api (~> 1.0)
opentelemetry-logs-sdk (0.4.0)
opentelemetry-api (~> 1.2)
opentelemetry-logs-api (~> 0.1)
opentelemetry-sdk (~> 1.3)
opentelemetry-registry (0.4.0)
opentelemetry-api (~> 1.1)
opentelemetry-sdk (1.10.0)
opentelemetry-api (~> 1.1)
opentelemetry-common (~> 0.20)
opentelemetry-registry (~> 0.2)
opentelemetry-semantic_conventions
opentelemetry-semantic_conventions (1.36.0)
opentelemetry-api (~> 1.0)
ostruct (0.6.3)
parallel (1.27.0)
parser (3.3.11.1)
Expand Down Expand Up @@ -231,6 +250,7 @@ DEPENDENCIES
concurrent-ruby
irb
oj (~> 3.16.10)
opentelemetry-logs-sdk
posthog-ruby!
prettier
railties (~> 7.1)
Expand Down Expand Up @@ -284,6 +304,13 @@ CHECKSUMS
nokogiri (1.19.2-x86_64-linux-gnu) sha256=fa8feca882b73e871a9845f3817a72e9734c8e974bdc4fbad6e4bc6e8076b94f
nokogiri (1.19.2-x86_64-linux-musl) sha256=93128448e61a9383a30baef041bf1f5817e22f297a1d400521e90294445069a8
oj (3.16.16) sha256=3635b36128991796434f55da8decc0de236a323535adcb36fc04e6d0253c013d
opentelemetry-api (1.8.0) sha256=3af51183daf0f56a164bc1579782245be70a40678566b9a393cbe5af28ea87c6
opentelemetry-common (0.23.0) sha256=da721190479d57bae0ad2207468f47f3e2c3b9a91024b5bc32c9d280183eb32c
opentelemetry-logs-api (0.2.0) sha256=0e9241b9b53a315101a6a5f2efbfa80278a0d39f9220915871f8d6057c39b117
opentelemetry-logs-sdk (0.4.0) sha256=860a5a3916054bf8b37a8a0b565d7ebabc37691a7b505eb0d21b734619fe8074
opentelemetry-registry (0.4.0) sha256=903fa6bfaa29eac1c1d73a4fdd29b850977b5353b84b8cdff11222c00ad2968f
opentelemetry-sdk (1.10.0) sha256=43719949be8df24dcaeb86ebbf75636cda87d51a01af2729499b92a48b80521a
opentelemetry-semantic_conventions (1.36.0) sha256=c1b1607dbc7853aac7f9e23f6e8b76969c45b07f2b812a4aa4383c19a3b0f617
ostruct (0.6.3) sha256=95a2ed4a4bd1d190784e666b47b2d3f078e4a9efda2fccf18f84ddc6538ed912
parallel (1.27.0) sha256=4ac151e1806b755fb4e2dc2332cbf0e54f2e24ba821ff2d3dcf86bf6dc4ae130
parser (3.3.11.1) sha256=d17ace7aabe3e72c3cc94043714be27cc6f852f104d81aa284c2281aecc65d54
Expand Down
61 changes: 61 additions & 0 deletions posthog-rails/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,64 @@ SDK usage examples and code snippets live in the official documentation so they

- [Ruby on Rails framework docs](https://posthog.com/docs/libraries/ruby-on-rails)
- [Ruby library docs](https://posthog.com/docs/libraries/ruby)

## PostHog Logs (optional)

`posthog-rails` can forward `Rails.logger` output to [PostHog Logs](https://posthog.com/docs/logs)
over OpenTelemetry (OTLP), automatically correlated with the request's PostHog
distinct ID and session ID.

This is opt-in and relies on the standard OpenTelemetry gems (Ruby 3.3+), which
are not bundled. Add them to your `Gemfile`:

```ruby
gem 'opentelemetry-sdk', require: false
gem 'opentelemetry-logs-sdk', require: false
gem 'opentelemetry-exporter-otlp-logs', require: false
```

`require: false` keeps the gems off the boot path — `posthog-rails` requires
them only when logs are enabled. It also avoids `opentelemetry-logs-sdk`'s
load-time `Configurator` patch, which would otherwise piggyback a second logs
pipeline onto an existing `OpenTelemetry::SDK.configure` (tracing) call.

Then enable it in `config/initializers/posthog.rb`:

```ruby
PostHog::Rails.configure do |config|
config.logs_enabled = true
end
```

When the OpenTelemetry gems are absent, the feature logs a single warning and
no-ops, so it is safe to enable conditionally.

Forwarding is capped at 6,000 records per minute by default to protect your
ingestion quota from runaway log volume; when the cap trips, one warning record
is emitted and further records are dropped for the remainder of the window.
Tune or disable it with `config.logs_max_records_per_minute` (set to `nil` or
`0` to disable; numeric strings such as ENV values are coerced).

To scrub PII (or drop records entirely) before they leave the app, set
`config.logs_before_send` to a proc that receives each record hash and returns
a modified hash to send or `nil` to drop it. If the callback raises, the
record is dropped.

If your app already uses OpenTelemetry tracing, log records emitted during a
traced request automatically carry the active `trace_id`/`span_id` — no
configuration needed.

`config.logs_level` filters what is forwarded to PostHog; it never changes
what your app logs. Setting it below the Rails logger level (e.g. `:debug`
with an `:info` app) does not make Rails or ActiveRecord generate extra
output — only records the app actually produces are forwarded.

Known limitations of the broadcast approach:

- `Rails.logger.silence` does not silence forwarding — silenced records still
ship to PostHog (the silencer only lowers the level of loggers that support
`local_level`).
- `Rails.logger.tagged` tags (including `config.log_tags` request IDs) are not
attached to forwarded records, and the non-block form
(`Rails.logger.tagged('X')`) returns a logger that bypasses forwarding
entirely.
40 changes: 40 additions & 0 deletions posthog-rails/examples/posthog.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,46 @@
# # 'MyCustom404Error',
# # 'MyCustomValidationError'
# ]

# --------------------------------------------------------------------------
# POSTHOG LOGS (OpenTelemetry) - opt-in
# --------------------------------------------------------------------------
# Forward Rails.logger output to PostHog Logs over OTLP, automatically
Comment thread
turnipdabeets marked this conversation as resolved.
# correlated with the request's distinct_id and session_id.
#
# Requires the OpenTelemetry gems (Ruby 3.3+) in your Gemfile. Use
# require: false — posthog-rails loads them only when logs are enabled:
# gem 'opentelemetry-sdk', require: false
# gem 'opentelemetry-logs-sdk', require: false
# gem 'opentelemetry-exporter-otlp-logs', require: false
#
# Enable log forwarding (default: false)
# config.logs_enabled = true

# Broadcast Rails.logger into PostHog Logs (default: true when logs enabled)
# config.logs_forward_rails_logger = true

# Minimum severity to forward; nil inherits Rails.logger's level (default: nil)
# config.logs_level = :info

# Maximum records forwarded per minute, protecting your ingestion quota from
# runaway log volume. Numeric strings (e.g. from ENV) are coerced.
# (default: 6000; set to nil or 0 to disable the cap)
# config.logs_max_records_per_minute = 6_000

# Modify or drop log records before they are sent, e.g. to scrub PII.
# Receives a hash (:timestamp, :severity, :body, :attributes — :severity is
# a symbol such as :warn); return the (modified) hash to send or nil to
# drop. Records are dropped if the callback raises. (default: nil)
# config.logs_before_send = proc { |record|
# next nil if record[:severity] == :debug
#
# record[:body] = record[:body].gsub(/\b[\w.+-]+@[\w-]+\.[\w.]+\b/, '[redacted email]')
# record
# }

# Logs reuse the same project token (api_key) and host configured below, so
# there is nothing extra to set. Logs are sent to <host>/i/v1/logs.
end

# You can also configure Rails options directly:
Expand Down
8 changes: 8 additions & 0 deletions posthog-rails/lib/generators/posthog/install_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ def show_readme
say ' - POSTHOG_API_KEY (required)'
say ' - POSTHOG_PERSONAL_API_KEY (optional, for feature flags)'
say ''
say 'Optional: forward Rails.logger to PostHog Logs', :yellow
say ' - Add to your Gemfile (requires Ruby 3.3+):'
say " gem 'opentelemetry-sdk', require: false"
say " gem 'opentelemetry-logs-sdk', require: false"
say " gem 'opentelemetry-exporter-otlp-logs', require: false"
say ' - Set config.logs_enabled = true in the initializer'
say ' - Docs: https://posthog.com/docs/logs'
say ''
say 'For more information, see: https://posthog.com/docs/libraries/ruby'
say ''
end
Expand Down
40 changes: 40 additions & 0 deletions posthog-rails/lib/generators/posthog/templates/posthog.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,46 @@
# # 'MyCustom404Error',
# # 'MyCustomValidationError'
# ]

# --------------------------------------------------------------------------
# POSTHOG LOGS (OpenTelemetry) - opt-in
# --------------------------------------------------------------------------
# Forward Rails.logger output to PostHog Logs over OTLP, automatically
# correlated with the request's distinct_id and session_id.
#
# Requires the OpenTelemetry gems (Ruby 3.3+) in your Gemfile. Use
# require: false — posthog-rails loads them only when logs are enabled:
# gem 'opentelemetry-sdk', require: false
# gem 'opentelemetry-logs-sdk', require: false
# gem 'opentelemetry-exporter-otlp-logs', require: false
#
# Enable log forwarding (default: false)
# config.logs_enabled = true

# Broadcast Rails.logger into PostHog Logs (default: true when logs enabled)
# config.logs_forward_rails_logger = true

# Minimum severity to forward; nil inherits Rails.logger's level (default: nil)
# config.logs_level = :info

# Maximum records forwarded per minute, protecting your ingestion quota from
# runaway log volume. Numeric strings (e.g. from ENV) are coerced.
# (default: 6000; set to nil or 0 to disable the cap)
# config.logs_max_records_per_minute = 6_000

# Modify or drop log records before they are sent, e.g. to scrub PII.
# Receives a hash (:timestamp, :severity, :body, :attributes — :severity is
# a symbol such as :warn); return the (modified) hash to send or nil to
# drop. Records are dropped if the callback raises. (default: nil)
# config.logs_before_send = proc { |record|
# next nil if record[:severity] == :debug
#
# record[:body] = record[:body].gsub(/\b[\w.+-]+@[\w-]+\.[\w.]+\b/, '[redacted email]')
# record
# }

# Logs reuse the same project token (api_key) and host configured below, so
# there is nothing extra to set. Logs are sent to <host>/i/v1/logs.
end

# You can also configure Rails options directly:
Expand Down
4 changes: 4 additions & 0 deletions posthog-rails/lib/posthog/rails.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
require 'posthog/rails/rescued_exception_interceptor'
require 'posthog/rails/active_job'
require 'posthog/rails/error_subscriber'
require 'posthog/rails/logs/severity'
require 'posthog/rails/logs/rate_limiter'
require 'posthog/rails/logs/appender'
require 'posthog/rails/logs/setup'
require 'posthog/rails/railtie'

module PostHog
Expand Down
31 changes: 31 additions & 0 deletions posthog-rails/lib/posthog/rails/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
module PostHog
module Rails
class Configuration
# Default cap on log records forwarded to PostHog Logs per minute.
DEFAULT_LOGS_MAX_RECORDS_PER_MINUTE = 6_000

# @return [Boolean] Whether to automatically capture exceptions from Rails. Defaults to false.
attr_accessor :auto_capture_exceptions

Expand All @@ -33,6 +36,29 @@ class Configuration
# posthog_distinct_id, distinct_id, id, pk, uuid in order.
attr_accessor :user_id_method

# @return [Boolean] Master switch for forwarding logs to PostHog Logs over OTLP. Defaults to false.
attr_accessor :logs_enabled

# @return [Boolean] Whether to broadcast Rails.logger output into the PostHog Logs sink. Defaults to true
# (only takes effect when {#logs_enabled} is true).
attr_accessor :logs_forward_rails_logger

# @return [Integer, Symbol, nil] Minimum severity to forward to PostHog Logs. When nil, inherits the
# current Rails.logger level. Accepts a Logger severity constant (e.g. Logger::INFO) or symbol (:info).
attr_accessor :logs_level

# @return [Integer, String, nil] Maximum log records forwarded to PostHog Logs per minute, protecting
# the ingestion quota from runaway log volume. Defaults to 6000. Numeric strings (e.g. from ENV) are
# coerced. Set to nil, 0, or a negative value to disable the cap; an unparseable value falls back to
# the default with a warning.
attr_accessor :logs_max_records_per_minute

# @return [Proc, nil] Callback invoked with each log record hash (:timestamp, :severity, :body,
# :attributes — where :severity is a symbol such as :warn) before it is sent to PostHog Logs.
# Return a (possibly modified) hash to send, or nil to drop the record — useful for scrubbing
# PII. If the callback raises, the record is dropped. Defaults to nil.
attr_accessor :logs_before_send

# @return [PostHog::Rails::Configuration]
def initialize
@auto_capture_exceptions = false
Expand All @@ -43,6 +69,11 @@ def initialize
@capture_user_context = true
@current_user_method = :current_user
@user_id_method = nil
@logs_enabled = false
@logs_forward_rails_logger = true
@logs_level = nil
@logs_max_records_per_minute = DEFAULT_LOGS_MAX_RECORDS_PER_MINUTE
@logs_before_send = nil
end

# Default exceptions that Rails apps typically don't want to track.
Expand Down
Loading