Skip to content

feat(rails): forward Rails.logger to PostHog Logs via OpenTelemetry#167

Open
johnnagro wants to merge 31 commits into
PostHog:mainfrom
noreastergroup:feat/posthog-rails-otel-logs
Open

feat(rails): forward Rails.logger to PostHog Logs via OpenTelemetry#167
johnnagro wants to merge 31 commits into
PostHog:mainfrom
noreastergroup:feat/posthog-rails-otel-logs

Conversation

@johnnagro

@johnnagro johnnagro commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

💡 Motivation and Context

Adds an opt-in integration that forwards Rails.logger output to PostHog Logs
(https://posthog.com/docs/logs) over OpenTelemetry/OTLP, automatically correlated
with the request's distinct_id and session_id captured by the existing
request-context middleware.
The OpenTelemetry gems are optional/soft dependencies loaded lazily, so the
feature no-ops with a single warning when absent (and on Ruby < 3.3 where the
logs SDK is unsupported). The logs token and host are derived from the configured
client (config.api_key / config.host) with ENV fallbacks, so logs always
target the same project as analytics. Disabled by default; enabled via
PostHog::Rails.config.logs_enabled.

💚 How did you test it?

  • Unit specs for the appender, setup (gems present/absent + token/host resolution),
    and railtie broadcast wiring (full suite green on Ruby 3.2.2, rubocop clean).
  • End-to-end in an ephemeral rails new app against a real PostHog project:
    Rails.logger output appears in PostHog Logs.

📝 Checklist

  • I reviewed the submitted code.
  • I added tests to verify the changes.
  • I updated the docs if needed.
  • No breaking change or entry added to the changelog.
Screen Shot 2026-06-04 at 7 24 04 PM

@johnnagro johnnagro requested a review from a team as a code owner June 4, 2026 18:30
@greptile-apps

greptile-apps Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
posthog-rails/lib/posthog/rails/logs/setup.rb:136-140
The `service.version` OTel resource attribute is being set to `PostHog::VERSION`, which is the SDK/integration gem version — not the Rails application's version. Per OpenTelemetry semantic conventions, `service.version` represents the deployed application version, so this would cause every service to report the PostHog gem version as its "version" in PostHog Logs. Consider using `telemetry.sdk.version` for the SDK version instead, and omitting `service.version` from the defaults (users can supply it via `logs_resource_attributes`).

```suggestion
            attrs = {
              'service.name' => service_name,
              'telemetry.sdk.name' => 'posthog-rails',
              'telemetry.sdk.version' => PostHog::VERSION,
              'deployment.environment' => ::Rails.env.to_s
            }
```

### Issue 2 of 2
spec/posthog/rails/logs/appender_spec.rb:40-46
Non-parametric severity tests per project convention — `Severity::MAPPING` defines 6 entries (DEBUG→5, INFO→9, WARN→13, ERROR→17, FATAL→21, UNKNOWN→9) but only INFO and ERROR are verified. An RSpec `using` table (e.g., a `described_class` parametric approach with `let`/`subject` per row, or a shared example loop over `MAPPING`) would cover all cases with no duplication and catch a mapping regression for WARN, FATAL, or the UNKNOWN→INFO fallback.

Reviews (1): Last reviewed commit: "feat(rails): forward Rails.logger to Pos..." | Re-trigger Greptile

Comment thread posthog-rails/lib/posthog/rails/logs/setup.rb Outdated
Comment thread spec/posthog/rails/logs/appender_spec.rb Outdated
@turnipdabeets

Copy link
Copy Markdown
Contributor

Cc @PostHog/logs

Comment thread lib/posthog/client.rb Outdated
Comment thread lib/posthog/client.rb Outdated
Comment thread posthog-rails/lib/posthog/rails/logs/appender.rb Outdated
Comment thread posthog-rails/lib/posthog/rails/logs/appender.rb Outdated
Comment thread posthog-rails/lib/posthog/rails/logs/setup.rb
Comment thread posthog-rails/lib/posthog/rails/logs/appender.rb Outdated
Comment thread posthog-rails/lib/posthog/rails/logs/setup.rb
Comment thread posthog-rails/lib/posthog/rails/logs/setup.rb
Comment thread posthog-rails/examples/posthog.rb
Comment thread posthog-rails/lib/posthog/rails/railtie.rb
@johnnagro

Copy link
Copy Markdown
Contributor Author

Thanks for taking the time to look this over and leave such thoughtful comments, I appreciate it @turnipdabeets @DanielVisca

I made some changes based on your feedback. There is one open question about feature flags.

Comment thread posthog-rails/lib/posthog/rails/logs/appender.rb Outdated
Comment thread posthog-rails/lib/posthog/rails/logs/appender.rb
Comment thread posthog-rails/lib/posthog/rails/logs/setup.rb
Comment thread posthog-rails/lib/posthog/rails/logs/setup.rb Outdated
Comment thread posthog-rails/lib/posthog/rails/configuration.rb Outdated
Comment thread posthog-rails/lib/posthog/rails/logs/appender.rb Outdated
Comment thread posthog-rails/lib/posthog/rails/logs/appender.rb Outdated
record = apply_before_send(record)
return if record.nil?

@otel_logger.on_emit(**record)

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.

Can we include traceId/spanId? The other SDKs' log records carry the trace fields, and the OTel logs SDK's on_emit should resolve the current active span from ambient context (or an explicit context: argument) — so apps already running OTel tracing may get logs↔trace correlation for free here. Worth verifying against our gem versions: if it already works; if not, it's likely a one-line context: argument.

@johnnagro johnnagro Jun 12, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Okay so, this "just works", I verified it against the source: on_emit defaults context: OpenTelemetry::Context.current and fills the top-level trace_id/span_id/trace_flags from the active span (https://github.com/open-telemetry/opentelemetry-ruby/blob/main/logs_sdk/lib/opentelemetry/sdk/logs/logger.rb). Apps that run the tracing instrumentation gems (opentelemetry-instrumentation-rails/-rack) get logs↔trace correlation for free; since our appender emits synchronously on the calling thread, the ambient context is the traced request's own.

My smoke test has no tracer, so the trace fields are empty as expected:

Screen Shot 2026-06-11 at 8 34 13 PM

I did add a line to the README documenting that traced requests get trace_id/span_id on their log records automatically.

Thoughts?

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 think as it's document that the tracing instrumentation gems are needed it should be ok. Ideally down the road we'd populate trace context ourselves so no extra gems are needed.

Comment thread posthog-rails/lib/posthog/rails/logs/appender.rb Outdated
Comment thread posthog-rails/lib/posthog/rails/logs/setup.rb Outdated
@turnipdabeets

Copy link
Copy Markdown
Contributor

PR is looking really good. We'll also need a .changeset file for release automation system to run

@johnnagro johnnagro requested a review from turnipdabeets June 12, 2026 02:12
@johnnagro

Copy link
Copy Markdown
Contributor Author

thanks, @turnipdabeets - please take another look

Comment thread posthog-rails/lib/posthog/rails/logs/severity.rb Outdated
@turnipdabeets

Copy link
Copy Markdown
Contributor

@johnnagro "Commits must have verified signatures" in order to merge - mind signing these?

johnnagro and others added 14 commits June 12, 2026 10:49
Add an opt-in integration that broadcasts Rails.logger output to PostHog
Logs over OTLP, automatically correlated with the request's distinct_id
and session_id captured by the existing request-context middleware.

The OpenTelemetry gems (opentelemetry-sdk, opentelemetry-logs-sdk,
opentelemetry-exporter-otlp-logs) are optional/soft dependencies loaded
lazily at runtime, so the feature no-ops with a single warning when they
are absent (and on Ruby < 3.3 where the logs SDK is unsupported).

The logs token and host are derived from the configured PostHog client
(config.api_key / config.host) with ENV fallbacks, so logs always target
the same project as analytics. Exposes api_key/host readers on the core
client to support this.

Disabled by default; enable via PostHog::Rails.config.logs_enabled.
Co-authored-by: Anna Garcia <11654201+turnipdabeets@users.noreply.github.com>
johnnagro added 16 commits June 12, 2026 10:49
…icates and level= are unaffected; document silence/tagged gaps
@johnnagro johnnagro force-pushed the feat/posthog-rails-otel-logs branch from dddc3a8 to ce9ca0e Compare June 12, 2026 14:53
@johnnagro

Copy link
Copy Markdown
Contributor Author

@johnnagro "Commits must have verified signatures" in order to merge - mind signing these?

np - signed.

…el-logs

# Conflicts:
#	posthog-rails/README.md
@johnnagro johnnagro requested a review from turnipdabeets June 12, 2026 17:36

@dustinbyrne dustinbyrne left a comment

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.

Hey @johnnagro, thanks for this! I took a look and have a few comments, but otherwise this is looking pretty solid

Comment on lines +86 to +91
def remember_client_options(options)
return unless options.is_a?(Hash)

@client_api_key = options[:api_key] || options['api_key']
@client_host = options[:host] || options['host']
end

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.

We should also consider test_mode here so that we don't emit logs in test

# Disable in test environment
config.test_mode = true if Rails.env.test?

Comment on lines +207 to +211
def body_for(message)
str = message.is_a?(String) ? message.dup : message.inspect
str = str.encode(Encoding::UTF_8, invalid: :replace, undef: :replace) unless str.encoding == Encoding::UTF_8
str.valid_encoding? ? str : str.scrub
end

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.

If logging an exception, e.g.:

Rails.logger.error(ex)

It'll render as something like <#RuntimeError: unknown error occurred> instead of including the backtrace

we should consider doing something like Logger::Formatter#msg2str

Comment on lines +39 to +43
def install!
return @appender if @installed

@installed = true
return nil unless require_otel_gems

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.

Worth checking here if the PostHog client is disabled and skipping installation if it is

We have this pattern in a lot of methods internal to the client:

def capture_exception(exception, distinct_id = nil, additional_properties = {}, flags: nil)
return false if @disabled

def capture(attrs)
return false if @disabled

@disabled isn't exposed, but I think it's fine to expose it as a public method. Maybe we opt for the positive case, though - client.enabled?

#
# @param timeout [Numeric] Max seconds to spend; see {SHUTDOWN_TIMEOUT_SECONDS}.
# @return [void]
def shutdown!(timeout: SHUTDOWN_TIMEOUT_SECONDS)

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.

nit considering this is private API space, but I think we should prefer a non-bang method name here (just shutdown)

it doesn't have a non-bang form, and it's not at risk of raising an exception

install! maybe has a stronger case given it's highly state changing, but I think we could do a non-bang there too

Comment thread Gemfile
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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants