miniapm 1.0.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.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +43 -0
  3. data/LICENSE +21 -0
  4. data/README.md +174 -0
  5. data/lib/generators/miniapm/install_generator.rb +27 -0
  6. data/lib/generators/miniapm/templates/README +19 -0
  7. data/lib/generators/miniapm/templates/initializer.rb +60 -0
  8. data/lib/miniapm/configuration.rb +176 -0
  9. data/lib/miniapm/context.rb +138 -0
  10. data/lib/miniapm/error_event.rb +130 -0
  11. data/lib/miniapm/exporters/errors.rb +67 -0
  12. data/lib/miniapm/exporters/otlp.rb +90 -0
  13. data/lib/miniapm/instrumentations/activejob.rb +271 -0
  14. data/lib/miniapm/instrumentations/activerecord.rb +123 -0
  15. data/lib/miniapm/instrumentations/base.rb +61 -0
  16. data/lib/miniapm/instrumentations/cache.rb +85 -0
  17. data/lib/miniapm/instrumentations/http/faraday.rb +112 -0
  18. data/lib/miniapm/instrumentations/http/httparty.rb +84 -0
  19. data/lib/miniapm/instrumentations/http/net_http.rb +99 -0
  20. data/lib/miniapm/instrumentations/rails/controller.rb +129 -0
  21. data/lib/miniapm/instrumentations/rails/railtie.rb +42 -0
  22. data/lib/miniapm/instrumentations/redis/redis.rb +135 -0
  23. data/lib/miniapm/instrumentations/redis/redis_client.rb +116 -0
  24. data/lib/miniapm/instrumentations/registry.rb +90 -0
  25. data/lib/miniapm/instrumentations/search/elasticsearch.rb +121 -0
  26. data/lib/miniapm/instrumentations/search/opensearch.rb +120 -0
  27. data/lib/miniapm/instrumentations/search/searchkick.rb +119 -0
  28. data/lib/miniapm/instrumentations/sidekiq.rb +185 -0
  29. data/lib/miniapm/middleware/error_handler.rb +120 -0
  30. data/lib/miniapm/middleware/rack.rb +103 -0
  31. data/lib/miniapm/span.rb +289 -0
  32. data/lib/miniapm/testing.rb +209 -0
  33. data/lib/miniapm/trace.rb +26 -0
  34. data/lib/miniapm/transport/batch_sender.rb +345 -0
  35. data/lib/miniapm/transport/http.rb +45 -0
  36. data/lib/miniapm/version.rb +5 -0
  37. data/lib/miniapm.rb +184 -0
  38. metadata +183 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 525eb11f5af8c0b35aedcb05f67523d4f390c56af7fd3fd1dc3169ad1f934967
4
+ data.tar.gz: '09aa0e5d42736b39c895174cdddc9aec4944de4168beae335c6e335b212fb3ba'
5
+ SHA512:
6
+ metadata.gz: bfd98917e4502be3f040ade6c9b5e1666d3a0e1f05d99cda07be033eee5f82d99e8e5584906d6e09514b01c2e8dd22d4ed9e6e76beab24cd16240d4a1d9e321d
7
+ data.tar.gz: 9b8f7707deb4cd5c3a941c87c5416a64188527fe19c605c3fdc37a27f1ac3df44cf0a6de3c5acc72f4ef0fe67bb870e711418ac28ded78c726fff9538a8077ac
data/CHANGELOG.md ADDED
@@ -0,0 +1,43 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [1.0.0] - 2026-01-03
11
+
12
+ ### Added
13
+ - Initial release
14
+ - OTLP trace export to MiniAPM server
15
+ - Error tracking with fingerprinting and parameter filtering
16
+ - W3C Trace Context support for distributed tracing
17
+ - Automatic instrumentation for:
18
+ - Rails (ActionController, ActionView)
19
+ - ActiveRecord
20
+ - ActiveJob (SolidQueue, Sidekiq adapter)
21
+ - Sidekiq
22
+ - Rails Cache
23
+ - Net::HTTP
24
+ - HTTParty
25
+ - Faraday
26
+ - Elasticsearch
27
+ - OpenSearch
28
+ - Searchkick
29
+ - Redis (redis-client and legacy redis gem)
30
+ - Async batched sending with configurable batch size and flush interval
31
+ - Sampling support with configurable sample rate
32
+ - Rails generator for easy setup (`rails g miniapm:install`)
33
+ - Testing helpers for capturing spans and errors in tests
34
+ - Health check endpoint verification
35
+ - Retry logic with exponential backoff for failed exports
36
+
37
+ ### Security
38
+ - Automatic parameter filtering for sensitive data
39
+ - SQL query sanitization option
40
+ - No sensitive data logged by default
41
+
42
+ [Unreleased]: https://github.com/miniapm/miniapm-ruby/compare/v1.0.0...HEAD
43
+ [1.0.0]: https://github.com/miniapm/miniapm-ruby/releases/tag/v1.0.0
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Chris Hasinski
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,174 @@
1
+ # MiniAPM Ruby Client
2
+
3
+ A lightweight, zero-dependency APM client for Rails applications. Exports traces in OTLP format, captures errors, and provides comprehensive instrumentation.
4
+
5
+ **Website:** [miniapm.com](https://miniapm.com)
6
+
7
+ ## Features
8
+
9
+ - **OTLP Compatible**: Exports traces in OpenTelemetry Protocol format
10
+ - **Error Tracking**: Automatic exception capture with fingerprinting
11
+ - **W3C Trace Context**: Distributed tracing across microservices
12
+ - **Zero Runtime Dependencies**: Uses only Ruby stdlib
13
+ - **Auto-instrumentation**: Detects and instruments installed gems
14
+ - **Non-blocking**: Async batched sending never blocks requests
15
+
16
+ ## Supported Instrumentations
17
+
18
+ | Category | Library | Method |
19
+ |----------|---------|--------|
20
+ | **Rails** | ActionController, ActionView | ActiveSupport::Notifications |
21
+ | **Database** | ActiveRecord | ActiveSupport::Notifications |
22
+ | **Background Jobs** | ActiveJob, SolidQueue | ActiveSupport::Notifications |
23
+ | **Background Jobs** | Sidekiq | Server Middleware |
24
+ | **Cache** | Rails Cache | ActiveSupport::Notifications |
25
+ | **HTTP Clients** | Net::HTTP | Monkey-patch |
26
+ | **HTTP Clients** | HTTParty | Monkey-patch |
27
+ | **HTTP Clients** | Faraday | Auto-injected Middleware |
28
+ | **Search** | Elasticsearch | Monkey-patch |
29
+ | **Search** | OpenSearch | Monkey-patch |
30
+ | **Search** | Searchkick | ActiveSupport::Notifications |
31
+ | **Redis** | redis-client | Middleware |
32
+ | **Redis** | redis (legacy) | Monkey-patch |
33
+
34
+ ## Installation
35
+
36
+ Add to your Gemfile:
37
+
38
+ ```ruby
39
+ gem 'miniapm'
40
+ ```
41
+
42
+ Then run:
43
+
44
+ ```bash
45
+ bundle install
46
+ rails generate miniapm:install
47
+ ```
48
+
49
+ ## Configuration
50
+
51
+ Set environment variables:
52
+
53
+ ```bash
54
+ export MINI_APM_URL="http://your-miniapm-server:3000"
55
+ export MINI_APM_API_KEY="your_project_api_key"
56
+ ```
57
+
58
+ Or configure in `config/initializers/miniapm.rb`:
59
+
60
+ ```ruby
61
+ MiniAPM.configure do |config|
62
+ # Required
63
+ config.endpoint = ENV["MINI_APM_URL"]
64
+ config.api_key = ENV["MINI_APM_API_KEY"]
65
+
66
+ # Service identification
67
+ config.service_name = "my-rails-app"
68
+ config.environment = Rails.env
69
+
70
+ # Sampling (0.0 to 1.0)
71
+ config.sample_rate = 1.0
72
+
73
+ # Batching
74
+ config.batch_size = 100
75
+ config.flush_interval = 5.0
76
+
77
+ # Configure instrumentations
78
+ config.instrument :activerecord, log_sql: true
79
+ config.instrument :redis, enabled: false
80
+
81
+ # Error filtering
82
+ config.ignored_exceptions = ["ActionController::RoutingError"]
83
+ config.filter_parameters = [:password, :token]
84
+
85
+ # Disable in test
86
+ config.enabled = !Rails.env.test?
87
+ end
88
+ ```
89
+
90
+ ## Manual Instrumentation
91
+
92
+ Create custom spans:
93
+
94
+ ```ruby
95
+ MiniAPM.span("process_order", category: :internal) do |span|
96
+ span.add_attribute("order.id", order.id)
97
+ span.add_attribute("order.total", order.total)
98
+
99
+ process_order(order)
100
+ end
101
+ ```
102
+
103
+ Report errors manually:
104
+
105
+ ```ruby
106
+ begin
107
+ risky_operation
108
+ rescue => e
109
+ MiniAPM.record_error(e, context: {
110
+ user_id: current_user.id,
111
+ params: { order_id: params[:id] }
112
+ })
113
+ raise
114
+ end
115
+ ```
116
+
117
+ ## Distributed Tracing
118
+
119
+ MiniAPM automatically propagates trace context using W3C Trace Context headers. When making HTTP requests with instrumented clients (Net::HTTP, HTTParty, Faraday), the `traceparent` header is automatically injected.
120
+
121
+ For incoming requests, MiniAPM extracts the trace context from the `traceparent` header to continue the trace.
122
+
123
+ ## Testing
124
+
125
+ Disable MiniAPM in tests:
126
+
127
+ ```ruby
128
+ # config/initializers/miniapm.rb
129
+ config.enabled = !Rails.env.test?
130
+ ```
131
+
132
+ Or use the test helpers:
133
+
134
+ ```ruby
135
+ require 'miniapm/testing'
136
+
137
+ RSpec.describe "MyFeature", :miniapm do
138
+ it "tracks spans" do
139
+ perform_action
140
+
141
+ expect(MiniAPM::Testing.recorded_spans).to include(
142
+ having_attributes(name: /process_action/)
143
+ )
144
+ end
145
+ end
146
+ ```
147
+
148
+ ## Configuration Options
149
+
150
+ | Option | Default | Description |
151
+ |--------|---------|-------------|
152
+ | `endpoint` | `http://localhost:3000` | MiniAPM server URL |
153
+ | `api_key` | `nil` | API key for authentication |
154
+ | `service_name` | `rails-app` | Service identifier |
155
+ | `environment` | `Rails.env` | Deployment environment |
156
+ | `sample_rate` | `1.0` | Sampling rate (0.0 to 1.0) |
157
+ | `batch_size` | `100` | Max spans per batch |
158
+ | `flush_interval` | `5.0` | Seconds between flushes |
159
+ | `max_queue_size` | `10000` | Max queued items |
160
+ | `enabled` | `true` | Enable/disable tracing |
161
+ | `auto_start` | `true` | Start on Rails boot |
162
+
163
+ ## Requirements
164
+
165
+ - Ruby 3.0+
166
+ - Rails 7.0+ (optional, for auto-setup)
167
+
168
+ ## License
169
+
170
+ MIT
171
+
172
+ ---
173
+
174
+ [Documentation](https://miniapm.com/docs) | [Source Code](https://github.com/miniapm/miniapm-ruby)
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module Miniapm
6
+ module Generators
7
+ class InstallGenerator < Rails::Generators::Base
8
+ source_root File.expand_path("templates", __dir__)
9
+
10
+ desc "Creates a MiniAPM initializer file at config/initializers/miniapm.rb"
11
+
12
+ def create_initializer_file
13
+ template "initializer.rb", "config/initializers/miniapm.rb"
14
+ end
15
+
16
+ def show_readme
17
+ readme "README" if behavior == :invoke
18
+ end
19
+
20
+ private
21
+
22
+ def readme(path)
23
+ say File.read(File.expand_path(path, self.class.source_root))
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,19 @@
1
+
2
+ ===============================================================================
3
+
4
+ MiniAPM has been installed!
5
+
6
+ Next steps:
7
+
8
+ 1. Set your API key in your environment:
9
+
10
+ export MINI_APM_API_KEY="your_project_api_key"
11
+ export MINI_APM_URL="http://your-miniapm-server:3000"
12
+
13
+ 2. Review the generated config/initializers/miniapm.rb file
14
+
15
+ 3. Start your Rails server and MiniAPM will begin collecting traces
16
+
17
+ For more information, see: https://github.com/hasik/miniapm
18
+
19
+ ===============================================================================
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ # MiniAPM Configuration
4
+ # Documentation: https://miniapm.com/docs
5
+ MiniAPM.configure do |config|
6
+ # Required: MiniAPM server endpoint
7
+ config.endpoint = ENV.fetch("MINI_APM_URL", "http://localhost:3000")
8
+
9
+ # Required: API key for authentication
10
+ config.api_key = ENV["MINI_APM_API_KEY"]
11
+
12
+ # Service identification
13
+ config.service_name = ENV.fetch("MINI_APM_SERVICE_NAME", "<%= Rails.application.class.module_parent_name.underscore.dasherize %>")
14
+ config.environment = Rails.env
15
+
16
+ # Optional: Service version for tracking deployments
17
+ # config.service_version = ENV["APP_VERSION"]
18
+
19
+ # Optional: Git SHA for deployment tracking
20
+ # config.git_sha = ENV["GIT_SHA"] || ENV["HEROKU_SLUG_COMMIT"]
21
+
22
+ # Batching configuration (defaults are usually fine)
23
+ # config.batch_size = 100 # Max spans per batch
24
+ # config.flush_interval = 5.0 # Seconds between flushes
25
+ # config.max_queue_size = 10_000 # Max queued items before dropping
26
+
27
+ # Sampling (1.0 = 100%, 0.1 = 10%)
28
+ # Useful for high-traffic applications
29
+ config.sample_rate = Rails.env.production? ? 1.0 : 1.0
30
+
31
+ # Enable/disable specific instrumentations
32
+ # config.instrument :activerecord, log_sql: true # Include SQL in spans
33
+ # config.instrument :redis, enabled: false # Disable Redis tracing
34
+
35
+ # Error tracking configuration
36
+ # Exceptions to ignore (won't be reported)
37
+ config.ignored_exceptions = [
38
+ "ActionController::RoutingError",
39
+ "ActionController::InvalidAuthenticityToken",
40
+ "ActionController::UnknownFormat",
41
+ "ActiveRecord::RecordNotFound"
42
+ ]
43
+
44
+ # Parameters to filter from error reports (merged with Rails defaults)
45
+ config.filter_parameters = Rails.application.config.filter_parameters
46
+
47
+ # Custom span modification (return false to drop span)
48
+ # config.before_send = ->(span) {
49
+ # # Add custom attributes
50
+ # span.add_attribute("custom.attribute", "value")
51
+ #
52
+ # # Return false to drop this span
53
+ # # return false if span.name.include?("health_check")
54
+ #
55
+ # true
56
+ # }
57
+
58
+ # Disable in test environment
59
+ config.enabled = !Rails.env.test?
60
+ end
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+
5
+ module MiniAPM
6
+ class Configuration
7
+ # Core settings
8
+ attr_accessor :endpoint # MiniAPM server URL
9
+ attr_accessor :api_key # Bearer token for authentication
10
+ attr_accessor :enabled # Enable/disable the gem
11
+ attr_accessor :auto_start # Auto-start on Rails boot
12
+
13
+ # Service identification
14
+ attr_accessor :service_name # e.g., "my-rails-app"
15
+ attr_accessor :service_version # e.g., "1.2.3"
16
+ attr_accessor :environment # e.g., "production"
17
+
18
+ # Metadata
19
+ attr_accessor :host # Hostname
20
+ attr_accessor :rails_version # Auto-detected
21
+ attr_accessor :ruby_version # Auto-detected
22
+ attr_accessor :git_sha # Git SHA for deploys
23
+
24
+ # Batching settings
25
+ attr_accessor :batch_size # Max spans per batch
26
+ attr_accessor :flush_interval # Seconds between flushes
27
+ attr_accessor :max_queue_size # Max queued items before dropping
28
+
29
+ # Instrumentation toggles
30
+ attr_reader :instrumentations
31
+
32
+ # Sampling
33
+ attr_accessor :sample_rate # 0.0 to 1.0
34
+
35
+ # Error filtering
36
+ attr_accessor :ignored_exceptions
37
+
38
+ # Parameter filtering
39
+ attr_accessor :filter_parameters
40
+
41
+ # Callbacks
42
+ attr_accessor :before_send # Proc to modify/filter spans
43
+
44
+ def initialize
45
+ @endpoint = ENV.fetch("MINI_APM_URL", "http://localhost:3000")
46
+ @api_key = ENV["MINI_APM_API_KEY"]
47
+ @enabled = true
48
+ @auto_start = true
49
+
50
+ @service_name = ENV.fetch("MINI_APM_SERVICE_NAME", "rails-app")
51
+ @service_version = ENV["MINI_APM_SERVICE_VERSION"]
52
+ @environment = ENV.fetch("RAILS_ENV", ENV.fetch("RACK_ENV", "development"))
53
+
54
+ @host = Socket.gethostname rescue "unknown"
55
+ @rails_version = defined?(Rails::VERSION::STRING) ? Rails::VERSION::STRING : nil
56
+ @ruby_version = RUBY_VERSION
57
+ @git_sha = ENV["GIT_SHA"] || ENV["HEROKU_SLUG_COMMIT"] || detect_git_sha
58
+
59
+ @batch_size = 100
60
+ @flush_interval = 5.0
61
+ @max_queue_size = 10_000
62
+
63
+ @instrumentations = InstrumentationConfig.new
64
+
65
+ @sample_rate = 1.0
66
+
67
+ @ignored_exceptions = [
68
+ "ActionController::RoutingError",
69
+ "ActionController::InvalidAuthenticityToken",
70
+ "ActionController::UnknownFormat",
71
+ "ActiveRecord::RecordNotFound"
72
+ ]
73
+
74
+ @filter_parameters = [:password, :password_confirmation, :token, :secret, :api_key, :access_token]
75
+
76
+ @before_send = nil
77
+ end
78
+
79
+ def instrument(name, enabled: true, **options)
80
+ @instrumentations.configure(name, enabled: enabled, **options)
81
+ end
82
+
83
+ # Validate configuration and raise on errors
84
+ def validate!
85
+ errors = []
86
+
87
+ # Validate endpoint
88
+ if @endpoint.nil? || @endpoint.empty?
89
+ errors << "endpoint is required"
90
+ else
91
+ begin
92
+ uri = URI.parse(@endpoint)
93
+ unless uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
94
+ errors << "endpoint must be an HTTP(S) URL"
95
+ end
96
+ rescue URI::InvalidURIError
97
+ errors << "endpoint is not a valid URL"
98
+ end
99
+ end
100
+
101
+ # Validate sample_rate
102
+ unless @sample_rate.is_a?(Numeric) && @sample_rate >= 0.0 && @sample_rate <= 1.0
103
+ errors << "sample_rate must be a number between 0.0 and 1.0"
104
+ end
105
+
106
+ # Validate batch settings
107
+ errors << "batch_size must be a positive integer" unless @batch_size.is_a?(Integer) && @batch_size > 0
108
+ errors << "flush_interval must be a positive number" unless @flush_interval.is_a?(Numeric) && @flush_interval > 0
109
+ errors << "max_queue_size must be a positive integer" unless @max_queue_size.is_a?(Integer) && @max_queue_size > 0
110
+
111
+ # Warn about missing api_key (not an error, as it might be set later)
112
+ if @api_key.nil? || @api_key.empty?
113
+ MiniAPM.logger.warn { "MiniAPM: api_key is not configured - requests will fail" }
114
+ end
115
+
116
+ raise ConfigurationError, "Invalid configuration: #{errors.join(', ')}" if errors.any?
117
+
118
+ true
119
+ end
120
+
121
+ def valid?
122
+ validate!
123
+ true
124
+ rescue ConfigurationError
125
+ false
126
+ end
127
+
128
+ private
129
+
130
+ def detect_git_sha
131
+ sha = `git rev-parse HEAD 2>/dev/null`.strip
132
+ sha.empty? ? nil : sha
133
+ rescue StandardError
134
+ nil
135
+ end
136
+ end
137
+
138
+ class InstrumentationConfig
139
+ DEFAULTS = {
140
+ rails: { enabled: true },
141
+ activerecord: { enabled: true, log_sql: false },
142
+ activejob: { enabled: true },
143
+ sidekiq: { enabled: true },
144
+ cache: { enabled: true },
145
+ net_http: { enabled: true },
146
+ httparty: { enabled: true },
147
+ faraday: { enabled: true },
148
+ opensearch: { enabled: true },
149
+ elasticsearch: { enabled: true },
150
+ searchkick: { enabled: true },
151
+ redis: { enabled: true },
152
+ redis_client: { enabled: true }
153
+ }.freeze
154
+
155
+ def initialize
156
+ @config = DEFAULTS.transform_values(&:dup)
157
+ end
158
+
159
+ def configure(name, **options)
160
+ @config[name.to_sym] ||= {}
161
+ @config[name.to_sym].merge!(options)
162
+ end
163
+
164
+ def [](name)
165
+ @config[name.to_sym] || { enabled: false }
166
+ end
167
+
168
+ def enabled?(name)
169
+ self[name][:enabled]
170
+ end
171
+
172
+ def options(name)
173
+ self[name]
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MiniAPM
4
+ # Fiber-safe context for trace propagation
5
+ # Uses Fiber.[] storage (Ruby 3.2+) with Thread.current fallback
6
+ module Context
7
+ TRACE_KEY = :miniapm_trace
8
+ SPAN_STACK_KEY = :miniapm_span_stack
9
+
10
+ class << self
11
+ # Ruby 3.2+ has Fiber.[] for fiber-local storage
12
+ # Fall back to Thread.current for older Ruby (not fiber-safe)
13
+ if Fiber.respond_to?(:[])
14
+ def current_trace
15
+ Fiber[TRACE_KEY]
16
+ end
17
+
18
+ def current_trace=(trace)
19
+ Fiber[TRACE_KEY] = trace
20
+ end
21
+
22
+ def span_stack
23
+ Fiber[SPAN_STACK_KEY] ||= []
24
+ end
25
+
26
+ def span_stack=(stack)
27
+ Fiber[SPAN_STACK_KEY] = stack
28
+ end
29
+ else
30
+ # Fallback for Ruby < 3.2 (not fiber-safe, but thread-safe)
31
+ def current_trace
32
+ Thread.current[TRACE_KEY]
33
+ end
34
+
35
+ def current_trace=(trace)
36
+ Thread.current[TRACE_KEY] = trace
37
+ end
38
+
39
+ def span_stack
40
+ Thread.current[SPAN_STACK_KEY] ||= []
41
+ end
42
+
43
+ def span_stack=(stack)
44
+ Thread.current[SPAN_STACK_KEY] = stack
45
+ end
46
+ end
47
+
48
+ def current_trace_id
49
+ current_trace&.trace_id
50
+ end
51
+
52
+ def current_span
53
+ span_stack.last
54
+ end
55
+
56
+ def push_span(span)
57
+ span_stack.push(span)
58
+ end
59
+
60
+ def pop_span
61
+ span_stack.pop
62
+ end
63
+
64
+ def with_span(span)
65
+ push_span(span)
66
+ yield span
67
+ ensure
68
+ pop_span
69
+ end
70
+
71
+ def with_trace(trace)
72
+ old_trace = current_trace
73
+ old_stack = span_stack
74
+
75
+ self.current_trace = trace
76
+ self.span_stack = []
77
+
78
+ yield trace
79
+ ensure
80
+ self.current_trace = old_trace
81
+ self.span_stack = old_stack
82
+ end
83
+
84
+ def clear!
85
+ self.current_trace = nil
86
+ self.span_stack = nil
87
+ end
88
+
89
+ def fiber_safe?
90
+ Fiber.respond_to?(:[])
91
+ end
92
+
93
+ # Extract trace context from incoming HTTP headers (W3C Trace Context)
94
+ # Format: 00-{trace_id}-{parent_span_id}-{flags}
95
+ # Example: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
96
+ def extract_from_headers(headers)
97
+ traceparent = headers["traceparent"] ||
98
+ headers["HTTP_TRACEPARENT"] ||
99
+ headers["Traceparent"]
100
+ return nil unless traceparent
101
+
102
+ parts = traceparent.to_s.split("-")
103
+ return nil unless parts.length == 4
104
+ return nil unless parts[0] == "00" # version check
105
+
106
+ trace_id = parts[1]
107
+ parent_span_id = parts[2]
108
+ flags = parts[3].to_i(16)
109
+
110
+ # Validate format
111
+ return nil unless trace_id.match?(/\A[0-9a-f]{32}\z/)
112
+ return nil unless parent_span_id.match?(/\A[0-9a-f]{16}\z/)
113
+
114
+ {
115
+ trace_id: trace_id,
116
+ parent_span_id: parent_span_id,
117
+ sampled: (flags & 0x01) == 1
118
+ }
119
+ end
120
+
121
+ # Inject trace context into outgoing HTTP headers (W3C Trace Context)
122
+ def inject_into_headers(headers)
123
+ return headers unless current_span
124
+
125
+ flags = current_trace&.sampled? ? "01" : "00"
126
+ traceparent = format(
127
+ "00-%s-%s-%s",
128
+ current_trace_id,
129
+ current_span.span_id,
130
+ flags
131
+ )
132
+
133
+ headers["traceparent"] = traceparent
134
+ headers
135
+ end
136
+ end
137
+ end
138
+ end