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

add railtie based RUM Rack instrumentation #26

Merged
merged 12 commits into from
Jul 19, 2022
Merged
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
2 changes: 1 addition & 1 deletion examples/rails-7-barebones/Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ gem "puma"
gem "rails", "~> 7.0.3"

# instrumentation
gem "opentelemetry-instrumentation-rails", "~> 0.21.0"
gem "opentelemetry-instrumentation-rails", "~> 0.22.0"
gem "splunk-otel", path: ENV.fetch("SPLUNK_OTEL_LOCATION", "../../")

# tests
Expand Down
1 change: 1 addition & 0 deletions lib/splunk/otel.rb
Original file line number Diff line number Diff line change
Expand Up @@ -167,3 +167,4 @@ def service_name_warning

require "splunk/otel/logging"
require "splunk/otel/common"
require "splunk/otel/instrumentation/action_pack"
11 changes: 10 additions & 1 deletion lib/splunk/otel/common.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ module Common
CORS_EXPOSE_HEADER = "Access-Control-Expose-Headers"
SERVER_TIMING_HEADER = "Server-Timing"

# rubocop:disable Lint/MissingCopEnableDirective
# rubocop:disable Metrics/PerceivedComplexity
# rubocop:disable Metrics/MethodLength
# rubocop:disable Metrics/AbcSize
def rum_headers(headers)
span = OpenTelemetry::Trace.current_span

Expand All @@ -20,7 +24,12 @@ def rum_headers(headers)
flags = span.context.trace_flags.sampled? ? "01" : "00"

trace_parent = [version, trace_id, span_id, flags]
headers[SERVER_TIMING_HEADER] = "traceparent;desc=\"#{trace_parent.join("-")}\""
headers[SERVER_TIMING_HEADER] = if (headers[SERVER_TIMING_HEADER] || "").empty?
"traceparent;desc=\"#{trace_parent.join("-")}\""
else
# rubocop:disable Layout/LineLength
"#{headers[SERVER_TIMING_HEADER]}, traceparent;desc=\"#{trace_parent.join("-")}\""
end

# TODO: check if this needs to be conditioned on CORS
headers[CORS_EXPOSE_HEADER] = if (headers[CORS_EXPOSE_HEADER] || "").empty?
Expand Down
14 changes: 14 additions & 0 deletions lib/splunk/otel/instrumentation/action_pack.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# frozen_string_literal: true

module Splunk
module Otel
module Instrumentation
# Contains the RUM instrumentation for the ActionPack gem
module ActionPack
jtmal-signalfx marked this conversation as resolved.
Show resolved Hide resolved
end
end
end
end

require_relative "./action_pack/instrumentation"
require_relative "./action_pack/version"
41 changes: 41 additions & 0 deletions lib/splunk/otel/instrumentation/action_pack/instrumentation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# frozen_string_literal: true

require "opentelemetry"
require "opentelemetry-instrumentation-base"

module Splunk
module Otel
module Instrumentation
module ActionPack
# The Instrumentation class contains logic to detect and install the ActionPack instrumentation
class Instrumentation < OpenTelemetry::Instrumentation::Base
jtmal-signalfx marked this conversation as resolved.
Show resolved Hide resolved
MINIMUM_VERSION = Gem::Version.new("5.2.0")

install do |_config|
require_railtie
end

present do
defined?(::ActionController)
end

compatible do
gem_version >= MINIMUM_VERSION
end

option :enable_recognize_route, default: false, validate: :boolean

private

def gem_version
::ActionPack.version
end

def require_railtie
require_relative "railtie"
end
end
end
end
end
end
28 changes: 28 additions & 0 deletions lib/splunk/otel/instrumentation/action_pack/railtie.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# frozen_string_literal: true

require "rack/etag"
require_relative "../rack"

module Splunk
module Otel
module Instrumentation
module ActionPack
# Install the Rack middleware for RUM responses
class Railtie < ::Rails::Railtie
config.before_initialize do |app|
Copy link
Contributor

@jtmal-signalfx jtmal-signalfx Jul 14, 2022

Choose a reason for hiding this comment

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

          config.before_initialize do |app|
            app.middleware.insert_before(
              ActionDispatch::ServerTiming,
              Splunk::Otel::Rack::RumMiddleware
            )
          end

the order is reversed. ActionDispatch::ServerTiming will overwrite server-timing, so we need to append after to their values.

case Rails.version
when /^7\./
# TODO: can be removed once https://github.com/rails/rails/issues/45607 is merged
app.middleware.insert_before(
::Rack::ETag,
Splunk::Otel::Rack::RumMiddleware
)
else
app.middleware.use Splunk::Otel::Rack::RumMiddleware
end
end
end
end
end
end
end
11 changes: 11 additions & 0 deletions lib/splunk/otel/instrumentation/action_pack/version.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# frozen_string_literal: true

module Splunk
module Otel
module Instrumentation
module ActionPack
VERSION = "0.1.0"
end
end
end
end
13 changes: 7 additions & 6 deletions splunk-otel.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,22 @@ Gem::Specification.new do |spec|
spec.add_dependency "opentelemetry-api", "~> 1.0"
spec.add_dependency "opentelemetry-exporter-jaeger", "~> 0.20.1"
spec.add_dependency "opentelemetry-exporter-otlp", "~> 0.21.0"
spec.add_dependency "opentelemetry-instrumentation-base", "~> 0.21.0"
spec.add_dependency "opentelemetry-propagator-b3", "~> 0.19.2"
spec.add_dependency "opentelemetry-sdk", "~> 1.0"

# development dependencies
spec.add_development_dependency "rack-test", "~> 1.1"
spec.add_development_dependency "rails"
spec.add_development_dependency "rake", "~> 13.0"
spec.add_development_dependency "rubocop", "~> 1.25"
spec.add_development_dependency "rubocop-rake", "~> 0.6.0"
spec.add_development_dependency "simplecov", "~> 0.21.2"
spec.add_development_dependency "test-unit", "~> 3.0"
spec.add_development_dependency "test-unit", "~> 3.0"

# for testing the rack middleware
spec.add_development_dependency "opentelemetry-instrumentation-action_pack", "~> 0.1.4"
spec.add_development_dependency "opentelemetry-instrumentation-rack", "~> 0.20"
spec.add_development_dependency "rack", "~> 2.2"
spec.add_development_dependency "opentelemetry-instrumentation-action_pack", "~> 0.2.0"
spec.add_development_dependency "opentelemetry-instrumentation-rack", "~> 0.21"
spec.add_development_dependency "rack", "~> 2.2"
spec.add_development_dependency "rack-test", "~> 1.1"

spec.metadata = {
"rubygems_mfa_required" => "true"
Expand Down
6 changes: 1 addition & 5 deletions test/splunk/instrumentation/rack_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,7 @@ module Splunk
class RumRackTest < Test::Unit::TestCase
include Rack::Test::Methods

EXPORTER = OpenTelemetry::SDK::Trace::Export::InMemorySpanExporter.new

def setup
EXPORTER.reset

with_env("OTEL_SERVICE_NAME" => "test-service") do
span_processor = OpenTelemetry::SDK::Trace::Export::SimpleSpanProcessor.new(EXPORTER)
Splunk::Otel.configure do |c|
Expand All @@ -26,7 +22,7 @@ def setup
end

def teardown
OpenTelemetry.tracer_provider.shutdown
reset_opentelemetry
end

def app
Expand Down
62 changes: 62 additions & 0 deletions test/splunk/instrumentation/rails_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# frozen_string_literal: true

require "rails"

require "test_helper"
require "opentelemetry/sdk"
require "opentelemetry/instrumentation/rack"
require "opentelemetry/instrumentation/action_pack"
require "splunk/otel"
require "splunk/otel/instrumentation/rack"
jtmal-signalfx marked this conversation as resolved.
Show resolved Hide resolved
require "splunk/otel/instrumentation/action_pack"
require "splunk/otel/instrumentation/action_pack/railtie"
require "rack/test"
require "test_helpers/app_config"

module Splunk
class RumRailsTest < Test::Unit::TestCase
include Rack::Test::Methods

def setup
with_env("OTEL_SERVICE_NAME" => "test-service") do
span_processor = OpenTelemetry::SDK::Trace::Export::SimpleSpanProcessor.new(EXPORTER)
Splunk::Otel.configure do |c|
c.add_span_processor span_processor
c.use "OpenTelemetry::Instrumentation::ActionPack"
c.use "Splunk::Otel::Instrumentation::ActionPack"
end
end
end

def teardown
reset_opentelemetry
end

def app
default_rails_app = AppConfig.initialize_app
::Rails.application = default_rails_app

default_rails_app
end

test "RUM response from Rack middleware" do
get "/ok"

assert last_response.ok?
assert_equal "OK", last_response.body

response_headers = last_response.headers
assert_equal "Server-Timing", response_headers["Access-Control-Expose-Headers"]

# the only started span is the one done by the Rack middleware
# so the 1 span in the exporter is the one returned in the response
assert_equal(1, EXPORTER.finished_spans.size)
span = EXPORTER.finished_spans.first
expected_trace_id = span.trace_id.unpack1("H*")
expected_span_id = span.span_id.unpack1("H*")

assert_match("traceparent;desc=\"00-#{expected_trace_id}-#{expected_span_id}-01\"",
response_headers["Server-Timing"])
end
end
end
16 changes: 16 additions & 0 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

require "test-unit"

EXPORTER = OpenTelemetry::SDK::Trace::Export::InMemorySpanExporter.new

def with_env(new_env)
env_to_reset = ENV.select { |k, _| new_env.key?(k) }
keys_to_delete = new_env.keys - ENV.keys
Expand All @@ -17,3 +19,17 @@ def with_env(new_env)
env_to_reset.each_pair { |k, v| ENV[k] = v }
keys_to_delete.each { |k| ENV.delete(k) }
end

def reset_opentelemetry
EXPORTER.reset

OpenTelemetry.instance_variable_set(
:@tracer_provider,
OpenTelemetry::Internal::ProxyTracerProvider.new
)

# OpenTelemetry will load the defaults
# on the next call to any of these methods
OpenTelemetry.error_handler = nil
OpenTelemetry.propagation = nil
end
91 changes: 91 additions & 0 deletions test/test_helpers/app_config.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# frozen_string_literal: true

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

class Application < Rails::Application; end
require "action_controller/railtie"
require_relative "middlewares"
require_relative "controllers"
require_relative "routes"

module AppConfig
extend self

def initialize_app(use_exceptions_app: false, remove_rack_tracer_middleware: false)
new_app = Application.new
new_app.config.secret_key_base = "secret_key_base"

# Ensure we don't see this Rails warning when testing
new_app.config.eager_load = false

# Prevent tests from creating log/*.log
new_app.config.logger = Logger.new(File::NULL)

new_app.config.filter_parameters = [:param_to_be_filtered]

apply_rails_configs(new_app)

remove_rack_middleware(new_app) if remove_rack_tracer_middleware
add_exceptions_app(new_app) if use_exceptions_app
add_middlewares(new_app)

new_app.initialize!

draw_routes(new_app)

new_app
end

private

def remove_rack_middleware(application)
application.middleware.delete(
OpenTelemetry::Instrumentation::Rack::Middlewares::TracerMiddleware
)
end

def add_exceptions_app(application)
application.config.exceptions_app = lambda do |env|
ExceptionsController.action(:show).call(env)
end
end

def add_middlewares(application)
application.middleware.insert_after(
ActionDispatch::DebugExceptions,
ExceptionRaisingMiddleware
)

application.middleware.insert_after(
ActionDispatch::DebugExceptions,
RedirectMiddleware
)
end

def apply_rails_configs(application)
case Rails.version
when /^6\.1/
apply_rails_6_1_configs(application)
when /^7\./
apply_rails_7_configs(application)
end
end

def apply_rails_6_1_configs(application)
# Required in Rails 6
application.config.hosts << "example.org"
end

def apply_rails_7_configs(application)
# Required in Rails 7
application.config.hosts << "example.org"

# Unfreeze values which may have been frozen on previous initializations.
ActiveSupport::Dependencies.autoload_paths =
ActiveSupport::Dependencies.autoload_paths.dup
ActiveSupport::Dependencies.autoload_once_paths =
ActiveSupport::Dependencies.autoload_once_paths.dup
end
end
8 changes: 8 additions & 0 deletions test/test_helpers/controllers.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# frozen_string_literal: true

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

require_relative "controllers/example_controller"
require_relative "controllers/exceptions_controller"
17 changes: 17 additions & 0 deletions test/test_helpers/controllers/example_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true

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

class ExampleController < ActionController::Base
include ::Rails.application.routes.url_helpers

def ok
render plain: "OK"
end

def internal_server_error
raise :internal_server_error
end
end
Loading