railswatch_gem 0.1.5 → 0.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.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +61 -93
  3. data/lib/railswatch_gem/configuration.rb +6 -44
  4. data/lib/railswatch_gem/helpers.rb +45 -54
  5. data/lib/railswatch_gem/instrumentation/ai_helper.rb +45 -0
  6. data/lib/railswatch_gem/instrumentation/anthropic.rb +61 -0
  7. data/lib/railswatch_gem/instrumentation/openai.rb +61 -0
  8. data/lib/railswatch_gem/railtie.rb +5 -12
  9. data/lib/railswatch_gem/version.rb +1 -1
  10. data/lib/railswatch_gem.rb +46 -63
  11. metadata +67 -29
  12. data/CHANGELOG.md +0 -5
  13. data/LICENSE.txt +0 -21
  14. data/Rakefile +0 -8
  15. data/lib/railswatch_gem/client.rb +0 -158
  16. data/lib/railswatch_gem/instrumentation/cache_instrumenter.rb +0 -69
  17. data/lib/railswatch_gem/instrumentation/commands_instrumenter.rb +0 -89
  18. data/lib/railswatch_gem/instrumentation/errors_instrumenter.rb +0 -68
  19. data/lib/railswatch_gem/instrumentation/jobs_instrumenter.rb +0 -82
  20. data/lib/railswatch_gem/instrumentation/logs_instrumenter.rb +0 -112
  21. data/lib/railswatch_gem/instrumentation/mail_instrumenter.rb +0 -65
  22. data/lib/railswatch_gem/instrumentation/models_instrumenter.rb +0 -90
  23. data/lib/railswatch_gem/instrumentation/notifications_instrumenter.rb +0 -63
  24. data/lib/railswatch_gem/instrumentation/outgoing_requests_instrumenter.rb +0 -99
  25. data/lib/railswatch_gem/instrumentation/queries_instrumenter.rb +0 -69
  26. data/lib/railswatch_gem/instrumentation/registry.rb +0 -49
  27. data/lib/railswatch_gem/instrumentation/requests_instrumenter.rb +0 -159
  28. data/lib/railswatch_gem/instrumentation/scheduled_tasks_instrumenter.rb +0 -85
  29. data/lib/railswatch_gem/middleware/request_context.rb +0 -28
  30. data/manual_test.rb +0 -103
  31. data/sig/railswatch_gem.rbs +0 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 31cef41294d874fdcf6b8e23b0f34d7f954359b27e60bcea834d178405dd9dc5
4
- data.tar.gz: 24f4ccf4c3d9617aad4340dd2b41cf31d8028dfd3b443361f9aa90b97afe9781
3
+ metadata.gz: 540f88538c4fa2d5a156e22d680839b8768ea224eff4719729c3548731fadc67
4
+ data.tar.gz: 40e65222f075c543739f096dd197e9e1aef3fa8d66568d8af9447cb44ff928a9
5
5
  SHA512:
6
- metadata.gz: 22fadd59c98cda3262e290dd2cbbed57d133469be81cb6f68951062b4dd25f51a92b4f972822d98681f9b9d5298ac23d21168a87f2907ddc0fb3a267a950f441
7
- data.tar.gz: 1aceb6e85b61bea68bb581c6590d6cee2847fbab7e5dfa95883e4a2371ffd617e462ff2f8d92a0766ca827d5c9e6dbf66ff3769c052ef6f7df618ecac14c9876
6
+ metadata.gz: c3cda587349e79a8908db25a6d6599bc6b08d0d57d5ac398cf4d8e108cc62290f4e281ee374d883076a50a13ae306c6088327b8efca9567a921b0a62d8980398
7
+ data.tar.gz: 718a3a2eff1bd4c83913ccdbdee9106ac0d372e2937d426ddca35c6805d0db90047c84f7eaa8e1225db4fc632c80f483326d1a113e0758f825d17bbb0b6cb509
data/README.md CHANGED
@@ -1,130 +1,98 @@
1
1
  # RailsWatch Gem
2
2
 
3
- RailsWatch is a lightweight, high-performance observability probe for Ruby on Rails applications. It captures requests, SQL queries, background jobs, logs, and exceptions in real-time and forwards them to your RailsWatch dashboard.
3
+ **Instant, zero-config observability for Ruby on Rails.**
4
4
 
5
- Designed to be the Rails equivalent of Laravel Nightwatch/Telescope, it provides deep insight into your application's lifecycle without the overhead of heavy APM tools.
5
+ The \`railswatch_gem\` automatically instruments your Rails application using **OpenTelemetry**. It captures request traces, database queries, background jobs, and—most importantly—provides deep visibility into your AI integration costs and latency (OpenAI & Anthropic).
6
6
 
7
- ## 🚀 Features
7
+ ## Features
8
8
 
9
- - **Zero-Config**: Automatically hooks into Rails via ActiveSupport::Notifications.
9
+ * **Auto-Instrumentation:** Zero code required to track Rails requests, Sidekiq jobs, Postgres queries, Redis commands, and Net::HTTP calls.
10
+ * **AI Monitoring:** Automatically tracks token usage, costs, model names, and latency for \`ruby-openai\` and \`anthropic\` gems.
11
+ * **Debug Dump:** A global \`rw()\` helper to instantly dump variables into your trace timeline without clogging your stdout logs.
12
+ * **Standard OTLP:** Uses the industry-standard OpenTelemetry Protocol.
10
13
 
11
- - **Full Lifecycle Tracking**:
12
- - HTTP Requests (Headers, Session, Params)
13
- - Database Queries (SQL, Duration)
14
- - Background Jobs (Sidekiq, ActiveJob)
15
- - Mail Deliveries
16
- - Cache Hits/Misses (Redis, Memcached)
17
- - Outgoing HTTP Requests (Net::HTTP)
18
- - Rake Tasks & CLI Commands
14
+ ## Installation
19
15
 
20
- - **Active Debugging**: Includes a global `rw()` helper to dump variables to your dashboard instantly.
16
+ Add this line to your application'\''s Gemfile:
21
17
 
22
- - **Performance First**: Uses a background thread and buffered queue to ensure your application performance is never impacted by monitoring.
23
-
24
- - **Safe**: Handles thread-safety and swallows internal errors to prevent bringing down your app.
25
-
26
- ## 📦 Installation
27
-
28
- Add this line to your application's Gemfile:
29
-
30
- ```ruby
31
- gem 'railswatch_gem'
32
- ```
18
+ gem '\''railswatch_gem'\''
33
19
 
34
20
  And then execute:
35
21
 
36
- ```bash
37
- bundle install
38
- ```
22
+ bundle install
39
23
 
40
- ## ⚙️ Configuration
24
+ ## Configuration
41
25
 
42
- Create an initializer file at `config/initializers/railswatch.rb`. You will need your Project Token from your RailsWatch dashboard settings.
26
+ The gem is configured entirely via environment variables. You do not need an initializer file.
43
27
 
44
- ```ruby
45
- # config/initializers/railswatch.rb
28
+ Add these to your \`.env\` file or production environment:
46
29
 
47
- RailswatchGem.configure do |config|
48
- # The URL of your Railswatch SaaS instance (or self-hosted)
49
- config.ingest_url = "https://api.railswatch.com/api/v1/ingest"
30
+ # Required
31
+ RAILSWATCH_API_KEY=rw_live_...
32
+ RAILSWATCH_SERVICE_NAME=my-rails-app
50
33
 
51
- # Your unique Project Token
52
- config.env_token = ENV.fetch("RAILSWATCH_TOKEN", "your-project-token")
34
+ # Optional (Defaults to standard RailsWatch ingest)
35
+ # RAILSWATCH_INGEST_URL=https://ingest.railswatch.com/v1/traces
53
36
 
54
- # Optional: Fine-tune performance
55
- # config.batch_size = 100 # Max events to send in one HTTP request
56
- # config.flush_interval = 2.0 # Send data every 2 seconds
57
- end
58
- ```
37
+ Once these variables are present, \`railswatch_gem\` will automatically start when Rails boots.
59
38
 
60
- That's it! Restart your application, and data will start flowing to your dashboard.
39
+ ## Usage
61
40
 
62
- ## 🛠 Usage
41
+ ### 1. Standard Observability
42
+ Just run your app!
43
+ * **Controllers:** Every request is traced.
44
+ * **Database:** SQL queries are captured as spans.
45
+ * **Jobs:** Sidekiq jobs are linked to the web requests that enqueued them.
63
46
 
64
- ### The rw() Helper
47
+ ### 2. AI Monitoring
48
+ If you use the \`ruby-openai\` or \`anthropic\` gems, we automatically patch them to record rich telemetry.
65
49
 
66
- Stop using `puts` or `binding.pry` in production. Use the global `rw()` helper (aliased as `railswatch_dump`) to send any variable directly to your "Dumps" dashboard tab.
50
+ **Supported Libraries:**
51
+ * \`ruby-openai\`
52
+ * \`anthropic\`
67
53
 
68
- ```ruby
69
- class UsersController < ApplicationController
70
- def create
71
- @user = User.new(user_params)
54
+ **What we capture:**
55
+ * Model Name (e.g., \`gpt-4\`, \`claude-3-sonnet\`)
56
+ * Token Usage (Prompt, Completion, and Total)
57
+ * Latency
58
+ * System Fingerprints
72
59
 
73
- # Inspect the user object in your dashboard without stopping the request
74
- rw(@user)
60
+ **Example:**
75
61
 
76
- if @user.save
77
- # ...
78
- end
79
- end
80
- end
81
- ```
82
-
83
- It returns the original object, so you can wrap expressions transparently:
62
+ # This API call will automatically appear in your RailsWatch dashboard
63
+ client = OpenAI::Client.new
64
+ client.chat(parameters: { model: "gpt-4", messages: [...] })
84
65
 
85
- ```ruby
86
- # The result is assigned to 'result' AND sent to the dashboard
87
- result = rw(ComplexCalculation.perform(x, y))
88
- ```
66
+ ### 3. The \`rw()\` Debug Helper
67
+ Stop using \`puts\` or \`Rails.logger.info\` for debugging complex objects. Use the global \`rw()\` helper.
89
68
 
90
- ### Manual Logging
69
+ It dumps the object into the **current trace timeline**, allowing you to see exactly *when* a variable had a specific value relative to your DB queries and API calls.
91
70
 
92
- RailsWatch automatically captures standard Rails.logger output. However, you can also manually record specific events if needed:
71
+ def index
72
+ @users = User.all
93
73
 
94
- ```ruby
95
- RailswatchGem.record({
96
- event_type: "custom_alert",
97
- message: "Something interesting happened",
98
- metadata: { user_id: 123 }
99
- })
100
- ```
74
+ # Dumps the array to RailsWatch without stopping execution
75
+ rw(@users)
101
76
 
102
- ## 🔒 Security
103
-
104
- - **Parameter Filtering**: RailsWatch respects your `Rails.application.config.filter_parameters`. Passwords and secrets defined there are scrubbed before leaving your server.
77
+ # You can also pass through values (returns the argument)
78
+ @user = rw(User.first)
79
+ end
105
80
 
106
- - **Async Processing**: Data is buffered in memory and sent asynchronously. If the ingestion API is down, your app continues running smoothly.
81
+ ## Troubleshooting
107
82
 
108
- ## 🧩 Supported Instrumenters
83
+ If you don'\''t see data in your dashboard:
109
84
 
110
- RailsWatch automatically instruments the following libraries if they are loaded:
85
+ 1. **Check the Logs:**
86
+ On boot, you should see: \`[Railswatch] Observability started for my-rails-app\`.
111
87
 
112
- | Instrumenter | Description |
113
- |-------------|-------------|
114
- | Requests | Controller actions, status codes, paths, IP addresses. |
115
- | Queries | ActiveRecord SQL queries (filters out SCHEMA/EXPLAIN). |
116
- | Jobs | ActiveJob & Sidekiq execution, latency, and retries. |
117
- | Mail | ActionMailer deliveries, recipients, and subjects. |
118
- | Cache | ActiveSupport::Cache reads, writes, and hit rates. |
119
- | Outbound | Net::HTTP requests to external APIs. |
120
- | Models | ActiveRecord Create/Update/Destroy events with changesets. |
121
- | Exceptions | Unhandled exceptions and Rails.error.report calls. |
122
- | Commands | Rake tasks and Thor CLI commands. |
88
+ 2. **Verify Environment Variables:**
89
+ Ensure \`RAILSWATCH_API_KEY\` is available in your shell/environment.
123
90
 
124
- ## 🤝 Contributing
91
+ 3. **OpenTelemetry Debugging:**
92
+ To see exactly what OTel is doing, enable the internal logger:
125
93
 
126
- Bug reports and pull requests are welcome on GitHub at https://github.com/yourusername/railswatch_gem. This project is intended to be a safe, welcoming space for collaboration.
94
+ OTEL_LOG_LEVEL=debug rails s
127
95
 
128
- ## 📄 License
96
+ ## License
129
97
 
130
- The gem is available as open source under the terms of the MIT License.
98
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -2,52 +2,14 @@
2
2
 
3
3
  module RailswatchGem
4
4
  class Configuration
5
- attr_accessor :env_token, :batch_size, :flush_interval, :ingest_url, :auto_boot
6
- attr_reader :enabled_events, :custom_event_blocks
7
-
8
- DEFAULT_EVENTS = %i[
9
- requests
10
- queries
11
- outgoing_requests
12
- jobs
13
- scheduled_tasks
14
- commands
15
- cache
16
- mail
17
- notifications
18
- logs
19
- errors
20
- models
21
- ].freeze
22
-
23
- DEFAULT_INGEST_URL = "https://api.railswatch.com/ingest".freeze
5
+ attr_accessor :api_key, :service_name, :ingest_url, :enabled
24
6
 
25
7
  def initialize
26
- @env_token = nil
27
- @batch_size = 200
28
- @flush_interval = 2.0
29
- @ingest_url = DEFAULT_INGEST_URL
30
- @auto_boot = true # Defaults to true for ease of use
31
-
32
- # all default event families enabled
33
- @enabled_events = DEFAULT_EVENTS.dup
34
-
35
- # user-supplied instrumentation hooks
36
- @custom_event_blocks = []
37
- end
38
-
39
- def enable_event(name)
40
- name = name.to_sym
41
- @enabled_events << name unless @enabled_events.include?(name)
42
- end
43
-
44
- def disable_event(name)
45
- @enabled_events.delete(name.to_sym)
46
- end
47
-
48
- # for custom subscriptions using the client
49
- def on_custom_event(&block)
50
- @custom_event_blocks << block if block
8
+ @api_key = ENV["RAILSWATCH_API_KEY"]
9
+ default_name = defined?(::Rails) ? ::Rails.application.class.module_parent_name.underscore.dasherize : "rails-app"
10
+ @service_name = ENV["RAILSWATCH_SERVICE_NAME"] || default_name
11
+ @ingest_url = ENV["RAILSWATCH_INGEST_URL"] || "https://ingest.railswatch.com/v1/traces"
12
+ @enabled = true
51
13
  end
52
14
  end
53
15
  end
@@ -1,71 +1,62 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "json"
4
+
3
5
  module RailswatchGem
4
6
  module Helpers
5
- # Usage: rw(user, params) or railswatch_dump(variable)
7
+ # Types that can be safely serialized to JSON without inspection
8
+ SAFE_JSON_TYPES = [Hash, Array, String, Integer, Float, TrueClass, FalseClass, NilClass].freeze
9
+ MAX_PAYLOAD_SIZE = 4096
10
+
6
11
  def railswatch_dump(*args)
7
- # 1. Capture the location where this was called (file:line)
8
- call_site = caller(1, 1).first
9
-
10
- # 2. Correlate with request if inside one
11
- request_id = Thread.current[:railswatch_request_id]
12
-
13
- # 3. Format the arguments for display
14
- formatted_args = args.map do |arg|
15
- format_dump_arg(arg)
16
- end
17
-
18
- payload = {
19
- event_type: "dump",
20
- timestamp: Time.now.utc.iso8601,
21
- values: formatted_args,
22
- location: call_site,
23
- request_id: request_id
24
- }
25
-
26
- # Send to client
27
- RailswatchGem.record(payload)
28
-
29
- # 4. Return original args to allow debugging without breaking chains:
30
- # return rw(result)
31
- if args.size == 1
32
- args.first
33
- else
34
- args
35
- end
12
+ span = OpenTelemetry::Trace.current_span
13
+ return return_value(args) unless span.recording?
14
+
15
+ file, line = parse_caller_location
16
+
17
+ json_content = serialize_args(args)
18
+
19
+ span.add_event("railswatch.dump", attributes: {
20
+ "code.filepath" => file,
21
+ "code.lineno" => line,
22
+ "railswatch.dump.content" => truncate_payload(json_content)
23
+ })
24
+
25
+ return_value(args)
36
26
  rescue => e
37
- warn "RailswatchGem: Dump failed: #{e.message}"
38
- # Always return args so we don't break the app flow on error
39
- args.size == 1 ? args.first : args
27
+ # Fail silently, return original args
28
+ return_value(args)
40
29
  end
41
30
 
42
- # Short alias for ease of use
43
31
  alias_method :rw, :railswatch_dump
44
32
 
45
33
  private
46
34
 
47
- def format_dump_arg(arg)
48
- case arg
49
- when Hash
50
- # Attempt to keep hashes as structures for the JSON viewer in the dashboard
51
- begin
52
- arg.transform_keys(&:to_s)
53
- rescue
54
- arg.inspect
55
- end
56
- when Array
57
- arg.map { |item| format_dump_arg(item) }
58
- when String, Numeric, TrueClass, FalseClass, NilClass
59
- arg
60
- else
61
- # For Active Record models or complex objects, .inspect gives the string representation
62
- # which is usually what developers want to see.
63
- arg.inspect
64
- end
35
+ def return_value(args)
36
+ args.size == 1 ? args.first : args
37
+ end
38
+
39
+ def parse_caller_location
40
+ # Returns ["/path/to/file.rb", "45"]
41
+ caller(2, 1).first.to_s.split(":")[0..1]
42
+ end
43
+
44
+ def serialize_args(args)
45
+ args.map { |arg| sanitize_arg(arg) }.to_json
46
+ end
47
+
48
+ def sanitize_arg(arg)
49
+ return arg if SAFE_JSON_TYPES.any? { |t| arg.is_a?(t) }
50
+
51
+ # Fallback for complex objects (ActiveRecord, Custom Classes)
52
+ arg.inspect
53
+ end
54
+
55
+ def truncate_payload(json)
56
+ return json if json.bytesize <= MAX_PAYLOAD_SIZE
57
+ json.byteslice(0, MAX_PAYLOAD_SIZE - 3) + "..."
65
58
  end
66
59
  end
67
60
  end
68
61
 
69
- # Automatically mix into the top-level Object so it's available everywhere
70
- # (Models, Controllers, Views, Console, Irb)
71
62
  Object.include(RailswatchGem::Helpers) if defined?(Object)
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailswatchGem
4
+ module Instrumentation
5
+ module AiHelper
6
+ MAX_CONTENT_SIZE = 4096
7
+
8
+ def trace_ai_request(tracer, span_name, attributes = {})
9
+ span = tracer.start_span(span_name, kind: :client)
10
+
11
+ OpenTelemetry::Trace.with_span(span) do
12
+ # 1. Set Standard Request Attributes
13
+ span.set_attribute("gen_ai.system", attributes[:system])
14
+ span.set_attribute("gen_ai.request.model", attributes[:model].to_s)
15
+
16
+ if attributes[:prompt_role]
17
+ span.set_attribute("gen_ai.prompt.role", attributes[:prompt_role].to_s)
18
+ end
19
+
20
+ if attributes[:prompt_content]
21
+ span.set_attribute("gen_ai.prompt.content", truncate(attributes[:prompt_content].to_s))
22
+ end
23
+
24
+ # 2. Yield span to the block so the provider can set its own stats
25
+ response = yield(span)
26
+
27
+ response
28
+ rescue => e
29
+ span.record_exception(e)
30
+ span.status = OpenTelemetry::Trace::Status.error("#{attributes[:system]} Request Failed: #{e.message}")
31
+ raise e
32
+ ensure
33
+ span.finish
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def truncate(str)
40
+ return str if str.bytesize <= MAX_CONTENT_SIZE
41
+ str.byteslice(0, MAX_CONTENT_SIZE - 3) + "..."
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "railswatch_gem/instrumentation/ai_helper"
4
+
5
+ module RailswatchGem
6
+ module Instrumentation
7
+ module Anthropic
8
+ extend AiHelper
9
+
10
+ def self.install
11
+ return unless defined?(::Anthropic::Client)
12
+ ::Anthropic::Client.prepend(Patches)
13
+ end
14
+
15
+ module Patches
16
+ def messages(parameters = {})
17
+ model = parameters[:model] || parameters["model"]
18
+ messages = parameters[:messages] || parameters["messages"]
19
+ last_msg = messages&.last
20
+
21
+ prompt_role = last_msg.is_a?(Hash) ? (last_msg[:role] || last_msg["role"]) : nil
22
+ prompt_content = last_msg.is_a?(Hash) ? (last_msg[:content] || last_msg["content"]) : nil
23
+
24
+ RailswatchGem::Instrumentation::Anthropic.trace_ai_request(
25
+ tracer,
26
+ "gen_ai.anthropic.messages",
27
+ system: "anthropic",
28
+ model: model,
29
+ prompt_role: prompt_role,
30
+ prompt_content: prompt_content
31
+ ) do |span|
32
+ response = super
33
+
34
+ # 2. Extract Stats (Anthropic Format)
35
+ if response.is_a?(Hash)
36
+ if (usage = response["usage"])
37
+ input = usage["input_tokens"].to_i
38
+ output = usage["output_tokens"].to_i
39
+ span.set_attribute("gen_ai.usage.input_tokens", input)
40
+ span.set_attribute("gen_ai.usage.output_tokens", output)
41
+ span.set_attribute("gen_ai.usage.total_tokens", input + output)
42
+ end
43
+
44
+ if response["stop_reason"]
45
+ span.set_attribute("gen_ai.response.finish_reason", response["stop_reason"].to_s)
46
+ end
47
+ end
48
+
49
+ response
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ def tracer
56
+ OpenTelemetry.tracer_provider.tracer("railswatch_gem-ai")
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "railswatch_gem/instrumentation/ai_helper"
4
+
5
+ module RailswatchGem
6
+ module Instrumentation
7
+ module OpenAI
8
+ extend AiHelper
9
+
10
+ def self.install
11
+ return unless defined?(::OpenAI::Client)
12
+ ::OpenAI::Client.prepend(Patches)
13
+ end
14
+
15
+ module Patches
16
+ def chat(parameters = {})
17
+ model = parameters[:model] || parameters["model"]
18
+ messages = parameters[:messages] || parameters["messages"]
19
+ last_msg = messages&.last
20
+
21
+ # Extract prompt safely
22
+ prompt_role = last_msg.is_a?(Hash) ? (last_msg[:role] || last_msg["role"]) : nil
23
+ prompt_content = last_msg.is_a?(Hash) ? (last_msg[:content] || last_msg["content"]) : nil
24
+
25
+ RailswatchGem::Instrumentation::OpenAI.trace_ai_request(
26
+ tracer,
27
+ "gen_ai.chat",
28
+ system: "openai",
29
+ model: model,
30
+ prompt_role: prompt_role,
31
+ prompt_content: prompt_content
32
+ ) do |span|
33
+ # 1. Call original method
34
+ response = super
35
+
36
+ # 2. Extract Stats (OpenAI Format)
37
+ if response.is_a?(Hash)
38
+ if (usage = response["usage"])
39
+ span.set_attribute("gen_ai.usage.input_tokens", usage["prompt_tokens"].to_i)
40
+ span.set_attribute("gen_ai.usage.output_tokens", usage["completion_tokens"].to_i)
41
+ span.set_attribute("gen_ai.usage.total_tokens", usage["total_tokens"].to_i)
42
+ end
43
+
44
+ if (choice = response.dig("choices", 0))
45
+ span.set_attribute("gen_ai.response.finish_reason", choice["finish_reason"].to_s)
46
+ end
47
+ end
48
+
49
+ response
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ def tracer
56
+ OpenTelemetry.tracer_provider.tracer("railswatch_gem-ai")
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -1,21 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "rails/railtie"
4
- require_relative "middleware/request_context"
5
-
6
3
  module RailswatchGem
7
4
  class Railtie < ::Rails::Railtie
8
- # Insert our middleware at the very top (index 0) to ensure we catch everything,
9
- # including potential errors in other middlewares.
10
- initializer "railswatch.middleware" do |app|
11
- app.middleware.insert_before 0, RailswatchGem::Middleware::RequestContext
12
- end
13
-
14
5
  # Automatically boot the instrumentation when Rails finishes initializing.
15
6
  config.after_initialize do
16
- # Only boot if the user hasn't disabled auto_boot in their config
17
- if RailswatchGem.configuration.auto_boot
18
- RailswatchGem.boot!
7
+ # Check if properly configured (e.g. via ENV vars or initializer)
8
+ if RailswatchGem.config.api_key.present?
9
+ RailswatchGem.start!
10
+ else
11
+ puts "[Railswatch] API Key not found. Observability disabled."
19
12
  end
20
13
  end
21
14
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailswatchGem
4
- VERSION = "0.1.5"
4
+ VERSION = "0.2.0"
5
5
  end