diff --git a/instrumentation/base/lib/opentelemetry/instrumentation/base.rb b/instrumentation/base/lib/opentelemetry/instrumentation/base.rb index ddf828955..42c6f0bb3 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,6 +205,8 @@ def initialize(name, version, install_blk, present_blk, @installed = false @options = options @tracer = OpenTelemetry::Trace::Tracer.new + # check to see if the API is defined here because the config isn't available yet + @meter = OpenTelemetry::Metrics::Meter.new if defined?(OpenTelemetry::Metrics) end # rubocop:enable Metrics/ParameterLists @@ -221,9 +223,20 @@ def install(config = {}) instance_exec(@config, &@install_blk) @tracer = OpenTelemetry.tracer_provider.tracer(name, version) + install_meter @installed = true end + def install_meter + @meter = OpenTelemetry.meter_provider.meter(name, version: version) if metrics_enabled? + end + + def metrics_enabled? + return @metrics_enabled if defined?(@metrics_enabled) + + @metrics_enabled ||= defined?(OpenTelemetry::Metrics) && @config[:send_metrics] + end + # Whether or not this instrumentation is installable in the current process. Will # be true when the instrumentation defines an install block, is not disabled # by environment or config, and the target library present and compatible. diff --git a/instrumentation/rack/lib/opentelemetry/instrumentation/rack/instrumentation.rb b/instrumentation/rack/lib/opentelemetry/instrumentation/rack/instrumentation.rb index 3bfb68a3c..91cd44577 100644 --- a/instrumentation/rack/lib/opentelemetry/instrumentation/rack/instrumentation.rb +++ b/instrumentation/rack/lib/opentelemetry/instrumentation/rack/instrumentation.rb @@ -29,7 +29,9 @@ 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 + # TODO: This option currently exclusively uses the event handler, should we support old and new Rack? + option :send_metrics, default: false, validate: :boolean # Temporary Helper for Sinatra and ActionPack middleware to use during installation # 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..e7dd6a351 100644 --- a/instrumentation/rack/lib/opentelemetry/instrumentation/rack/middlewares/event_handler.rb +++ b/instrumentation/rack/lib/opentelemetry/instrumentation/rack/middlewares/event_handler.rb @@ -203,6 +203,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 @@ -262,6 +263,43 @@ def create_span(parent_context, request) span.add_event('http.proxy.request.started', timestamp: request_start_time) unless request_start_time.nil? span end + + # Metrics stuff + HTTP_SERVER_REQUEST_DURATION_ATTRS_FROM_SPAN = %w[http.method http.scheme http.route http.status_code http.host].freeze + + def metrics_enabled? + OpenTelemetry::Instrumentation::Rack::Instrumentation.instance.metrics_enabled? + end + + def meter + return unless metrics_enabled? + + OpenTelemetry::Instrumentation::Rack::Instrumentation.instance.meter + end + + def http_server_request_duration_histogram + return unless metrics_enabled? + + @http_server_request_duration_histogram ||= meter.create_histogram( + 'http.server.request.duration', + unit: 's', + description: 'Duration of HTTP server requests.' + ) + end + + def record_http_server_request_duration_metric(span) + return unless metrics_enabled? + + # find span duration + # end - start / a billion to convert nanoseconds to seconds + duration = (span.end_timestamp - span.start_timestamp) / Float(10**9) + # glean attributes + attrs = span.attributes.select { |k, _v| HTTP_SERVER_REQUEST_DURATION_ATTRS_FROM_SPAN.include?(k) } + # set error + attrs['error.type'] = span.status.description if span.status.code == OpenTelemetry::Trace::Status::ERROR + + http_server_request_duration_histogram.record(duration, attributes: attrs) + end end end end