From 45748b4436ce55d9b2ff45c1d85367d61059819c Mon Sep 17 00:00:00 2001 From: Abdelkader Boudih Date: Mon, 8 Dec 2025 02:25:25 +0100 Subject: [PATCH] feat: add OTLP adapter for OpenTelemetry Protocol support Add generic OTLP adapter that works with any OTLP-compatible backend: - SigNoz, Jaeger, Grafana Tempo, Honeycomb, etc. - Uses HTTP/JSON transport on port 4318 (/v1/traces) - Supports exception capture with full OTLP structure - Includes trace ID and span ID generation DRY improvements: - Add StacktraceBuilder concern for common frame building - Add TraceContext concern for trace/span ID generation Configuration helpers: - use_otlp() - generic OTLP endpoint - use_signoz() - SigNoz convenience helper - use_jaeger() - Jaeger convenience helper - use_tempo() - Grafana Tempo convenience helper --- README.md | 19 ++ .../adapters/concerns/stacktrace_builder.rb | 70 ++++++ .../adapters/concerns/trace_context.rb | 52 ++++ lib/lapsoss/adapters/otlp_adapter.rb | 231 ++++++++++++++++++ lib/lapsoss/configuration.rb | 24 ++ lib/lapsoss/scrubber.rb | 2 +- .../cassettes/appsignal_capture_exception.yml | 66 +++++ test/cassettes/appsignal_capture_message.yml | 41 ++++ test/cassettes/appsignal_with_breadcrumbs.yml | 44 ++++ test/cassettes/otlp_capture_exception.yml | 47 ++++ test/cassettes/otlp_capture_message.yml | 36 +++ test/cassettes/otlp_with_api_key.yml | 49 ++++ test/cassettes/otlp_with_code_context.yml | 47 ++++ test/cassettes/otlp_with_headers.yml | 49 ++++ test/cassettes/otlp_with_tags.yml | 47 ++++ test/cassettes/otlp_with_user.yml | 47 ++++ test/event_handling_test.rb | 4 +- test/otlp_adapter_test.rb | 202 +++++++++++++++ test/test_helper.rb | 3 + 19 files changed, 1077 insertions(+), 3 deletions(-) create mode 100644 lib/lapsoss/adapters/concerns/stacktrace_builder.rb create mode 100644 lib/lapsoss/adapters/concerns/trace_context.rb create mode 100644 lib/lapsoss/adapters/otlp_adapter.rb create mode 100644 test/cassettes/otlp_capture_exception.yml create mode 100644 test/cassettes/otlp_capture_message.yml create mode 100644 test/cassettes/otlp_with_api_key.yml create mode 100644 test/cassettes/otlp_with_code_context.yml create mode 100644 test/cassettes/otlp_with_headers.yml create mode 100644 test/cassettes/otlp_with_tags.yml create mode 100644 test/cassettes/otlp_with_user.yml create mode 100644 test/otlp_adapter_test.rb diff --git a/README.md b/README.md index f91631b..1692884 100644 --- a/README.md +++ b/README.md @@ -221,6 +221,7 @@ All adapters are pure Ruby implementations with no external SDK dependencies: - **Insight Hub** (formerly Bugsnag) - Error tracking with breadcrumbs - **Telebugs** - Sentry-compatible protocol (perfect for self-hosted alternatives) - **OpenObserve** - Open-source observability platform (logs, metrics, traces) +- **OTLP** - OpenTelemetry Protocol (works with SigNoz, Jaeger, Tempo, Honeycomb, etc.) ## Configuration @@ -270,6 +271,24 @@ Lapsoss.configure do |config| end ``` +### Using OTLP (OpenTelemetry Protocol) + +```ruby +# Works with SigNoz, Jaeger, Tempo, Honeycomb, Datadog, etc. +Lapsoss.configure do |config| + config.use_otlp( + endpoint: ENV['OTLP_ENDPOINT'], # e.g., "http://localhost:4318" + service_name: "my-rails-app", + headers: { "X-Custom-Header" => "value" } # optional custom headers + ) + + # Or use convenience helpers for specific services + config.use_signoz(signoz_api_key: ENV['SIGNOZ_API_KEY']) + config.use_jaeger(endpoint: "http://jaeger:4318") + config.use_tempo(endpoint: "http://tempo:4318") +end +``` + ### Advanced Configuration ```ruby diff --git a/lib/lapsoss/adapters/concerns/stacktrace_builder.rb b/lib/lapsoss/adapters/concerns/stacktrace_builder.rb new file mode 100644 index 0000000..5f73b0a --- /dev/null +++ b/lib/lapsoss/adapters/concerns/stacktrace_builder.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require "active_support/concern" +require "active_support/core_ext/object/blank" + +module Lapsoss + module Adapters + module Concerns + # Shared stacktrace building logic for adapters + # Provides consistent frame formatting across Sentry, OpenObserve, OTLP, etc. + module StacktraceBuilder + extend ActiveSupport::Concern + + # Build frames from event backtrace + # @param event [Lapsoss::Event] The event with backtrace_frames + # @param reverse [Boolean] Reverse frame order (Sentry expects oldest-to-newest) + # @return [Array] Array of formatted frame hashes + def build_frames(event, reverse: false) + return [] unless event.has_backtrace? + + frames = event.backtrace_frames.map { |frame| build_frame(frame) } + reverse ? frames.reverse : frames + end + + # Build a single frame hash from a BacktraceFrame + # @param frame [Lapsoss::BacktraceFrame] The frame to format + # @return [Hash] Formatted frame hash + def build_frame(frame) + frame_hash = { + filename: frame.filename, + abs_path: frame.absolute_path || frame.filename, + function: frame.method_name || frame.function, + lineno: frame.line_number, + in_app: frame.in_app + } + + add_code_context(frame_hash, frame) if frame.code_context.present? + + frame_hash.compact + end + + # Build raw stacktrace string for logging/simple formats + # @param event [Lapsoss::Event] The event with backtrace_frames + # @return [Array] Array of formatted frame strings + def build_raw_stacktrace(event) + return [] unless event.has_backtrace? + + event.backtrace_frames.map do |frame| + "#{frame.absolute_path || frame.filename}:#{frame.line_number} in `#{frame.method_name}`" + end + end + + # Build stacktrace as single string (for OTLP exception.stacktrace) + # @param event [Lapsoss::Event] The event with backtrace_frames + # @return [String] Newline-separated stacktrace + def build_stacktrace_string(event) + build_raw_stacktrace(event).join("\n") + end + + private + + def add_code_context(frame_hash, frame) + frame_hash[:pre_context] = frame.code_context[:pre_context] + frame_hash[:context_line] = frame.code_context[:context_line] + frame_hash[:post_context] = frame.code_context[:post_context] + end + end + end + end +end diff --git a/lib/lapsoss/adapters/concerns/trace_context.rb b/lib/lapsoss/adapters/concerns/trace_context.rb new file mode 100644 index 0000000..f08a8be --- /dev/null +++ b/lib/lapsoss/adapters/concerns/trace_context.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require "active_support/concern" +require "securerandom" + +module Lapsoss + module Adapters + module Concerns + # Trace context utilities for OTLP and distributed tracing + # Provides trace/span ID generation and timestamp formatting + module TraceContext + extend ActiveSupport::Concern + + # Generate a W3C Trace Context compliant trace ID (32 hex chars = 128 bits) + # @return [String] 32 character hex string + def generate_trace_id + SecureRandom.hex(16) + end + + # Generate a W3C Trace Context compliant span ID (16 hex chars = 64 bits) + # @return [String] 16 character hex string + def generate_span_id + SecureRandom.hex(8) + end + + # Convert time to nanoseconds since Unix epoch (OTLP format) + # @param time [Time] The time to convert + # @return [Integer] Nanoseconds since Unix epoch + def timestamp_nanos(time) + time ||= Time.current + (time.to_f * 1_000_000_000).to_i + end + + # Convert time to microseconds since Unix epoch (OpenObserve format) + # @param time [Time] The time to convert + # @return [Integer] Microseconds since Unix epoch + def timestamp_micros(time) + time ||= Time.current + (time.to_f * 1_000_000).to_i + end + + # Convert time to milliseconds since Unix epoch + # @param time [Time] The time to convert + # @return [Integer] Milliseconds since Unix epoch + def timestamp_millis(time) + time ||= Time.current + (time.to_f * 1_000).to_i + end + end + end + end +end diff --git a/lib/lapsoss/adapters/otlp_adapter.rb b/lib/lapsoss/adapters/otlp_adapter.rb new file mode 100644 index 0000000..e3fc63e --- /dev/null +++ b/lib/lapsoss/adapters/otlp_adapter.rb @@ -0,0 +1,231 @@ +# frozen_string_literal: true + +require "active_support/core_ext/object/blank" + +module Lapsoss + module Adapters + # OTLP adapter - sends errors via OpenTelemetry Protocol + # Works with any OTLP-compatible backend: SigNoz, Jaeger, Tempo, Honeycomb, etc. + # Docs: https://opentelemetry.io/docs/specs/otlp/ + class OtlpAdapter < Base + include Concerns::HttpDelivery + include Concerns::StacktraceBuilder + include Concerns::TraceContext + include Concerns::EnvelopeBuilder + + # OTLP status codes + STATUS_CODE_UNSET = 0 + STATUS_CODE_OK = 1 + STATUS_CODE_ERROR = 2 + + # OTLP span kinds + SPAN_KIND_INTERNAL = 1 + + DEFAULT_ENDPOINT = "http://localhost:4318" + + def initialize(name, settings = {}) + super + + @endpoint = settings[:endpoint].presence || ENV["OTLP_ENDPOINT"] || DEFAULT_ENDPOINT + @headers = settings[:headers] || {} + @service_name = settings[:service_name].presence || ENV["OTEL_SERVICE_NAME"] || "rails" + @environment = settings[:environment].presence || ENV["OTEL_ENVIRONMENT"] || "production" + + # Support common auth patterns + if (api_key = settings[:api_key].presence || ENV["OTLP_API_KEY"]) + @headers["Authorization"] = "Bearer #{api_key}" + end + + if (signoz_key = settings[:signoz_api_key].presence || ENV["SIGNOZ_API_KEY"]) + @headers["signoz-access-token"] = signoz_key + end + + setup_endpoint + end + + def capture(event) + deliver(event.scrubbed) + end + + def capabilities + super.merge( + breadcrumbs: false, + code_context: true, + data_scrubbing: true + ) + end + + private + + def setup_endpoint + uri = URI.parse(@endpoint) + @api_endpoint = "#{uri.scheme}://#{uri.host}:#{uri.port}" + @api_path = "/v1/traces" + end + + def build_payload(event) + { + resourceSpans: [ build_resource_spans(event) ] + } + end + + def build_resource_spans(event) + { + resource: build_resource(event), + scopeSpans: [ build_scope_spans(event) ] + } + end + + def build_resource(event) + attributes = [ + { key: "service.name", value: { stringValue: @service_name } }, + { key: "deployment.environment", value: { stringValue: event.environment.presence || @environment } }, + { key: "telemetry.sdk.name", value: { stringValue: "lapsoss" } }, + { key: "telemetry.sdk.version", value: { stringValue: Lapsoss::VERSION } }, + { key: "telemetry.sdk.language", value: { stringValue: "ruby" } } + ] + + # Add user context as resource attributes if available + if event.user_context.present? + event.user_context.each do |key, value| + attributes << { key: "user.#{key}", value: attribute_value(value) } + end + end + + { attributes: attributes } + end + + def build_scope_spans(event) + { + scope: { + name: "lapsoss", + version: Lapsoss::VERSION + }, + spans: [ build_span(event) ] + } + end + + def build_span(event) + now = timestamp_nanos(event.timestamp) + span_name = event.type == :exception ? event.exception_type : "message" + + span = { + traceId: generate_trace_id, + spanId: generate_span_id, + name: span_name, + kind: SPAN_KIND_INTERNAL, + startTimeUnixNano: now.to_s, + endTimeUnixNano: now.to_s, + status: build_status(event), + attributes: build_span_attributes(event) + } + + # Add exception event for exception types + if event.type == :exception + span[:events] = [ build_exception_event(event) ] + end + + span + end + + def build_status(event) + if event.type == :exception || event.level == :error || event.level == :fatal + { code: STATUS_CODE_ERROR, message: event.exception_message || event.message || "Error" } + else + { code: STATUS_CODE_OK } + end + end + + def build_span_attributes(event) + attributes = [] + + # Add tags + event.tags&.each do |key, value| + attributes << { key: key.to_s, value: attribute_value(value) } + end + + # Add extra data + event.extra&.each do |key, value| + attributes << { key: "extra.#{key}", value: attribute_value(value) } + end + + # Add request context + if event.request_context.present? + event.request_context.each do |key, value| + attributes << { key: "http.#{key}", value: attribute_value(value) } + end + end + + # Add transaction name + if event.transaction.present? + attributes << { key: "transaction.name", value: { stringValue: event.transaction } } + end + + # Add fingerprint + if event.fingerprint.present? + attributes << { key: "error.fingerprint", value: { stringValue: event.fingerprint } } + end + + # Add message for message events + if event.type == :message && event.message.present? + attributes << { key: "message", value: { stringValue: event.message } } + end + + attributes + end + + def build_exception_event(event) + attributes = [ + { key: "exception.type", value: { stringValue: event.exception_type } }, + { key: "exception.message", value: { stringValue: event.exception_message } } + ] + + # Add stacktrace + if event.has_backtrace? + attributes << { + key: "exception.stacktrace", + value: { stringValue: build_stacktrace_string(event) } + } + end + + { + name: "exception", + timeUnixNano: timestamp_nanos(event.timestamp).to_s, + attributes: attributes + } + end + + # Convert Ruby value to OTLP attribute value + def attribute_value(value) + case value + when String + { stringValue: value } + when Integer + { intValue: value.to_s } + when Float + { doubleValue: value } + when TrueClass, FalseClass + { boolValue: value } + when Array + { arrayValue: { values: value.map { |v| attribute_value(v) } } } + else + { stringValue: value.to_s } + end + end + + def serialize_payload(payload) + json = ActiveSupport::JSON.encode(payload) + + if json.bytesize >= compress_threshold + [ ActiveSupport::Gzip.compress(json), true ] + else + [ json, false ] + end + end + + def adapter_specific_headers + @headers.dup + end + end + end +end diff --git a/lib/lapsoss/configuration.rb b/lib/lapsoss/configuration.rb index 89653cd..514864f 100644 --- a/lib/lapsoss/configuration.rb +++ b/lib/lapsoss/configuration.rb @@ -122,6 +122,30 @@ def use_openobserve(name: :openobserve, **settings) register_adapter(name, :openobserve, **settings) end + # Convenience method for OTLP (OpenTelemetry Protocol) + # Works with SigNoz, Jaeger, Tempo, Honeycomb, etc. + def use_otlp(name: :otlp, **settings) + register_adapter(name, :otlp, **settings) + end + + # Convenience method for SigNoz (OTLP-compatible) + def use_signoz(name: :signoz, **settings) + settings[:endpoint] ||= "http://localhost:4318" + register_adapter(name, :otlp, **settings) + end + + # Convenience method for Jaeger (OTLP-compatible) + def use_jaeger(name: :jaeger, **settings) + settings[:endpoint] ||= "http://localhost:4318" + register_adapter(name, :otlp, **settings) + end + + # Convenience method for Grafana Tempo (OTLP-compatible) + def use_tempo(name: :tempo, **settings) + settings[:endpoint] ||= "http://localhost:4318" + register_adapter(name, :otlp, **settings) + end + # Apply configuration by registering all adapters def apply! Registry.instance.clear! diff --git a/lib/lapsoss/scrubber.rb b/lib/lapsoss/scrubber.rb index 091ce9e..473d1a2 100644 --- a/lib/lapsoss/scrubber.rb +++ b/lib/lapsoss/scrubber.rb @@ -78,7 +78,7 @@ def mask_value(value) end def random_mask(value) - length = [[value.to_s.length, 3].max, 32].min + length = [ [ value.to_s.length, 3 ].max, 32 ].min "*" * length end diff --git a/test/cassettes/appsignal_capture_exception.yml b/test/cassettes/appsignal_capture_exception.yml index 1bbae93..b3110ad 100644 --- a/test/cassettes/appsignal_capture_exception.yml +++ b/test/cassettes/appsignal_capture_exception.yml @@ -259,4 +259,70 @@ http_interactions: encoding: UTF-8 string: '' recorded_at: Fri, 12 Sep 2025 14:13:31 GMT +- request: + method: post + uri: https://appsignal-endpoint.net/errors?api_key= + body: + encoding: UTF-8 + string: '{"timestamp":1765157073,"namespace":"backend","error":{"name":"StandardError","message":"Test + error from Lapsoss","backtrace":["/path/to/ruby/lib/ruby/gems/3.4.0/gems/vcr-6.3.1/lib/vcr/util/variable_args_block_caller.rb:9:in + ''VCR::VariableArgsBlockCaller#call_block''","/path/to/ruby/lib/ruby/gems/3.4.0/gems/vcr-6.3.1/lib/vcr.rb:194:in + ''VCR#use_cassette''","/path/to/project/lapsoss/test/appsignal_adapter_test.rb:12:in + ''block in \u003cclass:AppsignalAdapterTest\u003e''","/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest/test.rb:94:in + ''block (2 levels) in Minitest::Test#run''","/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest/test.rb:190:in + ''Minitest::Test#capture_exceptions''","/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest/test.rb:89:in + ''block in Minitest::Test#run''","/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest.rb:368:in + ''Minitest::Runnable#time_it''","/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest/test.rb:88:in + ''Minitest::Test#run''","/path/to/ruby/lib/ruby/gems/3.4.0/gems/activesupport-8.0.2.1/lib/active_support/executor/test_helper.rb:5:in + ''block in ActiveSupport::Executor::TestHelper#run''","/path/to/ruby/lib/ruby/gems/3.4.0/gems/activesupport-8.0.2.1/lib/active_support/execution_wrapper.rb:104:in + ''ActiveSupport::ExecutionWrapper.perform''","/path/to/ruby/lib/ruby/gems/3.4.0/gems/activesupport-8.0.2.1/lib/active_support/executor/test_helper.rb:5:in + ''ActiveSupport::Executor::TestHelper#run''","/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest.rb:1208:in + ''Minitest.run_one_method''","/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest.rb:447:in + ''Minitest::Runnable.run_one_method''","/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest.rb:434:in + ''block (2 levels) in Minitest::Runnable.run''","/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest.rb:430:in + ''Array#each''","/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest.rb:430:in + ''block in Minitest::Runnable.run''","/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest.rb:472:in + ''Minitest::Runnable.on_signal''","/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest.rb:459:in + ''Minitest::Runnable.with_info_handler''","/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest.rb:429:in + ''Minitest::Runnable.run''","/path/to/ruby/lib/ruby/gems/3.4.0/gems/railties-8.0.2.1/lib/rails/test_unit/line_filtering.rb:10:in + ''Rails::LineFiltering#run''","/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest.rb:332:in + ''block in Minitest.__run''","/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest.rb:332:in + ''Array#map''","/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest.rb:332:in + ''Minitest.__run''","/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest.rb:288:in + ''Minitest.run''","/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest.rb:86:in + ''block in Minitest.autorun''"]},"tags":{"test":"true","environment":"test"},"params":{},"environment":{"hostname":"","app_name":"lapsoss-test","environment":null},"breadcrumbs":[]}' + headers: + User-Agent: + - lapsoss/ + Content-Type: + - application/json; charset=UTF-8 + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Mon, 08 Dec 2025 01:24:34 GMT + Content-Length: + - '0' + Connection: + - close + Vary: + - origin, access-control-request-method, access-control-request-headers + Access-Control-Allow-Origin: + - "*" + Access-Control-Expose-Headers: + - "*" + X-Appsignal-Max-Body: + - 500k + body: + encoding: UTF-8 + string: '' + recorded_at: Mon, 08 Dec 2025 01:24:34 GMT recorded_with: VCR 6.3.1 diff --git a/test/cassettes/appsignal_capture_message.yml b/test/cassettes/appsignal_capture_message.yml index 40c7b31..a0c0047 100644 --- a/test/cassettes/appsignal_capture_message.yml +++ b/test/cassettes/appsignal_capture_message.yml @@ -164,4 +164,45 @@ http_interactions: encoding: UTF-8 string: '' recorded_at: Fri, 12 Sep 2025 14:13:31 GMT +- request: + method: post + uri: https://appsignal-endpoint.net/errors?api_key= + body: + encoding: UTF-8 + string: '{"action":"log_message","path":"/","exception":{"name":"LogMessage","message":"Critical + error from Lapsoss","backtrace":[]},"tags":{"source":"test"},"params":{},"environment":{"hostname":"","app_name":"lapsoss-test","environment":null},"breadcrumbs":[]}' + headers: + User-Agent: + - lapsoss/ + Content-Type: + - application/json; charset=UTF-8 + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Mon, 08 Dec 2025 01:24:34 GMT + Content-Length: + - '0' + Connection: + - close + Vary: + - origin, access-control-request-method, access-control-request-headers + Access-Control-Allow-Origin: + - "*" + Access-Control-Expose-Headers: + - "*" + X-Appsignal-Max-Body: + - 500k + body: + encoding: UTF-8 + string: '' + recorded_at: Mon, 08 Dec 2025 01:24:34 GMT recorded_with: VCR 6.3.1 diff --git a/test/cassettes/appsignal_with_breadcrumbs.yml b/test/cassettes/appsignal_with_breadcrumbs.yml index 92dcf3d..2c54704 100644 --- a/test/cassettes/appsignal_with_breadcrumbs.yml +++ b/test/cassettes/appsignal_with_breadcrumbs.yml @@ -176,4 +176,48 @@ http_interactions: encoding: UTF-8 string: '' recorded_at: Fri, 12 Sep 2025 14:13:30 GMT +- request: + method: post + uri: https://appsignal-endpoint.net/errors?api_key= + body: + encoding: UTF-8 + string: '{"timestamp":1765157074,"namespace":"backend","error":{"name":"RuntimeError","message":"Error + after breadcrumbs","backtrace":[]},"tags":{},"params":{},"environment":{"hostname":"","app_name":"lapsoss-test","environment":null},"breadcrumbs":[{"message":"User + clicked button","type":"navigation","metadata":{"button":"submit"},"timestamp":"2025-12-08T01:24:34.976Z"},{"message":"API + request started","type":"http","metadata":{"url":"/api/test"},"timestamp":"2025-12-08T01:24:34.976Z"},{"message":"API + request failed","type":"error","metadata":{"status":500},"timestamp":"2025-12-08T01:24:34.976Z"}]}' + headers: + User-Agent: + - lapsoss/ + Content-Type: + - application/json; charset=UTF-8 + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Mon, 08 Dec 2025 01:24:35 GMT + Content-Length: + - '0' + Connection: + - close + Vary: + - origin, access-control-request-method, access-control-request-headers + Access-Control-Allow-Origin: + - "*" + Access-Control-Expose-Headers: + - "*" + X-Appsignal-Max-Body: + - 500k + body: + encoding: UTF-8 + string: '' + recorded_at: Mon, 08 Dec 2025 01:24:35 GMT recorded_with: VCR 6.3.1 diff --git a/test/cassettes/otlp_capture_exception.yml b/test/cassettes/otlp_capture_exception.yml new file mode 100644 index 0000000..31e1192 --- /dev/null +++ b/test/cassettes/otlp_capture_exception.yml @@ -0,0 +1,47 @@ +--- +http_interactions: +- request: + method: post + uri: "/v1/traces" + body: + encoding: UTF-8 + string: '{"resourceSpans":[{"resource":{"attributes":[{"key":"service.name","value":{"stringValue":"lapsoss-test"}},{"key":"deployment.environment","value":{"stringValue":"production"}},{"key":"telemetry.sdk.name","value":{"stringValue":"lapsoss"}},{"key":"telemetry.sdk.version","value":{"stringValue":"0.4.11"}},{"key":"telemetry.sdk.language","value":{"stringValue":"ruby"}}]},"scopeSpans":[{"scope":{"name":"lapsoss","version":"0.4.11"},"spans":[{"traceId":"e9e7683eb300dd2c07e984b3c55ccf8c","spanId":"9006b60c0e044016","name":"StandardError","kind":1,"startTimeUnixNano":"1765156858238023936","endTimeUnixNano":"1765156858238023936","status":{"code":2,"message":"Test + error from Lapsoss"},"attributes":[],"events":[{"name":"exception","timeUnixNano":"1765156858238023936","attributes":[{"key":"exception.type","value":{"stringValue":"StandardError"}},{"key":"exception.message","value":{"stringValue":"Test + error from Lapsoss"}},{"key":"exception.stacktrace","value":{"stringValue":"/path/to/ruby/lib/ruby/gems/3.4.0/gems/vcr-6.3.1/lib/vcr/util/variable_args_block_caller.rb:9 + in `VCR::VariableArgsBlockCaller#call_block`\n/path/to/ruby/lib/ruby/gems/3.4.0/gems/vcr-6.3.1/lib/vcr.rb:194 + in `VCR#use_cassette`\ntest/otlp_adapter_test.rb:14 in `block in \u003cclass:OtlpAdapterTest\u003e`\n/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest/test.rb:94 + in `block (2 levels) in Minitest::Test#run`\n/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest/test.rb:190 + in `Minitest::Test#capture_exceptions`\n/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest/test.rb:89 + in `block in Minitest::Test#run`\n/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest.rb:368 + in `Minitest::Runnable#time_it`\n/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest/test.rb:88 + in `Minitest::Test#run`\n/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest.rb:1208 + in `Minitest.run_one_method`\n/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest.rb:447 + in `Minitest::Runnable.run_one_method`\n/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest.rb:434 + in `block (2 levels) in Minitest::Runnable.run`"}}]}]}]}]}]}' + headers: + User-Agent: + - lapsoss/ + Content-Type: + - application/json + X-Lapsoss-Version: + - "" + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Content-Type: + - application/json + Date: + - Mon, 08 Dec 2025 01:20:58 GMT + Content-Length: + - '21' + body: + encoding: UTF-8 + string: '{"partialSuccess":{}}' + recorded_at: Mon, 08 Dec 2025 01:20:58 GMT +recorded_with: VCR 6.3.1 diff --git a/test/cassettes/otlp_capture_message.yml b/test/cassettes/otlp_capture_message.yml new file mode 100644 index 0000000..48d6120 --- /dev/null +++ b/test/cassettes/otlp_capture_message.yml @@ -0,0 +1,36 @@ +--- +http_interactions: +- request: + method: post + uri: "/v1/traces" + body: + encoding: UTF-8 + string: '{"resourceSpans":[{"resource":{"attributes":[{"key":"service.name","value":{"stringValue":"lapsoss-test"}},{"key":"deployment.environment","value":{"stringValue":"production"}},{"key":"telemetry.sdk.name","value":{"stringValue":"lapsoss"}},{"key":"telemetry.sdk.version","value":{"stringValue":"0.4.11"}},{"key":"telemetry.sdk.language","value":{"stringValue":"ruby"}}]},"scopeSpans":[{"scope":{"name":"lapsoss","version":"0.4.11"},"spans":[{"traceId":"bbe4950c95e1ebb06acd8cacf6bbb229","spanId":"b2be3e6e6d25d907","name":"message","kind":1,"startTimeUnixNano":"1765156858208641024","endTimeUnixNano":"1765156858208641024","status":{"code":1},"attributes":[{"key":"message","value":{"stringValue":"Info + message from Lapsoss"}}]}]}]}]}' + headers: + User-Agent: + - lapsoss/ + Content-Type: + - application/json + X-Lapsoss-Version: + - "" + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Content-Type: + - application/json + Date: + - Mon, 08 Dec 2025 01:20:58 GMT + Content-Length: + - '21' + body: + encoding: UTF-8 + string: '{"partialSuccess":{}}' + recorded_at: Mon, 08 Dec 2025 01:20:58 GMT +recorded_with: VCR 6.3.1 diff --git a/test/cassettes/otlp_with_api_key.yml b/test/cassettes/otlp_with_api_key.yml new file mode 100644 index 0000000..25c4f70 --- /dev/null +++ b/test/cassettes/otlp_with_api_key.yml @@ -0,0 +1,49 @@ +--- +http_interactions: +- request: + method: post + uri: "/v1/traces" + body: + encoding: UTF-8 + string: '{"resourceSpans":[{"resource":{"attributes":[{"key":"service.name","value":{"stringValue":"rails"}},{"key":"deployment.environment","value":{"stringValue":"production"}},{"key":"telemetry.sdk.name","value":{"stringValue":"lapsoss"}},{"key":"telemetry.sdk.version","value":{"stringValue":"0.4.11"}},{"key":"telemetry.sdk.language","value":{"stringValue":"ruby"}}]},"scopeSpans":[{"scope":{"name":"lapsoss","version":"0.4.11"},"spans":[{"traceId":"dd18644ca68705c7ad4f67dd08d4765d","spanId":"80e2e8ad8efa21ad","name":"StandardError","kind":1,"startTimeUnixNano":"1765156858220989952","endTimeUnixNano":"1765156858220989952","status":{"code":2,"message":"Error + with API key"},"attributes":[],"events":[{"name":"exception","timeUnixNano":"1765156858220989952","attributes":[{"key":"exception.type","value":{"stringValue":"StandardError"}},{"key":"exception.message","value":{"stringValue":"Error + with API key"}},{"key":"exception.stacktrace","value":{"stringValue":"/path/to/ruby/lib/ruby/gems/3.4.0/gems/vcr-6.3.1/lib/vcr/util/variable_args_block_caller.rb:9 + in `VCR::VariableArgsBlockCaller#call_block`\n/path/to/ruby/lib/ruby/gems/3.4.0/gems/vcr-6.3.1/lib/vcr.rb:194 + in `VCR#use_cassette`\ntest/otlp_adapter_test.rb:104 in `block in \u003cclass:OtlpAdapterTest\u003e`\n/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest/test.rb:94 + in `block (2 levels) in Minitest::Test#run`\n/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest/test.rb:190 + in `Minitest::Test#capture_exceptions`\n/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest/test.rb:89 + in `block in Minitest::Test#run`\n/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest.rb:368 + in `Minitest::Runnable#time_it`\n/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest/test.rb:88 + in `Minitest::Test#run`\n/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest.rb:1208 + in `Minitest.run_one_method`\n/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest.rb:447 + in `Minitest::Runnable.run_one_method`\n/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest.rb:434 + in `block (2 levels) in Minitest::Runnable.run`"}}]}]}]}]}]}' + headers: + User-Agent: + - lapsoss/ + Content-Type: + - application/json + X-Lapsoss-Version: + - "" + Authorization: + - "" + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Content-Type: + - application/json + Date: + - Mon, 08 Dec 2025 01:20:58 GMT + Content-Length: + - '21' + body: + encoding: UTF-8 + string: '{"partialSuccess":{}}' + recorded_at: Mon, 08 Dec 2025 01:20:58 GMT +recorded_with: VCR 6.3.1 diff --git a/test/cassettes/otlp_with_code_context.yml b/test/cassettes/otlp_with_code_context.yml new file mode 100644 index 0000000..61c353b --- /dev/null +++ b/test/cassettes/otlp_with_code_context.yml @@ -0,0 +1,47 @@ +--- +http_interactions: +- request: + method: post + uri: "/v1/traces" + body: + encoding: UTF-8 + string: '{"resourceSpans":[{"resource":{"attributes":[{"key":"service.name","value":{"stringValue":"lapsoss-test"}},{"key":"deployment.environment","value":{"stringValue":"production"}},{"key":"telemetry.sdk.name","value":{"stringValue":"lapsoss"}},{"key":"telemetry.sdk.version","value":{"stringValue":"0.4.11"}},{"key":"telemetry.sdk.language","value":{"stringValue":"ruby"}}]},"scopeSpans":[{"scope":{"name":"lapsoss","version":"0.4.11"},"spans":[{"traceId":"0f8619119e64553e45348b643d3410fb","spanId":"c35fa0ea5e236b5c","name":"NoMethodError","kind":1,"startTimeUnixNano":"1765156858196570880","endTimeUnixNano":"1765156858196570880","status":{"code":2,"message":"undefined + method `foo'' for nil:NilClass"},"attributes":[],"events":[{"name":"exception","timeUnixNano":"1765156858196570880","attributes":[{"key":"exception.type","value":{"stringValue":"NoMethodError"}},{"key":"exception.message","value":{"stringValue":"undefined + method `foo'' for nil:NilClass"}},{"key":"exception.stacktrace","value":{"stringValue":"/path/to/ruby/lib/ruby/gems/3.4.0/gems/vcr-6.3.1/lib/vcr/util/variable_args_block_caller.rb:9 + in `VCR::VariableArgsBlockCaller#call_block`\n/path/to/ruby/lib/ruby/gems/3.4.0/gems/vcr-6.3.1/lib/vcr.rb:194 + in `VCR#use_cassette`\ntest/otlp_adapter_test.rb:68 in `block in \u003cclass:OtlpAdapterTest\u003e`\n/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest/test.rb:94 + in `block (2 levels) in Minitest::Test#run`\n/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest/test.rb:190 + in `Minitest::Test#capture_exceptions`\n/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest/test.rb:89 + in `block in Minitest::Test#run`\n/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest.rb:368 + in `Minitest::Runnable#time_it`\n/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest/test.rb:88 + in `Minitest::Test#run`\n/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest.rb:1208 + in `Minitest.run_one_method`\n/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest.rb:447 + in `Minitest::Runnable.run_one_method`\n/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest.rb:434 + in `block (2 levels) in Minitest::Runnable.run`"}}]}]}]}]}]}' + headers: + User-Agent: + - lapsoss/ + Content-Type: + - application/json + X-Lapsoss-Version: + - "" + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Content-Type: + - application/json + Date: + - Mon, 08 Dec 2025 01:20:58 GMT + Content-Length: + - '21' + body: + encoding: UTF-8 + string: '{"partialSuccess":{}}' + recorded_at: Mon, 08 Dec 2025 01:20:58 GMT +recorded_with: VCR 6.3.1 diff --git a/test/cassettes/otlp_with_headers.yml b/test/cassettes/otlp_with_headers.yml new file mode 100644 index 0000000..1e9454f --- /dev/null +++ b/test/cassettes/otlp_with_headers.yml @@ -0,0 +1,49 @@ +--- +http_interactions: +- request: + method: post + uri: "/v1/traces" + body: + encoding: UTF-8 + string: '{"resourceSpans":[{"resource":{"attributes":[{"key":"service.name","value":{"stringValue":"rails"}},{"key":"deployment.environment","value":{"stringValue":"production"}},{"key":"telemetry.sdk.name","value":{"stringValue":"lapsoss"}},{"key":"telemetry.sdk.version","value":{"stringValue":"0.4.11"}},{"key":"telemetry.sdk.language","value":{"stringValue":"ruby"}}]},"scopeSpans":[{"scope":{"name":"lapsoss","version":"0.4.11"},"spans":[{"traceId":"af64d6b41d2aab4b000768b900094c7b","spanId":"9101baf7d8e8fa60","name":"StandardError","kind":1,"startTimeUnixNano":"1765156858229969920","endTimeUnixNano":"1765156858229969920","status":{"code":2,"message":"Error + with custom headers"},"attributes":[],"events":[{"name":"exception","timeUnixNano":"1765156858229969920","attributes":[{"key":"exception.type","value":{"stringValue":"StandardError"}},{"key":"exception.message","value":{"stringValue":"Error + with custom headers"}},{"key":"exception.stacktrace","value":{"stringValue":"/path/to/ruby/lib/ruby/gems/3.4.0/gems/vcr-6.3.1/lib/vcr/util/variable_args_block_caller.rb:9 + in `VCR::VariableArgsBlockCaller#call_block`\n/path/to/ruby/lib/ruby/gems/3.4.0/gems/vcr-6.3.1/lib/vcr.rb:194 + in `VCR#use_cassette`\ntest/otlp_adapter_test.rb:87 in `block in \u003cclass:OtlpAdapterTest\u003e`\n/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest/test.rb:94 + in `block (2 levels) in Minitest::Test#run`\n/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest/test.rb:190 + in `Minitest::Test#capture_exceptions`\n/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest/test.rb:89 + in `block in Minitest::Test#run`\n/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest.rb:368 + in `Minitest::Runnable#time_it`\n/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest/test.rb:88 + in `Minitest::Test#run`\n/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest.rb:1208 + in `Minitest.run_one_method`\n/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest.rb:447 + in `Minitest::Runnable.run_one_method`\n/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest.rb:434 + in `block (2 levels) in Minitest::Runnable.run`"}}]}]}]}]}]}' + headers: + User-Agent: + - lapsoss/ + Content-Type: + - application/json + X-Lapsoss-Version: + - "" + X-Custom-Header: + - custom-value + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Content-Type: + - application/json + Date: + - Mon, 08 Dec 2025 01:20:58 GMT + Content-Length: + - '21' + body: + encoding: UTF-8 + string: '{"partialSuccess":{}}' + recorded_at: Mon, 08 Dec 2025 01:20:58 GMT +recorded_with: VCR 6.3.1 diff --git a/test/cassettes/otlp_with_tags.yml b/test/cassettes/otlp_with_tags.yml new file mode 100644 index 0000000..f257594 --- /dev/null +++ b/test/cassettes/otlp_with_tags.yml @@ -0,0 +1,47 @@ +--- +http_interactions: +- request: + method: post + uri: "/v1/traces" + body: + encoding: UTF-8 + string: '{"resourceSpans":[{"resource":{"attributes":[{"key":"service.name","value":{"stringValue":"lapsoss-test"}},{"key":"deployment.environment","value":{"stringValue":"production"}},{"key":"telemetry.sdk.name","value":{"stringValue":"lapsoss"}},{"key":"telemetry.sdk.version","value":{"stringValue":"0.4.11"}},{"key":"telemetry.sdk.language","value":{"stringValue":"ruby"}}]},"scopeSpans":[{"scope":{"name":"lapsoss","version":"0.4.11"},"spans":[{"traceId":"be18942ed93de6d9da530e084413a37b","spanId":"0458c68cbec35bfe","name":"ArgumentError","kind":1,"startTimeUnixNano":"1765156858116051968","endTimeUnixNano":"1765156858116051968","status":{"code":2,"message":"Error + with tags"},"attributes":[{"key":"feature","value":{"stringValue":"checkout"}},{"key":"version","value":{"stringValue":"2.0"}},{"key":"extra.order_id","value":{"stringValue":"12345"}},{"key":"extra.user_tier","value":{"stringValue":"premium"}}],"events":[{"name":"exception","timeUnixNano":"1765156858116051968","attributes":[{"key":"exception.type","value":{"stringValue":"ArgumentError"}},{"key":"exception.message","value":{"stringValue":"Error + with tags"}},{"key":"exception.stacktrace","value":{"stringValue":"/path/to/ruby/lib/ruby/gems/3.4.0/gems/vcr-6.3.1/lib/vcr/util/variable_args_block_caller.rb:9 + in `VCR::VariableArgsBlockCaller#call_block`\n/path/to/ruby/lib/ruby/gems/3.4.0/gems/vcr-6.3.1/lib/vcr.rb:194 + in `VCR#use_cassette`\ntest/otlp_adapter_test.rb:51 in `block in \u003cclass:OtlpAdapterTest\u003e`\n/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest/test.rb:94 + in `block (2 levels) in Minitest::Test#run`\n/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest/test.rb:190 + in `Minitest::Test#capture_exceptions`\n/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest/test.rb:89 + in `block in Minitest::Test#run`\n/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest.rb:368 + in `Minitest::Runnable#time_it`\n/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest/test.rb:88 + in `Minitest::Test#run`\n/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest.rb:1208 + in `Minitest.run_one_method`\n/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest.rb:447 + in `Minitest::Runnable.run_one_method`\n/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest.rb:434 + in `block (2 levels) in Minitest::Runnable.run`"}}]}]}]}]}]}' + headers: + User-Agent: + - lapsoss/ + Content-Type: + - application/json + X-Lapsoss-Version: + - "" + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Content-Type: + - application/json + Date: + - Mon, 08 Dec 2025 01:20:58 GMT + Content-Length: + - '21' + body: + encoding: UTF-8 + string: '{"partialSuccess":{}}' + recorded_at: Mon, 08 Dec 2025 01:20:58 GMT +recorded_with: VCR 6.3.1 diff --git a/test/cassettes/otlp_with_user.yml b/test/cassettes/otlp_with_user.yml new file mode 100644 index 0000000..dd54f75 --- /dev/null +++ b/test/cassettes/otlp_with_user.yml @@ -0,0 +1,47 @@ +--- +http_interactions: +- request: + method: post + uri: "/v1/traces" + body: + encoding: UTF-8 + string: '{"resourceSpans":[{"resource":{"attributes":[{"key":"service.name","value":{"stringValue":"lapsoss-test"}},{"key":"deployment.environment","value":{"stringValue":"production"}},{"key":"telemetry.sdk.name","value":{"stringValue":"lapsoss"}},{"key":"telemetry.sdk.version","value":{"stringValue":"0.4.11"}},{"key":"telemetry.sdk.language","value":{"stringValue":"ruby"}},{"key":"user.id","value":{"intValue":"123"}},{"key":"user.username","value":{"stringValue":"testuser"}},{"key":"user.email","value":{"stringValue":"[FILTERED]"}}]},"scopeSpans":[{"scope":{"name":"lapsoss","version":"0.4.11"},"spans":[{"traceId":"31c93a9f768ab81624288240c6c9add8","spanId":"bcb44565f10407a9","name":"RuntimeError","kind":1,"startTimeUnixNano":"1765156858213530112","endTimeUnixNano":"1765156858213530112","status":{"code":2,"message":"Error + with user context"},"attributes":[],"events":[{"name":"exception","timeUnixNano":"1765156858213530112","attributes":[{"key":"exception.type","value":{"stringValue":"RuntimeError"}},{"key":"exception.message","value":{"stringValue":"Error + with user context"}},{"key":"exception.stacktrace","value":{"stringValue":"/path/to/ruby/lib/ruby/gems/3.4.0/gems/vcr-6.3.1/lib/vcr/util/variable_args_block_caller.rb:9 + in `VCR::VariableArgsBlockCaller#call_block`\n/path/to/ruby/lib/ruby/gems/3.4.0/gems/vcr-6.3.1/lib/vcr.rb:194 + in `VCR#use_cassette`\ntest/otlp_adapter_test.rb:35 in `block in \u003cclass:OtlpAdapterTest\u003e`\n/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest/test.rb:94 + in `block (2 levels) in Minitest::Test#run`\n/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest/test.rb:190 + in `Minitest::Test#capture_exceptions`\n/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest/test.rb:89 + in `block in Minitest::Test#run`\n/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest.rb:368 + in `Minitest::Runnable#time_it`\n/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest/test.rb:88 + in `Minitest::Test#run`\n/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest.rb:1208 + in `Minitest.run_one_method`\n/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest.rb:447 + in `Minitest::Runnable.run_one_method`\n/path/to/ruby/lib/ruby/gems/3.4.0/gems/minitest-5.25.5/lib/minitest.rb:434 + in `block (2 levels) in Minitest::Runnable.run`"}}]}]}]}]}]}' + headers: + User-Agent: + - lapsoss/ + Content-Type: + - application/json + X-Lapsoss-Version: + - "" + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Content-Type: + - application/json + Date: + - Mon, 08 Dec 2025 01:20:58 GMT + Content-Length: + - '21' + body: + encoding: UTF-8 + string: '{"partialSuccess":{}}' + recorded_at: Mon, 08 Dec 2025 01:20:58 GMT +recorded_with: VCR 6.3.1 diff --git a/test/event_handling_test.rb b/test/event_handling_test.rb index 358eb17..c32e5f9 100644 --- a/test/event_handling_test.rb +++ b/test/event_handling_test.rb @@ -39,7 +39,7 @@ def capture(event) end test "exclusion filter prevents delivery" do - filter = Lapsoss::ExclusionFilter.new(excluded_exceptions: [ArgumentError]) + filter = Lapsoss::ExclusionFilter.new(excluded_exceptions: [ ArgumentError ]) Lapsoss.configure do |config| config.async = false @@ -55,7 +55,7 @@ def capture(event) test "error handler accepts three arguments for delivery failures" do calls = [] - handler = ->(adapter, event, error) { calls << [adapter, event, error] } + handler = ->(adapter, event, error) { calls << [ adapter, event, error ] } failing_adapter = Class.new(Lapsoss::Adapters::Base) do include Lapsoss::Adapters::Concerns::EnvelopeBuilder diff --git a/test/otlp_adapter_test.rb b/test/otlp_adapter_test.rb new file mode 100644 index 0000000..aef97fe --- /dev/null +++ b/test/otlp_adapter_test.rb @@ -0,0 +1,202 @@ +# frozen_string_literal: true + +require_relative "test_helper" + +class OtlpAdapterTest < ActiveSupport::TestCase + # OTLP payloads contain dynamic trace IDs and timestamps, so we match only on method/uri + VCR_OPTIONS = { match_requests_on: %i[method uri] }.freeze + + setup do + @adapter = Lapsoss::Adapters::OtlpAdapter.new(:otlp, + endpoint: ENV["OTLP_ENDPOINT"] || "http://localhost:4318", + service_name: "lapsoss-test" + ) + end + + test "captures exception to OTLP endpoint" do + VCR.use_cassette("otlp_capture_exception", VCR_OPTIONS) do + error = StandardError.new("Test error from Lapsoss") + error.set_backtrace(caller) + + event = Lapsoss::Event.build(type: :exception, exception: error) + + response = @adapter.capture(event) + assert response + end + end + + test "captures message to OTLP endpoint" do + VCR.use_cassette("otlp_capture_message", VCR_OPTIONS) do + event = Lapsoss::Event.build(type: :message, message: "Info message from Lapsoss", level: :info) + + response = @adapter.capture(event) + assert response + end + end + + test "captures exception with user context" do + VCR.use_cassette("otlp_with_user", VCR_OPTIONS) do + error = RuntimeError.new("Error with user context") + error.set_backtrace(caller) + + event = Lapsoss::Event.build(type: :exception, + exception: error, + context: { + user: { id: 123, username: "testuser", email: "test@example.com" } + }) + + response = @adapter.capture(event) + assert response + end + end + + test "captures exception with tags and extra data" do + VCR.use_cassette("otlp_with_tags", VCR_OPTIONS) do + error = ArgumentError.new("Error with tags") + error.set_backtrace(caller) + + event = Lapsoss::Event.build(type: :exception, + exception: error, + context: { + tags: { feature: "checkout", version: "2.0" }, + extra: { order_id: "12345", user_tier: "premium" } + }) + + response = @adapter.capture(event) + assert response + end + end + + test "captures exception with code context" do + VCR.use_cassette("otlp_with_code_context", VCR_OPTIONS) do + error = NoMethodError.new("undefined method `foo' for nil:NilClass") + error.set_backtrace(caller) + + event = Lapsoss::Event.build(type: :exception, exception: error) + + response = @adapter.capture(event) + assert response + end + end + + test "uses default endpoint when not specified" do + adapter = Lapsoss::Adapters::OtlpAdapter.new(:otlp, {}) + + assert adapter.enabled? + # Default endpoint is localhost:4318 + end + + test "supports custom headers" do + VCR.use_cassette("otlp_with_headers", VCR_OPTIONS) do + adapter = Lapsoss::Adapters::OtlpAdapter.new(:otlp, + endpoint: ENV["OTLP_ENDPOINT"] || "http://localhost:4318", + headers: { "X-Custom-Header" => "custom-value" } + ) + + error = StandardError.new("Error with custom headers") + error.set_backtrace(caller) + + event = Lapsoss::Event.build(type: :exception, exception: error) + + response = adapter.capture(event) + assert response + end + end + + test "supports api_key authentication" do + VCR.use_cassette("otlp_with_api_key", VCR_OPTIONS) do + adapter = Lapsoss::Adapters::OtlpAdapter.new(:otlp, + endpoint: ENV["OTLP_ENDPOINT"] || "http://localhost:4318", + api_key: "test-api-key" + ) + + error = StandardError.new("Error with API key") + error.set_backtrace(caller) + + event = Lapsoss::Event.build(type: :exception, exception: error) + + response = adapter.capture(event) + assert response + end + end + + test "configuration helper use_otlp registers adapter" do + Lapsoss.configure do |config| + config.use_otlp( + endpoint: "http://localhost:4318", + service_name: "test-service" + ) + end + Lapsoss.configuration.apply! + + adapter = Lapsoss::Registry.instance[:otlp] + assert_not_nil adapter + assert_kind_of Lapsoss::Adapters::OtlpAdapter, adapter + end + + test "configuration helper use_signoz registers OTLP adapter" do + Lapsoss.configure do |config| + config.use_signoz( + signoz_api_key: "test-key" + ) + end + Lapsoss.configuration.apply! + + adapter = Lapsoss::Registry.instance[:signoz] + assert_not_nil adapter + assert_kind_of Lapsoss::Adapters::OtlpAdapter, adapter + end + + test "configuration helper use_jaeger registers OTLP adapter" do + Lapsoss.configure do |config| + config.use_jaeger( + endpoint: "http://jaeger:4318" + ) + end + Lapsoss.configuration.apply! + + adapter = Lapsoss::Registry.instance[:jaeger] + assert_not_nil adapter + assert_kind_of Lapsoss::Adapters::OtlpAdapter, adapter + end + + test "builds valid OTLP payload structure" do + error = StandardError.new("Test error") + error.set_backtrace(caller) + + event = Lapsoss::Event.build(type: :exception, exception: error) + payload = @adapter.send(:build_payload, event.scrubbed) + + assert payload.key?(:resourceSpans) + assert_equal 1, payload[:resourceSpans].size + + resource_span = payload[:resourceSpans].first + assert resource_span.key?(:resource) + assert resource_span.key?(:scopeSpans) + + # Check resource attributes + resource = resource_span[:resource] + service_attr = resource[:attributes].find { |a| a[:key] == "service.name" } + assert_not_nil service_attr + assert_equal "lapsoss-test", service_attr[:value][:stringValue] + + # Check scope spans + scope_span = resource_span[:scopeSpans].first + assert_equal "lapsoss", scope_span[:scope][:name] + + # Check span + span = scope_span[:spans].first + assert span.key?(:traceId) + assert span.key?(:spanId) + assert_equal "StandardError", span[:name] + assert_equal 2, span[:status][:code] # STATUS_CODE_ERROR + + # Check exception event + assert span.key?(:events) + exception_event = span[:events].first + assert_equal "exception", exception_event[:name] + + type_attr = exception_event[:attributes].find { |a| a[:key] == "exception.type" } + assert_equal "StandardError", type_attr[:value][:stringValue] + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 1282138..3f3822a 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -30,6 +30,9 @@ config.filter_sensitive_data("") { ENV["OPENOBSERVE_ENDPOINT"] || "http://localhost:5080" } config.filter_sensitive_data("") { ENV["OPENOBSERVE_USERNAME"] || "seuros@example.com" } config.filter_sensitive_data("") { ENV["OPENOBSERVE_PASSWORD"] || "ShipItFast!" } + config.filter_sensitive_data("") { ENV["OTLP_ENDPOINT"] || "http://localhost:4318" } + config.filter_sensitive_data("") { ENV["OTLP_API_KEY"] || "test-api-key" } + config.filter_sensitive_data("") { ENV["SIGNOZ_API_KEY"] || "test-signoz-key" } config.filter_sensitive_data("") { ENV["AUTHORIZATION"] } config.filter_sensitive_data("") { ENV["HTTP_COOKIE"] }