https://github.com/joyfulprogramming/observable
Instrument method calls with OpenTelemetry
https://github.com/joyfulprogramming/observable
open-telemetry opentelemetry rails ruby
Last synced: 9 months ago
JSON representation
Instrument method calls with OpenTelemetry
- Host: GitHub
- URL: https://github.com/joyfulprogramming/observable
- Owner: JoyfulProgramming
- License: mit
- Created: 2024-04-22T04:02:36.000Z (about 2 years ago)
- Default Branch: main
- Last Pushed: 2025-10-04T22:20:55.000Z (9 months ago)
- Last Synced: 2025-10-04T23:33:07.378Z (9 months ago)
- Topics: open-telemetry, opentelemetry, rails, ruby
- Language: Ruby
- Homepage:
- Size: 123 KB
- Stars: 2
- Watchers: 1
- Forks: 1
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE.txt
- Code of conduct: CODE_OF_CONDUCT.md
Awesome Lists containing this project
README
# Observable
Automatic OpenTelemetry instrumentation for Ruby methods with configurable serialization, PII filtering, and argument tracking.
## Getting Started
```bash
bundle add observable
```
Or
```ruby
# Gemfile
gem 'observable'
```
Basic usage:
```ruby
require 'observable'
class UserService
def initialize
@instrumenter = Observable::Instrumenter.new
end
def create_user(name, email)
@instrumenter.instrument(binding) do
User.create(name: name, email: email)
end
end
end
```
OpenTelemetry spans are automatically created with method names, arguments, return values, and exceptions.
## Configuration
Configure globally or per-instrumenter:
```ruby
# Global configuration
Observable::Configuration.configure do |config|
config.tracer_name = "my_app"
config.transport = :otel
config.app_namespace = "my_app"
config.attribute_namespace = "my_app"
config.track_return_values = true
config.serialization_depth = {default: 2, "MyClass" => 3}
config.formatters = {default: :to_h, "MyClass" => :to_formatted_h}
config.pii_filters = [/password/i, /secret/i]
end
# Per-instrumenter configuration
config = Observable::Configuration.new
config.track_return_values = false
instrumenter = Observable::Instrumenter.new(config: config)
```
### Configuration Options
- `tracer_name`: `"observable"` - Name for the OpenTelemetry tracer
- `transport`: `:otel` - Uses OpenTelemetry SDK
- `app_namespace`: `"app"` - Namespace for application-specific attributes
- `attribute_namespace`: `"app"` - Namespace for span attributes
- `track_return_values`: `true` - Captures method return values in spans
- `serialization_depth`: `{default: 2}` - Per-class serialization depth limits (Hash or Integer for backward compatibility)
- `formatters`: `{default: :to_h}` - Object serialization methods by class name
- `pii_filters`: `[]` - Regex patterns to filter sensitive data from spans
## OpenTelemetry Integration
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.
## Custom Formatters
Control how domain objects are serialized in spans by configuring custom formatters.
```ruby
Observable::Configuration.configure do |config|
config.formatters = {
default: :to_h,
'YourCustomClass' => :to_formatted_h
}
config.serialization_depth = {
default: 2,
'YourCustomClass' => 3
}
end
```
## Example
A domain object `Customer` has an `Invoice`.
### Objective
Only send the invoice ID to the trace to save data.
### Background
Imagine domain objects are `Dry::Struct` value objects:
```ruby
class Customer < Dry::Struct
attribute :id, Dry.Types::String
attribute :name, Dry.Types::String
attribute :Invoice, Invoice
end
class Invoice < Dry::Struct
attribute :id, Dry.Types::String
attribute :status, Dry.Types::String
attribute :line_items, Dry.Types::Array
end
```
### Solution
1. Define custom formatting method - `#to_formatted_h`
```diff
class Customer < Dry::Struct
attribute :id, Dry.Types::String
attribute :name, Dry.Types::String
attribute :Invoice, Invoice
+ def to_formatted_h
+ {
+ id: id,
+ name: name,
+ invoice: {
+ id: invoice.id
+ }
+ }
+ end
end
```
2. Configure observable:
```ruby
Observable::Configuration.configure do |config|
config.formatters = {
default: :to_h,
'Customer' => :to_formatted_h
}
config.serialization_depth = {
default: 2,
'Customer' => 3
}
end
```
The instrumenter tries class-specific formatters first, then falls back to the default formatter, then `to_s`.
## Benefits
Why use this library? Why not write Otel attributes manually?
* **Zero-touch instrumentation** - Wrap any method call without modifying existing code or manually creating spans
* **Production-ready safety** - Built-in PII filtering, serialization depth limits, and exception handling prevent common observability pitfalls
* **Standardized telemetry** - Consistent span naming, attribute structure, and OpenTelemetry compliance across your entire application
## License
MIT License. See LICENSE file for details.