observable 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.claude/settings.local.json +9 -0
- data/.ruby-version +1 -0
- data/.standard.yml +3 -1
- data/CLAUDE.md +113 -0
- data/Gemfile +0 -6
- data/Gemfile.lock +122 -0
- data/README.md +156 -25
- data/Rakefile +2 -4
- data/example.rb +55 -0
- data/lib/observable/instrumenter.rb +388 -0
- data/lib/observable/version.rb +1 -1
- data/lib/observable.rb +6 -1
- data/observable.gemspec +47 -0
- metadata +119 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '08d7169aa0bffabba61651eb5fd9d5b798f0d17a3adbcc0406471d1138ca84c5'
|
4
|
+
data.tar.gz: e0ab04bad74667894f89368ca2f3bfedb3040a51010d6b62ee91beb63fcb84b4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 244a0c1bf4902d735d38a51c236f0bbde64d63d27b630eb13d8ffe07436b56f4dff651707cd7bd5850bcabc851b024082f6077ee801b714f03a12eea762669bd
|
7
|
+
data.tar.gz: ac2f872738807294c54aa5e36c7c91760939dcf749066406e13bafd5053cf2b8acb74dc5535976556de513da7b2ecfadf9866a56c5bd267d9dfd80ce3a53d157
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
3.4.1
|
data/.standard.yml
CHANGED
data/CLAUDE.md
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
# CLAUDE.md
|
2
|
+
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
4
|
+
|
5
|
+
## Project Overview
|
6
|
+
|
7
|
+
This is the Observable gem - a Ruby library that provides OpenTelemetry instrumentation for method calls with configurable serialization, PII filtering, and argument tracking. It automatically captures method invocation details, arguments, return values, and exceptions as OpenTelemetry spans.
|
8
|
+
|
9
|
+
## Core Architecture
|
10
|
+
|
11
|
+
### Main Components
|
12
|
+
|
13
|
+
- **Observable::Instrumenter** (`lib/observable/instrumenter.rb`) - The core instrumentation engine that wraps method calls with OpenTelemetry spans
|
14
|
+
- **Observable::Configuration** (`lib/observable/instrumenter.rb`) - Uses Dry::Configurable for flexible configuration management
|
15
|
+
- **ArgumentExtractor** (`lib/observable/instrumenter.rb`) - Extracts method arguments from Ruby bindings using introspection
|
16
|
+
- **CallerInformation** (`lib/observable/instrumenter.rb`) - Value object containing method metadata (name, namespace, filepath, line number, arguments)
|
17
|
+
|
18
|
+
### Key Features
|
19
|
+
|
20
|
+
- **Automatic method instrumentation** - Wrap any method call with `instrumenter.instrument(binding) { ... }`
|
21
|
+
- **Argument tracking** - Captures method parameters with PII filtering capabilities
|
22
|
+
- **Return value serialization** - Configurable tracking of method return values
|
23
|
+
- **Exception handling** - Automatically captures and records exceptions in spans
|
24
|
+
- **Class/instance method detection** - Distinguishes between static (`.`) and instance (`#`) methods
|
25
|
+
- **Configurable serialization** - Supports custom formatters and max depth limits
|
26
|
+
|
27
|
+
## Development Commands
|
28
|
+
|
29
|
+
### Running Tests
|
30
|
+
```bash
|
31
|
+
bundle exec rake test
|
32
|
+
```
|
33
|
+
|
34
|
+
### Running Individual Tests
|
35
|
+
```bash
|
36
|
+
ruby -Itest test/unit/instrumenter_test.rb
|
37
|
+
```
|
38
|
+
|
39
|
+
### Linting
|
40
|
+
```bash
|
41
|
+
bundle exec standardrb
|
42
|
+
```
|
43
|
+
|
44
|
+
### Dependencies
|
45
|
+
```bash
|
46
|
+
bundle install
|
47
|
+
```
|
48
|
+
|
49
|
+
### Interactive Console
|
50
|
+
```bash
|
51
|
+
bin/console
|
52
|
+
```
|
53
|
+
|
54
|
+
### Build Gem
|
55
|
+
```bash
|
56
|
+
bundle exec rake build
|
57
|
+
```
|
58
|
+
|
59
|
+
### Release Process
|
60
|
+
```bash
|
61
|
+
# Update version in lib/observable/version.rb
|
62
|
+
bundle exec rake release
|
63
|
+
```
|
64
|
+
|
65
|
+
## Configuration System
|
66
|
+
|
67
|
+
The gem uses Dry::Configurable with these key settings:
|
68
|
+
|
69
|
+
- `transport` - Transport mechanism (default: `:otel`)
|
70
|
+
- `app_namespace` - Application namespace for spans
|
71
|
+
- `attribute_namespace` - Attribute namespace prefix
|
72
|
+
- `tracer_names` - OpenTelemetry tracer configuration
|
73
|
+
- `formatters` - Object serialization methods (default: `:to_h`)
|
74
|
+
- `pii_filters` - Regex patterns to filter sensitive data
|
75
|
+
- `track_return_values` - Enable/disable return value capture (default: true)
|
76
|
+
|
77
|
+
## Usage Pattern
|
78
|
+
|
79
|
+
The primary usage pattern involves:
|
80
|
+
|
81
|
+
1. Create an instrumenter instance: `Observable::Instrumenter.new`
|
82
|
+
2. Wrap method calls: `instrumenter.instrument(binding) { method_logic }`
|
83
|
+
3. The instrumenter automatically:
|
84
|
+
- Extracts method name, class, and arguments from `binding`
|
85
|
+
- Creates OpenTelemetry spans with standardized naming (`Class#method` or `Class.method`)
|
86
|
+
- Serializes arguments and return values with PII filtering
|
87
|
+
- Handles exceptions and sets appropriate span status
|
88
|
+
|
89
|
+
## Testing Structure
|
90
|
+
|
91
|
+
- **Unit tests** in `test/unit/` - Test individual components
|
92
|
+
- **Support helpers** in `test/support/` - Test utilities and mocks
|
93
|
+
- Uses Minitest framework
|
94
|
+
- Custom tracing test helpers for OpenTelemetry span verification
|
95
|
+
|
96
|
+
## Dependencies
|
97
|
+
|
98
|
+
**Runtime:**
|
99
|
+
- `opentelemetry-sdk` ~> 1.5 - OpenTelemetry instrumentation
|
100
|
+
- `dry-configurable` >= 0.13.0, < 2.0 - Configuration management
|
101
|
+
- `dry-struct` ~> 1.4 - Value objects
|
102
|
+
|
103
|
+
**Development:**
|
104
|
+
- `minitest` ~> 5.14 - Testing framework
|
105
|
+
- `standard` ~> 1.40 - Ruby linting and formatting
|
106
|
+
- `rake` ~> 13.0 - Build automation
|
107
|
+
|
108
|
+
## Code Style
|
109
|
+
|
110
|
+
- Uses Standard Ruby for linting and formatting
|
111
|
+
- Ruby version: 3.4+ (configured in `.ruby-version` and `.standard.yml`)
|
112
|
+
- Frozen string literals enforced
|
113
|
+
- Follows Ruby community conventions for method naming and structure
|
data/Gemfile
CHANGED
data/Gemfile.lock
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
observable (0.1.1)
|
5
|
+
dry-configurable (>= 0.13.0, < 2.0)
|
6
|
+
dry-struct (~> 1.4)
|
7
|
+
opentelemetry-sdk (~> 1.5)
|
8
|
+
|
9
|
+
GEM
|
10
|
+
remote: https://rubygems.org/
|
11
|
+
specs:
|
12
|
+
ast (2.4.3)
|
13
|
+
bigdecimal (3.2.3)
|
14
|
+
concurrent-ruby (1.3.5)
|
15
|
+
docile (1.4.1)
|
16
|
+
dry-configurable (1.3.0)
|
17
|
+
dry-core (~> 1.1)
|
18
|
+
zeitwerk (~> 2.6)
|
19
|
+
dry-core (1.1.0)
|
20
|
+
concurrent-ruby (~> 1.0)
|
21
|
+
logger
|
22
|
+
zeitwerk (~> 2.6)
|
23
|
+
dry-inflector (1.2.0)
|
24
|
+
dry-logic (1.6.0)
|
25
|
+
bigdecimal
|
26
|
+
concurrent-ruby (~> 1.0)
|
27
|
+
dry-core (~> 1.1)
|
28
|
+
zeitwerk (~> 2.6)
|
29
|
+
dry-struct (1.8.0)
|
30
|
+
dry-core (~> 1.1)
|
31
|
+
dry-types (~> 1.8, >= 1.8.2)
|
32
|
+
ice_nine (~> 0.11)
|
33
|
+
zeitwerk (~> 2.6)
|
34
|
+
dry-types (1.8.3)
|
35
|
+
bigdecimal (~> 3.0)
|
36
|
+
concurrent-ruby (~> 1.0)
|
37
|
+
dry-core (~> 1.0)
|
38
|
+
dry-inflector (~> 1.0)
|
39
|
+
dry-logic (~> 1.4)
|
40
|
+
zeitwerk (~> 2.6)
|
41
|
+
ice_nine (0.11.2)
|
42
|
+
json (2.13.2)
|
43
|
+
language_server-protocol (3.17.0.5)
|
44
|
+
lint_roller (1.1.0)
|
45
|
+
logger (1.7.0)
|
46
|
+
minitest (5.25.5)
|
47
|
+
opentelemetry-api (1.6.0)
|
48
|
+
opentelemetry-common (0.22.0)
|
49
|
+
opentelemetry-api (~> 1.0)
|
50
|
+
opentelemetry-registry (0.4.0)
|
51
|
+
opentelemetry-api (~> 1.1)
|
52
|
+
opentelemetry-sdk (1.8.1)
|
53
|
+
opentelemetry-api (~> 1.1)
|
54
|
+
opentelemetry-common (~> 0.20)
|
55
|
+
opentelemetry-registry (~> 0.2)
|
56
|
+
opentelemetry-semantic_conventions
|
57
|
+
opentelemetry-semantic_conventions (1.11.0)
|
58
|
+
opentelemetry-api (~> 1.0)
|
59
|
+
parallel (1.27.0)
|
60
|
+
parser (3.3.9.0)
|
61
|
+
ast (~> 2.4.1)
|
62
|
+
racc
|
63
|
+
prism (1.4.0)
|
64
|
+
racc (1.8.1)
|
65
|
+
rainbow (3.1.1)
|
66
|
+
rake (13.3.0)
|
67
|
+
regexp_parser (2.11.2)
|
68
|
+
rubocop (1.75.8)
|
69
|
+
json (~> 2.3)
|
70
|
+
language_server-protocol (~> 3.17.0.2)
|
71
|
+
lint_roller (~> 1.1.0)
|
72
|
+
parallel (~> 1.10)
|
73
|
+
parser (>= 3.3.0.2)
|
74
|
+
rainbow (>= 2.2.2, < 4.0)
|
75
|
+
regexp_parser (>= 2.9.3, < 3.0)
|
76
|
+
rubocop-ast (>= 1.44.0, < 2.0)
|
77
|
+
ruby-progressbar (~> 1.7)
|
78
|
+
unicode-display_width (>= 2.4.0, < 4.0)
|
79
|
+
rubocop-ast (1.46.0)
|
80
|
+
parser (>= 3.3.7.2)
|
81
|
+
prism (~> 1.4)
|
82
|
+
rubocop-performance (1.25.0)
|
83
|
+
lint_roller (~> 1.1)
|
84
|
+
rubocop (>= 1.75.0, < 2.0)
|
85
|
+
rubocop-ast (>= 1.38.0, < 2.0)
|
86
|
+
ruby-progressbar (1.13.0)
|
87
|
+
simplecov (0.22.0)
|
88
|
+
docile (~> 1.1)
|
89
|
+
simplecov-html (~> 0.11)
|
90
|
+
simplecov_json_formatter (~> 0.1)
|
91
|
+
simplecov-html (0.13.2)
|
92
|
+
simplecov_json_formatter (0.1.4)
|
93
|
+
standard (1.50.0)
|
94
|
+
language_server-protocol (~> 3.17.0.2)
|
95
|
+
lint_roller (~> 1.0)
|
96
|
+
rubocop (~> 1.75.5)
|
97
|
+
standard-custom (~> 1.0.0)
|
98
|
+
standard-performance (~> 1.8)
|
99
|
+
standard-custom (1.0.2)
|
100
|
+
lint_roller (~> 1.0)
|
101
|
+
rubocop (~> 1.50)
|
102
|
+
standard-performance (1.8.0)
|
103
|
+
lint_roller (~> 1.1)
|
104
|
+
rubocop-performance (~> 1.25.0)
|
105
|
+
unicode-display_width (3.1.5)
|
106
|
+
unicode-emoji (~> 4.0, >= 4.0.4)
|
107
|
+
unicode-emoji (4.0.4)
|
108
|
+
zeitwerk (2.7.3)
|
109
|
+
|
110
|
+
PLATFORMS
|
111
|
+
arm64-darwin-23
|
112
|
+
ruby
|
113
|
+
|
114
|
+
DEPENDENCIES
|
115
|
+
minitest (~> 5.14)
|
116
|
+
observable!
|
117
|
+
rake (~> 13.0)
|
118
|
+
simplecov (~> 0.22)
|
119
|
+
standard (~> 1.40)
|
120
|
+
|
121
|
+
BUNDLED WITH
|
122
|
+
2.6.3
|
data/README.md
CHANGED
@@ -1,37 +1,168 @@
|
|
1
1
|
# Observable
|
2
2
|
|
3
|
-
|
3
|
+
Automatic OpenTelemetry instrumentation for Ruby methods with configurable serialization, PII filtering, and argument tracking.
|
4
4
|
|
5
|
-
|
5
|
+
## Getting Started
|
6
6
|
|
7
|
-
|
7
|
+
```bash
|
8
|
+
bundle add observable
|
9
|
+
```
|
8
10
|
|
9
|
-
|
11
|
+
Or
|
10
12
|
|
11
|
-
|
13
|
+
```ruby
|
14
|
+
# Gemfile
|
15
|
+
gem 'observable'
|
16
|
+
```
|
12
17
|
|
13
|
-
|
18
|
+
Basic usage:
|
14
19
|
|
15
|
-
|
20
|
+
```ruby
|
21
|
+
require 'observable'
|
16
22
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
23
|
+
class UserService
|
24
|
+
def initialize
|
25
|
+
@instrumenter = Observable::Instrumenter.new
|
26
|
+
end
|
27
|
+
|
28
|
+
def create_user(name, email)
|
29
|
+
@instrumenter.instrument(binding) do
|
30
|
+
User.create(name: name, email: email)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
```
|
35
|
+
|
36
|
+
OpenTelemetry spans are automatically created with method names, arguments, return values, and exceptions.
|
37
|
+
|
38
|
+
## Configuration
|
39
|
+
|
40
|
+
Configure globally or per-instrumenter:
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
# Global configuration
|
44
|
+
Observable::Configuration.configure do |config|
|
45
|
+
config.tracer_name = "my_app"
|
46
|
+
config.transport = :otel
|
47
|
+
config.app_namespace = "my_app"
|
48
|
+
config.attribute_namespace = "my_app"
|
49
|
+
config.track_return_values = true
|
50
|
+
config.serialization_depth = {default: 2, "MyClass" => 3}
|
51
|
+
config.formatters = {default: :to_h, "MyClass" => :to_formatted_h}
|
52
|
+
config.pii_filters = [/password/i, /secret/i]
|
53
|
+
end
|
54
|
+
|
55
|
+
# Per-instrumenter configuration
|
56
|
+
config = Observable::Configuration.new
|
57
|
+
config.track_return_values = false
|
58
|
+
instrumenter = Observable::Instrumenter.new(config: config)
|
59
|
+
```
|
60
|
+
|
61
|
+
### Configuration Options
|
62
|
+
|
63
|
+
- `tracer_name`: `"observable"` - Name for the OpenTelemetry tracer
|
64
|
+
- `transport`: `:otel` - Uses OpenTelemetry SDK
|
65
|
+
- `app_namespace`: `"app"` - Namespace for application-specific attributes
|
66
|
+
- `attribute_namespace`: `"app"` - Namespace for span attributes
|
67
|
+
- `track_return_values`: `true` - Captures method return values in spans
|
68
|
+
- `serialization_depth`: `{default: 2}` - Per-class serialization depth limits (Hash or Integer for backward compatibility)
|
69
|
+
- `formatters`: `{default: :to_h}` - Object serialization methods by class name
|
70
|
+
- `pii_filters`: `[]` - Regex patterns to filter sensitive data from spans
|
71
|
+
|
72
|
+
## OpenTelemetry Integration
|
73
|
+
|
74
|
+
This library seamlessly integrates with OpenTelemetry, the industry-standard observability framework. Spans are automatically created with standardized naming (`Class#method` or `Class.method`) and include rich metadata about method invocations, making your Ruby applications immediately observable without manual instrumentation.
|
75
|
+
|
76
|
+
## Custom Formatters
|
77
|
+
|
78
|
+
Control how domain objects are serialized in spans by configuring custom formatters.
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
Observable::Configuration.configure do |config|
|
82
|
+
config.formatters = {
|
83
|
+
default: :to_h,
|
84
|
+
'YourCustomClass' => :to_formatted_h
|
85
|
+
}
|
86
|
+
config.serialization_depth = {
|
87
|
+
default: 2,
|
88
|
+
'YourCustomClass' => 3
|
89
|
+
}
|
90
|
+
end
|
91
|
+
```
|
92
|
+
|
93
|
+
## Example
|
94
|
+
|
95
|
+
A domain object `Customer` has an `Invoice`.
|
96
|
+
|
97
|
+
### Objective
|
98
|
+
|
99
|
+
Only send the invoice ID to the trace to save data.
|
100
|
+
|
101
|
+
### Background
|
102
|
+
|
103
|
+
Imagine domain objects are `Dry::Struct` value objects:
|
104
|
+
|
105
|
+
```ruby
|
106
|
+
class Customer < Dry::Struct
|
107
|
+
attribute :id, Dry.Types::String
|
108
|
+
attribute :name, Dry.Types::String
|
109
|
+
attribute :Invoice, Invoice
|
110
|
+
end
|
111
|
+
|
112
|
+
class Invoice < Dry::Struct
|
113
|
+
attribute :id, Dry.Types::String
|
114
|
+
attribute :status, Dry.Types::String
|
115
|
+
attribute :line_items, Dry.Types::Array
|
116
|
+
end
|
117
|
+
```
|
118
|
+
|
119
|
+
### Solution
|
120
|
+
|
121
|
+
1. Define custom formatting method - `#to_formatted_h`
|
122
|
+
|
123
|
+
```diff
|
124
|
+
class Customer < Dry::Struct
|
125
|
+
attribute :id, Dry.Types::String
|
126
|
+
attribute :name, Dry.Types::String
|
127
|
+
attribute :Invoice, Invoice
|
128
|
+
|
129
|
+
+ def to_formatted_h
|
130
|
+
+ {
|
131
|
+
+ id: id,
|
132
|
+
+ name: name,
|
133
|
+
+ invoice: {
|
134
|
+
+ id: invoice.id
|
135
|
+
+ }
|
136
|
+
+ }
|
137
|
+
+ end
|
138
|
+
end
|
139
|
+
```
|
140
|
+
|
141
|
+
2. Configure observable:
|
142
|
+
|
143
|
+
```ruby
|
144
|
+
Observable::Configuration.configure do |config|
|
145
|
+
config.formatters = {
|
146
|
+
default: :to_h,
|
147
|
+
'Customer' => :to_formatted_h
|
148
|
+
}
|
149
|
+
config.serialization_depth = {
|
150
|
+
default: 2,
|
151
|
+
'Customer' => 3
|
152
|
+
}
|
153
|
+
end
|
154
|
+
```
|
155
|
+
|
156
|
+
The instrumenter tries class-specific formatters first, then falls back to the default formatter, then `to_s`.
|
157
|
+
|
158
|
+
## Benefits
|
159
|
+
|
160
|
+
Why use this library? Why not write Otel attributes manually?
|
161
|
+
|
162
|
+
* **Zero-touch instrumentation** - Wrap any method call without modifying existing code or manually creating spans
|
163
|
+
* **Production-ready safety** - Built-in PII filtering, serialization depth limits, and exception handling prevent common observability pitfalls
|
164
|
+
* **Standardized telemetry** - Consistent span naming, attribute structure, and OpenTelemetry compliance across your entire application
|
30
165
|
|
31
166
|
## License
|
32
167
|
|
33
|
-
|
34
|
-
|
35
|
-
## Code of Conduct
|
36
|
-
|
37
|
-
Everyone interacting in the Observable project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/johngallagher/observable/blob/main/CODE_OF_CONDUCT.md).
|
168
|
+
MIT License. See LICENSE file for details.
|
data/Rakefile
CHANGED
@@ -6,9 +6,7 @@ require "rake/testtask"
|
|
6
6
|
Rake::TestTask.new(:test) do |t|
|
7
7
|
t.libs << "test"
|
8
8
|
t.libs << "lib"
|
9
|
-
t.test_files = FileList["test
|
9
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
10
10
|
end
|
11
11
|
|
12
|
-
|
13
|
-
|
14
|
-
task default: %i[test standard]
|
12
|
+
task default: %i[test]
|
data/example.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require_relative "lib/observable"
|
4
|
+
|
5
|
+
# Configure the instrumenter
|
6
|
+
Observable::Configuration.configure do |config|
|
7
|
+
config.app_namespace = "example_app"
|
8
|
+
config.track_return_values = true
|
9
|
+
config.pii_filters = [/password/, /token/, /secret/]
|
10
|
+
end
|
11
|
+
|
12
|
+
class ExampleService
|
13
|
+
def initialize
|
14
|
+
@instrumenter = Observable::Instrumenter.new
|
15
|
+
end
|
16
|
+
|
17
|
+
def process_order(order_id, customer_email, amount)
|
18
|
+
@instrumenter.instrument(binding) do
|
19
|
+
# Simulate some processing
|
20
|
+
puts "Processing order #{order_id} for #{customer_email}, amount: #{amount}"
|
21
|
+
|
22
|
+
# Return a result hash
|
23
|
+
{
|
24
|
+
order_id: order_id,
|
25
|
+
status: "processed",
|
26
|
+
processed_at: Time.now.to_i,
|
27
|
+
amount: amount
|
28
|
+
}
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.static_method(data)
|
33
|
+
instrumenter = Observable::Instrumenter.new
|
34
|
+
instrumenter.instrument(binding) do
|
35
|
+
puts "Processing data: #{data.inspect}"
|
36
|
+
data.transform_values(&:upcase) if data.is_a?(Hash)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Example usage
|
42
|
+
puts "=== Observable Example ==="
|
43
|
+
puts
|
44
|
+
|
45
|
+
service = ExampleService.new
|
46
|
+
result = service.process_order("ORD-123", "customer@example.com", 99.99)
|
47
|
+
puts "Result: #{result}"
|
48
|
+
|
49
|
+
puts
|
50
|
+
|
51
|
+
static_result = ExampleService.static_method({name: "john", city: "nyc"})
|
52
|
+
puts "Static result: #{static_result}"
|
53
|
+
|
54
|
+
puts "\n=== Check OpenTelemetry spans were created ==="
|
55
|
+
# In a real application, these spans would be exported to your observability platform
|
@@ -0,0 +1,388 @@
|
|
1
|
+
require "opentelemetry/sdk"
|
2
|
+
require "dry/configurable"
|
3
|
+
|
4
|
+
module Observable
|
5
|
+
class Configuration
|
6
|
+
extend Dry::Configurable
|
7
|
+
|
8
|
+
setting :tracer_name, default: "observable"
|
9
|
+
setting :transport, default: :otel
|
10
|
+
setting :app_namespace, default: "app"
|
11
|
+
setting :attribute_namespace, default: "app"
|
12
|
+
setting :formatters, default: {default: :to_h}
|
13
|
+
setting :pii_filters, default: []
|
14
|
+
setting :serialization_depth, default: {default: 2}
|
15
|
+
setting :track_return_values, default: true
|
16
|
+
end
|
17
|
+
|
18
|
+
class Instrumenter
|
19
|
+
attr_reader :last_captured_method_name, :last_captured_namespace, :config
|
20
|
+
|
21
|
+
def initialize(tracer: nil, config: nil)
|
22
|
+
@config = config || Configuration.config
|
23
|
+
@tracer = tracer || OpenTelemetry.tracer_provider.tracer(@config.tracer_name)
|
24
|
+
end
|
25
|
+
|
26
|
+
def instrument(caller_binding = nil, &block)
|
27
|
+
@caller_binding = caller_binding
|
28
|
+
caller_info = extract_caller_information
|
29
|
+
create_instrumented_span(caller_info, &block)
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
# Extracts all information about the calling method in one place
|
35
|
+
def extract_caller_information
|
36
|
+
caller_location = find_caller_location
|
37
|
+
namespace = extract_actual_class_name
|
38
|
+
is_class_method = determine_if_class_method
|
39
|
+
|
40
|
+
CallerInformation.new(
|
41
|
+
method_name: caller_location.label,
|
42
|
+
namespace: namespace,
|
43
|
+
filepath: caller_location.path,
|
44
|
+
line_number: caller_location.lineno,
|
45
|
+
arguments: extract_arguments_from_caller(caller_location),
|
46
|
+
is_class_method: is_class_method
|
47
|
+
)
|
48
|
+
end
|
49
|
+
|
50
|
+
def find_caller_location
|
51
|
+
locations = caller_locations(1, 10)
|
52
|
+
raise InstrumentationError, "Unable to determine caller location" if locations.nil? || locations.empty?
|
53
|
+
|
54
|
+
instrumenter_file = __FILE__
|
55
|
+
caller_location = locations.find { |loc| loc.path != instrumenter_file }
|
56
|
+
|
57
|
+
caller_location || locations.first
|
58
|
+
rescue => e
|
59
|
+
raise InstrumentationError, "Error finding caller location: #{e.message}"
|
60
|
+
end
|
61
|
+
|
62
|
+
def extract_namespace_from_path(file_path)
|
63
|
+
return "UnknownClass" if file_path.nil? || file_path.empty?
|
64
|
+
|
65
|
+
file_name = File.basename(file_path, ".*")
|
66
|
+
return "UnknownClass" if file_name.empty?
|
67
|
+
|
68
|
+
file_name.split("_").map(&:capitalize).join
|
69
|
+
rescue
|
70
|
+
"UnknownClass"
|
71
|
+
end
|
72
|
+
|
73
|
+
def extract_actual_class_name
|
74
|
+
if @caller_binding
|
75
|
+
begin
|
76
|
+
caller_self = @caller_binding.eval("self")
|
77
|
+
if caller_self.is_a?(Class)
|
78
|
+
caller_self.name
|
79
|
+
else
|
80
|
+
caller_self.class.name
|
81
|
+
end
|
82
|
+
rescue
|
83
|
+
extract_namespace_from_path(@caller_binding.eval("__FILE__"))
|
84
|
+
end
|
85
|
+
else
|
86
|
+
caller_location = find_caller_location
|
87
|
+
extract_namespace_from_path(caller_location.path)
|
88
|
+
end
|
89
|
+
rescue
|
90
|
+
"UnknownClass"
|
91
|
+
end
|
92
|
+
|
93
|
+
def determine_if_class_method
|
94
|
+
return false unless @caller_binding
|
95
|
+
|
96
|
+
begin
|
97
|
+
caller_self = @caller_binding.eval("self")
|
98
|
+
caller_self.is_a?(Class)
|
99
|
+
rescue
|
100
|
+
false
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def create_instrumented_span(caller_info, &block)
|
105
|
+
separator = caller_info.is_class_method ? "." : "#"
|
106
|
+
|
107
|
+
if caller_info.method_name.include?("#") || caller_info.method_name.include?(".")
|
108
|
+
span_name = caller_info.method_name
|
109
|
+
method_name_only = caller_info.method_name.split(/[#.]/).last
|
110
|
+
else
|
111
|
+
span_name = "#{caller_info.namespace}#{separator}#{caller_info.method_name}"
|
112
|
+
method_name_only = caller_info.method_name
|
113
|
+
end
|
114
|
+
|
115
|
+
@last_captured_method_name = method_name_only
|
116
|
+
@last_captured_namespace = caller_info.namespace
|
117
|
+
@tracer.in_span(span_name) do |span|
|
118
|
+
set_span_attributes(span, caller_info)
|
119
|
+
|
120
|
+
begin
|
121
|
+
result = block.call
|
122
|
+
span.set_attribute("error", false)
|
123
|
+
serialize_return_value(span, result)
|
124
|
+
result
|
125
|
+
rescue => e
|
126
|
+
span.set_attribute("error", true)
|
127
|
+
span.set_attribute("error.type", e.class.name)
|
128
|
+
span.set_attribute("error.message", e.message)
|
129
|
+
span.status = OpenTelemetry::Trace::Status.error(e.message)
|
130
|
+
raise
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def set_span_attributes(span, caller_info)
|
136
|
+
span.set_attribute("code.function", caller_info.method_name)
|
137
|
+
span.set_attribute("code.namespace", caller_info.namespace)
|
138
|
+
span.set_attribute("code.filepath", caller_info.filepath)
|
139
|
+
span.set_attribute("code.lineno", caller_info.line_number)
|
140
|
+
|
141
|
+
if @config.app_namespace
|
142
|
+
span.set_attribute("app.namespace", @config.app_namespace)
|
143
|
+
end
|
144
|
+
|
145
|
+
caller_info.arguments.each do |param_index, param_data|
|
146
|
+
if param_data.is_a?(Hash) && param_data.key?(:value) && param_data.key?(:param_name)
|
147
|
+
serialize_argument(span, "code.arguments.#{param_index}", param_data[:value], param_data[:param_name])
|
148
|
+
else
|
149
|
+
serialize_argument(span, "code.arguments.#{param_index}", param_data, param_index)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def serialize_argument(span, attribute_prefix, value, param_name = nil, depth = 0)
|
155
|
+
if param_name && should_filter_pii?(param_name)
|
156
|
+
span.set_attribute(attribute_prefix, "[FILTERED]")
|
157
|
+
return
|
158
|
+
end
|
159
|
+
|
160
|
+
case value
|
161
|
+
when String, Numeric, TrueClass, FalseClass, NilClass
|
162
|
+
span.set_attribute(attribute_prefix, value)
|
163
|
+
when Hash
|
164
|
+
serialize_hash(span, attribute_prefix, value, depth)
|
165
|
+
when Array
|
166
|
+
serialize_array(span, attribute_prefix, value, depth)
|
167
|
+
else
|
168
|
+
serialize_object(span, attribute_prefix, value, depth)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def serialize_argument_with_custom_depth(span, attribute_prefix, value, custom_max_depth, param_name = nil, depth = 0)
|
173
|
+
if param_name && should_filter_pii?(param_name)
|
174
|
+
span.set_attribute(attribute_prefix, "[FILTERED]")
|
175
|
+
return
|
176
|
+
end
|
177
|
+
|
178
|
+
case value
|
179
|
+
when String, Numeric, TrueClass, FalseClass, NilClass
|
180
|
+
span.set_attribute(attribute_prefix, value)
|
181
|
+
when Hash
|
182
|
+
serialize_hash_with_custom_depth(span, attribute_prefix, value, custom_max_depth, depth)
|
183
|
+
when Array
|
184
|
+
serialize_array_with_custom_depth(span, attribute_prefix, value, custom_max_depth, depth)
|
185
|
+
else
|
186
|
+
serialize_object_with_custom_depth(span, attribute_prefix, value, custom_max_depth, depth)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def serialize_return_value(span, value)
|
191
|
+
return unless @config.track_return_values
|
192
|
+
|
193
|
+
case value
|
194
|
+
when NilClass
|
195
|
+
span.set_attribute("code.return", "nil")
|
196
|
+
when String, Numeric, TrueClass, FalseClass
|
197
|
+
span.set_attribute("code.return", value)
|
198
|
+
when Hash
|
199
|
+
serialize_hash(span, "code.return", value, 2)
|
200
|
+
when Array
|
201
|
+
serialize_array(span, "code.return", value, 2)
|
202
|
+
else
|
203
|
+
serialize_object(span, "code.return", value, 2)
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
def serialize_hash(span, prefix, hash, depth = 0)
|
208
|
+
max_depth = get_serialization_depth_for_class("Hash")
|
209
|
+
return if depth > max_depth
|
210
|
+
|
211
|
+
hash.each do |key, value|
|
212
|
+
key_str = key.to_s
|
213
|
+
next if should_filter_pii?(key_str)
|
214
|
+
serialize_argument(span, "#{prefix}.#{key_str}", value, nil, depth + 1)
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
def serialize_hash_with_custom_depth(span, prefix, hash, custom_max_depth, depth = 0)
|
219
|
+
return if depth > custom_max_depth
|
220
|
+
|
221
|
+
hash.each do |key, value|
|
222
|
+
key_str = key.to_s
|
223
|
+
next if should_filter_pii?(key_str)
|
224
|
+
serialize_argument_with_custom_depth(span, "#{prefix}.#{key_str}", value, custom_max_depth, nil, depth + 1)
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
def serialize_array_with_custom_depth(span, prefix, array, custom_max_depth, depth = 0)
|
229
|
+
return if depth > custom_max_depth
|
230
|
+
|
231
|
+
items_to_process = [array.length, 10].min
|
232
|
+
array.first(items_to_process).each_with_index do |item, index|
|
233
|
+
serialize_argument_with_custom_depth(span, "#{prefix}.#{index}", item, custom_max_depth, nil, depth + 1)
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
def serialize_object_with_custom_depth(span, prefix, obj, custom_max_depth, depth = 0)
|
238
|
+
if depth >= custom_max_depth
|
239
|
+
span.set_attribute("#{prefix}.class", obj.class.name)
|
240
|
+
span.set_attribute(prefix, obj.to_s)
|
241
|
+
return
|
242
|
+
end
|
243
|
+
|
244
|
+
span.set_attribute("#{prefix}.class", obj.class.name)
|
245
|
+
|
246
|
+
formatter = find_formatter_for_class(obj.class.name)
|
247
|
+
|
248
|
+
if formatter && obj.respond_to?(formatter)
|
249
|
+
result = obj.send(formatter)
|
250
|
+
if result.is_a?(Hash)
|
251
|
+
serialize_hash_with_custom_depth(span, prefix, result, custom_max_depth, depth)
|
252
|
+
return
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
default_formatter = @config.formatters[:default]
|
257
|
+
if default_formatter && obj.respond_to?(default_formatter)
|
258
|
+
result = obj.send(default_formatter)
|
259
|
+
if result.is_a?(Hash)
|
260
|
+
serialize_hash_with_custom_depth(span, prefix, result, custom_max_depth, depth)
|
261
|
+
return
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
span.set_attribute(prefix, obj.to_s)
|
266
|
+
end
|
267
|
+
|
268
|
+
def serialize_array(span, prefix, array, depth = 0)
|
269
|
+
max_depth = get_serialization_depth_for_class("Array")
|
270
|
+
return if depth > max_depth
|
271
|
+
|
272
|
+
items_to_process = [array.length, 10].min
|
273
|
+
array.first(items_to_process).each_with_index do |item, index|
|
274
|
+
serialize_argument(span, "#{prefix}.#{index}", item, nil, depth + 1)
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
def serialize_object(span, prefix, obj, depth = 0)
|
279
|
+
max_depth = get_serialization_depth_for_class(obj.class.name)
|
280
|
+
|
281
|
+
# If we've reached the depth limit for this class, just set the class name
|
282
|
+
if depth >= max_depth
|
283
|
+
span.set_attribute("#{prefix}.class", obj.class.name)
|
284
|
+
span.set_attribute(prefix, obj.to_s)
|
285
|
+
return
|
286
|
+
end
|
287
|
+
|
288
|
+
span.set_attribute("#{prefix}.class", obj.class.name)
|
289
|
+
|
290
|
+
formatter = find_formatter_for_class(obj.class.name)
|
291
|
+
|
292
|
+
if formatter && obj.respond_to?(formatter)
|
293
|
+
result = obj.send(formatter)
|
294
|
+
if result.is_a?(Hash)
|
295
|
+
# When object converts to hash, the hash starts fresh at depth 0
|
296
|
+
# but we need to enforce the object's depth limit
|
297
|
+
serialize_hash_with_custom_depth(span, prefix, result, max_depth, 0)
|
298
|
+
return
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
default_formatter = @config.formatters[:default]
|
303
|
+
if default_formatter && obj.respond_to?(default_formatter)
|
304
|
+
result = obj.send(default_formatter)
|
305
|
+
if result.is_a?(Hash)
|
306
|
+
# When object converts to hash, the hash starts fresh at depth 0
|
307
|
+
# but we need to enforce the object's depth limit
|
308
|
+
serialize_hash_with_custom_depth(span, prefix, result, max_depth, 0)
|
309
|
+
return
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
span.set_attribute(prefix, obj.to_s)
|
314
|
+
end
|
315
|
+
|
316
|
+
def find_formatter_for_class(class_name)
|
317
|
+
@config.formatters[class_name]
|
318
|
+
end
|
319
|
+
|
320
|
+
def get_serialization_depth_for_class(class_name)
|
321
|
+
if @config.serialization_depth.is_a?(Integer)
|
322
|
+
return @config.serialization_depth
|
323
|
+
end
|
324
|
+
|
325
|
+
@config.serialization_depth[class_name] || @config.serialization_depth[:default] || @config.serialization_depth["default"] || 2
|
326
|
+
end
|
327
|
+
|
328
|
+
def should_filter_pii?(param_name)
|
329
|
+
return false if @config.pii_filters.empty?
|
330
|
+
|
331
|
+
param_name_str = param_name.to_s.downcase
|
332
|
+
@config.pii_filters.any? { |pattern| pattern.match?(param_name_str) }
|
333
|
+
end
|
334
|
+
|
335
|
+
def extract_arguments_from_caller(caller_location)
|
336
|
+
ArgumentExtractor.new(caller_location, @caller_binding).extract
|
337
|
+
end
|
338
|
+
|
339
|
+
CallerInformation = Struct.new(:method_name, :namespace, :filepath, :line_number, :arguments, :is_class_method, keyword_init: true)
|
340
|
+
|
341
|
+
class ArgumentExtractor
|
342
|
+
def initialize(caller_location, caller_binding = nil)
|
343
|
+
@caller_location = caller_location
|
344
|
+
@caller_binding = caller_binding
|
345
|
+
@method_name = caller_location.label
|
346
|
+
end
|
347
|
+
|
348
|
+
def extract
|
349
|
+
return {} unless @caller_binding
|
350
|
+
extract_from_binding
|
351
|
+
end
|
352
|
+
|
353
|
+
private
|
354
|
+
|
355
|
+
def extract_from_binding
|
356
|
+
@caller_binding.local_variables
|
357
|
+
args = {}
|
358
|
+
|
359
|
+
extract_local_variables_with_numeric_indexing(args)
|
360
|
+
|
361
|
+
args
|
362
|
+
rescue
|
363
|
+
{}
|
364
|
+
end
|
365
|
+
|
366
|
+
def extract_local_variables_with_numeric_indexing(args)
|
367
|
+
local_vars = @caller_binding.local_variables
|
368
|
+
positional_index = 0
|
369
|
+
|
370
|
+
local_vars.each do |var_name|
|
371
|
+
var_name_str = var_name.to_s
|
372
|
+
|
373
|
+
next if var_name_str.start_with?("_") || var_name == :instrumenter
|
374
|
+
|
375
|
+
value = @caller_binding.local_variable_get(var_name)
|
376
|
+
|
377
|
+
args[positional_index.to_s] = {
|
378
|
+
value: value,
|
379
|
+
param_name: var_name_str
|
380
|
+
}
|
381
|
+
positional_index += 1
|
382
|
+
end
|
383
|
+
end
|
384
|
+
end
|
385
|
+
|
386
|
+
class InstrumentationError < StandardError; end
|
387
|
+
end
|
388
|
+
end
|
data/lib/observable/version.rb
CHANGED
data/lib/observable.rb
CHANGED
@@ -1,8 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative "observable/version"
|
4
|
+
require_relative "observable/instrumenter"
|
4
5
|
|
5
6
|
module Observable
|
6
7
|
class Error < StandardError; end
|
7
|
-
|
8
|
+
|
9
|
+
# Convenience method to create a new instrumenter instance
|
10
|
+
def self.instrumenter(config: nil)
|
11
|
+
Instrumenter.new(config: config)
|
12
|
+
end
|
8
13
|
end
|
data/observable.gemspec
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/observable/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "observable"
|
7
|
+
spec.version = Observable::VERSION
|
8
|
+
spec.authors = ["John Gallagher"]
|
9
|
+
spec.email = ["john@synapticmishap.co.uk"]
|
10
|
+
|
11
|
+
spec.summary = "OpenTelemetry instrumentation library for Ruby methods"
|
12
|
+
spec.description = "A Ruby gem that provides automated OpenTelemetry instrumentation for method calls with configurable serialization, PII filtering, and argument tracking"
|
13
|
+
spec.homepage = "https://github.com/JoyfulProgramming/observable"
|
14
|
+
spec.license = "MIT"
|
15
|
+
spec.required_ruby_version = ">= 3.0.0"
|
16
|
+
|
17
|
+
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
18
|
+
|
19
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
20
|
+
spec.metadata["source_code_uri"] = "https://github.com/JoyfulProgramming/observable"
|
21
|
+
spec.metadata["changelog_uri"] = "https://github.com/JoyfulProgramming/observable/CHANGELOG.md"
|
22
|
+
|
23
|
+
# Specify which files should be added to the gem when it is released.
|
24
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
25
|
+
spec.files = Dir.chdir(__dir__) do
|
26
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
27
|
+
(f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
|
28
|
+
end
|
29
|
+
end
|
30
|
+
spec.bindir = "exe"
|
31
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
32
|
+
spec.require_paths = ["lib"]
|
33
|
+
|
34
|
+
# Runtime dependencies
|
35
|
+
spec.add_dependency "opentelemetry-sdk", "~> 1.5"
|
36
|
+
spec.add_dependency "dry-configurable", ">= 0.13.0", "< 2.0"
|
37
|
+
spec.add_dependency "dry-struct", "~> 1.4"
|
38
|
+
|
39
|
+
# Development dependencies
|
40
|
+
spec.add_development_dependency "minitest", "~> 5.14"
|
41
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
42
|
+
spec.add_development_dependency "simplecov", "~> 0.22"
|
43
|
+
spec.add_development_dependency "standard", "~> 1.40"
|
44
|
+
|
45
|
+
# For more information and examples about making a new gem, check out our
|
46
|
+
# guide at: https://bundler.io/guides/creating_gem.html
|
47
|
+
end
|
metadata
CHANGED
@@ -1,31 +1,142 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: observable
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- John Gallagher
|
8
|
-
autorequire:
|
9
8
|
bindir: exe
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
12
|
-
dependencies:
|
13
|
-
|
10
|
+
date: 2025-09-07 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: opentelemetry-sdk
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - "~>"
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '1.5'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - "~>"
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '1.5'
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: dry-configurable
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 0.13.0
|
33
|
+
- - "<"
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: '2.0'
|
36
|
+
type: :runtime
|
37
|
+
prerelease: false
|
38
|
+
version_requirements: !ruby/object:Gem::Requirement
|
39
|
+
requirements:
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: 0.13.0
|
43
|
+
- - "<"
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '2.0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: dry-struct
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
requirements:
|
50
|
+
- - "~>"
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: '1.4'
|
53
|
+
type: :runtime
|
54
|
+
prerelease: false
|
55
|
+
version_requirements: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - "~>"
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '1.4'
|
60
|
+
- !ruby/object:Gem::Dependency
|
61
|
+
name: minitest
|
62
|
+
requirement: !ruby/object:Gem::Requirement
|
63
|
+
requirements:
|
64
|
+
- - "~>"
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: '5.14'
|
67
|
+
type: :development
|
68
|
+
prerelease: false
|
69
|
+
version_requirements: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - "~>"
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: '5.14'
|
74
|
+
- !ruby/object:Gem::Dependency
|
75
|
+
name: rake
|
76
|
+
requirement: !ruby/object:Gem::Requirement
|
77
|
+
requirements:
|
78
|
+
- - "~>"
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: '13.0'
|
81
|
+
type: :development
|
82
|
+
prerelease: false
|
83
|
+
version_requirements: !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - "~>"
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '13.0'
|
88
|
+
- !ruby/object:Gem::Dependency
|
89
|
+
name: simplecov
|
90
|
+
requirement: !ruby/object:Gem::Requirement
|
91
|
+
requirements:
|
92
|
+
- - "~>"
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '0.22'
|
95
|
+
type: :development
|
96
|
+
prerelease: false
|
97
|
+
version_requirements: !ruby/object:Gem::Requirement
|
98
|
+
requirements:
|
99
|
+
- - "~>"
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0.22'
|
102
|
+
- !ruby/object:Gem::Dependency
|
103
|
+
name: standard
|
104
|
+
requirement: !ruby/object:Gem::Requirement
|
105
|
+
requirements:
|
106
|
+
- - "~>"
|
107
|
+
- !ruby/object:Gem::Version
|
108
|
+
version: '1.40'
|
109
|
+
type: :development
|
110
|
+
prerelease: false
|
111
|
+
version_requirements: !ruby/object:Gem::Requirement
|
112
|
+
requirements:
|
113
|
+
- - "~>"
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
version: '1.40'
|
116
|
+
description: A Ruby gem that provides automated OpenTelemetry instrumentation for
|
117
|
+
method calls with configurable serialization, PII filtering, and argument tracking
|
14
118
|
email:
|
15
119
|
- john@synapticmishap.co.uk
|
16
120
|
executables: []
|
17
121
|
extensions: []
|
18
122
|
extra_rdoc_files: []
|
19
123
|
files:
|
124
|
+
- ".claude/settings.local.json"
|
125
|
+
- ".ruby-version"
|
20
126
|
- ".standard.yml"
|
21
127
|
- CHANGELOG.md
|
128
|
+
- CLAUDE.md
|
22
129
|
- CODE_OF_CONDUCT.md
|
23
130
|
- Gemfile
|
131
|
+
- Gemfile.lock
|
24
132
|
- LICENSE.txt
|
25
133
|
- README.md
|
26
134
|
- Rakefile
|
135
|
+
- example.rb
|
27
136
|
- lib/observable.rb
|
137
|
+
- lib/observable/instrumenter.rb
|
28
138
|
- lib/observable/version.rb
|
139
|
+
- observable.gemspec
|
29
140
|
- sig/observable.rbs
|
30
141
|
homepage: https://github.com/JoyfulProgramming/observable
|
31
142
|
licenses:
|
@@ -35,7 +146,6 @@ metadata:
|
|
35
146
|
homepage_uri: https://github.com/JoyfulProgramming/observable
|
36
147
|
source_code_uri: https://github.com/JoyfulProgramming/observable
|
37
148
|
changelog_uri: https://github.com/JoyfulProgramming/observable/CHANGELOG.md
|
38
|
-
post_install_message:
|
39
149
|
rdoc_options: []
|
40
150
|
require_paths:
|
41
151
|
- lib
|
@@ -43,15 +153,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
43
153
|
requirements:
|
44
154
|
- - ">="
|
45
155
|
- !ruby/object:Gem::Version
|
46
|
-
version:
|
156
|
+
version: 3.0.0
|
47
157
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
48
158
|
requirements:
|
49
159
|
- - ">="
|
50
160
|
- !ruby/object:Gem::Version
|
51
161
|
version: '0'
|
52
162
|
requirements: []
|
53
|
-
rubygems_version: 3.
|
54
|
-
signing_key:
|
163
|
+
rubygems_version: 3.6.2
|
55
164
|
specification_version: 4
|
56
|
-
summary:
|
165
|
+
summary: OpenTelemetry instrumentation library for Ruby methods
|
57
166
|
test_files: []
|