packrat-rails 0.1.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 66ef80151ead412a9237f8e93fc4d916ba4041b43518e4d7b25c9470ace3d90b
4
+ data.tar.gz: 26eb535015852176b0aa847ae220a0204b83f0bf6d201723f7cf2284b5ecd4ff
5
+ SHA512:
6
+ metadata.gz: b9e188a68993bca9520e3300c20cc30ac19a122a19f369373aa20c278d07db7a740faceef69397cf34a17f9782628b2d29124b794f4d8c6ed6f8e0f06fa89911
7
+ data.tar.gz: ccf0946cbdd06bc2b30ee3ce01b234040853948cbdcab1d0578337165cb27114b30861a9b7035dc09a7022b920482fea7b7f8e4a92904730dff3c01888a9ccf5
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Packrat Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,70 @@
1
+ # packrat-rails
2
+
3
+ **To turn it off, set `PACKRAT_DISABLED=1`.** The app then boots as if the gem were not installed.
4
+
5
+ OpenTelemetry for Rails, configured to send traces, logs and exceptions to
6
+ [Packrat](https://packrat.sh) with one key.
7
+
8
+ ```sh
9
+ bundle add packrat-rails
10
+ bin/rails generate packrat --ingest-key=packrat_ingest_…
11
+ bin/rails server
12
+ ```
13
+
14
+ The generator creates `config/initializers/packrat.rb` with the ingest key from your Packrat setup
15
+ page:
16
+
17
+ ```ruby
18
+ Packrat.configure do |config|
19
+ config.ingest_key = ENV.fetch("PACKRAT_INGEST_KEY", "packrat_ingest_…")
20
+ end
21
+ ```
22
+
23
+ An ingest key can only send data, so you can commit the file. To keep the key out of the repository,
24
+ run the generator without `--ingest-key` and set `PACKRAT_INGEST_KEY` where the app runs.
25
+
26
+ The generator also turns on the [Packrat plugin for Claude Code](https://packrat.sh/docs/claude-code-plugin)
27
+ in `.claude/settings.json`. Commit the file, and everyone who opens Claude Code in the repository and
28
+ trusts the folder gets the plugin. If the file exists, the generator adds the plugin and keeps the rest.
29
+ To leave the file alone, pass `--no-claude-code`.
30
+
31
+ On boot the gem writes one line to the Rails log. It names the service and environment the app
32
+ reports under:
33
+
34
+ ```
35
+ Packrat: sending traces and logs for shop/production to https://ingest.packrat.sh (rack, action_pack, active_job, active_record, net_http, pg, logger)
36
+ ```
37
+
38
+ ## Configuration
39
+
40
+ The ingest key and the ingest URL are the only settings in `Packrat.configure`. Everything else is an environment
41
+ variable.
42
+
43
+ | Variable | Default | Note |
44
+ | ------------------------------- | --------------------------- | ------------------------------------------------------------ |
45
+ | `PACKRAT_INGEST_KEY` | `config.ingest_key` | Wins over the initializer. With neither, the gem sends nothing and says so |
46
+ | `PACKRAT_INGEST_URL` | `config.ingest_url`, else `https://ingest.packrat.sh` | A local Packrat is `http://localhost:3100`. `--ingest-url` on the generator writes `config.ingest_url` |
47
+ | `PACKRAT_DISABLED` | unset | `1` turns the gem off in any environment |
48
+ | `PACKRAT_OTEL_INSTRUMENTATIONS` | the list below | Comma-separated names to add; a leading `-` removes one |
49
+
50
+ The instrumentations are `rack`, `action_pack`, `action_view`, `active_job`, `active_record`,
51
+ `net_http`, `pg`, `mysql2`, `sidekiq`, `redis`, `aws_sdk`, `concurrent_ruby`, `rake` and `logger`. One for a library that is not in the app installs nothing.
52
+ `PACKRAT_OTEL_INSTRUMENTATIONS=faraday,-action_view` adds Faraday (with
53
+ `opentelemetry-instrumentation-faraday` in the Gemfile) and removes Action View. `action_mailer` and
54
+ `active_storage` cannot be added: they must load before boot ends.
55
+
56
+ The service name is `OTEL_SERVICE_NAME`, else `service.name` in `OTEL_RESOURCE_ATTRIBUTES`, else the
57
+ app's module name (`Shop::Application` is `shop`). The environment is `Rails.env` unless
58
+ `OTEL_RESOURCE_ATTRIBUTES` names one. The version is `BUILD_COMMIT`, `HEROKU_SLUG_COMMIT` or
59
+ `SOURCE_VERSION`. The instance is `DYNO`, else the host name. A standard `OTEL_*` variable that is set always wins.
60
+
61
+ The gem does nothing in the test environment.
62
+
63
+ ## Beside other tools
64
+
65
+ - **Nothing else:** the gem configures OpenTelemetry.
66
+ - **Your own OpenTelemetry setup:** the gem leaves it as it is and adds a second exporter, to Packrat.
67
+ If your exporter already sends to Packrat, it adds nothing.
68
+ - **Sentry:** both run. The gem does not touch Sentry.
69
+ - **Datadog's tracer:** the gem does nothing. Dual-ship from the Datadog Agent instead:
70
+ https://packrat.sh/docs/datadog
@@ -0,0 +1,62 @@
1
+ require "json"
2
+ require "rails/generators"
3
+
4
+ module Packrat
5
+ module Generators
6
+ # bin/rails generate packrat --ingest-key=packrat_ingest_...
7
+ class PackratGenerator < Rails::Generators::Base
8
+ namespace "packrat"
9
+ source_root File.expand_path("templates", __dir__)
10
+
11
+ desc "Creates config/initializers/packrat.rb, which gives the packrat-rails gem your ingest key, " \
12
+ "and turns on the Packrat plugin for Claude Code in .claude/settings.json."
13
+ class_option :ingest_key, type: :string, desc: "The ingest key from your Packrat setup page"
14
+ class_option :ingest_url, type: :string, desc: "Where to send, when it isn't https://ingest.packrat.sh"
15
+ class_option :claude_code, type: :boolean, default: true,
16
+ desc: "Turn on the Packrat plugin for Claude Code in .claude/settings.json"
17
+
18
+ CLAUDE_SETTINGS = ".claude/settings.json"
19
+ MARKETPLACE = { "source" => { "source" => "github", "repo" => "packratlabs/claude-plugin" } }.freeze
20
+ PLUGIN = "packrat@packrat"
21
+
22
+ def create_initializer
23
+ template "initializer.rb.tt", "config/initializers/packrat.rb"
24
+ end
25
+
26
+ # Everyone who opens Claude Code in the repository and trusts the folder gets the plugin. Other
27
+ # settings in the file are kept, and so is a plugin someone turned off on purpose.
28
+ def enable_claude_code_plugin
29
+ return unless options[:claude_code]
30
+
31
+ path = File.join(destination_root, CLAUDE_SETTINGS)
32
+ return create_file(CLAUDE_SETTINGS, settings_json({})) unless File.exist?(path)
33
+
34
+ current = JSON.parse(File.read(path))
35
+ merged = settings_json(current)
36
+ return say_status(:identical, CLAUDE_SETTINGS, :blue) if merged == File.read(path)
37
+
38
+ File.write(path, merged)
39
+ say_status :update, CLAUDE_SETTINGS
40
+ rescue JSON::ParserError
41
+ say_status :skip, "#{CLAUDE_SETTINGS} isn't valid JSON; add the Packrat plugin to it yourself", :yellow
42
+ end
43
+
44
+ private
45
+
46
+ def ingest_key
47
+ options[:ingest_key].to_s.strip
48
+ end
49
+
50
+ def ingest_url
51
+ options[:ingest_url].to_s.strip
52
+ end
53
+
54
+ def settings_json(settings)
55
+ settings = settings.dup
56
+ settings["extraKnownMarketplaces"] = { "packrat" => MARKETPLACE }.merge(settings["extraKnownMarketplaces"] || {})
57
+ settings["enabledPlugins"] = { PLUGIN => true }.merge(settings["enabledPlugins"] || {})
58
+ "#{JSON.pretty_generate(settings)}\n"
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,12 @@
1
+ # Sends your app's exceptions, traces and logs to Packrat: https://packrat.sh/docs/ruby-on-rails
2
+ # PACKRAT_INGEST_KEY, when it is set, wins over the key written here.
3
+ Packrat.configure do |config|
4
+ <% if ingest_key.empty? -%>
5
+ config.ingest_key = ENV["PACKRAT_INGEST_KEY"]
6
+ <% else -%>
7
+ config.ingest_key = ENV.fetch("PACKRAT_INGEST_KEY", <%= ingest_key.inspect %>)
8
+ <% end -%>
9
+ <% unless ingest_url.empty? -%>
10
+ config.ingest_url = <%= ingest_url.inspect %>
11
+ <% end -%>
12
+ end
@@ -0,0 +1,139 @@
1
+ require "socket"
2
+ require "uri"
3
+
4
+ module Packrat
5
+ # Everything the gem reads, all of it from the environment. An OTEL_* variable that is set always
6
+ # wins: the gem fills in only what the standard variables leave empty.
7
+ class Config
8
+ DEFAULT_URL = "https://ingest.packrat.sh".freeze
9
+ INSTRUMENTATIONS = %w[rack action_pack action_view active_job active_record net_http pg mysql2 sidekiq redis aws_sdk concurrent_ruby rake logger].freeze
10
+ COMMIT_VARIABLES = %w[BUILD_COMMIT HEROKU_SLUG_COMMIT SOURCE_VERSION].freeze
11
+ REDACTED = "[redacted]".freeze
12
+ DISTRO_ATTRIBUTES = { "telemetry.distro.name" => "packrat-rails", "telemetry.distro.version" => VERSION }.freeze
13
+
14
+ attr_reader :rails_env, :app_name
15
+ attr_writer :ingest_key, :ingest_url
16
+
17
+ def initialize(env: ENV, rails_env: nil, app_name: nil)
18
+ @env = env
19
+ @rails_env = rails_env.to_s
20
+ @app_name = app_name
21
+ end
22
+
23
+ # The variable wins over the initializer, so a deploy can change the key without a commit.
24
+ def key
25
+ present(@env["PACKRAT_INGEST_KEY"]) || present(@ingest_key)
26
+ end
27
+
28
+ def url
29
+ (present(@env["PACKRAT_INGEST_URL"]) || present(@ingest_url) || DEFAULT_URL).chomp("/")
30
+ end
31
+
32
+ def traces_endpoint
33
+ "#{url}/v1/traces"
34
+ end
35
+
36
+ def logs_endpoint
37
+ "#{url}/v1/logs"
38
+ end
39
+
40
+ def headers
41
+ { "Authorization" => key }
42
+ end
43
+
44
+ # Why the gem does nothing, or nil when it runs. Test is silent: a line per test process is noise.
45
+ def off_reason
46
+ if rails_env == "test" then nil
47
+ elsif flag?("PACKRAT_DISABLED") then "PACKRAT_DISABLED is set, so nothing is sent"
48
+ elsif sdk_disabled? then "OTEL_SDK_DISABLED is true, so nothing is sent"
49
+ elsif key.nil? then "no ingest key is set (PACKRAT_INGEST_KEY or config.ingest_key), so nothing is sent"
50
+ end
51
+ end
52
+
53
+ def enabled?
54
+ allowed? && !key.nil?
55
+ end
56
+
57
+ # Everything but the key. The middleware is placed before config/initializers set the key.
58
+ def allowed?
59
+ rails_env != "test" && !flag?("PACKRAT_DISABLED") && !sdk_disabled?
60
+ end
61
+
62
+ # PACKRAT_OTEL_INSTRUMENTATIONS=sidekiq,-pg adds sidekiq to the allowlist and takes pg out.
63
+ def instrumentations
64
+ @env["PACKRAT_OTEL_INSTRUMENTATIONS"].to_s.split(",").map(&:strip).reject(&:empty?)
65
+ .reduce(INSTRUMENTATIONS.dup) do |names, entry|
66
+ entry.start_with?("-") ? names - [ entry.delete_prefix("-") ] : names | [ entry ]
67
+ end
68
+ end
69
+
70
+ def service_name
71
+ present(@env["OTEL_SERVICE_NAME"]) || env_attributes["service.name"] || app_name
72
+ end
73
+
74
+ def environment
75
+ env_attributes["deployment.environment.name"] || env_attributes["deployment.environment"] || rails_env
76
+ end
77
+
78
+ def service_version
79
+ env_attributes["service.version"] || COMMIT_VARIABLES.lazy.map { |name| present(@env[name]) }.find(&:itself)
80
+ end
81
+
82
+ # Only what the standard variables leave unset; the SDK's default resource already holds theirs.
83
+ def resource_attributes
84
+ {}.tap do |attributes|
85
+ attributes["service.name"] = app_name if app_name && service_name == app_name
86
+ unless env_attributes.key?("deployment.environment.name") || env_attributes.key?("deployment.environment")
87
+ attributes["deployment.environment.name"] = rails_env
88
+ end
89
+ version = service_version
90
+ attributes["service.version"] = version if version && !env_attributes.key?("service.version")
91
+ # DYNO names the process on Heroku and build.io: web.1, worker.2.
92
+ attributes["service.instance.id"] = present(@env["DYNO"]) || Socket.gethostname unless env_attributes.key?("service.instance.id")
93
+ # Packrat reads telemetry.distro.name to tell this gem's data from a hand-built setup.
94
+ DISTRO_ATTRIBUTES.each { |name, value| attributes[name] = value unless env_attributes.key?(name) }
95
+ end
96
+ end
97
+
98
+ def set?(name)
99
+ !present(@env[name]).nil?
100
+ end
101
+
102
+ # The app's own exporter names the ingest host, so a second exporter would send every span twice.
103
+ def app_exports_to_packrat?
104
+ ours = URI(url).then { |uri| [ uri.host, uri.port ] }
105
+ %w[OTEL_EXPORTER_OTLP_ENDPOINT OTEL_EXPORTER_OTLP_TRACES_ENDPOINT].any? do |name|
106
+ (URI(present(@env[name]).to_s).then { |uri| [ uri.host, uri.port ] } rescue nil) == ours
107
+ end
108
+ end
109
+
110
+ def inspect
111
+ "#<#{self.class.name} url=#{url} key=#{key ? REDACTED : 'unset'} service=#{service_name} environment=#{environment}>"
112
+ end
113
+ alias to_s inspect
114
+
115
+ private
116
+
117
+ # The SDK reads OTEL_RESOURCE_ATTRIBUTES the same way: split on commas, then on "=".
118
+ def env_attributes
119
+ @env_attributes ||= @env["OTEL_RESOURCE_ATTRIBUTES"].to_s.split(",").filter_map do |pair|
120
+ name, value = pair.split("=", 2).map { |part| part.to_s.strip }
121
+ [ name, value ] unless name.empty? || value.empty?
122
+ end.to_h
123
+ end
124
+
125
+ def sdk_disabled?
126
+ @env["OTEL_SDK_DISABLED"].to_s.strip.casecmp?("true")
127
+ end
128
+
129
+ def flag?(name)
130
+ value = present(@env[name])
131
+ !value.nil? && !%w[0 false no].include?(value.downcase)
132
+ end
133
+
134
+ def present(value)
135
+ value = value.to_s.strip
136
+ value.empty? ? nil : value
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,71 @@
1
+ require "json"
2
+
3
+ module Packrat
4
+ module Otel
5
+ # Everything Rails.error hears -- an error that escapes a request or a job, one the app rescues and
6
+ # reports itself, one in a runner script -- recorded on the span it happened in, or on a span of its
7
+ # own when there is none.
8
+ class ErrorSpans
9
+ NAME = "Rails.error"
10
+ # Rails puts the live controller and job objects in the context.
11
+ SKIPPED_KEYS = %i[controller job].freeze
12
+ # Rails reports these after the request or job instrumentation has recorded the same error.
13
+ INSTRUMENTED_SOURCES = %w[application.action_dispatch job.sidekiq].freeze
14
+ # A cache failure falls back to the source of truth; Sentry skips these too.
15
+ CACHE_STORE_SOURCE = /_cache_store\.active_support\z/
16
+
17
+ def report(error, handled:, severity:, context:, source: nil)
18
+ return if skipped?(handled, source)
19
+
20
+ exception = error.is_a?(Exception) ? error : RuntimeError.new(error.to_s)
21
+ attributes = exception_attributes(exception, handled, severity, source).merge(context_attributes(context))
22
+ span = OpenTelemetry::Trace.current_span
23
+ return span.record_exception(exception, attributes:) if span.recording?
24
+
25
+ span = OpenTelemetry.tracer_provider.tracer("rails.error").start_root_span(NAME)
26
+ span.record_exception(exception, attributes:)
27
+ span.finish
28
+ rescue StandardError => error
29
+ OpenTelemetry.handle_error(exception: error, message: "Packrat::Otel::ErrorSpans#report failed")
30
+ end
31
+
32
+ private
33
+
34
+ def skipped?(handled, source)
35
+ (!handled && INSTRUMENTED_SOURCES.include?(source)) || CACHE_STORE_SOURCE.match?(source.to_s)
36
+ end
37
+
38
+ # Span events are not scrubbed on export, so the message and the stack are scrubbed here.
39
+ def exception_attributes(exception, handled, severity, source)
40
+ {
41
+ "exception.message" => scrub(exception.message),
42
+ "exception.stacktrace" => scrub(exception.full_message(highlight: false, order: :top)),
43
+ "exception.handled" => handled,
44
+ "rails.error.severity" => severity.to_s,
45
+ "rails.error.source" => source
46
+ }.compact
47
+ end
48
+
49
+ def context_attributes(context)
50
+ values = context.except(*SKIPPED_KEYS, :tags)
51
+ values = values.merge(context[:tags]) if context[:tags].is_a?(Hash)
52
+
53
+ values.each_with_object({}) do |(key, value), attributes|
54
+ attributes["rails.error.context.#{key}"] = attribute_value(value) unless value.nil?
55
+ end
56
+ end
57
+
58
+ def attribute_value(value)
59
+ case value
60
+ when Integer, Float, true, false then value
61
+ when Hash, Array then scrub(value.to_json)
62
+ else scrub(value.to_s)
63
+ end
64
+ end
65
+
66
+ def scrub(text)
67
+ Scrubber.scrub_text(text.to_s.encode("UTF-8", invalid: :replace, undef: :replace, replace: "�"))
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,25 @@
1
+ module Packrat
2
+ module Otel
3
+ # A trace that starts with a request (server) or a job (consumer) is work. One that starts with
4
+ # anything else is a poll: Solid Queue and Solid Cable each ask Postgres for work about ten times
5
+ # a second, and every poll would otherwise be a trace of its own. Children never reach this class;
6
+ # parent_based routes only roots here, and a child follows its root. A Rake task is work too,
7
+ # though its span is internal; when Rails boots inside the task, the root is its execute.
8
+ class RootKindSampler
9
+ KEEP = %i[server consumer].freeze
10
+ NAMES = [ ErrorSpans::NAME, "rake.invoke", "rake.execute" ].freeze
11
+
12
+ def should_sample?(trace_id:, parent_context:, links:, name:, kind:, attributes:)
13
+ keep = KEEP.include?(kind) || NAMES.include?(name)
14
+ decision = keep ? OpenTelemetry::SDK::Trace::Samplers::Decision::RECORD_AND_SAMPLE
15
+ : OpenTelemetry::SDK::Trace::Samplers::Decision::DROP
16
+ tracestate = OpenTelemetry::Trace.current_span(parent_context).context.tracestate
17
+ OpenTelemetry::SDK::Trace::Samplers::Result.new(decision:, tracestate:)
18
+ end
19
+
20
+ def description
21
+ "RootKindSampler{#{KEEP.join(',')}}"
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,111 @@
1
+ require "cgi"
2
+
3
+ module Packrat
4
+ module Otel
5
+ # Removes credentials before telemetry leaves the process. Packrat archives raw data and never
6
+ # rewrites it, so anything that survives here is kept for good.
7
+ #
8
+ # Query strings are kept on purpose: they carry what a request was about, which is most of what
9
+ # a trace is read for. Only the values of credential-named parameters are replaced.
10
+ module Scrubber
11
+ REDACTED = "[REDACTED]".freeze
12
+ URL_KEYS = %w[http.target http.url url.full url.query].freeze
13
+ HEADER_PREFIXES = %w[http.request.header. http.response.header.].freeze
14
+ # A whole parameter name, or one segment of a snake_case name: api_key and access_token match,
15
+ # keyword and monkey do not.
16
+ CREDENTIAL_PARAM = /\A(?:[a-z0-9]+_)*(?:key|token|password|passwd|secret|signature|sig|auth|credential)(?:_[a-z0-9]+)*\z/i
17
+ # Free text is redacted by pattern: log lines and error messages interpolate URLs, which is
18
+ # where a credential usually appears.
19
+ CREDENTIAL_IN_TEXT = /
20
+ (?<name>\b(?:[a-z0-9]+_)?(?:api_key|access_token|key|token|password|passwd|secret|signature|sig|auth|credential)\s*=\s*)
21
+ (?<value>[^\s&"'<>\]]+)
22
+ /ix
23
+ BEARER = %r{\b(?<scheme>Bearer|Basic)\s+(?<value>[A-Za-z0-9._~+/-]+=*)}i
24
+ ANSI = /\e\[[0-9;]*m/
25
+
26
+ class << self
27
+ def scrub_text(text)
28
+ return text unless text.is_a?(String)
29
+
30
+ text.gsub(CREDENTIAL_IN_TEXT) { "#{Regexp.last_match(:name)}#{REDACTED}" }
31
+ .gsub(BEARER) { "#{Regexp.last_match(:scheme)} #{REDACTED}" }
32
+ end
33
+
34
+ # The same hash when nothing needs to change, so callers can tell by identity.
35
+ def scrub_attributes(attributes)
36
+ return attributes if attributes.nil?
37
+ return attributes unless attributes.any? { |key, value| header?(key) || redactable_url?(key, value) }
38
+
39
+ attributes.each_with_object({}) do |(key, value), scrubbed|
40
+ next if header?(key)
41
+
42
+ scrubbed[key] = URL_KEYS.include?(key) ? redact_query(value) : value
43
+ end
44
+ end
45
+
46
+ # SpanData and LogRecordData are frozen structs, so a changed one is rebuilt from its fields.
47
+ def scrub_spans(spans)
48
+ spans.map do |span|
49
+ attributes = scrub_attributes(span.attributes)
50
+ attributes.equal?(span.attributes) ? span : span.class.new(*span.to_h.merge(attributes:).values)
51
+ end
52
+ end
53
+
54
+ def scrub_log_records(records)
55
+ records.map do |record|
56
+ attributes = scrub_attributes(record.attributes)
57
+ body = record.body.is_a?(String) ? scrub_text(record.body.gsub(ANSI, "")) : record.body
58
+ next record if attributes.equal?(record.attributes) && body == record.body
59
+
60
+ record.class.new(*record.to_h.merge(attributes:, body:).values)
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ def header?(key)
67
+ HEADER_PREFIXES.any? { |prefix| key.to_s.start_with?(prefix) }
68
+ end
69
+
70
+ def redactable_url?(key, value)
71
+ URL_KEYS.include?(key) && value.to_s.include?("=")
72
+ end
73
+
74
+ def redact_query(url)
75
+ head, separator, query = url.to_s.include?("?") ? url.to_s.partition("?") : [ "", "", url.to_s ]
76
+ return url if query.empty?
77
+
78
+ redacted = query.split("&").map do |pair|
79
+ name, equals, _value = pair.partition("=")
80
+ equals.empty? || !CREDENTIAL_PARAM.match?(CGI.unescape(name)) ? pair : "#{name}=#{REDACTED}"
81
+ end
82
+ "#{head}#{separator}#{redacted.join('&')}"
83
+ end
84
+ end
85
+ end
86
+
87
+ class ScrubbingSpanExporter
88
+ def initialize(exporter)
89
+ @exporter = exporter
90
+ end
91
+
92
+ def export(spans, timeout: nil)
93
+ @exporter.export(Scrubber.scrub_spans(spans), timeout:)
94
+ end
95
+
96
+ def force_flush(timeout: nil)
97
+ @exporter.force_flush(timeout:)
98
+ end
99
+
100
+ def shutdown(timeout: nil)
101
+ @exporter.shutdown(timeout:)
102
+ end
103
+ end
104
+
105
+ class ScrubbingLogExporter < ScrubbingSpanExporter
106
+ def export(records, timeout: nil)
107
+ @exporter.export(Scrubber.scrub_log_records(records), timeout:)
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,250 @@
1
+ require "uri"
2
+
3
+ module Packrat
4
+ module Otel
5
+ CLASSES = {
6
+ "rack" => "OpenTelemetry::Instrumentation::Rack",
7
+ "action_pack" => "OpenTelemetry::Instrumentation::ActionPack",
8
+ "active_job" => "OpenTelemetry::Instrumentation::ActiveJob",
9
+ "active_record" => "OpenTelemetry::Instrumentation::ActiveRecord",
10
+ "net_http" => "OpenTelemetry::Instrumentation::Net::HTTP",
11
+ "pg" => "OpenTelemetry::Instrumentation::PG",
12
+ "mysql2" => "OpenTelemetry::Instrumentation::Mysql2",
13
+ "logger" => "OpenTelemetry::Instrumentation::Logger"
14
+ }.freeze
15
+ # The health check runs every few seconds and would otherwise be most of what is stored.
16
+ OPTIONS = { "rack" => { untraced_endpoints: [ "/up" ] } }.freeze
17
+ # These install through the Active Support instrumentation and do not require it themselves.
18
+ PREREQUISITES = { "action_view" => "active_support" }.freeze
19
+ # These set up in an after_initialize block of their own Railtie, which cannot run once boot has
20
+ # reached Packrat's.
21
+ BOOT_ONLY = %w[action_mailer active_storage].freeze
22
+ ACTION_VIEW_EVENTS = %w[render_template render_partial render_collection render_layout].map { |event| "#{event}.action_view" }.freeze
23
+ EXIT_TIMEOUT = 5
24
+
25
+ # dogstatsd-ruby defines Datadog::Statsd without the tracer, hence the method check.
26
+ def self.datadog?
27
+ !!(defined?(::Datadog) && ::Datadog.respond_to?(:configuration))
28
+ end
29
+
30
+ # The one decision, made at the end of boot: configure OpenTelemetry when nothing has, add a
31
+ # Packrat exporter beside the app's own when something has, and stay out of Datadog's way.
32
+ class Setup
33
+ attr_reader :outcome, :line, :installed, :warnings
34
+
35
+ def initialize(config:, logger:, errors:)
36
+ @config = config
37
+ @logger = logger
38
+ @errors = errors
39
+ @installed = []
40
+ @warnings = []
41
+ end
42
+
43
+ def call
44
+ provider = OpenTelemetry.tracer_provider
45
+ if !@config.enabled? then off
46
+ elsif Otel.datadog? then datadog
47
+ elsif provider.is_a?(OpenTelemetry::SDK::Trace::TracerProvider) then append(provider)
48
+ else configure
49
+ end
50
+ @logger.info(@line) if @line
51
+ self
52
+ end
53
+
54
+ # The Rack span, for the middleware the Railtie put in the stack before this decision was made.
55
+ def rack(app)
56
+ return app unless @outcome == :configured && @installed.include?("rack")
57
+
58
+ middleware, *args = instrumentation("rack").middleware_args
59
+ middleware.new(app, *args)
60
+ rescue StandardError, LoadError => error
61
+ failed("rack middleware", error)
62
+ app
63
+ end
64
+
65
+ private
66
+
67
+ def off
68
+ @outcome = :off
69
+ @line = @config.off_reason && "Packrat: #{@config.off_reason}"
70
+ end
71
+
72
+ def datadog
73
+ @outcome = :datadog
74
+ @line = "Packrat: Datadog found; configure the Agent to dual-ship, see https://packrat.sh/docs/datadog"
75
+ end
76
+
77
+ def configure
78
+ logs = step("logs SDK") { load_logs }
79
+ spans = step("traces exporter") { span_processor }
80
+ records = logs && step("logs exporter") { log_record_processor }
81
+ return @line = "Packrat: OpenTelemetry was not configured, so nothing is sent" unless spans && step("SDK") { configure_sdk(spans, records) }
82
+
83
+ @outcome = :configured
84
+ step("sampler") { use_sampler } unless @config.set?("OTEL_TRACES_SAMPLER")
85
+ @config.instrumentations.each { |name| step("instrumentation #{name}") { install(name) } }
86
+ step("Rails.error") { @errors.subscribe(ErrorSpans.new) }
87
+ step("at_exit") { at_exit { shutdown_providers } }
88
+ @line = "Packrat: sending #{signals(records)} for #{@config.service_name}/#{@config.environment} " \
89
+ "to #{@config.url} (#{@installed.join(', ')})"
90
+ end
91
+
92
+ def append(provider)
93
+ if @config.app_exports_to_packrat?
94
+ @outcome = :already
95
+ return @line = "Packrat: the app's OpenTelemetry exporter already sends to #{@config.url}"
96
+ end
97
+
98
+ spans = step("traces exporter") { span_processor }
99
+ return @line = "Packrat: the app's OpenTelemetry was left alone, and nothing is sent" unless spans && step("traces processor") { provider.add_span_processor(spans) }
100
+
101
+ @outcome = :appended
102
+ records = sdk_logger_provider && logger_instrumented? && step("logs exporter") { log_record_processor }
103
+ records = nil unless records && step("logs processor") { sdk_logger_provider.add_log_record_processor(records) }
104
+ errors = !error_spans? && step("Rails.error") { @errors.subscribe(ErrorSpans.new) }
105
+ step("at_exit") { at_exit { [ spans, records ].compact.each { |processor| processor.shutdown(timeout: EXIT_TIMEOUT) } } }
106
+
107
+ attributes = provider.resource.attribute_enumerator.to_h
108
+ name = attributes["service.name"]
109
+ environment = attributes["deployment.environment.name"] || attributes["deployment.environment"]
110
+ @line = "Packrat: sending a copy of the app's #{signals(records)} for #{name}/#{environment} to #{@config.url}"
111
+ @line += "; Rails.error is now recorded on spans, which the app's own exporter also receives" if errors
112
+ if name.nil? || name.start_with?("unknown_service")
113
+ @line += ". Warning: the app's OpenTelemetry resource has no service.name, and the setup screen looks for the app by name"
114
+ end
115
+ end
116
+
117
+ # Every step fails on its own: a warning that names it, and boot goes on. LoadError is not a
118
+ # StandardError, and a missing gem is the likely failure.
119
+ def step(name)
120
+ yield || true
121
+ rescue StandardError, LoadError => error
122
+ failed(name, error)
123
+ nil
124
+ end
125
+
126
+ def failed(name, error)
127
+ text = Packrat.redact(error.message.lines.first.to_s.strip, @config.key)
128
+ message = "Packrat: #{name} failed (#{error.class}: #{text}); the app runs without it"
129
+ @warnings << message
130
+ @logger.warn(message)
131
+ end
132
+
133
+ # Required here rather than at the top of the gem: the logs SDK prepends a hook to
134
+ # SDK::Configurator, and an app that configures OpenTelemetry itself would then get a logger
135
+ # provider exporting to localhost:4318 that it never asked for.
136
+ def load_logs
137
+ require "opentelemetry-logs-sdk"
138
+ require "opentelemetry-exporter-otlp-logs"
139
+ end
140
+
141
+ def span_processor
142
+ OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(
143
+ ScrubbingSpanExporter.new(OpenTelemetry::Exporter::OTLP::Exporter.new(endpoint: @config.traces_endpoint, headers: @config.headers))
144
+ )
145
+ end
146
+
147
+ def log_record_processor
148
+ OpenTelemetry::SDK::Logs::Export::BatchLogRecordProcessor.new(
149
+ ScrubbingLogExporter.new(OpenTelemetry::Exporter::OTLP::Logs::LogsExporter.new(endpoint: @config.logs_endpoint, headers: @config.headers))
150
+ )
151
+ rescue NameError => error
152
+ raise unless error.name == :Utilities
153
+
154
+ # The logs exporter from 0.6 calls Common::Utilities, which otlp-common has from 0.3, and still
155
+ # declares "~> 0.2"; Bundler keeps an older lock. A bound in the gemspec would need Ruby 3.3.
156
+ raise NameError, "#{error.message}; run bundle update opentelemetry-exporter-otlp-common"
157
+ end
158
+
159
+ def configure_sdk(spans, records)
160
+ OpenTelemetry::SDK.configure do |sdk|
161
+ sdk.resource = OpenTelemetry::SDK::Resources::Resource.create(@config.resource_attributes)
162
+ sdk.add_span_processor(spans)
163
+ sdk.add_log_record_processor(records) if records
164
+ end
165
+ end
166
+
167
+ def use_sampler
168
+ OpenTelemetry.tracer_provider.sampler =
169
+ OpenTelemetry::SDK::Trace::Samplers.parent_based(root: RootKindSampler.new)
170
+ end
171
+
172
+ def install(name)
173
+ raise ArgumentError, "the #{name} instrumentation must load before boot ends, which Packrat does not do" if BOOT_ONLY.include?(name)
174
+
175
+ require "opentelemetry-instrumentation-#{PREREQUISITES[name]}" if PREREQUISITES[name]
176
+ require "opentelemetry-instrumentation-#{name}"
177
+ skip_railtie(name) if %w[action_pack action_view].include?(name)
178
+ return unless instrumentation(name).install(OPTIONS.fetch(name, {}))
179
+
180
+ subscribe_action_view if name == "action_view"
181
+
182
+ prerequisite = PREREQUISITES[name]
183
+ @installed << prerequisite if prerequisite && !@installed.include?(prerequisite) && instrumentation(prerequisite).installed?
184
+ @installed << name
185
+ end
186
+
187
+ # Action Pack's install loads a Railtie whose one job is to put the Rack span in the middleware
188
+ # stack. At the end of boot the stack is frozen, so loading it raises; Packrat::RackSpan does
189
+ # that job, and the file is marked as loaded instead. Action View's is skipped too: see
190
+ # subscribe_action_view.
191
+ def skip_railtie(name)
192
+ _, path = $LOAD_PATH.resolve_feature_path("opentelemetry/instrumentation/#{name}/railtie")
193
+ path = File.realpath(path)
194
+ $LOADED_FEATURES << path unless $LOADED_FEATURES.include?(path)
195
+ end
196
+
197
+ # Action View's Railtie subscribes in config.after_initialize. Loaded while Packrat's own
198
+ # after_initialize block runs, that block runs at once and again when Rails reaches it in the
199
+ # list it is walking. Two subscribers share one payload key, so every render left a context
200
+ # attached and each later request on the thread became a child of the last.
201
+ def subscribe_action_view
202
+ instrumentation("active_support").install({})
203
+ action_view = instrumentation("action_view")
204
+ formatter = OpenTelemetry::Instrumentation::ActiveSupport::LEGACY_NAME_FORMATTER if action_view.config[:legacy_span_names]
205
+ ACTION_VIEW_EVENTS.each do |event|
206
+ OpenTelemetry::Instrumentation::ActiveSupport.subscribe(
207
+ action_view.tracer, event, action_view.config[:notification_payload_transform],
208
+ action_view.config[:disallowed_notification_payload_keys], span_name_formatter: formatter
209
+ )
210
+ end
211
+ end
212
+
213
+ def instrumentation(name)
214
+ namespace = CLASSES[name] || begin
215
+ constant = OpenTelemetry::Instrumentation.constants.find { |found| found.to_s.downcase == name.delete("_") }
216
+ "OpenTelemetry::Instrumentation::#{constant || raise(NameError, "no instrumentation named #{name}")}"
217
+ end
218
+ Object.const_get("#{namespace}::Instrumentation").instance
219
+ end
220
+
221
+ def shutdown_providers
222
+ OpenTelemetry.tracer_provider.shutdown(timeout: EXIT_TIMEOUT)
223
+ OpenTelemetry.logger_provider.shutdown(timeout: EXIT_TIMEOUT) if sdk_logger_provider
224
+ end
225
+
226
+ def sdk_logger_provider
227
+ return unless defined?(OpenTelemetry::SDK::Logs::LoggerProvider) && OpenTelemetry.respond_to?(:logger_provider)
228
+
229
+ provider = OpenTelemetry.logger_provider
230
+ provider if provider.is_a?(OpenTelemetry::SDK::Logs::LoggerProvider)
231
+ end
232
+
233
+ # A logger provider nothing writes to is common: the logs SDK makes one whenever it is loaded,
234
+ # and an app may send its log lines through a provider of its own.
235
+ def logger_instrumented?
236
+ defined?(OpenTelemetry::Instrumentation::Logger::Instrumentation) &&
237
+ OpenTelemetry::Instrumentation::Logger::Instrumentation.instance.installed?
238
+ end
239
+
240
+ # ActiveSupport::ErrorReporter keeps its subscribers private.
241
+ def error_spans?
242
+ Array(@errors.instance_variable_get(:@subscribers)).any? { |subscriber| subscriber.class.name.to_s.end_with?("ErrorSpans") }
243
+ end
244
+
245
+ def signals(records)
246
+ records ? "traces and logs" : "traces"
247
+ end
248
+ end
249
+ end
250
+ end
@@ -0,0 +1,23 @@
1
+ module Packrat
2
+ # The Rack instrumentation is middleware, and the stack is built before the end of boot, where
3
+ # Packrat decides whether it configures OpenTelemetry at all. So the Railtie puts this in early,
4
+ # and it becomes the Rack span or passes every request straight through once that is decided.
5
+ class RackSpan
6
+ def initialize(app)
7
+ @app = app
8
+ @lock = Mutex.new
9
+ end
10
+
11
+ def call(env)
12
+ (@inner || inner).call(env)
13
+ end
14
+
15
+ private
16
+
17
+ def inner
18
+ return @app unless Packrat.setup
19
+
20
+ @lock.synchronize { @inner ||= Packrat.setup.rack(@app) }
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ require "rails/railtie"
2
+
3
+ module Packrat
4
+ class Railtie < Rails::Railtie
5
+ initializer "packrat.middleware" do |app|
6
+ Packrat.guard("middleware") do
7
+ app.config.middleware.insert_before(0, RackSpan) if Packrat.config.allowed? && !Otel.datadog?
8
+ end
9
+ end
10
+
11
+ # The decision must come after every after_initialize block, so that an app that configures
12
+ # OpenTelemetry late has done so. to_prepare runs after config/initializers registered theirs, so
13
+ # a block registered here is the last. An initializer declared after: :finisher_hook is not the
14
+ # same thing: the sort pulls the whole Finisher ahead of config/initializers. to_prepare runs
15
+ # again on every reload in development, hence the flag.
16
+ config.to_prepare do
17
+ next if Packrat.registered
18
+
19
+ Packrat.registered = true
20
+ Rails.application.config.after_initialize { Packrat.guard("boot") { Packrat.start } }
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,3 @@
1
+ module Packrat
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1 @@
1
+ require_relative "packrat"
data/lib/packrat.rb ADDED
@@ -0,0 +1,48 @@
1
+ require "logger"
2
+ require "opentelemetry/sdk"
3
+ require "opentelemetry/exporter/otlp"
4
+ require_relative "packrat/version"
5
+ require_relative "packrat/config"
6
+ require_relative "packrat/otel/scrubber"
7
+ require_relative "packrat/otel/error_spans"
8
+ require_relative "packrat/otel/root_kind_sampler"
9
+ require_relative "packrat/otel"
10
+ require_relative "packrat/rack_span"
11
+ require_relative "packrat/railtie" if defined?(Rails::Railtie)
12
+
13
+ module Packrat
14
+ class << self
15
+ attr_reader :setup
16
+ attr_accessor :registered
17
+
18
+ def config
19
+ @config ||= Config.new(
20
+ env: ENV, rails_env: Rails.env, app_name: Rails.application.class.module_parent_name.underscore
21
+ )
22
+ end
23
+
24
+ # Packrat.configure { |config| config.ingest_key = "..." }, from config/initializers/packrat.rb.
25
+ def configure
26
+ yield config
27
+ end
28
+
29
+ # An app adds to what the gem configured with ActiveSupport.on_load(:packrat) { |setup| ... }.
30
+ def start
31
+ @setup = Otel::Setup.new(config:, logger: Rails.logger, errors: Rails.error).call
32
+ ActiveSupport.run_load_hooks(:packrat, @setup)
33
+ end
34
+
35
+ # The outermost catch: whatever goes wrong, the app boots without Packrat.
36
+ def guard(name)
37
+ yield
38
+ rescue StandardError, LoadError => error
39
+ key = (config.key rescue nil)
40
+ Rails.logger&.warn("Packrat: #{name} failed (#{error.class}: #{redact(error.message, key)}); the app runs without it")
41
+ nil
42
+ end
43
+
44
+ def redact(text, key)
45
+ key ? text.to_s.gsub(key, Config::REDACTED) : text.to_s
46
+ end
47
+ end
48
+ end
metadata ADDED
@@ -0,0 +1,323 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: packrat-rails
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Packrat Labs
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-10-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: railties
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '7.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '7.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: opentelemetry-sdk
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '1.2'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '1.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: opentelemetry-exporter-otlp
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0.26'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0.26'
55
+ - !ruby/object:Gem::Dependency
56
+ name: opentelemetry-logs-sdk
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0.1'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0.1'
69
+ - !ruby/object:Gem::Dependency
70
+ name: opentelemetry-exporter-otlp-logs
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0.1'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0.1'
83
+ - !ruby/object:Gem::Dependency
84
+ name: opentelemetry-instrumentation-rack
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0.24'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0.24'
97
+ - !ruby/object:Gem::Dependency
98
+ name: opentelemetry-instrumentation-action_pack
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0.9'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0.9'
111
+ - !ruby/object:Gem::Dependency
112
+ name: opentelemetry-instrumentation-active_job
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0.7'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0.7'
125
+ - !ruby/object:Gem::Dependency
126
+ name: opentelemetry-instrumentation-active_record
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0.7'
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0.7'
139
+ - !ruby/object:Gem::Dependency
140
+ name: opentelemetry-instrumentation-net_http
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0.22'
146
+ type: :runtime
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0.22'
153
+ - !ruby/object:Gem::Dependency
154
+ name: opentelemetry-instrumentation-pg
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0.27'
160
+ type: :runtime
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0.27'
167
+ - !ruby/object:Gem::Dependency
168
+ name: opentelemetry-instrumentation-mysql2
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0.27'
174
+ type: :runtime
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0.27'
181
+ - !ruby/object:Gem::Dependency
182
+ name: opentelemetry-instrumentation-sidekiq
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - ">="
186
+ - !ruby/object:Gem::Version
187
+ version: '0.25'
188
+ type: :runtime
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - ">="
193
+ - !ruby/object:Gem::Version
194
+ version: '0.25'
195
+ - !ruby/object:Gem::Dependency
196
+ name: opentelemetry-instrumentation-action_view
197
+ requirement: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - ">="
200
+ - !ruby/object:Gem::Version
201
+ version: '0.7'
202
+ type: :runtime
203
+ prerelease: false
204
+ version_requirements: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - ">="
207
+ - !ruby/object:Gem::Version
208
+ version: '0.7'
209
+ - !ruby/object:Gem::Dependency
210
+ name: opentelemetry-instrumentation-redis
211
+ requirement: !ruby/object:Gem::Requirement
212
+ requirements:
213
+ - - ">="
214
+ - !ruby/object:Gem::Version
215
+ version: '0.25'
216
+ type: :runtime
217
+ prerelease: false
218
+ version_requirements: !ruby/object:Gem::Requirement
219
+ requirements:
220
+ - - ">="
221
+ - !ruby/object:Gem::Version
222
+ version: '0.25'
223
+ - !ruby/object:Gem::Dependency
224
+ name: opentelemetry-instrumentation-aws_sdk
225
+ requirement: !ruby/object:Gem::Requirement
226
+ requirements:
227
+ - - ">="
228
+ - !ruby/object:Gem::Version
229
+ version: '0.5'
230
+ type: :runtime
231
+ prerelease: false
232
+ version_requirements: !ruby/object:Gem::Requirement
233
+ requirements:
234
+ - - ">="
235
+ - !ruby/object:Gem::Version
236
+ version: '0.5'
237
+ - !ruby/object:Gem::Dependency
238
+ name: opentelemetry-instrumentation-concurrent_ruby
239
+ requirement: !ruby/object:Gem::Requirement
240
+ requirements:
241
+ - - ">="
242
+ - !ruby/object:Gem::Version
243
+ version: '0.21'
244
+ type: :runtime
245
+ prerelease: false
246
+ version_requirements: !ruby/object:Gem::Requirement
247
+ requirements:
248
+ - - ">="
249
+ - !ruby/object:Gem::Version
250
+ version: '0.21'
251
+ - !ruby/object:Gem::Dependency
252
+ name: opentelemetry-instrumentation-rake
253
+ requirement: !ruby/object:Gem::Requirement
254
+ requirements:
255
+ - - ">="
256
+ - !ruby/object:Gem::Version
257
+ version: '0.2'
258
+ type: :runtime
259
+ prerelease: false
260
+ version_requirements: !ruby/object:Gem::Requirement
261
+ requirements:
262
+ - - ">="
263
+ - !ruby/object:Gem::Version
264
+ version: '0.2'
265
+ - !ruby/object:Gem::Dependency
266
+ name: opentelemetry-instrumentation-logger
267
+ requirement: !ruby/object:Gem::Requirement
268
+ requirements:
269
+ - - ">="
270
+ - !ruby/object:Gem::Version
271
+ version: '0.1'
272
+ type: :runtime
273
+ prerelease: false
274
+ version_requirements: !ruby/object:Gem::Requirement
275
+ requirements:
276
+ - - ">="
277
+ - !ruby/object:Gem::Version
278
+ version: '0.1'
279
+ description:
280
+ email:
281
+ executables: []
282
+ extensions: []
283
+ extra_rdoc_files: []
284
+ files:
285
+ - LICENSE
286
+ - README.md
287
+ - lib/generators/packrat/packrat_generator.rb
288
+ - lib/generators/packrat/templates/initializer.rb.tt
289
+ - lib/packrat-rails.rb
290
+ - lib/packrat.rb
291
+ - lib/packrat/config.rb
292
+ - lib/packrat/otel.rb
293
+ - lib/packrat/otel/error_spans.rb
294
+ - lib/packrat/otel/root_kind_sampler.rb
295
+ - lib/packrat/otel/scrubber.rb
296
+ - lib/packrat/rack_span.rb
297
+ - lib/packrat/railtie.rb
298
+ - lib/packrat/version.rb
299
+ homepage: https://packrat.sh/docs/ruby-on-rails
300
+ licenses:
301
+ - MIT
302
+ metadata:
303
+ source_code_uri: https://github.com/packratlabs/packrat-rails
304
+ post_install_message:
305
+ rdoc_options: []
306
+ require_paths:
307
+ - lib
308
+ required_ruby_version: !ruby/object:Gem::Requirement
309
+ requirements:
310
+ - - ">="
311
+ - !ruby/object:Gem::Version
312
+ version: '3.1'
313
+ required_rubygems_version: !ruby/object:Gem::Requirement
314
+ requirements:
315
+ - - ">="
316
+ - !ruby/object:Gem::Version
317
+ version: '0'
318
+ requirements: []
319
+ rubygems_version: 3.5.22
320
+ signing_key:
321
+ specification_version: 4
322
+ summary: OpenTelemetry for Rails, configured to send to Packrat with one key
323
+ test_files: []