obtrace-sdk-ruby 1.0.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c7468e55374f6c051c0def7f03f87796ebab9d4d69ff05132965e66b57bdbadb
4
- data.tar.gz: a1d5629df790ebe12837155bb1bf94a9c61edd4ac737450ca4a3a24466dc144f
3
+ metadata.gz: e0395a5b1787dc1cfc6c7f5385d4c78736580d700efefded562fa2b29d8aeb26
4
+ data.tar.gz: 7100b663a95f5e2ce98432c649fdeb77057993643fc46ef2cc74ca9820ea5abd
5
5
  SHA512:
6
- metadata.gz: 77d495287ed0a0f010b57c08e860cec323a138f8a7c23e46051a9bd23abc8b8cc5c4dd6d717c08fefb8aac49d6285d57329846c830bd5a30dcaf3b4cb84bfbbe
7
- data.tar.gz: 49fb1d19befa93b2d97485d70d1bf3106e64d340c3e26eb96f7c035e63adfbdff963da1b05787e2b06caedde5f8c7d61ab23fb4282378c29e3766b4ef12b6312
6
+ metadata.gz: 1f42388bc450d57fa956efed164373acf6d357e652b79d60a5c3f3369ced325575ebf0e05ba51bb1122e8d861612f50b70ad4ba3656d079f46f957e34c38f231
7
+ data.tar.gz: fd240b922f666c45515dfa963b0c77fe44aacfc144102dcf3e5cc4e560390d9e2e468280ebca82e0bf56a774bd3c86870fd93dfd5ddee5ac70bc2bf4b337e7e2
@@ -1,208 +1,135 @@
1
- require "json"
2
1
  require "net/http"
2
+ require "json"
3
3
  require "uri"
4
- require "thread"
5
- require_relative "context"
6
- require_relative "otlp"
7
- require_relative "http_instrumentation"
8
- require_relative "logger_capture"
4
+ require_relative "otel_setup"
9
5
 
10
6
  module ObtraceSDK
11
7
  class Client
12
- def initialize(cfg)
13
- raise ArgumentError, "api_key, ingest_base_url and service_name are required" if cfg.api_key.to_s.empty? || cfg.ingest_base_url.to_s.empty? || cfg.service_name.to_s.empty?
8
+ @@initialized = false
14
9
 
15
- @cfg = cfg
16
- @queue = []
17
- @lock = Mutex.new
18
- @http = nil
19
- @http_uri = nil
20
- @circuit_failures = 0
21
- @circuit_open_until = Time.at(0)
22
- @seen_exceptions = {}
23
- @seen_lock = Mutex.new
24
-
25
- install_exception_tracepoint
26
- HttpInstrumentation.install(self) if @cfg.auto_instrument_http
27
- LoggerCapture.install(self) if @cfg.auto_capture_logs
28
- at_exit { capture_fatal; shutdown }
29
- end
10
+ attr_reader :tracer, :meter, :handshake_ok
30
11
 
31
- def log(level, message, context = nil)
32
- enqueue("/otlp/v1/logs", Otlp.logs_payload(@cfg, level, truncate(message, 32768), context))
33
- end
12
+ def initialize(cfg)
13
+ if @@initialized
14
+ warn("[obtrace-sdk-ruby] already initialized, skipping duplicate init")
15
+ return
16
+ end
34
17
 
35
- def metric(name, value, unit = "1", context = nil)
36
- warn("[obtrace-sdk-ruby] non-canonical metric name: #{name}") if @cfg.validate_semantic_metrics && @cfg.debug && !SemanticMetrics.semantic_metric?(name)
37
- enqueue("/otlp/v1/metrics", Otlp.metric_payload(@cfg, truncate(name, 1024), value, unit, context))
38
- end
18
+ raise ArgumentError, "api_key, ingest_base_url and service_name are required" if cfg.api_key.to_s.empty? || cfg.ingest_base_url.to_s.empty? || cfg.service_name.to_s.empty?
39
19
 
40
- def span(name, trace_id: nil, span_id: nil, start_unix_nano: nil, end_unix_nano: nil, status_code: nil, status_message: "", attrs: nil)
41
- trace_id ||= Context.random_hex(16)
42
- span_id ||= Context.random_hex(8)
43
- start_ns = start_unix_nano || Otlp.now_unix_nano
44
- end_ns = end_unix_nano || Otlp.now_unix_nano
20
+ @@initialized = true
21
+ @cfg = cfg
22
+ @handshake_ok = false
23
+ @tracer_provider = OtelSetup.configure(cfg)
24
+ @tracer = @tracer_provider.tracer("obtrace-sdk-ruby", ObtraceSDK::VERSION)
25
+ @meter = nil
26
+ @meter_warning_logged = false
45
27
 
46
- name = truncate(name, 32768)
47
- if attrs
48
- attrs = attrs.transform_values { |v| v.is_a?(String) ? truncate(v, 4096) : v }
28
+ begin
29
+ @meter = OpenTelemetry.meter_provider.meter("obtrace-sdk-ruby", ObtraceSDK::VERSION)
30
+ rescue => _e
49
31
  end
50
32
 
51
- enqueue("/otlp/v1/traces", Otlp.span_payload(@cfg, name, trace_id, span_id, start_ns, end_ns, status_code, status_message, attrs))
52
- { trace_id: trace_id, span_id: span_id }
33
+ Thread.new { perform_handshake }
34
+ at_exit { shutdown }
53
35
  end
54
36
 
55
- def inject_propagation(headers = {}, trace_id: nil, span_id: nil, session_id: nil)
56
- Context.ensure_propagation_headers(headers, trace_id: trace_id, span_id: span_id, session_id: session_id)
37
+ private def perform_handshake
38
+ base = @cfg.ingest_base_url.to_s.chomp("/")
39
+ return if base.empty?
40
+ uri = URI("#{base}/v1/init")
41
+ payload = JSON.generate({
42
+ sdk: "obtrace-sdk-ruby",
43
+ sdk_version: "1.2.0",
44
+ service_name: @cfg.service_name,
45
+ service_version: @cfg.service_version.to_s,
46
+ runtime: "ruby",
47
+ runtime_version: RUBY_VERSION,
48
+ })
49
+ http = Net::HTTP.new(uri.host, uri.port)
50
+ http.use_ssl = uri.scheme == "https"
51
+ http.open_timeout = 5
52
+ http.read_timeout = 5
53
+ req = Net::HTTP::Post.new(uri)
54
+ req["Content-Type"] = "application/json"
55
+ req["Authorization"] = "Bearer #{@cfg.api_key}"
56
+ req.body = payload
57
+ resp = http.request(req)
58
+ if resp.code.to_i == 200
59
+ @handshake_ok = true
60
+ warn("[obtrace-sdk-ruby] init handshake OK") if @cfg.debug
61
+ elsif @cfg.debug
62
+ warn("[obtrace-sdk-ruby] init handshake failed: #{resp.code}")
63
+ end
64
+ rescue => e
65
+ warn("[obtrace-sdk-ruby] init handshake error: #{e.message}") if @cfg.debug
57
66
  end
58
67
 
59
- def flush
60
- batch = []
61
- @lock.synchronize do
62
- return if Time.now < @circuit_open_until
63
- half_open = @circuit_failures >= 5
64
- if half_open
65
- return if @queue.empty?
66
- batch = [@queue.shift]
67
- else
68
- batch = @queue.dup
69
- @queue.clear
70
- end
68
+ def log(level, message, context = nil)
69
+ attributes = { "obtrace.log.level" => level.to_s }
70
+ if context
71
+ context.each { |k, v| attributes["obtrace.attr.#{k}"] = v.to_s }
71
72
  end
72
- batch.each do |item|
73
- success = send_item(item)
74
- @lock.synchronize do
75
- if success
76
- if @circuit_failures > 0
77
- warn("[obtrace-sdk-ruby] circuit breaker closed") if @cfg.debug
78
- @circuit_failures = 0
79
- @circuit_open_until = Time.at(0)
80
- end
81
- else
82
- @circuit_failures += 1
83
- if @circuit_failures >= 5
84
- @circuit_open_until = Time.now + 30
85
- warn("[obtrace-sdk-ruby] circuit breaker opened") if @cfg.debug
86
- end
87
- end
88
- end
73
+ attributes["log.severity"] = level.to_s.upcase
74
+ attributes["log.message"] = message.to_s
75
+
76
+ span = OpenTelemetry::Trace.current_span
77
+ if span && span.context.valid?
78
+ span.add_event("log", attributes: attributes)
79
+ else
80
+ @tracer.in_span("log.#{level}", attributes: attributes) { |_s| }
89
81
  end
90
82
  end
91
83
 
92
- def shutdown
93
- @tracepoint&.disable
94
- flush
95
- @lock.synchronize do
96
- if @http
97
- @http.finish rescue nil
98
- @http = nil
99
- @http_uri = nil
100
- end
101
- end
102
- end
84
+ def metric(name, value, unit = "1", context = nil)
85
+ warn("[obtrace-sdk-ruby] non-canonical metric name: #{name}") if @cfg.validate_semantic_metrics && @cfg.debug && !SemanticMetrics.semantic_metric?(name)
103
86
 
104
- private
105
-
106
- def install_exception_tracepoint
107
- client = self
108
- @tracepoint = TracePoint.new(:raise) do |tp|
109
- ex = tp.raised_exception
110
- eid = ex.object_id
111
- seen = client.instance_variable_get(:@seen_lock).synchronize do
112
- cache = client.instance_variable_get(:@seen_exceptions)
113
- next true if cache[eid]
114
- cache[eid] = true
115
- cache.shift if cache.size > 200
116
- false
117
- end
118
- unless seen
119
- bt = (ex.backtrace || []).first(10).join("\n")
120
- client.log("error", "#{ex.class}: #{ex.message}", {
121
- "exception.type" => ex.class.to_s,
122
- "exception.message" => ex.message.to_s,
123
- "exception.stacktrace" => bt,
124
- "code.filepath" => tp.path.to_s,
125
- "code.lineno" => tp.lineno.to_s,
126
- "auto.source" => "tracepoint"
127
- })
87
+ unless @meter
88
+ unless @meter_warning_logged
89
+ warn("[obtrace-sdk-ruby] meter provider not available, metrics will be dropped")
90
+ @meter_warning_logged = true
128
91
  end
92
+ return
129
93
  end
130
- @tracepoint.enable
131
- end
132
94
 
133
- def capture_fatal
134
- return unless $!
135
- ex = $!
136
- bt = (ex.backtrace || []).first(10).join("\n")
137
- log("fatal", "#{ex.class}: #{ex.message}", {
138
- "exception.type" => ex.class.to_s,
139
- "exception.message" => ex.message.to_s,
140
- "exception.stacktrace" => bt,
141
- "auto.source" => "at_exit"
142
- })
143
- end
95
+ attrs = {}
96
+ context&.each { |k, v| attrs[k.to_s] = v.to_s }
144
97
 
145
- def truncate(s, max)
146
- return s if s.length <= max
147
- s[0, max] + "...[truncated]"
98
+ gauge = @meter.create_gauge(name.to_s, unit: unit.to_s)
99
+ gauge.set(value.to_f, attributes: attrs)
148
100
  end
149
101
 
150
- def enqueue(endpoint, payload)
151
- @lock.synchronize do
152
- if @queue.length >= @cfg.max_queue_size
153
- @queue.shift
154
- warn("[obtrace-sdk-ruby] queue full, dropping oldest item") if @cfg.debug
155
- end
156
- @queue << { endpoint: endpoint, payload: payload.dup.freeze }
102
+ def span(name, attrs: nil, &block)
103
+ span_attrs = {}
104
+ if attrs
105
+ attrs.each { |k, v| span_attrs[k.to_s] = v.is_a?(Numeric) || v.is_a?(TrueClass) || v.is_a?(FalseClass) ? v : v.to_s }
157
106
  end
158
- end
159
107
 
160
- def connection_for(uri)
161
- if @http && @http_uri && @http_uri.host == uri.host && @http_uri.port == uri.port
162
- begin
163
- return @http if @http.started?
164
- rescue StandardError
108
+ if block
109
+ @tracer.in_span(name.to_s, attributes: span_attrs) do |s|
110
+ block.call(s)
165
111
  end
112
+ else
113
+ @tracer.in_span(name.to_s, attributes: span_attrs) { |_s| }
166
114
  end
167
-
168
- @http.finish rescue nil if @http
169
- @http = Net::HTTP.new(uri.host, uri.port)
170
- @http.use_ssl = uri.scheme == "https"
171
- @http.read_timeout = @cfg.request_timeout_sec
172
- @http.start
173
- @http_uri = uri
174
- @http
175
115
  end
176
116
 
177
- def send_item(item)
178
- uri = URI.parse("#{@cfg.ingest_base_url.to_s.sub(%r{/$}, "")}#{item[:endpoint]}")
179
- http = connection_for(uri)
180
- req = Net::HTTP::Post.new(uri.request_uri)
181
- req["Authorization"] = "Bearer #{@cfg.api_key}"
182
- req["Content-Type"] = "application/json"
183
- (@cfg.default_headers || {}).each { |k, v| req[k] = v.to_s }
184
- req.body = JSON.generate(item[:payload])
117
+ def capture_error(exception, context = nil)
118
+ exception = Exception.new(exception.to_s) unless exception.is_a?(Exception)
185
119
 
186
- retries = 0
187
- begin
188
- res = http.request(req)
189
- if res.code.to_i >= 300
190
- warn("[obtrace-sdk-ruby] status=#{res.code} endpoint=#{item[:endpoint]} body=#{res.body}") if @cfg.debug
191
- return false
192
- end
193
- true
194
- rescue StandardError => e
195
- if retries < 2
196
- retries += 1
197
- sleep 1
198
- @http.finish rescue nil
199
- @http = nil
200
- http = connection_for(uri)
201
- retry
202
- end
203
- warn("[obtrace-sdk-ruby] send failed endpoint=#{item[:endpoint]} err=#{e.message}") if @cfg.debug
204
- false
120
+ attrs = {}
121
+ context&.each { |k, v| attrs[k.to_s] = v.to_s }
122
+
123
+ @tracer.in_span("error", attributes: attrs) do |s|
124
+ s.record_exception(exception)
125
+ s.status = OpenTelemetry::Trace::Status.error(exception.message.to_s)
205
126
  end
206
127
  end
128
+
129
+ alias_method :capture_exception, :capture_error
130
+
131
+ def shutdown
132
+ @tracer_provider.shutdown if @tracer_provider.respond_to?(:shutdown)
133
+ end
207
134
  end
208
135
  end
@@ -3,84 +3,58 @@ module ObtraceSDK
3
3
  def initialize(app, client)
4
4
  @app = app
5
5
  @client = client
6
+ @tracer = client.tracer
6
7
  end
7
8
 
8
9
  def call(env)
9
- trace_id = extract_trace_id(env) || Context.random_hex(16)
10
- span_id = Context.random_hex(8)
11
- start_ns = Otlp.now_unix_nano
12
- start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
10
+ span_name = "#{env["REQUEST_METHOD"]} #{env["PATH_INFO"]}"
13
11
 
14
- status = nil
15
- error = nil
12
+ extracted_context = extract_context(env)
16
13
 
17
- begin
18
- status, headers, body = @app.call(env)
19
- [status, headers, body]
20
- rescue => e
21
- error = e
22
- raise
23
- ensure
24
- end_ns = Otlp.now_unix_nano
25
- duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000).round(2)
26
- status_code = error ? 500 : status.to_i
14
+ span_attrs = {
15
+ "http.method" => env["REQUEST_METHOD"].to_s,
16
+ "http.target" => env["PATH_INFO"].to_s,
17
+ "http.host" => env["HTTP_HOST"].to_s,
18
+ "http.scheme" => (env["rack.url_scheme"] || "http").to_s,
19
+ "http.user_agent" => env["HTTP_USER_AGENT"].to_s,
20
+ "net.peer.ip" => (env["HTTP_X_FORWARDED_FOR"] || env["REMOTE_ADDR"]).to_s
21
+ }
27
22
 
28
- attrs = {
29
- "http.method" => env["REQUEST_METHOD"],
30
- "http.target" => env["PATH_INFO"],
31
- "http.host" => env["HTTP_HOST"].to_s,
32
- "http.scheme" => env["rack.url_scheme"].to_s,
33
- "http.status_code" => status_code,
34
- "http.duration_ms" => duration_ms,
35
- "http.user_agent" => env["HTTP_USER_AGENT"].to_s,
36
- "net.peer.ip" => (env["HTTP_X_FORWARDED_FOR"] || env["REMOTE_ADDR"]).to_s
37
- }
38
-
39
- if error
40
- attrs["error"] = true
41
- attrs["error.type"] = error.class.to_s
42
- attrs["error.message"] = error.message.to_s
43
- end
44
-
45
- if env["QUERY_STRING"] && !env["QUERY_STRING"].empty?
46
- attrs["http.query"] = env["QUERY_STRING"]
47
- end
48
-
49
- @client.span(
50
- "#{env["REQUEST_METHOD"]} #{env["PATH_INFO"]}",
51
- trace_id: trace_id,
52
- span_id: span_id,
53
- start_unix_nano: start_ns,
54
- end_unix_nano: end_ns,
55
- status_code: status_code >= 400 ? status_code : nil,
56
- status_message: error ? error.message : "",
57
- attrs: attrs
58
- )
23
+ if env["QUERY_STRING"] && !env["QUERY_STRING"].empty?
24
+ span_attrs["http.query"] = env["QUERY_STRING"]
25
+ end
59
26
 
60
- level = if status_code >= 500
61
- "error"
62
- elsif status_code >= 400
63
- "warn"
64
- else
65
- "info"
27
+ OpenTelemetry::Context.with_current(extracted_context) do
28
+ @tracer.in_span(span_name, attributes: span_attrs, kind: :server) do |s|
29
+ begin
30
+ status, headers, body = @app.call(env)
31
+ s.set_attribute("http.status_code", status.to_i)
32
+ if status.to_i >= 500
33
+ s.status = OpenTelemetry::Trace::Status.error("HTTP #{status}")
34
+ end
35
+ [status, headers, body]
36
+ rescue => e
37
+ s.record_exception(e)
38
+ s.status = OpenTelemetry::Trace::Status.error(e.message.to_s)
39
+ s.set_attribute("http.status_code", 500)
40
+ raise
41
+ end
66
42
  end
67
-
68
- @client.log(
69
- level,
70
- "#{env["REQUEST_METHOD"]} #{env["PATH_INFO"]} #{status_code} #{duration_ms}ms",
71
- attrs
72
- )
73
43
  end
74
44
  end
75
45
 
76
46
  private
77
47
 
78
- def extract_trace_id(env)
48
+ def extract_context(env)
79
49
  traceparent = env["HTTP_TRACEPARENT"]
80
- return nil unless traceparent
81
- parts = traceparent.split("-")
82
- return nil unless parts.length >= 3
83
- parts[1]
50
+ return OpenTelemetry::Context.current unless traceparent
51
+
52
+ OpenTelemetry.propagation.extract(
53
+ env,
54
+ getter: OpenTelemetry::Common::Propagation.rack_env_getter
55
+ )
56
+ rescue
57
+ OpenTelemetry::Context.current
84
58
  end
85
59
  end
86
60
  end
@@ -0,0 +1,71 @@
1
+ require "opentelemetry/sdk"
2
+ require "opentelemetry-exporter-otlp"
3
+
4
+ module ObtraceSDK
5
+ module OtelSetup
6
+ module_function
7
+
8
+ def configure(cfg)
9
+ endpoint = "#{cfg.ingest_base_url.to_s.sub(%r{/$}, "")}/otlp/v1/traces"
10
+
11
+ exporter = OpenTelemetry::Exporter::OTLP::Exporter.new(
12
+ endpoint: endpoint,
13
+ headers: {
14
+ "Authorization" => "Bearer #{cfg.api_key}"
15
+ }.merge(cfg.default_headers || {}),
16
+ timeout: cfg.request_timeout_sec
17
+ )
18
+
19
+ resource_attrs = {
20
+ "service.name" => cfg.service_name,
21
+ "service.version" => cfg.service_version,
22
+ "deployment.environment" => cfg.env || "dev",
23
+ "runtime.name" => "ruby"
24
+ }
25
+ resource_attrs["obtrace.tenant_id"] = cfg.tenant_id if cfg.tenant_id
26
+ resource_attrs["obtrace.project_id"] = cfg.project_id if cfg.project_id
27
+ resource_attrs["obtrace.app_id"] = cfg.app_id if cfg.app_id
28
+ resource_attrs["obtrace.env"] = cfg.env if cfg.env
29
+
30
+ OpenTelemetry::SDK.configure do |c|
31
+ c.resource = OpenTelemetry::SDK::Resources::Resource.create(resource_attrs)
32
+ c.add_span_processor(
33
+ OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(exporter)
34
+ )
35
+ auto_detect_instrumentations(c)
36
+ end
37
+
38
+ OpenTelemetry.propagation = OpenTelemetry::Trace::Propagation::TraceContext.text_map_propagator
39
+
40
+ OpenTelemetry.tracer_provider
41
+ end
42
+
43
+ def auto_detect_instrumentations(config)
44
+ instrumentations = [
45
+ ["OpenTelemetry::Instrumentation::Net::HTTP", "opentelemetry-instrumentation-net_http"],
46
+ ["OpenTelemetry::Instrumentation::Rack", "opentelemetry-instrumentation-rack"],
47
+ ["OpenTelemetry::Instrumentation::Rails", "opentelemetry-instrumentation-rails"],
48
+ ["OpenTelemetry::Instrumentation::PG", "opentelemetry-instrumentation-pg"],
49
+ ["OpenTelemetry::Instrumentation::Redis", "opentelemetry-instrumentation-redis"],
50
+ ["OpenTelemetry::Instrumentation::Sidekiq", "opentelemetry-instrumentation-sidekiq"],
51
+ ["OpenTelemetry::Instrumentation::Faraday", "opentelemetry-instrumentation-faraday"],
52
+ ["OpenTelemetry::Instrumentation::ActiveRecord", "opentelemetry-instrumentation-active_record"]
53
+ ]
54
+
55
+ threads = instrumentations.map do |class_name, gem_name|
56
+ Thread.new(class_name, gem_name) do |cn, gn|
57
+ begin
58
+ require gn.gsub("-", "/")
59
+ rescue LoadError
60
+ end
61
+ end
62
+ end
63
+ threads.each(&:join)
64
+
65
+ instrumentations.each do |class_name, gem_name|
66
+ klass = Object.const_get(class_name) rescue next
67
+ config.use(class_name) if klass
68
+ end
69
+ end
70
+ end
71
+ end
@@ -2,10 +2,10 @@ require "obtrace_sdk"
2
2
 
3
3
  module ObtraceSDK
4
4
  class Railtie < ::Rails::Railtie
5
- initializer "obtrace_sdk.configure" do |app|
6
- api_key = credentials_fetch("obtrace_api_key") || ENV["OBTRACE_API_KEY"]
7
- ingest_url = credentials_fetch("obtrace_ingest_url") || ENV["OBTRACE_INGEST_BASE_URL"] || "https://inject.obtrace.ai"
8
- service_name = credentials_fetch("obtrace_service_name") || ENV["OBTRACE_SERVICE_NAME"] || ::Rails.application.class.module_parent_name.underscore rescue "rails-app"
5
+ config.after_initialize do |app|
6
+ api_key = Railtie.credentials_fetch("obtrace_api_key") || ENV["OBTRACE_API_KEY"]
7
+ ingest_url = Railtie.credentials_fetch("obtrace_ingest_url") || ENV["OBTRACE_INGEST_BASE_URL"] || "https://inject.obtrace.ai"
8
+ service_name = Railtie.credentials_fetch("obtrace_service_name") || ENV["OBTRACE_SERVICE_NAME"] || ::Rails.application.class.module_parent_name.underscore rescue "rails-app"
9
9
 
10
10
  next unless api_key
11
11
 
@@ -14,9 +14,9 @@ module ObtraceSDK
14
14
  ingest_base_url: ingest_url,
15
15
  service_name: service_name,
16
16
  env: ::Rails.env.to_s,
17
- tenant_id: credentials_fetch("obtrace_tenant_id") || ENV["OBTRACE_TENANT_ID"],
18
- project_id: credentials_fetch("obtrace_project_id") || ENV["OBTRACE_PROJECT_ID"],
19
- app_id: credentials_fetch("obtrace_app_id") || ENV["OBTRACE_APP_ID"],
17
+ tenant_id: Railtie.credentials_fetch("obtrace_tenant_id") || ENV["OBTRACE_TENANT_ID"],
18
+ project_id: Railtie.credentials_fetch("obtrace_project_id") || ENV["OBTRACE_PROJECT_ID"],
19
+ app_id: Railtie.credentials_fetch("obtrace_app_id") || ENV["OBTRACE_APP_ID"],
20
20
  debug: ENV["OBTRACE_DEBUG"] == "true"
21
21
  )
22
22
 
@@ -26,7 +26,7 @@ module ObtraceSDK
26
26
  app.middleware.insert(0, ObtraceSDK::Middleware, client)
27
27
  end
28
28
 
29
- def credentials_fetch(key)
29
+ def self.Railtie.credentials_fetch(key)
30
30
  ::Rails.application.credentials.send(key) rescue nil
31
31
  end
32
32
  end
@@ -1,11 +1,10 @@
1
1
  module ObtraceSDK
2
2
  class Config
3
3
  attr_accessor :api_key, :ingest_base_url, :tenant_id, :project_id, :app_id, :env
4
- attr_accessor :service_name, :service_version, :max_queue_size, :request_timeout_sec
4
+ attr_accessor :service_name, :service_version, :request_timeout_sec
5
5
  attr_accessor :default_headers, :debug, :validate_semantic_metrics
6
- attr_accessor :auto_instrument_http, :auto_capture_logs
7
6
 
8
- def initialize(api_key:, ingest_base_url:, service_name:, tenant_id: nil, project_id: nil, app_id: nil, env: "dev", service_version: "1.0.0", max_queue_size: 1000, request_timeout_sec: 5, default_headers: {}, validate_semantic_metrics: false, debug: false, auto_instrument_http: true, auto_capture_logs: true)
7
+ def initialize(api_key:, ingest_base_url:, service_name:, tenant_id: nil, project_id: nil, app_id: nil, env: "dev", service_version: "1.0.0", request_timeout_sec: 5, default_headers: {}, validate_semantic_metrics: false, debug: false)
9
8
  @api_key = api_key
10
9
  @ingest_base_url = ingest_base_url
11
10
  @tenant_id = tenant_id
@@ -14,13 +13,10 @@ module ObtraceSDK
14
13
  @env = env
15
14
  @service_name = service_name
16
15
  @service_version = service_version
17
- @max_queue_size = max_queue_size
18
16
  @request_timeout_sec = request_timeout_sec
19
17
  @default_headers = default_headers
20
18
  @validate_semantic_metrics = validate_semantic_metrics
21
19
  @debug = debug
22
- @auto_instrument_http = auto_instrument_http
23
- @auto_capture_logs = auto_capture_logs
24
20
  end
25
21
  end
26
22
  end
@@ -1,3 +1,3 @@
1
1
  module ObtraceSDK
2
- VERSION = "1.0.2"
2
+ VERSION = "1.2.0"
3
3
  end
data/lib/obtrace_sdk.rb CHANGED
@@ -1,9 +1,6 @@
1
+ require_relative "obtrace_sdk/version"
1
2
  require_relative "obtrace_sdk/types"
2
- require_relative "obtrace_sdk/context"
3
- require_relative "obtrace_sdk/otlp"
4
- require_relative "obtrace_sdk/http_instrumentation"
5
- require_relative "obtrace_sdk/logger_capture"
6
- require_relative "obtrace_sdk/middleware"
7
- require_relative "obtrace_sdk/client"
8
- require_relative "obtrace_sdk/framework"
9
3
  require_relative "obtrace_sdk/semantic_metrics"
4
+ require_relative "obtrace_sdk/otel_setup"
5
+ require_relative "obtrace_sdk/client"
6
+ require_relative "obtrace_sdk/middleware"
metadata CHANGED
@@ -1,23 +1,65 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: obtrace-sdk-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Obtrace
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-03-28 00:00:00.000000000 Z
11
+ date: 2026-03-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: net-http
14
+ name: opentelemetry-sdk
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.3'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: opentelemetry-api
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.3'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.3'
41
+ - !ruby/object:Gem::Dependency
42
+ name: opentelemetry-exporter-otlp
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.26'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.26'
55
+ - !ruby/object:Gem::Dependency
56
+ name: opentelemetry-instrumentation-net_http
15
57
  requirement: !ruby/object:Gem::Requirement
16
58
  requirements:
17
59
  - - ">="
18
60
  - !ruby/object:Gem::Version
19
61
  version: '0'
20
- type: :runtime
62
+ type: :development
21
63
  prerelease: false
22
64
  version_requirements: !ruby/object:Gem::Requirement
23
65
  requirements:
@@ -25,13 +67,27 @@ dependencies:
25
67
  - !ruby/object:Gem::Version
26
68
  version: '0'
27
69
  - !ruby/object:Gem::Dependency
28
- name: json
70
+ name: opentelemetry-instrumentation-rack
29
71
  requirement: !ruby/object:Gem::Requirement
30
72
  requirements:
31
73
  - - ">="
32
74
  - !ruby/object:Gem::Version
33
75
  version: '0'
34
- type: :runtime
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: opentelemetry-instrumentation-rails
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
35
91
  prerelease: false
36
92
  version_requirements: !ruby/object:Gem::Requirement
37
93
  requirements:
@@ -50,12 +106,8 @@ files:
50
106
  - README.md
51
107
  - lib/obtrace_sdk.rb
52
108
  - lib/obtrace_sdk/client.rb
53
- - lib/obtrace_sdk/context.rb
54
- - lib/obtrace_sdk/framework.rb
55
- - lib/obtrace_sdk/http_instrumentation.rb
56
- - lib/obtrace_sdk/logger_capture.rb
57
109
  - lib/obtrace_sdk/middleware.rb
58
- - lib/obtrace_sdk/otlp.rb
110
+ - lib/obtrace_sdk/otel_setup.rb
59
111
  - lib/obtrace_sdk/rails.rb
60
112
  - lib/obtrace_sdk/semantic_metrics.rb
61
113
  - lib/obtrace_sdk/types.rb
@@ -1,18 +0,0 @@
1
- require "securerandom"
2
-
3
- module ObtraceSDK
4
- module Context
5
- module_function
6
-
7
- def random_hex(bytes)
8
- SecureRandom.hex(bytes)
9
- end
10
-
11
- def ensure_propagation_headers(headers = {}, trace_id: nil, span_id: nil, session_id: nil)
12
- out = (headers || {}).dup
13
- out["traceparent"] ||= "00-#{trace_id || random_hex(16)}-#{span_id || random_hex(8)}-01"
14
- out["x-obtrace-session-id"] ||= session_id if session_id && !session_id.empty?
15
- out
16
- end
17
- end
18
- end
@@ -1,15 +0,0 @@
1
- module ObtraceSDK
2
- module Framework
3
- module_function
4
-
5
- # Rack-compatible middleware baseline used by Rails.
6
- def rack_middleware(client, app)
7
- lambda do |env|
8
- client.log("info", "request.start", { method: env["REQUEST_METHOD"], path: env["PATH_INFO"] })
9
- status, headers, body = app.call(env)
10
- client.log("info", "request.finish", { status: status.to_i })
11
- [status, headers, body]
12
- end
13
- end
14
- end
15
- end
@@ -1,122 +0,0 @@
1
- require "net/http"
2
-
3
- module ObtraceSDK
4
- module HttpInstrumentation
5
- @client = nil
6
- @installed = false
7
-
8
- module_function
9
-
10
- def install(client)
11
- return if @installed
12
- @client = client
13
- @installed = true
14
-
15
- Net::HTTP.prepend(NetHttpPatch)
16
- end
17
-
18
- def client
19
- @client
20
- end
21
-
22
- def installed?
23
- @installed
24
- end
25
-
26
- def reset!
27
- @client = nil
28
- @installed = false
29
- end
30
-
31
- module NetHttpPatch
32
- def request(req, body = nil, &block)
33
- client = ObtraceSDK::HttpInstrumentation.client
34
- unless client
35
- return super
36
- end
37
-
38
- uri = URI.parse("#{use_ssl? ? 'https' : 'http'}://#{address}:#{port}#{req.path}")
39
-
40
- if uri.host && ObtraceSDK::HttpInstrumentation.own_endpoint?(uri)
41
- return super
42
- end
43
-
44
- trace_id = ObtraceSDK::Context.random_hex(16)
45
- span_id = ObtraceSDK::Context.random_hex(8)
46
-
47
- req["traceparent"] ||= "00-#{trace_id}-#{span_id}-01"
48
-
49
- start_ns = ObtraceSDK::Otlp.now_unix_nano
50
- start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
51
- status_code = nil
52
- error = nil
53
-
54
- begin
55
- response = super
56
- status_code = response.code.to_i
57
- response
58
- rescue => e
59
- error = e
60
- raise
61
- ensure
62
- end_ns = ObtraceSDK::Otlp.now_unix_nano
63
- duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000).round(2)
64
-
65
- attrs = {
66
- "http.method" => req.method,
67
- "http.url" => uri.to_s,
68
- "http.host" => uri.host.to_s,
69
- "net.peer.port" => uri.port.to_s,
70
- "http.duration_ms" => duration_ms,
71
- "auto.source" => "http_instrumentation"
72
- }
73
-
74
- if status_code
75
- attrs["http.status_code"] = status_code
76
- end
77
-
78
- if error
79
- attrs["error"] = true
80
- attrs["error.type"] = error.class.to_s
81
- attrs["error.message"] = error.message.to_s
82
- end
83
-
84
- span_status = if error
85
- status_code = 500
86
- 500
87
- elsif status_code && status_code >= 400
88
- status_code
89
- else
90
- nil
91
- end
92
-
93
- client.span(
94
- "HTTP #{req.method}",
95
- trace_id: trace_id,
96
- span_id: span_id,
97
- start_unix_nano: start_ns,
98
- end_unix_nano: end_ns,
99
- status_code: span_status,
100
- status_message: error ? error.message : "",
101
- attrs: attrs
102
- )
103
-
104
- client.log(
105
- status_code && status_code >= 400 ? "warn" : "info",
106
- "HTTP #{req.method} #{uri.host}#{uri.path} #{status_code || 'ERR'}",
107
- attrs
108
- )
109
- end
110
- end
111
- end
112
-
113
- def self.own_endpoint?(uri)
114
- return false unless @client
115
- cfg = @client.instance_variable_get(:@cfg)
116
- return false unless cfg
117
- ingest_uri = URI.parse(cfg.ingest_base_url.to_s) rescue nil
118
- return false unless ingest_uri
119
- uri.host == ingest_uri.host && uri.port == ingest_uri.port
120
- end
121
- end
122
- end
@@ -1,68 +0,0 @@
1
- require "logger"
2
-
3
- module ObtraceSDK
4
- module LoggerCapture
5
- @client = nil
6
- @installed = false
7
-
8
- SEVERITY_MAP = {
9
- 0 => "debug",
10
- 1 => "info",
11
- 2 => "warn",
12
- 3 => "error",
13
- 4 => "fatal",
14
- 5 => "unknown"
15
- }.freeze
16
-
17
- module_function
18
-
19
- def install(client)
20
- return if @installed
21
- @client = client
22
- @installed = true
23
-
24
- ::Logger.prepend(LoggerPatch)
25
- end
26
-
27
- def client
28
- @client
29
- end
30
-
31
- def installed?
32
- @installed
33
- end
34
-
35
- def reset!
36
- @client = nil
37
- @installed = false
38
- end
39
-
40
- module LoggerPatch
41
- def add(severity, message = nil, progname = nil, &block)
42
- result = super
43
-
44
- client = ObtraceSDK::LoggerCapture.client
45
- if client
46
- if message.nil?
47
- if block
48
- msg = block.call
49
- else
50
- msg = progname
51
- end
52
- else
53
- msg = message
54
- end
55
-
56
- if msg && !msg.to_s.empty?
57
- level = ObtraceSDK::LoggerCapture::SEVERITY_MAP[severity] || "unknown"
58
- attrs = { "auto.source" => "logger_capture" }
59
- attrs["logger.progname"] = progname.to_s if progname && message
60
- client.log(level, msg.to_s, attrs)
61
- end
62
- end
63
-
64
- result
65
- end
66
- end
67
- end
68
- end
@@ -1,127 +0,0 @@
1
- module ObtraceSDK
2
- module Otlp
3
- module_function
4
-
5
- def attrs(hash)
6
- return [] if hash.nil?
7
- hash.map do |k, v|
8
- value =
9
- case v
10
- when TrueClass, FalseClass
11
- { "boolValue" => v }
12
- when Numeric
13
- { "doubleValue" => v.to_f }
14
- else
15
- { "stringValue" => v.to_s }
16
- end
17
- { "key" => k.to_s, "value" => value }
18
- end
19
- end
20
-
21
- def resource(cfg)
22
- base = {
23
- "service.name" => cfg.service_name,
24
- "service.version" => cfg.service_version,
25
- "deployment.environment" => cfg.env || "dev",
26
- "runtime.name" => "ruby"
27
- }
28
- base["obtrace.tenant_id"] = cfg.tenant_id if cfg.tenant_id
29
- base["obtrace.project_id"] = cfg.project_id if cfg.project_id
30
- base["obtrace.app_id"] = cfg.app_id if cfg.app_id
31
- base["obtrace.env"] = cfg.env if cfg.env
32
- attrs(base)
33
- end
34
-
35
- def now_unix_nano
36
- (Time.now.to_f * 1_000_000_000).to_i.to_s
37
- end
38
-
39
- def logs_payload(cfg, level, message, context = nil)
40
- context_attrs = { "obtrace.log.level" => level }
41
- if context
42
- context.each { |k, v| context_attrs["obtrace.attr.#{k}"] = v }
43
- end
44
-
45
- {
46
- "resourceLogs" => [
47
- {
48
- "resource" => { "attributes" => resource(cfg) },
49
- "scopeLogs" => [
50
- {
51
- "scope" => { "name" => "obtrace-sdk-ruby", "version" => "1.0.0" },
52
- "logRecords" => [
53
- {
54
- "timeUnixNano" => now_unix_nano,
55
- "severityText" => level.to_s.upcase,
56
- "body" => { "stringValue" => message.to_s },
57
- "attributes" => attrs(context_attrs)
58
- }
59
- ]
60
- }
61
- ]
62
- }
63
- ]
64
- }
65
- end
66
-
67
- def metric_payload(cfg, name, value, unit = "1", context = nil)
68
- {
69
- "resourceMetrics" => [
70
- {
71
- "resource" => { "attributes" => resource(cfg) },
72
- "scopeMetrics" => [
73
- {
74
- "scope" => { "name" => "obtrace-sdk-ruby", "version" => "1.0.0" },
75
- "metrics" => [
76
- {
77
- "name" => name.to_s,
78
- "unit" => unit.to_s,
79
- "gauge" => {
80
- "dataPoints" => [
81
- {
82
- "timeUnixNano" => now_unix_nano,
83
- "asDouble" => value.to_f,
84
- "attributes" => attrs(context || {})
85
- }
86
- ]
87
- }
88
- }
89
- ]
90
- }
91
- ]
92
- }
93
- ]
94
- }
95
- end
96
-
97
- def span_payload(cfg, name, trace_id, span_id, start_unix_nano, end_unix_nano, status_code = nil, status_message = "", attrs_hash = nil)
98
- {
99
- "resourceSpans" => [
100
- {
101
- "resource" => { "attributes" => resource(cfg) },
102
- "scopeSpans" => [
103
- {
104
- "scope" => { "name" => "obtrace-sdk-ruby", "version" => "1.0.0" },
105
- "spans" => [
106
- {
107
- "traceId" => trace_id,
108
- "spanId" => span_id,
109
- "name" => name.to_s,
110
- "kind" => 3,
111
- "startTimeUnixNano" => start_unix_nano,
112
- "endTimeUnixNano" => end_unix_nano,
113
- "attributes" => attrs(attrs_hash || {}),
114
- "status" => {
115
- "code" => status_code && status_code.to_i >= 400 ? 2 : 1,
116
- "message" => status_message.to_s
117
- }
118
- }
119
- ]
120
- }
121
- ]
122
- }
123
- ]
124
- }
125
- end
126
- end
127
- end