diff --git a/instrumentation/base/lib/opentelemetry/instrumentation/base.rb b/instrumentation/base/lib/opentelemetry/instrumentation/base.rb index ddf828955..13de6fea4 100644 --- a/instrumentation/base/lib/opentelemetry/instrumentation/base.rb +++ b/instrumentation/base/lib/opentelemetry/instrumentation/base.rb @@ -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 @@ -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. @@ -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 diff --git a/instrumentation/rack/lib/opentelemetry/instrumentation/rack/instrumentation.rb b/instrumentation/rack/lib/opentelemetry/instrumentation/rack/instrumentation.rb index 3bfb68a3c..1bd811082 100644 --- a/instrumentation/rack/lib/opentelemetry/instrumentation/rack/instrumentation.rb +++ b/instrumentation/rack/lib/opentelemetry/instrumentation/rack/instrumentation.rb @@ -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 diff --git a/instrumentation/rack/lib/opentelemetry/instrumentation/rack/middlewares/event_handler.rb b/instrumentation/rack/lib/opentelemetry/instrumentation/rack/middlewares/event_handler.rb index 5475a4fc5..cb64bd6f5 100644 --- a/instrumentation/rack/lib/opentelemetry/instrumentation/rack/middlewares/event_handler.rb +++ b/instrumentation/rack/lib/opentelemetry/instrumentation/rack/middlewares/event_handler.rb @@ -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., @@ -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 @@ -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