posthog-rails 3.11.1 → 3.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/generators/posthog/install_generator.rb +8 -0
- data/lib/generators/posthog/templates/posthog.rb +42 -0
- data/lib/posthog/rails/configuration.rb +31 -0
- data/lib/posthog/rails/logs/appender.rb +263 -0
- data/lib/posthog/rails/logs/rate_limiter.rb +48 -0
- data/lib/posthog/rails/logs/setup.rb +256 -0
- data/lib/posthog/rails/logs/severity.rb +49 -0
- data/lib/posthog/rails/railtie.rb +71 -2
- data/lib/posthog/rails.rb +4 -0
- metadata +7 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2fadda53222559f077f30facbf0d0ecf6edfba62e116aaf5eb48bba8bd8cac5f
|
|
4
|
+
data.tar.gz: 4bc7f6753ef7bd80fca9418c679085ae894441b5efcb06bd4d31348a30ec2ef7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ddf76534a44251b45498328de34a5414b27aa3886de584c2f62b953658a5fafa2f77a6dac0f4938502e4a86dc905b35f7f5e4423fcb1e981995552cb6ea0fe02
|
|
7
|
+
data.tar.gz: 2f117183afbf4ec5cce029afe3e4170739b1f52707c4465a2a3312891bf10cc2a0d0202c37622ea32e6ccae6d4e2e211057b8fdbe1851cdf66d33414de086a8e
|
|
@@ -23,6 +23,14 @@ module Posthog
|
|
|
23
23
|
say ' - POSTHOG_API_KEY (required)'
|
|
24
24
|
say ' - POSTHOG_PERSONAL_API_KEY (optional, for feature flags)'
|
|
25
25
|
say ''
|
|
26
|
+
say 'Optional: forward Rails.logger to PostHog Logs', :yellow
|
|
27
|
+
say ' - Add to your Gemfile (requires Ruby 3.3+):'
|
|
28
|
+
say " gem 'opentelemetry-sdk', require: false"
|
|
29
|
+
say " gem 'opentelemetry-logs-sdk', '>= 0.6.0', require: false"
|
|
30
|
+
say " gem 'opentelemetry-exporter-otlp-logs', require: false"
|
|
31
|
+
say ' - Set config.logs_enabled = true in the initializer'
|
|
32
|
+
say ' - Docs: https://posthog.com/docs/logs'
|
|
33
|
+
say ''
|
|
26
34
|
say 'For more information, see: https://posthog.com/docs/libraries/ruby'
|
|
27
35
|
say ''
|
|
28
36
|
end
|
|
@@ -43,6 +43,48 @@ PostHog::Rails.configure do |config|
|
|
|
43
43
|
# # 'MyCustom404Error',
|
|
44
44
|
# # 'MyCustomValidationError'
|
|
45
45
|
# ]
|
|
46
|
+
|
|
47
|
+
# --------------------------------------------------------------------------
|
|
48
|
+
# POSTHOG LOGS (OpenTelemetry) - opt-in
|
|
49
|
+
# --------------------------------------------------------------------------
|
|
50
|
+
# Forward Rails.logger output to PostHog Logs over OTLP, automatically
|
|
51
|
+
# correlated with the request's distinct_id and session_id.
|
|
52
|
+
#
|
|
53
|
+
# Requires the OpenTelemetry gems (Ruby 3.3+) in your Gemfile. Use
|
|
54
|
+
# require: false — posthog-rails loads them only when logs are enabled.
|
|
55
|
+
# logs-sdk must be >= 0.6.0 (older versions lack LogRecordData#event_name,
|
|
56
|
+
# which the OTLP exporter needs, raising NoMethodError on export):
|
|
57
|
+
# gem 'opentelemetry-sdk', require: false
|
|
58
|
+
# gem 'opentelemetry-logs-sdk', '>= 0.6.0', require: false
|
|
59
|
+
# gem 'opentelemetry-exporter-otlp-logs', require: false
|
|
60
|
+
#
|
|
61
|
+
# Enable log forwarding (default: false)
|
|
62
|
+
# config.logs_enabled = true
|
|
63
|
+
|
|
64
|
+
# Broadcast Rails.logger into PostHog Logs (default: true when logs enabled)
|
|
65
|
+
# config.logs_forward_rails_logger = true
|
|
66
|
+
|
|
67
|
+
# Minimum severity to forward; nil inherits Rails.logger's level (default: nil)
|
|
68
|
+
# config.logs_level = :info
|
|
69
|
+
|
|
70
|
+
# Maximum records forwarded per minute, protecting your ingestion quota from
|
|
71
|
+
# runaway log volume. Numeric strings (e.g. from ENV) are coerced.
|
|
72
|
+
# (default: 6000; set to nil or 0 to disable the cap)
|
|
73
|
+
# config.logs_max_records_per_minute = 6_000
|
|
74
|
+
|
|
75
|
+
# Modify or drop log records before they are sent, e.g. to scrub PII.
|
|
76
|
+
# Receives a hash (:timestamp, :severity, :body, :attributes — :severity is
|
|
77
|
+
# a symbol such as :warn); return the (modified) hash to send or nil to
|
|
78
|
+
# drop. Records are dropped if the callback raises. (default: nil)
|
|
79
|
+
# config.logs_before_send = proc { |record|
|
|
80
|
+
# next nil if record[:severity] == :debug
|
|
81
|
+
#
|
|
82
|
+
# record[:body] = record[:body].gsub(/\b[\w.+-]+@[\w-]+\.[\w.]+\b/, '[redacted email]')
|
|
83
|
+
# record
|
|
84
|
+
# }
|
|
85
|
+
|
|
86
|
+
# Logs reuse the same project token (api_key) and host configured below, so
|
|
87
|
+
# there is nothing extra to set. Logs are sent to <host>/i/v1/logs.
|
|
46
88
|
end
|
|
47
89
|
|
|
48
90
|
# You can also configure Rails options directly:
|
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
module PostHog
|
|
8
8
|
module Rails
|
|
9
9
|
class Configuration
|
|
10
|
+
# Default cap on log records forwarded to PostHog Logs per minute.
|
|
11
|
+
DEFAULT_LOGS_MAX_RECORDS_PER_MINUTE = 6_000
|
|
12
|
+
|
|
10
13
|
# @return [Boolean] Whether to automatically capture exceptions from Rails. Defaults to false.
|
|
11
14
|
attr_accessor :auto_capture_exceptions
|
|
12
15
|
|
|
@@ -33,6 +36,29 @@ module PostHog
|
|
|
33
36
|
# posthog_distinct_id, distinct_id, id, pk, uuid in order.
|
|
34
37
|
attr_accessor :user_id_method
|
|
35
38
|
|
|
39
|
+
# @return [Boolean] Master switch for forwarding logs to PostHog Logs over OTLP. Defaults to false.
|
|
40
|
+
attr_accessor :logs_enabled
|
|
41
|
+
|
|
42
|
+
# @return [Boolean] Whether to broadcast Rails.logger output into the PostHog Logs sink. Defaults to true
|
|
43
|
+
# (only takes effect when {#logs_enabled} is true).
|
|
44
|
+
attr_accessor :logs_forward_rails_logger
|
|
45
|
+
|
|
46
|
+
# @return [Integer, Symbol, nil] Minimum severity to forward to PostHog Logs. When nil, inherits the
|
|
47
|
+
# current Rails.logger level. Accepts a Logger severity constant (e.g. Logger::INFO) or symbol (:info).
|
|
48
|
+
attr_accessor :logs_level
|
|
49
|
+
|
|
50
|
+
# @return [Integer, String, nil] Maximum log records forwarded to PostHog Logs per minute, protecting
|
|
51
|
+
# the ingestion quota from runaway log volume. Defaults to 6000. Numeric strings (e.g. from ENV) are
|
|
52
|
+
# coerced. Set to nil, 0, or a negative value to disable the cap; an unparseable value falls back to
|
|
53
|
+
# the default with a warning.
|
|
54
|
+
attr_accessor :logs_max_records_per_minute
|
|
55
|
+
|
|
56
|
+
# @return [Proc, nil] Callback invoked with each log record hash (:timestamp, :severity, :body,
|
|
57
|
+
# :attributes — where :severity is a symbol such as :warn) before it is sent to PostHog Logs.
|
|
58
|
+
# Return a (possibly modified) hash to send, or nil to drop the record — useful for scrubbing
|
|
59
|
+
# PII. If the callback raises, the record is dropped. Defaults to nil.
|
|
60
|
+
attr_accessor :logs_before_send
|
|
61
|
+
|
|
36
62
|
# @return [PostHog::Rails::Configuration]
|
|
37
63
|
def initialize
|
|
38
64
|
@auto_capture_exceptions = false
|
|
@@ -43,6 +69,11 @@ module PostHog
|
|
|
43
69
|
@capture_user_context = true
|
|
44
70
|
@current_user_method = :current_user
|
|
45
71
|
@user_id_method = nil
|
|
72
|
+
@logs_enabled = false
|
|
73
|
+
@logs_forward_rails_logger = true
|
|
74
|
+
@logs_level = nil
|
|
75
|
+
@logs_max_records_per_minute = DEFAULT_LOGS_MAX_RECORDS_PER_MINUTE
|
|
76
|
+
@logs_before_send = nil
|
|
46
77
|
end
|
|
47
78
|
|
|
48
79
|
# Default exceptions that Rails apps typically don't want to track.
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'logger'
|
|
4
|
+
require 'time'
|
|
5
|
+
require 'posthog/internal/context'
|
|
6
|
+
require 'posthog/logging'
|
|
7
|
+
require 'posthog/rails/logs/severity'
|
|
8
|
+
|
|
9
|
+
module PostHog
|
|
10
|
+
module Rails
|
|
11
|
+
module Logs
|
|
12
|
+
# A `Logger`-compatible sink that forwards each log record to an
|
|
13
|
+
# OpenTelemetry logger as an OTLP log record.
|
|
14
|
+
#
|
|
15
|
+
# It is designed to be broadcast alongside the app's existing
|
|
16
|
+
# `Rails.logger` so that ordinary `Rails.logger.info(...)` calls flow to
|
|
17
|
+
# PostHog Logs in addition to the normal output. Each record is stamped
|
|
18
|
+
# with the request-scoped PostHog identity captured by
|
|
19
|
+
# {PostHog::Rails::RequestContext}.
|
|
20
|
+
#
|
|
21
|
+
# Thread-safety: intentionally lock-free apart from the optional rate
|
|
22
|
+
# limiter's counter. Emitting touches no shared mutable state
|
|
23
|
+
# (`@otel_logger` is assigned once, attributes are built per call, and
|
|
24
|
+
# `Internal::Context.current` is thread/fiber-local), and the OTel
|
|
25
|
+
# BatchLogRecordProcessor synchronizes its buffer internally — the same
|
|
26
|
+
# split as stdlib `Logger`, which locks in `LogDevice`, not
|
|
27
|
+
# `Logger#add`. A mutex around emit would serialize all app logging
|
|
28
|
+
# needlessly.
|
|
29
|
+
#
|
|
30
|
+
# @api private
|
|
31
|
+
class Appender < ::Logger
|
|
32
|
+
SELF_LOG_PREFIX = '[posthog-ruby]'
|
|
33
|
+
SELF_LOG_PROGNAME = 'PostHog'
|
|
34
|
+
# Maps PostHog event-property names (as stored in Internal::Context) to
|
|
35
|
+
# the OTel semantic-convention attribute names used on log records,
|
|
36
|
+
# matching the web SDK so one filter works across SDKs.
|
|
37
|
+
REQUEST_ATTRIBUTE_NAMES = {
|
|
38
|
+
'$current_url' => 'url.full',
|
|
39
|
+
'$request_method' => 'http.request.method',
|
|
40
|
+
'$request_path' => 'url.path'
|
|
41
|
+
}.freeze
|
|
42
|
+
|
|
43
|
+
# @param otel_logger [#on_emit] An OpenTelemetry logger.
|
|
44
|
+
# @param level [Integer, nil] Minimum severity to forward.
|
|
45
|
+
# @param rate_limiter [PostHog::Rails::Logs::RateLimiter, nil] Optional cap on
|
|
46
|
+
# forwarded records, protecting the ingestion quota from runaway log volume.
|
|
47
|
+
# @param before_send [#call, nil] Optional callback invoked with each record hash
|
|
48
|
+
# (:timestamp, :severity, :body, :attributes — where :severity is a symbol such
|
|
49
|
+
# as :warn) before it is emitted. Return a (possibly modified) hash to send, or
|
|
50
|
+
# nil to drop — useful for scrubbing PII. If the callback raises, the record is
|
|
51
|
+
# dropped.
|
|
52
|
+
def initialize(otel_logger, level: nil, rate_limiter: nil, before_send: nil)
|
|
53
|
+
super(nil)
|
|
54
|
+
@otel_logger = otel_logger
|
|
55
|
+
@rate_limiter = rate_limiter
|
|
56
|
+
@before_send = before_send
|
|
57
|
+
# The forwarding threshold deliberately does NOT live in Logger#level.
|
|
58
|
+
# Rails 7.1+ BroadcastLogger computes #level as the min and #debug?
|
|
59
|
+
# etc. as the any? across sinks, so storing it there would widen the
|
|
60
|
+
# app-wide predicates (logs_level = :debug would flip
|
|
61
|
+
# Rails.logger.debug? true and make e.g. ActiveRecord start
|
|
62
|
+
# generating SQL debug lines), and a broadcast-wide
|
|
63
|
+
# `Rails.logger.level =` would clobber the configured logs_level.
|
|
64
|
+
# Pinning the inherited level to UNKNOWN keeps this sink invisible
|
|
65
|
+
# to those calculations; filtering happens against @threshold in #add.
|
|
66
|
+
@threshold = level || ::Logger::DEBUG
|
|
67
|
+
self.level = ::Logger::UNKNOWN
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Re-entrancy guard key. Fiber-local (Thread.current[]), which is what
|
|
71
|
+
# recursion needs: if anything inside #add logs through a broadcast
|
|
72
|
+
# that includes this appender (e.g. a logs_before_send callback calling
|
|
73
|
+
# Rails.logger), the nested call would recurse until SystemStackError —
|
|
74
|
+
# which, as an Exception, escapes the rescue below and breaks the app.
|
|
75
|
+
REENTRANCY_KEY = :posthog_rails_logs_emitting
|
|
76
|
+
|
|
77
|
+
# Mirrors `Logger#add` message/progname resolution, then emits to OTel
|
|
78
|
+
# instead of writing to a log device.
|
|
79
|
+
#
|
|
80
|
+
# @return [Boolean] Always true so it composes with broadcast loggers.
|
|
81
|
+
def add(severity, message = nil, progname = nil)
|
|
82
|
+
return true if Thread.current[REENTRANCY_KEY]
|
|
83
|
+
|
|
84
|
+
begin
|
|
85
|
+
Thread.current[REENTRANCY_KEY] = true
|
|
86
|
+
|
|
87
|
+
severity ||= ::Logger::UNKNOWN
|
|
88
|
+
return true if severity < @threshold
|
|
89
|
+
|
|
90
|
+
if message.nil?
|
|
91
|
+
if block_given?
|
|
92
|
+
message = yield
|
|
93
|
+
else
|
|
94
|
+
message = progname
|
|
95
|
+
progname = nil
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
return true if message.nil?
|
|
100
|
+
return true if self_log?(message, progname)
|
|
101
|
+
|
|
102
|
+
record = apply_before_send(build_record(severity, message, progname))
|
|
103
|
+
return true if record.nil?
|
|
104
|
+
|
|
105
|
+
case @rate_limiter&.record
|
|
106
|
+
when :reject
|
|
107
|
+
return true
|
|
108
|
+
when :reject_first
|
|
109
|
+
emit_rate_cap_notice
|
|
110
|
+
return true
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
emit(record)
|
|
114
|
+
true
|
|
115
|
+
rescue StandardError => e
|
|
116
|
+
# Never let log forwarding break the calling code path, but leave
|
|
117
|
+
# one breadcrumb: a persistent emit failure would otherwise drop
|
|
118
|
+
# 100% of records with no signal anywhere.
|
|
119
|
+
warn_emit_error(e)
|
|
120
|
+
true
|
|
121
|
+
ensure
|
|
122
|
+
Thread.current[REENTRANCY_KEY] = nil
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
private
|
|
127
|
+
|
|
128
|
+
def build_record(severity, message, progname)
|
|
129
|
+
{
|
|
130
|
+
timestamp: Time.now,
|
|
131
|
+
severity: Severity.name_for(severity),
|
|
132
|
+
body: body_for(message),
|
|
133
|
+
attributes: attributes_for(progname)
|
|
134
|
+
}
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def emit(record)
|
|
138
|
+
# The before_send callback sees a single :severity enum; the OTel
|
|
139
|
+
# number/text pair is derived here so the two can never be set
|
|
140
|
+
# inconsistently.
|
|
141
|
+
severity_number, severity_text = Severity.for_name(record[:severity])
|
|
142
|
+
@otel_logger.on_emit(
|
|
143
|
+
timestamp: record[:timestamp],
|
|
144
|
+
severity_number: severity_number,
|
|
145
|
+
severity_text: severity_text,
|
|
146
|
+
body: record[:body],
|
|
147
|
+
attributes: record[:attributes]
|
|
148
|
+
)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# One discoverable notice per window so truncation isn't silent. Emitted
|
|
152
|
+
# directly (bypassing before_send) so a scrubber can't accidentally
|
|
153
|
+
# suppress the only signal that records are being dropped.
|
|
154
|
+
def emit_rate_cap_notice
|
|
155
|
+
emit(
|
|
156
|
+
timestamp: Time.now,
|
|
157
|
+
severity: :warn,
|
|
158
|
+
body: "PostHog Logs rate cap reached (#{@rate_limiter.limit} records/minute); " \
|
|
159
|
+
'dropping further records for the remainder of this window',
|
|
160
|
+
attributes: {}
|
|
161
|
+
)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Runs before the rate-cap check (matching the other PostHog SDKs) so
|
|
165
|
+
# records dropped by the callback never consume window budget — a
|
|
166
|
+
# before_send that drops noisy logs must not starve the legitimate
|
|
167
|
+
# records behind them.
|
|
168
|
+
#
|
|
169
|
+
# Unlike the events before_send (which sends the original event when the
|
|
170
|
+
# callback raises), a failing callback drops the record: the likeliest
|
|
171
|
+
# use is PII scrubbing, where shipping the unscrubbed original is worse
|
|
172
|
+
# than losing the line.
|
|
173
|
+
def apply_before_send(record)
|
|
174
|
+
return record unless @before_send
|
|
175
|
+
|
|
176
|
+
result = @before_send.call(record)
|
|
177
|
+
return result if result.is_a?(Hash)
|
|
178
|
+
|
|
179
|
+
# nil is an intentional drop and stays silent; any other type is
|
|
180
|
+
# likely a bug (e.g. a proc whose last expression isn't the record).
|
|
181
|
+
warn_before_send("returned #{result.class} instead of a Hash or nil") unless result.nil?
|
|
182
|
+
nil
|
|
183
|
+
rescue StandardError => e
|
|
184
|
+
warn_before_send("raised (#{e.class}: #{e.message})")
|
|
185
|
+
nil
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def warn_before_send(description)
|
|
189
|
+
# Benign race: concurrent first failures may warn more than once.
|
|
190
|
+
return if @before_send_warned
|
|
191
|
+
|
|
192
|
+
@before_send_warned = true
|
|
193
|
+
PostHog::Logging.logger.warn("logs_before_send #{description}; dropping the record")
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def warn_emit_error(error)
|
|
197
|
+
# Benign race: concurrent first failures may warn more than once.
|
|
198
|
+
return if @emit_error_warned
|
|
199
|
+
|
|
200
|
+
@emit_error_warned = true
|
|
201
|
+
PostHog::Logging.logger.warn(
|
|
202
|
+
"PostHog Logs failed to emit a record (#{error.class}: #{error.message}); " \
|
|
203
|
+
'further failures will be dropped silently'
|
|
204
|
+
)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def body_for(message)
|
|
208
|
+
str = stringify(message)
|
|
209
|
+
str = str.encode(Encoding::UTF_8, invalid: :replace, undef: :replace) unless str.encoding == Encoding::UTF_8
|
|
210
|
+
str.valid_encoding? ? str : str.scrub
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Mirrors stdlib Logger::Formatter#msg2str so the body matches what the
|
|
214
|
+
# file logger writes — most importantly, an Exception renders with its
|
|
215
|
+
# class, message, and backtrace instead of a backtrace-less #inspect.
|
|
216
|
+
# Array() guards a never-raised exception whose backtrace is nil. The
|
|
217
|
+
# returned String is always freshly built (dup/interpolation/inspect)
|
|
218
|
+
# so a logs_before_send callback can mutate it without touching frozen
|
|
219
|
+
# app strings.
|
|
220
|
+
def stringify(message)
|
|
221
|
+
case message
|
|
222
|
+
when String
|
|
223
|
+
message.dup
|
|
224
|
+
when Exception
|
|
225
|
+
"#{message.message} (#{message.class})\n#{Array(message.backtrace).join("\n")}"
|
|
226
|
+
else
|
|
227
|
+
message.inspect
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def attributes_for(progname)
|
|
232
|
+
attributes = {}
|
|
233
|
+
# Ruby's progname is the closest analog to the OTel-world "logger name";
|
|
234
|
+
# logger.name is the key users coming from other ecosystems will expect.
|
|
235
|
+
attributes['logger.name'] = progname.to_s if progname
|
|
236
|
+
|
|
237
|
+
context = Internal::Context.current
|
|
238
|
+
return attributes unless context
|
|
239
|
+
|
|
240
|
+
attributes['posthogDistinctId'] = context.distinct_id if context.distinct_id
|
|
241
|
+
attributes['sessionId'] = context.session_id if context.session_id
|
|
242
|
+
|
|
243
|
+
properties = context.properties || {}
|
|
244
|
+
REQUEST_ATTRIBUTE_NAMES.each do |key, attribute_name|
|
|
245
|
+
value = properties[key] || properties[key.to_sym]
|
|
246
|
+
attributes[attribute_name] = value if value
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
attributes
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def self_log?(message, progname)
|
|
253
|
+
return true if progname.to_s == SELF_LOG_PROGNAME
|
|
254
|
+
|
|
255
|
+
# PrefixedLogger always places the prefix at the start of the message,
|
|
256
|
+
# so start_with? suffices and avoids suppressing app logs that merely
|
|
257
|
+
# mention the SDK mid-string.
|
|
258
|
+
message.is_a?(String) && message.start_with?(SELF_LOG_PREFIX)
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PostHog
|
|
4
|
+
module Rails
|
|
5
|
+
module Logs
|
|
6
|
+
# Fixed-window rate limiter protecting the PostHog Logs ingestion quota
|
|
7
|
+
# from runaway log volume (PostHog Logs bills by data ingested).
|
|
8
|
+
#
|
|
9
|
+
# Thread-safe: the counter is the one piece of shared mutable state in
|
|
10
|
+
# the logs pipeline, guarded by a mutex scoped to a counter bump.
|
|
11
|
+
#
|
|
12
|
+
# @api private
|
|
13
|
+
class RateLimiter
|
|
14
|
+
WINDOW_SECONDS = 60
|
|
15
|
+
|
|
16
|
+
# @return [Integer] Maximum records allowed per window.
|
|
17
|
+
attr_reader :limit
|
|
18
|
+
|
|
19
|
+
# @param limit [Integer] Maximum records allowed per {WINDOW_SECONDS} window.
|
|
20
|
+
def initialize(limit)
|
|
21
|
+
@limit = limit
|
|
22
|
+
@mutex = Mutex.new
|
|
23
|
+
@window = nil
|
|
24
|
+
@count = 0
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Records one attempt and returns the verdict.
|
|
28
|
+
#
|
|
29
|
+
# @return [Symbol] :allow when under the cap, :reject_first for the
|
|
30
|
+
# first rejection of a window (callers may emit a single notice),
|
|
31
|
+
# :reject thereafter.
|
|
32
|
+
def record
|
|
33
|
+
@mutex.synchronize do
|
|
34
|
+
window = Process.clock_gettime(Process::CLOCK_MONOTONIC).to_i / WINDOW_SECONDS
|
|
35
|
+
if window != @window
|
|
36
|
+
@window = window
|
|
37
|
+
@count = 0
|
|
38
|
+
end
|
|
39
|
+
@count += 1
|
|
40
|
+
next :allow if @count <= @limit
|
|
41
|
+
|
|
42
|
+
@count == @limit + 1 ? :reject_first : :reject
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'logger'
|
|
4
|
+
require 'posthog/logging'
|
|
5
|
+
require 'posthog/rails/configuration'
|
|
6
|
+
require 'posthog/rails/logs/appender'
|
|
7
|
+
require 'posthog/rails/logs/rate_limiter'
|
|
8
|
+
|
|
9
|
+
module PostHog
|
|
10
|
+
module Rails
|
|
11
|
+
module Logs
|
|
12
|
+
# Bootstraps the OpenTelemetry logs pipeline that ships PostHog Logs.
|
|
13
|
+
#
|
|
14
|
+
# The OpenTelemetry gems are optional/soft dependencies. They are required
|
|
15
|
+
# lazily here so that apps which do not enable logs (or run on a Ruby
|
|
16
|
+
# version the logs SDK does not support) are unaffected.
|
|
17
|
+
#
|
|
18
|
+
# @api private
|
|
19
|
+
module Setup
|
|
20
|
+
# Bounds the at_exit flush. Without a timeout, the batch processor
|
|
21
|
+
# joins its worker thread unbounded and the exporter retries each
|
|
22
|
+
# batch with backoff — during an outage that can eat the whole
|
|
23
|
+
# SIGTERM grace period and starve the events client of its flush.
|
|
24
|
+
SHUTDOWN_TIMEOUT_SECONDS = 2
|
|
25
|
+
|
|
26
|
+
class << self
|
|
27
|
+
# @return [OpenTelemetry::SDK::Logs::LoggerProvider, nil]
|
|
28
|
+
attr_reader :provider
|
|
29
|
+
|
|
30
|
+
# @return [PostHog::Rails::Logs::Appender, nil]
|
|
31
|
+
attr_reader :appender
|
|
32
|
+
|
|
33
|
+
# Build the logs pipeline and return the broadcastable appender.
|
|
34
|
+
#
|
|
35
|
+
# Idempotent: subsequent calls return the previously built appender
|
|
36
|
+
# (or nil if setup was skipped).
|
|
37
|
+
#
|
|
38
|
+
# @return [PostHog::Rails::Logs::Appender, nil]
|
|
39
|
+
def install
|
|
40
|
+
return @appender if @installed
|
|
41
|
+
|
|
42
|
+
@installed = true
|
|
43
|
+
|
|
44
|
+
# Respect the core client's test_mode: when it is on, the client
|
|
45
|
+
# swaps in a NoopWorker so events never ship, and the logs pipeline
|
|
46
|
+
# should likewise stay off so test suites don't emit real records.
|
|
47
|
+
# Intentional state, so skip quietly (no warning).
|
|
48
|
+
return nil if @client_test_mode
|
|
49
|
+
|
|
50
|
+
return nil unless require_otel_gems
|
|
51
|
+
|
|
52
|
+
config = PostHog::Rails.config
|
|
53
|
+
token = resolve_token
|
|
54
|
+
if token.nil?
|
|
55
|
+
warn_once(
|
|
56
|
+
'PostHog Logs enabled but no project token could be resolved ' \
|
|
57
|
+
'(set config.api_key or POSTHOG_API_KEY); skipping.'
|
|
58
|
+
)
|
|
59
|
+
return nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
@provider = build_provider(token)
|
|
63
|
+
otel_logger = @provider.logger(name: 'posthog-rails', version: PostHog::VERSION)
|
|
64
|
+
level = resolve_level(config.logs_level) || rails_logger_level
|
|
65
|
+
@appender = Appender.new(
|
|
66
|
+
otel_logger,
|
|
67
|
+
level: level,
|
|
68
|
+
rate_limiter: build_rate_limiter(config),
|
|
69
|
+
before_send: config.logs_before_send
|
|
70
|
+
)
|
|
71
|
+
rescue StandardError => e
|
|
72
|
+
warn_once("Failed to initialize PostHog Logs: #{e.message}")
|
|
73
|
+
nil
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Shut the pipeline down, flushing buffered records.
|
|
77
|
+
#
|
|
78
|
+
# @param timeout [Numeric] Max seconds to spend; see {SHUTDOWN_TIMEOUT_SECONDS}.
|
|
79
|
+
# @return [void]
|
|
80
|
+
def shutdown(timeout: SHUTDOWN_TIMEOUT_SECONDS)
|
|
81
|
+
@provider&.shutdown(timeout: timeout)
|
|
82
|
+
rescue StandardError => e
|
|
83
|
+
logger.warn("Error shutting down PostHog Logs: #{e.message}")
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Remembers the api_key/host the PostHog client was initialized with
|
|
87
|
+
# (called by PostHog.init) so the logs pipeline can reuse them without
|
|
88
|
+
# the core client exposing public readers.
|
|
89
|
+
#
|
|
90
|
+
# @api private
|
|
91
|
+
# @param options [Hash] The options passed to {PostHog::Client.new}.
|
|
92
|
+
# @return [void]
|
|
93
|
+
def remember_client_options(options)
|
|
94
|
+
return unless options.is_a?(Hash)
|
|
95
|
+
|
|
96
|
+
@client_api_key = options[:api_key] || options['api_key']
|
|
97
|
+
@client_host = options[:host] || options['host']
|
|
98
|
+
@client_test_mode = options[:test_mode] || options['test_mode']
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Resets memoized state. Intended for tests.
|
|
102
|
+
#
|
|
103
|
+
# @return [void]
|
|
104
|
+
def reset
|
|
105
|
+
@installed = false
|
|
106
|
+
@provider = nil
|
|
107
|
+
@appender = nil
|
|
108
|
+
@warned = false
|
|
109
|
+
@client_api_key = nil
|
|
110
|
+
@client_host = nil
|
|
111
|
+
@client_test_mode = nil
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
private
|
|
115
|
+
|
|
116
|
+
# The logs token is the same project token the core client uses
|
|
117
|
+
# (i.e. config.api_key, captured by PostHog.init), falling back to
|
|
118
|
+
# ENV['POSTHOG_API_KEY'].
|
|
119
|
+
def resolve_token
|
|
120
|
+
normalize(@client_api_key) || normalize(ENV.fetch('POSTHOG_API_KEY', nil))
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# The logs host follows the core client's configured host (captured by
|
|
124
|
+
# PostHog.init), falling back to ENV['POSTHOG_HOST'] and finally the
|
|
125
|
+
# US cloud endpoint.
|
|
126
|
+
def resolve_host
|
|
127
|
+
normalize(@client_host) ||
|
|
128
|
+
normalize(ENV.fetch('POSTHOG_HOST', nil)) ||
|
|
129
|
+
'https://us.i.posthog.com'
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# nil, 0, and negative values intentionally disable the cap. Numeric
|
|
133
|
+
# strings (e.g. from ENV) are coerced — deliberately via Integer()
|
|
134
|
+
# rather than to_i, since "abc".to_i == 0 would silently disable the
|
|
135
|
+
# cap. Unparseable values warn and fall back to the default cap:
|
|
136
|
+
# a misconfiguration should not switch the protection off.
|
|
137
|
+
def build_rate_limiter(config)
|
|
138
|
+
raw = config.logs_max_records_per_minute
|
|
139
|
+
return nil if raw.nil?
|
|
140
|
+
|
|
141
|
+
limit = Integer(raw, exception: false)
|
|
142
|
+
if limit.nil?
|
|
143
|
+
logger.warn(
|
|
144
|
+
"logs_max_records_per_minute=#{raw.inspect} is not a number; using the default cap " \
|
|
145
|
+
"of #{Configuration::DEFAULT_LOGS_MAX_RECORDS_PER_MINUTE} records/minute"
|
|
146
|
+
)
|
|
147
|
+
limit = Configuration::DEFAULT_LOGS_MAX_RECORDS_PER_MINUTE
|
|
148
|
+
end
|
|
149
|
+
return nil unless limit.positive?
|
|
150
|
+
|
|
151
|
+
RateLimiter.new(limit)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def require_otel_gems
|
|
155
|
+
require 'opentelemetry-sdk'
|
|
156
|
+
require 'opentelemetry-logs-sdk'
|
|
157
|
+
require 'opentelemetry/exporter/otlp_logs'
|
|
158
|
+
true
|
|
159
|
+
rescue LoadError => e
|
|
160
|
+
warn_once(
|
|
161
|
+
"PostHog Logs enabled but the OpenTelemetry gems are missing (#{e.message}). " \
|
|
162
|
+
"Add 'opentelemetry-sdk', 'opentelemetry-logs-sdk', and " \
|
|
163
|
+
"'opentelemetry-exporter-otlp-logs' (each with require: false) to your Gemfile " \
|
|
164
|
+
'to enable log forwarding.'
|
|
165
|
+
)
|
|
166
|
+
false
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def build_provider(token)
|
|
170
|
+
resource = OpenTelemetry::SDK::Resources::Resource.create(resource_attributes)
|
|
171
|
+
provider = OpenTelemetry::SDK::Logs::LoggerProvider.new(resource: resource)
|
|
172
|
+
exporter = OpenTelemetry::Exporter::OTLP::Logs::LogsExporter.new(
|
|
173
|
+
endpoint: logs_endpoint(resolve_host),
|
|
174
|
+
headers: { 'Authorization' => "Bearer #{token}" }
|
|
175
|
+
)
|
|
176
|
+
processor = OpenTelemetry::SDK::Logs::Export::BatchLogRecordProcessor.new(exporter)
|
|
177
|
+
provider.add_log_record_processor(processor)
|
|
178
|
+
provider
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def resource_attributes
|
|
182
|
+
# service.version is intentionally omitted. Per OpenTelemetry semantic
|
|
183
|
+
# conventions it is the deployed application's version, not this gem's.
|
|
184
|
+
# The posthog-rails name/version travel with each record via the
|
|
185
|
+
# instrumentation scope (see LoggerProvider#logger above).
|
|
186
|
+
{
|
|
187
|
+
'service.name' => service_name,
|
|
188
|
+
'deployment.environment' => ::Rails.env.to_s
|
|
189
|
+
}
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def service_name
|
|
193
|
+
app = ::Rails.application
|
|
194
|
+
return 'unknown_service' unless app
|
|
195
|
+
|
|
196
|
+
name = app.class.respond_to?(:module_parent_name) ? app.class.module_parent_name : nil
|
|
197
|
+
name && !name.empty? ? name.to_s : 'unknown_service'
|
|
198
|
+
rescue StandardError
|
|
199
|
+
'unknown_service'
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def logs_endpoint(host)
|
|
203
|
+
base = (host || 'https://us.i.posthog.com').to_s.sub(%r{/+\z}, '')
|
|
204
|
+
"#{base}/i/v1/logs"
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def resolve_level(level)
|
|
208
|
+
return nil if level.nil?
|
|
209
|
+
return level if level.is_a?(Integer)
|
|
210
|
+
|
|
211
|
+
# const_get can return non-severity values without raising: Logger's
|
|
212
|
+
# own VERSION/SEV_LABEL, and (via the default inherited lookup) any
|
|
213
|
+
# top-level all-caps constant like ENV/ARGV/STDOUT. The Integer guard
|
|
214
|
+
# rejects them so they don't later blow up the severity comparison.
|
|
215
|
+
# (inherit must stay true — the severity constants live in the
|
|
216
|
+
# included Logger::Severity module.)
|
|
217
|
+
result = ::Logger.const_get(level.to_s.upcase)
|
|
218
|
+
raise NameError, 'not a severity integer' unless result.is_a?(Integer)
|
|
219
|
+
|
|
220
|
+
result
|
|
221
|
+
rescue NameError
|
|
222
|
+
warn_once(
|
|
223
|
+
"Invalid logs_level #{level.inspect}; expected one of :debug, :info, :warn, " \
|
|
224
|
+
':error, :fatal, :unknown (or an Integer). Falling back to the Rails logger level.'
|
|
225
|
+
)
|
|
226
|
+
nil
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def rails_logger_level
|
|
230
|
+
::Rails.logger&.level
|
|
231
|
+
rescue StandardError
|
|
232
|
+
nil
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def normalize(value)
|
|
236
|
+
return nil unless value.is_a?(String)
|
|
237
|
+
|
|
238
|
+
stripped = value.strip
|
|
239
|
+
stripped.empty? ? nil : stripped
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def warn_once(message)
|
|
243
|
+
return if @warned
|
|
244
|
+
|
|
245
|
+
@warned = true
|
|
246
|
+
logger.warn(message)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def logger
|
|
250
|
+
PostHog::Logging.logger
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'logger'
|
|
4
|
+
|
|
5
|
+
module PostHog
|
|
6
|
+
module Rails
|
|
7
|
+
module Logs
|
|
8
|
+
# Maps Ruby `Logger` severities to OpenTelemetry log severity numbers and text.
|
|
9
|
+
#
|
|
10
|
+
# OpenTelemetry defines severity ranges (DEBUG=5-8, INFO=9-12, WARN=13-16,
|
|
11
|
+
# ERROR=17-20, FATAL=21-24); we map each Ruby level to the base of its range.
|
|
12
|
+
#
|
|
13
|
+
# @api private
|
|
14
|
+
module Severity
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
# @param severity [Integer, nil] A Ruby `Logger` severity constant.
|
|
18
|
+
# @return [Symbol] The severity name (:debug, :info, :warn, :error, :fatal).
|
|
19
|
+
def name_for(severity)
|
|
20
|
+
NAMES.fetch(severity, :info)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @param name [Symbol, String, nil] A severity name such as :warn.
|
|
24
|
+
# @return [Array(Integer, String)] OpenTelemetry severity number and text;
|
|
25
|
+
# unrecognized names fall back to INFO.
|
|
26
|
+
def for_name(name)
|
|
27
|
+
OTEL.fetch(name.to_s.downcase.to_sym, OTEL[:info])
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
NAMES = {
|
|
31
|
+
::Logger::DEBUG => :debug,
|
|
32
|
+
::Logger::INFO => :info,
|
|
33
|
+
::Logger::WARN => :warn,
|
|
34
|
+
::Logger::ERROR => :error,
|
|
35
|
+
::Logger::FATAL => :fatal,
|
|
36
|
+
::Logger::UNKNOWN => :info
|
|
37
|
+
}.freeze
|
|
38
|
+
|
|
39
|
+
OTEL = {
|
|
40
|
+
debug: [5, 'DEBUG'],
|
|
41
|
+
info: [9, 'INFO'],
|
|
42
|
+
warn: [13, 'WARN'],
|
|
43
|
+
error: [17, 'ERROR'],
|
|
44
|
+
fatal: [21, 'FATAL']
|
|
45
|
+
}.freeze
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -34,8 +34,21 @@ module PostHog
|
|
|
34
34
|
options = config.to_client_options
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
-
#
|
|
37
|
+
# Let the PostHog Logs pipeline reuse the same api_key/host without
|
|
38
|
+
# the core client exposing public readers.
|
|
39
|
+
PostHog::Rails::Logs::Setup.remember_client_options(options) if defined?(PostHog::Rails::Logs::Setup)
|
|
40
|
+
|
|
41
|
+
# Create the PostHog client. If a client already exists, shut it down
|
|
42
|
+
# after replacement so repeated init calls do not leave background
|
|
43
|
+
# resources from the previous instance running.
|
|
44
|
+
previous_client = @client
|
|
38
45
|
@client = PostHog::Client.new(options)
|
|
46
|
+
begin
|
|
47
|
+
previous_client&.shutdown
|
|
48
|
+
rescue StandardError => e
|
|
49
|
+
PostHog::Logging.logger.warn("Failed to shut down previous PostHog client: #{e.message}")
|
|
50
|
+
end
|
|
51
|
+
@client
|
|
39
52
|
end
|
|
40
53
|
|
|
41
54
|
# Define delegated methods using metaprogramming
|
|
@@ -118,12 +131,20 @@ module PostHog
|
|
|
118
131
|
register_error_subscriber if rails_version_above_7?
|
|
119
132
|
end
|
|
120
133
|
|
|
134
|
+
# Opt-in: forward logs to PostHog Logs over OTLP
|
|
135
|
+
config.after_initialize do
|
|
136
|
+
install_posthog_logs if PostHog::Rails.config&.logs_enabled
|
|
137
|
+
end
|
|
138
|
+
|
|
121
139
|
# Ensure PostHog shuts down gracefully (register only once)
|
|
122
140
|
config.after_initialize do
|
|
123
141
|
next if @posthog_at_exit_registered
|
|
124
142
|
|
|
125
143
|
@posthog_at_exit_registered = true
|
|
126
|
-
at_exit
|
|
144
|
+
at_exit do
|
|
145
|
+
PostHog::Rails::Logs::Setup.shutdown
|
|
146
|
+
PostHog.client&.shutdown if PostHog.initialized?
|
|
147
|
+
end
|
|
127
148
|
end
|
|
128
149
|
|
|
129
150
|
# @api private
|
|
@@ -144,6 +165,54 @@ module PostHog
|
|
|
144
165
|
app.config.middleware.insert_before(target, middleware)
|
|
145
166
|
end
|
|
146
167
|
|
|
168
|
+
# Build the PostHog Logs pipeline and broadcast Rails.logger into it.
|
|
169
|
+
#
|
|
170
|
+
# @api private
|
|
171
|
+
# @return [void]
|
|
172
|
+
def self.install_posthog_logs
|
|
173
|
+
unless PostHog.initialized?
|
|
174
|
+
# logs_enabled is an explicit opt-in, so leave a breadcrumb instead
|
|
175
|
+
# of silently skipping when PostHog.init never ran.
|
|
176
|
+
PostHog::Logging.logger.warn(
|
|
177
|
+
'PostHog Logs is enabled but PostHog.init has not been called; ' \
|
|
178
|
+
'skipping log forwarding. Call PostHog.init in your initializer.'
|
|
179
|
+
)
|
|
180
|
+
return
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Mirror the core client: when it is disabled (missing/blank api_key)
|
|
184
|
+
# every capture no-ops, so log forwarding should stay off too. The
|
|
185
|
+
# client already logs its own missing-api_key error, so skip quietly.
|
|
186
|
+
return unless PostHog.client.enabled?
|
|
187
|
+
|
|
188
|
+
appender = PostHog::Rails::Logs::Setup.install
|
|
189
|
+
return if appender.nil?
|
|
190
|
+
|
|
191
|
+
broadcast_rails_logger(appender) if PostHog::Rails.config&.logs_forward_rails_logger
|
|
192
|
+
rescue StandardError => e
|
|
193
|
+
PostHog::Logging.logger.warn("Failed to set up PostHog Logs: #{e.message}")
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Attach the appender to Rails.logger, supporting both the Rails 7.1+
|
|
197
|
+
# BroadcastLogger and the older ActiveSupport::Logger.broadcast mechanism.
|
|
198
|
+
#
|
|
199
|
+
# @api private
|
|
200
|
+
# @return [void]
|
|
201
|
+
def self.broadcast_rails_logger(appender)
|
|
202
|
+
logger = ::Rails.logger
|
|
203
|
+
return unless logger
|
|
204
|
+
|
|
205
|
+
if logger.respond_to?(:broadcast_to)
|
|
206
|
+
logger.broadcast_to(appender)
|
|
207
|
+
elsif defined?(ActiveSupport::Logger) && ActiveSupport::Logger.respond_to?(:broadcast)
|
|
208
|
+
logger.extend(ActiveSupport::Logger.broadcast(appender))
|
|
209
|
+
else
|
|
210
|
+
PostHog::Logging.logger.warn(
|
|
211
|
+
'PostHog Logs could not broadcast Rails.logger; no compatible broadcast mechanism found.'
|
|
212
|
+
)
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
147
216
|
# @api private
|
|
148
217
|
# @return [void]
|
|
149
218
|
def self.register_error_subscriber
|
data/lib/posthog/rails.rb
CHANGED
|
@@ -8,6 +8,10 @@ require 'posthog/rails/capture_exceptions'
|
|
|
8
8
|
require 'posthog/rails/rescued_exception_interceptor'
|
|
9
9
|
require 'posthog/rails/active_job'
|
|
10
10
|
require 'posthog/rails/error_subscriber'
|
|
11
|
+
require 'posthog/rails/logs/severity'
|
|
12
|
+
require 'posthog/rails/logs/rate_limiter'
|
|
13
|
+
require 'posthog/rails/logs/appender'
|
|
14
|
+
require 'posthog/rails/logs/setup'
|
|
11
15
|
require 'posthog/rails/railtie'
|
|
12
16
|
|
|
13
17
|
module PostHog
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: posthog-rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.12.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- PostHog
|
|
@@ -29,14 +29,14 @@ dependencies:
|
|
|
29
29
|
requirements:
|
|
30
30
|
- - "~>"
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: '3.
|
|
32
|
+
version: '3.12'
|
|
33
33
|
type: :runtime
|
|
34
34
|
prerelease: false
|
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
37
|
- - "~>"
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: '3.
|
|
39
|
+
version: '3.12'
|
|
40
40
|
description: Automatic exception tracking and instrumentation for Ruby on Rails applications
|
|
41
41
|
using PostHog
|
|
42
42
|
email: engineering@posthog.com
|
|
@@ -52,6 +52,10 @@ files:
|
|
|
52
52
|
- lib/posthog/rails/capture_exceptions.rb
|
|
53
53
|
- lib/posthog/rails/configuration.rb
|
|
54
54
|
- lib/posthog/rails/error_subscriber.rb
|
|
55
|
+
- lib/posthog/rails/logs/appender.rb
|
|
56
|
+
- lib/posthog/rails/logs/rate_limiter.rb
|
|
57
|
+
- lib/posthog/rails/logs/setup.rb
|
|
58
|
+
- lib/posthog/rails/logs/severity.rb
|
|
55
59
|
- lib/posthog/rails/parameter_filter.rb
|
|
56
60
|
- lib/posthog/rails/railtie.rb
|
|
57
61
|
- lib/posthog/rails/request_context.rb
|