pingops 0.0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f2dd00a33589bfeaca93f9755d53075f18e54c02796e1af084584d4400291445
4
+ data.tar.gz: 54f27e3fc4a35cc93b017f1e226a7a57fe82f61fad8bb79f19f739efd2daca04
5
+ SHA512:
6
+ metadata.gz: 198fc7dfec12b0edbee02e08d21657321198783328b2ea96fc90d9818bc4506969117e9944b72f90f01c3c17d8f91152326fe516de5ef7e4ecce5e9eb8f3bf97
7
+ data.tar.gz: ae880a1d85dbb023f70a21bccd140b17377042ea67d4370b751ed975420c2f6634e697d6eef8da579e174d3af1acbb18d9c00a12ef879f1b3906d110ecad3af5
data/CHANGELOG.md ADDED
@@ -0,0 +1,26 @@
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
+ ## [0.0.1] - 2026-02-01
9
+
10
+ ### Added
11
+
12
+ - Initial release of the PingOps Ruby SDK
13
+ - Automatic HTTP instrumentation for Net::HTTP
14
+ - Manual tracing with `Pingops.start_trace`
15
+ - Configuration via hash, file (JSON/YAML), or environment variables
16
+ - Domain allow/deny list filtering with path matching
17
+ - Header filtering and redaction with multiple strategies
18
+ - Request/response body capture with size limits
19
+ - Batched and immediate export modes
20
+ - OpenTelemetry SDK integration
21
+ - Deterministic trace ID generation from seed
22
+ - Custom trace attributes (userId, sessionId, tags, metadata)
23
+ - Per-trace body capture override
24
+ - Rails integration support
25
+ - Auto-initialization via register pattern
26
+ - Comprehensive test suite
data/README.md ADDED
@@ -0,0 +1,377 @@
1
+ # PingOps Ruby SDK
2
+
3
+ A production-grade Ruby SDK for PingOps that provides automatic HTTP tracing with OpenTelemetry integration.
4
+
5
+ ## Features
6
+
7
+ - **Automatic HTTP Instrumentation** - Captures outgoing HTTP requests from `Net::HTTP`, Faraday, and HTTPClient
8
+ - **OpenTelemetry Compatible** - Built on OpenTelemetry API and SDK for seamless integration
9
+ - **Manual Tracing** - Start traces with custom attributes (userId, sessionId, tags, metadata)
10
+ - **Domain Filtering** - Allow/deny lists with path matching
11
+ - **Header Redaction** - Automatic redaction of sensitive headers
12
+ - **Body Capture** - Configurable request/response body capture with size limits
13
+ - **Flexible Configuration** - File-based (JSON/YAML) or programmatic configuration with environment variable support
14
+
15
+ ## Installation
16
+
17
+ Add this line to your application's Gemfile:
18
+
19
+ ```ruby
20
+ gem 'pingops'
21
+ ```
22
+
23
+ And then execute:
24
+
25
+ ```bash
26
+ bundle install
27
+ ```
28
+
29
+ Or install it yourself:
30
+
31
+ ```bash
32
+ gem install pingops
33
+ ```
34
+
35
+ ## Quick Start
36
+
37
+ ```ruby
38
+ require 'pingops'
39
+
40
+ # Initialize the SDK
41
+ Pingops.initialize(
42
+ api_key: 'your-api-key',
43
+ base_url: 'https://api.pingops.com',
44
+ service_name: 'my-service'
45
+ )
46
+
47
+ # Trace HTTP requests
48
+ Pingops.start_trace(attributes: { user_id: 'user-123' }) do
49
+ # All HTTP requests in this block are automatically traced
50
+ response = Net::HTTP.get(URI('https://api.example.com/users'))
51
+ end
52
+
53
+ # Shutdown when done
54
+ Pingops.shutdown
55
+ ```
56
+
57
+ ## Configuration
58
+
59
+ ### Programmatic Configuration
60
+
61
+ ```ruby
62
+ Pingops.initialize(
63
+ api_key: 'your-api-key',
64
+ base_url: 'https://api.pingops.com',
65
+ service_name: 'my-service',
66
+ debug: false,
67
+
68
+ # Body capture settings
69
+ capture_request_body: true,
70
+ capture_response_body: true,
71
+ max_request_body_size: 4096,
72
+ max_response_body_size: 4096,
73
+
74
+ # Domain filtering
75
+ domain_allow_list: [
76
+ { domain: 'api.example.com' },
77
+ { domain: '.mycompany.com', paths: ['/api/', '/v1/'] }
78
+ ],
79
+ domain_deny_list: [
80
+ { domain: 'internal.mycompany.com' }
81
+ ],
82
+
83
+ # Header filtering
84
+ headers_allow_list: ['content-type', 'accept'],
85
+ headers_deny_list: ['x-internal-secret'],
86
+
87
+ # Header redaction
88
+ header_redaction: {
89
+ sensitive_patterns: ['authorization', 'cookie', 'api-key'],
90
+ strategy: 'replace', # replace, partial, partial_end, remove
91
+ redaction_string: '[REDACTED]',
92
+ visible_chars: 4,
93
+ enabled: true
94
+ },
95
+
96
+ # Export settings
97
+ export_mode: 'batched', # or 'immediate'
98
+ batch_size: 50,
99
+ batch_timeout: 5000
100
+ )
101
+ ```
102
+
103
+ ### File-Based Configuration
104
+
105
+ ```ruby
106
+ # Load from JSON file
107
+ Pingops.initialize('/path/to/pingops.json')
108
+
109
+ # Or with config_file key
110
+ Pingops.initialize(config_file: '/path/to/pingops.yaml')
111
+ ```
112
+
113
+ Example `pingops.json`:
114
+
115
+ ```json
116
+ {
117
+ "apiKey": "your-api-key",
118
+ "baseUrl": "https://api.pingops.com",
119
+ "serviceName": "my-service",
120
+ "debug": false,
121
+ "captureRequestBody": true,
122
+ "captureResponseBody": true,
123
+ "domainAllowList": [
124
+ { "domain": "api.example.com" }
125
+ ]
126
+ }
127
+ ```
128
+
129
+ ### Environment Variables
130
+
131
+ Environment variables override file and programmatic configuration:
132
+
133
+ | Variable | Description |
134
+ |----------|-------------|
135
+ | `PINGOPS_API_KEY` | API key for authentication |
136
+ | `PINGOPS_BASE_URL` | PingOps backend URL |
137
+ | `PINGOPS_SERVICE_NAME` | Service name for traces |
138
+ | `PINGOPS_DEBUG` | Enable debug logging ("true") |
139
+ | `PINGOPS_BATCH_SIZE` | Batch size for export |
140
+ | `PINGOPS_BATCH_TIMEOUT` | Batch timeout in milliseconds |
141
+ | `PINGOPS_EXPORT_MODE` | Export mode (batched/immediate) |
142
+ | `PINGOPS_CONFIG_FILE` | Config file path (for auto-init) |
143
+
144
+ ## API Reference
145
+
146
+ ### `Pingops.initialize(config)`
147
+
148
+ Initialize the SDK. Idempotent - subsequent calls are no-ops.
149
+
150
+ ```ruby
151
+ # With hash
152
+ Pingops.initialize(api_key: 'key', base_url: 'url', service_name: 'name')
153
+
154
+ # With file path
155
+ Pingops.initialize('/path/to/config.json')
156
+
157
+ # With config_file key
158
+ Pingops.initialize(config_file: '/path/to/config.yaml')
159
+ ```
160
+
161
+ ### `Pingops.shutdown`
162
+
163
+ Shutdown the SDK, flushing any pending spans.
164
+
165
+ ```ruby
166
+ Pingops.shutdown
167
+ ```
168
+
169
+ ### `Pingops.start_trace(options, &block)`
170
+
171
+ Start a trace and execute the block within the trace context.
172
+
173
+ ```ruby
174
+ result = Pingops.start_trace(
175
+ attributes: {
176
+ user_id: 'user-123',
177
+ session_id: 'session-456',
178
+ tags: ['production', 'web'],
179
+ metadata: { 'request_id' => 'abc123' },
180
+ capture_request_body: true,
181
+ capture_response_body: true
182
+ },
183
+ seed: 'optional-seed-for-deterministic-trace-id'
184
+ ) do
185
+ # Your code here
186
+ Net::HTTP.get(URI('https://api.example.com'))
187
+ end
188
+ ```
189
+
190
+ ### `Pingops.active_trace_id`
191
+
192
+ Get the current trace ID (within a trace block).
193
+
194
+ ```ruby
195
+ Pingops.start_trace do
196
+ trace_id = Pingops.active_trace_id
197
+ # => "a1b2c3d4e5f6..."
198
+ end
199
+ ```
200
+
201
+ ### `Pingops.active_span_id`
202
+
203
+ Get the current span ID (within a trace block).
204
+
205
+ ```ruby
206
+ Pingops.start_trace do
207
+ span_id = Pingops.active_span_id
208
+ # => "a1b2c3d4..."
209
+ end
210
+ ```
211
+
212
+ ### `Pingops.initialized?`
213
+
214
+ Check if the SDK is initialized.
215
+
216
+ ```ruby
217
+ Pingops.initialized? # => true/false
218
+ ```
219
+
220
+ ## Domain Rules
221
+
222
+ Domain rules support exact and suffix matching:
223
+
224
+ ```ruby
225
+ domain_allow_list: [
226
+ # Exact match
227
+ { domain: 'api.example.com' },
228
+
229
+ # Suffix match (starts with .)
230
+ { domain: '.mycompany.com' }, # Matches api.mycompany.com, app.mycompany.com, etc.
231
+
232
+ # With path restriction
233
+ { domain: 'api.example.com', paths: ['/api/', '/v1/'] },
234
+
235
+ # With body capture override
236
+ {
237
+ domain: 'external-api.com',
238
+ capture_request_body: true,
239
+ capture_response_body: false
240
+ },
241
+
242
+ # With header override
243
+ {
244
+ domain: 'partner-api.com',
245
+ headers_allow_list: ['content-type', 'x-partner-id']
246
+ }
247
+ ]
248
+ ```
249
+
250
+ ## Header Redaction
251
+
252
+ Headers matching sensitive patterns are automatically redacted:
253
+
254
+ ```ruby
255
+ header_redaction: {
256
+ sensitive_patterns: [
257
+ 'authorization',
258
+ 'cookie',
259
+ 'x-api-key'
260
+ ],
261
+ strategy: 'replace', # Entire value replaced
262
+ # strategy: 'partial', # First N chars visible: "Bear..."
263
+ # strategy: 'partial_end', # Last N chars visible: "...oken"
264
+ # strategy: 'remove', # Header removed entirely
265
+ redaction_string: '[REDACTED]',
266
+ visible_chars: 4,
267
+ enabled: true
268
+ }
269
+ ```
270
+
271
+ Default sensitive patterns include: authorization, cookie, api-key, token, secret, password, and many more.
272
+
273
+ ## Auto-Initialization
274
+
275
+ For simple setups, use the register pattern:
276
+
277
+ ```bash
278
+ # Set environment variables
279
+ export PINGOPS_API_KEY=your-key
280
+ export PINGOPS_BASE_URL=https://api.pingops.com
281
+ export PINGOPS_SERVICE_NAME=my-service
282
+
283
+ # Run with auto-init
284
+ ruby -r pingops/register your_app.rb
285
+ ```
286
+
287
+ Or in your application:
288
+
289
+ ```ruby
290
+ require 'pingops/register'
291
+
292
+ # SDK is automatically initialized from environment
293
+ Pingops.start_trace { ... }
294
+ ```
295
+
296
+ ## Rails Integration
297
+
298
+ Create `config/initializers/pingops.rb`:
299
+
300
+ ```ruby
301
+ require 'pingops'
302
+
303
+ if Rails.env.production? || Rails.env.staging?
304
+ Pingops.initialize(
305
+ api_key: ENV.fetch('PINGOPS_API_KEY'),
306
+ base_url: ENV.fetch('PINGOPS_BASE_URL'),
307
+ service_name: Rails.application.class.module_parent_name,
308
+ debug: Rails.env.development?
309
+ )
310
+ end
311
+
312
+ at_exit { Pingops.shutdown if Pingops.initialized? }
313
+ ```
314
+
315
+ Add tracing to controllers:
316
+
317
+ ```ruby
318
+ class ApplicationController < ActionController::Base
319
+ around_action :trace_request
320
+
321
+ private
322
+
323
+ def trace_request
324
+ Pingops.start_trace(
325
+ attributes: {
326
+ user_id: current_user&.id,
327
+ session_id: session.id,
328
+ tags: [Rails.env, controller_name],
329
+ metadata: {
330
+ 'controller' => controller_name,
331
+ 'action' => action_name
332
+ }
333
+ }
334
+ ) { yield }
335
+ end
336
+ end
337
+ ```
338
+
339
+ ## Requirements
340
+
341
+ - Ruby 3.0+
342
+ - OpenTelemetry SDK 1.4+
343
+
344
+ ## Development
345
+
346
+ ```bash
347
+ # Install dependencies
348
+ bundle install
349
+
350
+ # Run tests
351
+ bundle exec rspec
352
+
353
+ # Run linter
354
+ bundle exec rubocop
355
+
356
+ # Generate documentation
357
+ bundle exec yard doc
358
+ ```
359
+
360
+ ## Publishing
361
+
362
+ ```bash
363
+ # 1) Bump version (lib/pingops/version.rb) and update CHANGELOG if needed
364
+
365
+ # 2) Build the gem
366
+ gem build pingops.gemspec
367
+
368
+ # 3) Authenticate to RubyGems (first time only)
369
+ gem signin
370
+
371
+ # 4) Push the built gem
372
+ gem push pingops-<version>.gem
373
+ ```
374
+
375
+ ## License
376
+
377
+ MIT License - see LICENSE file for details.
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+
5
+ module Pingops
6
+ module Core
7
+ # Body capture utilities for request/response bodies
8
+ module BodyCapture
9
+ class << self
10
+ # Determine if request body should be captured for a span
11
+ # Priority: Context > Domain rule > Global config
12
+ #
13
+ # @param context [OpenTelemetry::Context] Current context
14
+ # @param domain_rule [DomainRule, nil] Matching domain rule
15
+ # @param global_config [Configuration] Global configuration
16
+ # @return [Boolean]
17
+ def should_capture_request_body?(context, domain_rule, global_config)
18
+ # 1. Check context first
19
+ context_value = ContextKeys.capture_request_body?(context)
20
+ return context_value unless context_value.nil?
21
+
22
+ # 2. Check domain rule
23
+ return domain_rule.capture_request_body if domain_rule && !domain_rule.capture_request_body.nil?
24
+
25
+ # 3. Fall back to global config
26
+ global_config.capture_request_body
27
+ end
28
+
29
+ # Determine if response body should be captured for a span
30
+ # Priority: Context > Domain rule > Global config
31
+ #
32
+ # @param context [OpenTelemetry::Context] Current context
33
+ # @param domain_rule [DomainRule, nil] Matching domain rule
34
+ # @param global_config [Configuration] Global configuration
35
+ # @return [Boolean]
36
+ def should_capture_response_body?(context, domain_rule, global_config)
37
+ # 1. Check context first
38
+ context_value = ContextKeys.capture_response_body?(context)
39
+ return context_value unless context_value.nil?
40
+
41
+ # 2. Check domain rule
42
+ return domain_rule.capture_response_body if domain_rule && !domain_rule.capture_response_body.nil?
43
+
44
+ # 3. Fall back to global config
45
+ global_config.capture_response_body
46
+ end
47
+
48
+ # Truncate body if it exceeds max size
49
+ # @param body [String] The body content
50
+ # @param max_size [Integer] Maximum size in bytes
51
+ # @return [String, nil] The body or nil if empty
52
+ def truncate_body(body, max_size)
53
+ return nil if body.nil? || body.empty?
54
+
55
+ if body.bytesize > max_size
56
+ "#{body.byteslice(0, max_size)}... [truncated, #{body.bytesize} bytes total]"
57
+ else
58
+ body
59
+ end
60
+ end
61
+
62
+ # Check if content encoding indicates compression
63
+ # @param encoding [String, nil] Content-Encoding header value
64
+ # @return [Boolean]
65
+ def compressed?(encoding)
66
+ return false if encoding.nil? || encoding.empty?
67
+
68
+ Constants::COMPRESSED_ENCODINGS.any? { |e| encoding.downcase.include?(e) }
69
+ end
70
+
71
+ # Encode compressed body as base64
72
+ # @param body [String] Raw body bytes
73
+ # @param max_size [Integer] Maximum size in bytes
74
+ # @return [String, nil] Base64 encoded body or nil
75
+ def encode_compressed_body(body, max_size)
76
+ return nil if body.nil? || body.empty?
77
+ return nil if body.bytesize > max_size
78
+
79
+ Base64.strict_encode64(body)
80
+ end
81
+
82
+ # Filter body attributes from span based on capture settings
83
+ # @param attributes [Hash] Span attributes
84
+ # @param capture_request [Boolean] Whether to keep request body
85
+ # @param capture_response [Boolean] Whether to keep response body
86
+ # @return [Hash] Filtered attributes
87
+ def filter_body_attributes(attributes, capture_request:, capture_response:)
88
+ result = attributes.dup
89
+
90
+ result.delete(Constants::ATTR_HTTP_REQUEST_BODY) unless capture_request
91
+
92
+ unless capture_response
93
+ result.delete(Constants::ATTR_HTTP_RESPONSE_BODY)
94
+ result.delete(Constants::ATTR_HTTP_RESPONSE_CONTENT_ENCODING)
95
+ end
96
+
97
+ result
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Pingops
6
+ module Core
7
+ # Main configuration class for the PingOps SDK
8
+ class Configuration
9
+ attr_accessor :api_key, :base_url, :service_name, :debug,
10
+ :headers_allow_list, :headers_deny_list,
11
+ :capture_request_body, :capture_response_body,
12
+ :max_request_body_size, :max_response_body_size,
13
+ :domain_allow_list, :domain_deny_list,
14
+ :header_redaction, :batch_size, :batch_timeout, :export_mode
15
+
16
+ def initialize(options = {})
17
+ @api_key = options[:api_key]
18
+ @base_url = options[:base_url]
19
+ @service_name = options[:service_name]
20
+ @debug = options[:debug] || false
21
+ @headers_allow_list = options[:headers_allow_list]&.map { |h| h.to_s.downcase }
22
+ @headers_deny_list = options[:headers_deny_list]&.map { |h| h.to_s.downcase }
23
+ @capture_request_body = options[:capture_request_body] || false
24
+ @capture_response_body = options[:capture_response_body] || false
25
+ @max_request_body_size = options[:max_request_body_size] || Constants::DEFAULT_MAX_REQUEST_BODY_SIZE
26
+ @max_response_body_size = options[:max_response_body_size] || Constants::DEFAULT_MAX_RESPONSE_BODY_SIZE
27
+ @batch_size = options[:batch_size] || Constants::DEFAULT_BATCH_SIZE
28
+ @batch_timeout = options[:batch_timeout] || Constants::DEFAULT_BATCH_TIMEOUT
29
+ @export_mode = options[:export_mode] || Constants::EXPORT_MODE_BATCHED
30
+
31
+ # Parse domain rules
32
+ @domain_allow_list = parse_domain_rules(options[:domain_allow_list])
33
+ @domain_deny_list = parse_domain_rules(options[:domain_deny_list])
34
+
35
+ # Parse header redaction config
36
+ @header_redaction = parse_header_redaction(options[:header_redaction])
37
+ end
38
+
39
+ # Check if configuration is valid (has required fields)
40
+ # @return [Boolean]
41
+ def valid?
42
+ !@base_url.nil? && !@base_url.empty? &&
43
+ !@service_name.nil? && !@service_name.empty?
44
+ end
45
+
46
+ # Validate and raise if invalid
47
+ # @raise [Pingops::ConfigurationError] if required fields are missing
48
+ def validate!
49
+ missing = []
50
+ missing << 'baseUrl (or PINGOPS_BASE_URL)' if @base_url.nil? || @base_url.empty?
51
+ missing << 'serviceName (or PINGOPS_SERVICE_NAME)' if @service_name.nil? || @service_name.empty?
52
+
53
+ return if missing.empty?
54
+
55
+ raise Pingops::ConfigurationError, "Missing required configuration: #{missing.join(', ')}"
56
+ end
57
+
58
+ # Create configuration from a file path
59
+ # @param file_path [String] Path to JSON or YAML config file
60
+ # @return [Configuration]
61
+ def self.from_file(file_path)
62
+ content = File.read(file_path)
63
+ data = if file_path.end_with?('.yaml', '.yml')
64
+ require 'yaml'
65
+ YAML.safe_load(content, symbolize_names: true)
66
+ else
67
+ JSON.parse(content, symbolize_names: true)
68
+ end
69
+
70
+ from_hash(data)
71
+ end
72
+
73
+ # Create configuration from a hash
74
+ # @param hash [Hash] Configuration hash
75
+ # @return [Configuration]
76
+ def self.from_hash(hash)
77
+ return hash if hash.is_a?(Configuration)
78
+
79
+ new(
80
+ api_key: hash[:api_key] || hash[:apiKey] || hash['apiKey'],
81
+ base_url: hash[:base_url] || hash[:baseUrl] || hash['baseUrl'],
82
+ service_name: hash[:service_name] || hash[:serviceName] || hash['serviceName'],
83
+ debug: hash[:debug] || hash['debug'],
84
+ headers_allow_list: hash[:headers_allow_list] || hash[:headersAllowList] || hash['headersAllowList'],
85
+ headers_deny_list: hash[:headers_deny_list] || hash[:headersDenyList] || hash['headersDenyList'],
86
+ capture_request_body: hash[:capture_request_body] || hash[:captureRequestBody] || hash['captureRequestBody'],
87
+ capture_response_body: hash[:capture_response_body] || hash[:captureResponseBody] ||
88
+ hash['captureResponseBody'],
89
+ max_request_body_size: hash[:max_request_body_size] || hash[:maxRequestBodySize] ||
90
+ hash['maxRequestBodySize'],
91
+ max_response_body_size: hash[:max_response_body_size] || hash[:maxResponseBodySize] ||
92
+ hash['maxResponseBodySize'],
93
+ domain_allow_list: hash[:domain_allow_list] || hash[:domainAllowList] || hash['domainAllowList'],
94
+ domain_deny_list: hash[:domain_deny_list] || hash[:domainDenyList] || hash['domainDenyList'],
95
+ header_redaction: hash[:header_redaction] || hash[:headerRedaction] || hash['headerRedaction'],
96
+ batch_size: hash[:batch_size] || hash[:batchSize] || hash['batchSize'],
97
+ batch_timeout: hash[:batch_timeout] || hash[:batchTimeout] || hash['batchTimeout'],
98
+ export_mode: hash[:export_mode] || hash[:exportMode] || hash['exportMode']
99
+ )
100
+ end
101
+
102
+ # Create configuration from environment variables only
103
+ # @return [Configuration]
104
+ def self.from_env
105
+ new(
106
+ api_key: ENV.fetch('PINGOPS_API_KEY', nil),
107
+ base_url: ENV.fetch('PINGOPS_BASE_URL', nil),
108
+ service_name: ENV.fetch('PINGOPS_SERVICE_NAME', nil),
109
+ debug: ENV.fetch('PINGOPS_DEBUG', nil) == 'true',
110
+ batch_size: env_int('PINGOPS_BATCH_SIZE'),
111
+ batch_timeout: env_int('PINGOPS_BATCH_TIMEOUT'),
112
+ export_mode: ENV.fetch('PINGOPS_EXPORT_MODE', nil)
113
+ )
114
+ end
115
+
116
+ # Load from file and merge with environment variables (env wins)
117
+ # @param file_path [String] Path to config file
118
+ # @return [Configuration]
119
+ def self.load_with_env(file_path)
120
+ file_config = from_file(file_path)
121
+ env_config = from_env
122
+
123
+ merge(file_config, env_config)
124
+ end
125
+
126
+ # Merge two configurations (second wins for non-nil values)
127
+ # @param base [Configuration] Base configuration
128
+ # @param override [Configuration] Override configuration
129
+ # @return [Configuration]
130
+ def self.merge(base, override)
131
+ new(
132
+ api_key: override.api_key || base.api_key,
133
+ base_url: override.base_url || base.base_url,
134
+ service_name: override.service_name || base.service_name,
135
+ debug: override.debug || base.debug,
136
+ headers_allow_list: override.headers_allow_list || base.headers_allow_list,
137
+ headers_deny_list: override.headers_deny_list || base.headers_deny_list,
138
+ capture_request_body: override.capture_request_body || base.capture_request_body,
139
+ capture_response_body: override.capture_response_body || base.capture_response_body,
140
+ max_request_body_size: non_default_or(override.max_request_body_size, base.max_request_body_size,
141
+ Constants::DEFAULT_MAX_REQUEST_BODY_SIZE),
142
+ max_response_body_size: non_default_or(override.max_response_body_size, base.max_response_body_size,
143
+ Constants::DEFAULT_MAX_RESPONSE_BODY_SIZE),
144
+ domain_allow_list: override.domain_allow_list&.any? ? override.domain_allow_list : base.domain_allow_list,
145
+ domain_deny_list: override.domain_deny_list&.any? ? override.domain_deny_list : base.domain_deny_list,
146
+ header_redaction: override.header_redaction || base.header_redaction,
147
+ batch_size: non_default_or(override.batch_size, base.batch_size, Constants::DEFAULT_BATCH_SIZE),
148
+ batch_timeout: non_default_or(override.batch_timeout, base.batch_timeout, Constants::DEFAULT_BATCH_TIMEOUT),
149
+ export_mode: non_default_or(override.export_mode, base.export_mode, Constants::EXPORT_MODE_BATCHED)
150
+ )
151
+ end
152
+
153
+ private
154
+
155
+ def parse_domain_rules(rules)
156
+ return nil if rules.nil?
157
+
158
+ rules.map { |rule| DomainRule.from_hash(rule) }
159
+ end
160
+
161
+ def parse_header_redaction(config)
162
+ HeaderRedactionConfig.from_hash(config)
163
+ end
164
+
165
+ class << self
166
+ private
167
+
168
+ def env_int(name)
169
+ value = ENV.fetch(name, nil)
170
+ value&.to_i
171
+ end
172
+
173
+ def non_default_or(override, base, default)
174
+ return override if override && override != default
175
+
176
+ base
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end