Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Metrics instrumentation prototype #9

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
14 changes: 13 additions & 1 deletion instrumentation/base/lib/opentelemetry/instrumentation/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ def infer_version
end
end

attr_reader :name, :version, :config, :installed, :tracer
attr_reader :name, :version, :config, :installed, :tracer, :meter

alias installed? installed

Expand All @@ -205,9 +205,20 @@ def initialize(name, version, install_blk, present_blk,
@installed = false
@options = options
@tracer = OpenTelemetry::Trace::Tracer.new
# Do we want to conditionally create a meter overall?
# @meter = OpenTelemetry::Metrics::Meter.new if metrics_enabled?
end
# rubocop:enable Metrics/ParameterLists

def metrics_enabled?
# We need the API as a dependency to call metrics-y things in instrumentation
# However, the user needs to install it separately from base, because we
# do not want base to rely on experimental code
return @metrics_enabled if defined?(@metrics_enabled)

@metrics_enabled ||= defined?(OpenTelemetry::Metrics) && @config[:send_metrics]
end

# Install instrumentation with the given config. The present? and compatible?
# will be run first, and install will return false if either fail. Will
# return true if install was completed successfully.
Expand All @@ -221,6 +232,7 @@ def install(config = {})

instance_exec(@config, &@install_blk)
@tracer = OpenTelemetry.tracer_provider.tracer(name, version)
@meter = OpenTelemetry.meter_provider.meter(name, version: version) if metrics_enabled?
@installed = true
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ class Instrumentation < OpenTelemetry::Instrumentation::Base
option :untraced_requests, default: nil, validate: :callable
option :response_propagators, default: [], validate: :array
# This option is only valid for applications using Rack 2.0 or greater
option :use_rack_events, default: true, validate: :boolean

option :use_rack_events, default: true, validate: :boolean
option :send_metrics, default: false, validate: :boolean
# Temporary Helper for Sinatra and ActionPack middleware to use during installation
#
# @example Default usage
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,46 @@ def untraced_request?(env)
false
end

# TODO: This one is long because I wanted to keep the stable semantic
# conventions, and (for now) emit attributes that matched the span
def record_http_server_request_duration_metric(span)
return unless metrics_enabled? && http_server_duration_histogram

# find span duration
# end - start / a billion to convert nanoseconds to seconds
duration = (span.end_timestamp - span.start_timestamp) / Float(10**9)
# Create attributes
#
attrs = {}
# pattern below goes
# stable convention
# attribute that matches rack spans (old convention)

# attrs['http.request.method']
attrs['http.method'] = span.attributes['http.method']

# attrs['url.scheme']
attrs['http.scheme'] = span.attributes['http.scheme']

# same in stable semconv
attrs['http.route'] = span.attributes['http.route']

# attrs['http.response.status.code']
attrs['http.status_code'] = span.attributes['http.status_code']

# attrs['server.address'] ???
# attrs['server.port'] ???
# span includes host and port
attrs['http.host'] = span.attributes['http.host']

# attrs not currently in span payload
# attrs['network.protocol.version']
# attrs['network.protocol.name']
attrs['error.type'] = span.status.description if span.status.code == OpenTelemetry::Trace::Status::ERROR

http_server_duration_histogram.record(duration, attributes: attrs)
end

# https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/data-http.md#name
#
# recommendation: span.name(s) should be low-cardinality (e.g.,
Expand Down Expand Up @@ -203,6 +243,7 @@ def detach_context(request)
token, span = request.env[OTEL_TOKEN_AND_SPAN]
span.finish
OpenTelemetry::Context.detach(token)
record_http_server_request_duration_metric(span)
rescue StandardError => e
OpenTelemetry.handle_error(exception: e)
end
Expand Down Expand Up @@ -247,6 +288,26 @@ def tracer
OpenTelemetry::Instrumentation::Rack::Instrumentation.instance.tracer
end

def metrics_enabled?
OpenTelemetry::Instrumentation::Rack::Instrumentation.instance.metrics_enabled?
end

def meter
# warn if no meter?
return @meter if defined?(@meter)

@meter = metrics_enabled? ? OpenTelemetry::Instrumentation::Rack::Instrumentation.instance.meter : nil
end

def http_server_duration_histogram
# Only want to make the histogram once
# Need to implement advice so we can update the buckets to match seconds instead of ms
return @http_server_duration_histogram if defined?(@http_server_duration_histogram)

@http_server_duration_histogram = nil unless meter
@http_server_duration_histogram = meter.create_histogram('http.server.request.duration', unit: 's', description: 'Duration of HTTP server requests.')
end

def config
OpenTelemetry::Instrumentation::Rack::Instrumentation.instance.config
end
Expand Down
Loading