Ops AI Ruby APM Configuration
Instrument your Ruby service with OpenTelemetry so Ops AI can analyze runtime exceptions with full code context. The instrumentation captures exception types, stack traces, and function bodies, and recognizes library files to provide cleaner signals, allowing Ops AI to analyze issues and suggest fixes with accurate context.
What you get#
- Automatic error capture: exceptions + messages, stack traces with line numbers.
- Code context: function names and bodies, file paths and ranges, with library-file detection (
/gems/,/ruby/paths).
Step 1: Install the Gems#
Add the following to your Gemfile:
1gem 'opentelemetry-sdk'
2gem 'opentelemetry-exporter-otlp'
3gem 'opentelemetry-instrumentation-all'Then run:
1bundle installStep 2: Initialize OpenTelemetry#
Configure the OpenTelemetry SDK at the top of your application entry point, before loading routes or framework code:
1require 'opentelemetry/sdk'
2require 'opentelemetry/exporter/otlp'
3require 'opentelemetry/instrumentation/all'
4
5OpenTelemetry::SDK.configure do |c|
6 c.service_name = ENV['OTEL_SERVICE_NAME'] || 'my-ruby-service'
7 c.use_all
8endSet the following environment variables before starting your app:
1export OTEL_SERVICE_NAME="my-ruby-service"
2export OTEL_EXPORTER_OTLP_ENDPOINT="https://<YOUR_WORKSPACE>.middleware.io:443"
3export OTEL_EXPORTER_OTLP_HEADERS="authorization=<MW_API_KEY>"Step 3: Capture Errors with Code Context#
Record each error with an explicit OTel span that includes structured stack details. This approach works regardless of whether auto-instrumentation creates a current span.
Call record_error_otel(err) wherever you catch an exception. It creates a dedicated span that carries the exception type, message, stacktrace, and structured exception.stack_details with the function body, file path, and line range for each frame.
1def otel_tracer
2 @otel_tracer ||= OpenTelemetry.tracer_provider.tracer('my-ruby-service')
3end
4
5def record_error_otel(err)
6 return unless defined?(OpenTelemetry)
7
8 stacktrace = (err.backtrace || []).join("\n")
9 stack_details = build_stack_details(err)
10 exception_attrs = {
11 'exception.type' => err.class.name,
12 'exception.message' => err.message,
13 'exception.stacktrace' => stacktrace,
14 'exception.escaped' => true,
15 'exception.stack_details' => stack_details.to_json,
16 }
17
18 current = OpenTelemetry::Trace.current_span
19 if current != OpenTelemetry::Trace::Span::INVALID
20 current.record_exception(err, attributes: exception_attrs)
21 current.status = OpenTelemetry::Trace::Status.error(err.message)
22 set_error_attrs(current, err, stacktrace)
23 end
24
25 otel_tracer.in_span(
26 "exception #{err.class.name}",
27 kind: :internal,
28 attributes: {
29 'exception.type' => err.class.name,
30 'exception.message' => err.message,
31 'exception.stacktrace' => stacktrace,
32 'exception.escaped' => true,
33 }
34 ) do |span|
35 span.record_exception(err, attributes: exception_attrs)
36 span.status = OpenTelemetry::Trace::Status.error(err.message)
37 set_error_attrs(span, err, stacktrace)
38 end
39rescue => e
40 $stderr.puts "OTel record_error failed: #{e.message}"
41endBuild structured stack details#
Parses the top 10 backtrace frames into structured data. For application code, it extracts the function body around the error line:
1def build_stack_details(err)
2 frames = (err.backtrace || []).first(10)
3 frames.map do |frame|
4 match = frame.match(/\A(.+):(\d+):in [`'](.+)'\z/)
5 next nil unless match
6
7 file_path = match[1]
8 line_num = match[2].to_i
9 func_name = match[3]
10 is_external = file_path.include?('/gems/') || file_path.include?('/ruby/')
11
12 detail = {
13 'exception.function_name' => func_name,
14 'exception.file' => file_path,
15 'exception.line' => line_num,
16 'exception.is_file_external' => is_external,
17 'exception.language' => 'ruby',
18 }
19
20 if !is_external && File.exist?(file_path)
21 begin
22 lines = File.readlines(file_path)
23 start_line, end_line, body = extract_function_body(lines, line_num, func_name)
24 if body
25 detail['exception.start_line'] = start_line
26 detail['exception.end_line'] = end_line
27 detail['exception.function_body'] = body
28 end
29 rescue
30 end
31 end
32
33 detail
34 end.compact
35endExtract function body#
Walks backward from the error line to find the function start (def, get, post, etc.) and forward to find end:
1def extract_function_body(lines, error_line, func_name)
2 start_idx = nil
3 (error_line - 1).downto(0) do |i|
4 if lines[i] =~ /\A\s*(def |get |post |put |delete |patch )/
5 start_idx = i
6 break
7 end
8 end
9 start_idx ||= [error_line - 6, 0].max
10
11 end_idx = nil
12 (error_line).upto([error_line + 20, lines.length - 1].min) do |i|
13 if lines[i] =~ /\A\s*end\s*$/ && i > error_line - 1
14 end_idx = i
15 break
16 end
17 end
18 end_idx ||= [error_line + 4, lines.length - 1].min
19
20 body = lines[start_idx..end_idx].join
21 [start_idx + 1, end_idx + 1, body]
22endSet error attributes on the span#
Sets the attributes that Middleware uses for error detection:
1def set_error_attrs(span, err, stacktrace)
2 span.set_attribute('error', true)
3 span.set_attribute('otel.status_code', 'ERROR')
4 span.set_attribute('otel.status_description', err.message)
5 span.set_attribute('error.type', err.class.name)
6 span.set_attribute('error.message', err.message)
7 span.set_attribute('error.stack', stacktrace)
8 span.set_attribute('http.status_code', 500)
9 span.set_attribute('http.response.status_code', 500)
10
11 if err.respond_to?(:cause) && err.cause
12 span.set_attribute('error.cause.type', err.cause.class.name)
13 span.set_attribute('error.cause.message', err.cause.message)
14 end
15
16 if defined?(request)
17 span.set_attribute('http.method', request.request_method)
18 span.set_attribute('http.url', request.url)
19 span.set_attribute('http.route', request.path_info)
20 end
21endStep 4: Use in Your Routes#
In a route handler:
1begin
2 risky_operation()
3rescue => err
4 record_error_otel(err)
5 status 500
6 json({ error: err.message })
7endIn a global error handler (Sinatra example):
1error do
2 err = env['sinatra.error']
3 record_error_otel(err) if err
4 status 500
5 json({ error: err&.message })
6endValidate & Troubleshoot#
- Gems installed? Run
bundle list | grep opentelemetryand confirmopentelemetry-sdkandopentelemetry-exporter-otlpare present. - OTel configured? Ensure
OpenTelemetry::SDK.configureruns before any routes are defined. - Error capture working? Trigger a test error and confirm the exception event appears with
exception.stack_detailsin the Middleware APM UI.
Need assistance or want to learn more about Middleware? Get in touch with us via our Contact Us or join our Slack channel.