Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions instrumentation/active_record/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,42 @@ OpenTelemetry::SDK.configure do |c|
end
```

## Configuration Options

The instrumentation supports the following configuration options:

- **enable_notifications_instrumentation:** Enables instrumentation of SQL queries using ActiveSupport notifications. When enabled, generates spans for each SQL query with additional metadata including operation names, async status, and caching information.
- Default: `false`

## Active Support Instrumentation

This instrumentation can optionally leverage `ActiveSupport::Notifications` to provide detailed SQL query instrumentation. When enabled via the `enable_notifications_instrumentation` configuration option, it subscribes to `sql.active_record` events to create spans for individual SQL queries.

### Enabling SQL Notifications

```ruby
OpenTelemetry::SDK.configure do |c|
c.use 'OpenTelemetry::Instrumentation::ActiveRecord',
enable_notifications_instrumentation: true
end
```

See the table below for details of what [Rails ActiveRecord Events](https://guides.rubyonrails.org/active_support_instrumentation.html#active-record) are recorded by this instrumentation:

| Event Name | Creates Span? | Notes |
| - | - | - |
| `sql.active_record` | :white_check_mark: | Creates an `internal` span for each SQL query with operation name, async status, and caching information |

### SQL Query Spans

When notifications instrumentation is enabled, each SQL query executed through ActiveRecord generates a span with:

- **Span name**: Derived from the query operation (e.g., `"User Create"`, `"Account Load"`, `"Post Update"`)
- **Span kind**: `internal`
- **Attributes**:
- `db.active_record.async` (boolean): Present and set to `true` for asynchronous queries
- `db.active_record.cached` (boolean): Present and set to `true` for cached query results

## Examples

Example usage can be seen in the [`./example/trace_demonstration.rb` file](https://github.com/open-telemetry/opentelemetry-ruby-contrib/blob/main/instrumentation/active_record/example/trace_demonstration.rb)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# frozen_string_literal: true

# Copyright The OpenTelemetry Authors
#
# SPDX-License-Identifier: Apache-2.0

require_relative 'handlers/sql_handler'

module OpenTelemetry
module Instrumentation
module ActiveRecord
# Module that contains custom event handlers for ActiveRecord notifications
module Handlers
module_function

# Subscribes Event Handlers to relevant ActiveRecord notifications
#
# The following events are recorded as spans:
# - sql.active_record
#
# @note this method is not thread safe and should not be used in a multi-threaded context
def subscribe
return unless Array(@subscriptions).empty?

config = ActiveRecord::Instrumentation.instance.config
return unless config[:enable_notifications_instrumentation]

sql_handler = Handlers::SqlHandler.new

@subscriptions = [
::ActiveSupport::Notifications.subscribe('sql.active_record', sql_handler)
]
end

# Removes Event Handler Subscriptions for ActiveRecord notifications
# @note this method is not thread-safe and should not be used in a multi-threaded context
def unsubscribe
@subscriptions&.each { |subscriber| ::ActiveSupport::Notifications.unsubscribe(subscriber) }
@subscriptions = nil
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# frozen_string_literal: true

# Copyright The OpenTelemetry Authors
#
# SPDX-License-Identifier: Apache-2.0

module OpenTelemetry
module Instrumentation
module ActiveRecord
module Handlers
# SqlHandler handles sql.active_record ActiveSupport notifications
class SqlHandler
# Invoked by ActiveSupport::Notifications at the start of the instrumentation block
#
# @param name [String] of the Event
# @param id [String] of the event
# @param payload [Hash] containing SQL execution information
# @return [Hash] the payload passed as a method argument
def start(name, id, payload)
span = tracer.start_span(
payload[:name] || 'SQL',
kind: :internal
)
token = OpenTelemetry::Context.attach(
OpenTelemetry::Trace.context_with_span(span)
)
payload[:__opentelemetry_span] = span
payload[:__opentelemetry_ctx_token] = token
rescue StandardError => e
OpenTelemetry.handle_error(exception: e)
end

# Invoked by ActiveSupport::Notifications at the end of the instrumentation block
#
# @param name [String] of the Event
# @param id [String] of the event
# @param payload [Hash] containing SQL execution information
def finish(name, id, payload)
attributes = {
'db.active_record.async' => payload[:async] == true,
'db.active_record.cached' => payload[:cached] == true
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thoughts about making this rails.active_record.* for consistency with the semconv pattern of not putting product specific attributes in the domain ie db namespace.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the feedback.

I wasn't sure what direction to head since there isn't any semconv guidance for ORM of database abstraction libraries.

Namespacing under rails works for me

}
span = payload.delete(:__opentelemetry_span)
span&.add_attributes(attributes)

token = payload.delete(:__opentelemetry_ctx_token)
return unless span && token

if (e = payload[:exception_object])
span.record_exception(e)
span.status = OpenTelemetry::Trace::Status.error('Unhandled exception')
end

span.finish
OpenTelemetry::Context.detach(token)
rescue StandardError => e
OpenTelemetry.handle_error(exception: e)
end

private

def tracer
OpenTelemetry::Instrumentation::ActiveRecord::Instrumentation.instance.tracer
end
end
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ module ActiveRecord
class Instrumentation < OpenTelemetry::Instrumentation::Base
MINIMUM_VERSION = Gem::Version.new('7')

install do |_config|
install do |config|
require_dependencies
patch_activerecord
subscribe_to_notifications if config[:enable_notifications_instrumentation]
end

present do
Expand All @@ -24,6 +25,8 @@ class Instrumentation < OpenTelemetry::Instrumentation::Base
gem_version >= MINIMUM_VERSION
end

option :enable_notifications_instrumentation, default: false, validate: :boolean

private

def gem_version
Expand All @@ -39,6 +42,7 @@ def require_dependencies
require_relative 'patches/transactions_class_methods'
require_relative 'patches/validations'
require_relative 'patches/relation_persistence'
require_relative 'handlers'
end

def patch_activerecord
Expand All @@ -57,6 +61,10 @@ def patch_activerecord
::ActiveRecord::Relation.prepend(Patches::RelationPersistence)
end
end

def subscribe_to_notifications
Handlers.subscribe
end
end
end
end
Expand Down
Loading
Loading