paid_ruby 0.1.1 → 0.5.0.pre.alpha1

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: e08b12d8087ea612ca1b405c5d6cfa6fab4374f4f89ad81df1a7042b31206c72
4
- data.tar.gz: 7dd1f1ef4c71fe4dd19d3c34e6eacd051a95087b74ec5be2f610efe9608e2a55
3
+ metadata.gz: a2dcc5d043c51e405cbfaf7218d192091d7ee650616596eef5c26ed790b52a78
4
+ data.tar.gz: 8e0462f0215cbdb94d810ed7c04309a4d1581bcba0efd02d9d49220451625ebc
5
5
  SHA512:
6
- metadata.gz: 6eed37d4f15ac6939c644a4c7db14fbcaeb3d5e9e67d16b983d3060790f0daeb6054b1e5beb0241470bdaeca393bdc119503dc2ffd893272e9d522c62031059c
7
- data.tar.gz: 931419aa3d4767c1df1f7c0ca332db5e629b73f7d38b3f8cdc61463626c726476ac89f327434ecd08bebdc631c8dbcc726484b925a6d4f3a821695b5220a4684
6
+ metadata.gz: 5f493692e13c4a3db9295e66268aa7e982a26425608f86306eca53fa3ad9e667ac389a93aab6292e16bd15686e6b947cc6df9861e387250172b245f6ad27094b
7
+ data.tar.gz: 36d9a998cc9813589c53842cb8293141df47625c8b6b3a84c2867ed08720bf9cecd2533d53e87e412ce7a4c85056e9daf7c1f5aa29b6daf08c8c11d9aa303340
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+
5
+ module Paid
6
+ module Tracing
7
+ # Provides a central logger for the gem.
8
+ # The log level can be configured via the PAID_LOG_LEVEL environment variable.
9
+ # Supported levels are DEBUG, INFO, WARN, ERROR, FATAL.
10
+ # If the variable is not set, the level defaults to FATAL to suppress output.
11
+ module Logging
12
+ def self.logger
13
+ @logger ||= begin
14
+ log_level_str = ENV["PAID_LOG_LEVEL"]&.upcase
15
+ level = if log_level_str && Logger.const_defined?(log_level_str)
16
+ Logger.const_get(log_level_str)
17
+ else
18
+ # Default to a level that shows no logs unless explicitly configured.
19
+ Logger::FATAL
20
+ end
21
+
22
+ logger = Logger.new($stdout)
23
+ logger.level = level
24
+ logger.formatter = proc do |severity, _datetime, _progname, msg|
25
+ "[Paid SDK] #{severity}: #{msg}\n"
26
+ end
27
+ logger
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "opentelemetry/sdk"
4
+ require "opentelemetry/exporter/otlp"
5
+ require_relative "logging"
6
+
7
+ module Paid
8
+ module Tracing
9
+ @token = nil
10
+
11
+ # Context keys to propagate external_customer_id and token to child spans.
12
+ # These are just keys, not the values themselves.
13
+ PAID_EXTERNAL_CUSTOMER_ID_KEY = OpenTelemetry::Context.create_key("paid.external_customer_id")
14
+ PAID_TOKEN_KEY = OpenTelemetry::Context.create_key("paid.token")
15
+
16
+ # @param api_key [String]
17
+ def self.initialize_tracing(api_key:)
18
+ endpoint = "https://collector.agentpaid.io:4318/v1/traces"
19
+ # endpoint = "http://localhost:4318/v1/traces"
20
+
21
+ @token = api_key
22
+
23
+ exporter = OpenTelemetry::Exporter::OTLP::Exporter.new(
24
+ endpoint: endpoint,
25
+ headers: {}
26
+ )
27
+ span_processor = OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(exporter)
28
+
29
+ OpenTelemetry::SDK.configure do |c|
30
+ c.add_span_processor(span_processor)
31
+ end
32
+
33
+ # Add an at_exit hook to ensure spans are flushed before the script exits.
34
+ at_exit do
35
+ OpenTelemetry.tracer_provider.shutdown
36
+ end
37
+
38
+ Logging.logger.info("Paid tracing initialized successfully")
39
+ rescue StandardError => e
40
+ Logging.logger.error("Failed to initialize Paid tracing: #{e.message}")
41
+ raise
42
+ end
43
+
44
+ def self.token
45
+ @token
46
+ end
47
+
48
+ # Getter for the OpenAI wrapper to retrieve the external_customer_id from the context.
49
+ def self.get_external_customer_id_from_context
50
+ OpenTelemetry::Context.current.value(PAID_EXTERNAL_CUSTOMER_ID_KEY)
51
+ end
52
+
53
+ # Getter for the OpenAI wrapper to retrieve the token from the context.
54
+ def self.get_token_from_context
55
+ OpenTelemetry::Context.current.value(PAID_TOKEN_KEY)
56
+ end
57
+
58
+ # @param external_customer_id [String]
59
+ # @param args [Array]
60
+ # @param block [Proc]
61
+ def self.capture(*args, external_customer_id:, &block)
62
+ token = self.token
63
+ unless token
64
+ Logging.logger.warn("No token found - tracing is not initialized and will not be captured")
65
+ return yield(*args) if block_given?
66
+ end
67
+
68
+ new_context_values = {
69
+ PAID_EXTERNAL_CUSTOMER_ID_KEY => external_customer_id,
70
+ PAID_TOKEN_KEY => token
71
+ }
72
+
73
+ # Execute the block within a new context containing our values.
74
+ OpenTelemetry::Context.with_values(new_context_values) do
75
+ tracer = OpenTelemetry.tracer_provider.tracer("paid.ruby")
76
+
77
+ tracer.in_span("paid.ruby:#{external_customer_id}") do |span|
78
+ span.set_attribute("external_customer_id", external_customer_id)
79
+ span.set_attribute("token", token)
80
+
81
+ begin
82
+ result = yield(*args) if block_given?
83
+ span.status = OpenTelemetry::Trace::Status.ok("Success")
84
+ result
85
+ rescue StandardError => e
86
+ span.status = OpenTelemetry::Trace::Status.error("Error: #{e.message}")
87
+ raise e
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openai"
4
+ require_relative "../tracing"
5
+
6
+ module Paid
7
+ module Tracing
8
+ module Wrappers
9
+ # A wrapper around the OpenAI::Client that provides automatic tracing for API calls.
10
+ class PaidOpenAI
11
+ def initialize(openai_client:)
12
+ @openai_client = openai_client
13
+ @tracer = OpenTelemetry.tracer_provider.tracer("paid.ruby")
14
+ end
15
+
16
+ # Wraps the OpenAI#chat method to create a child span.
17
+ def chat(parameters:)
18
+ wrap_call(operation: "chat", model: parameters[:model]) do
19
+ @openai_client.chat(parameters: parameters)
20
+ end
21
+ end
22
+
23
+ # Wraps the OpenAI#embeddings method to create a child span.
24
+ def embeddings(parameters:)
25
+ wrap_call(operation: "embeddings", model: parameters[:model]) do
26
+ @openai_client.embeddings(parameters: parameters)
27
+ end
28
+ end
29
+
30
+ # Returns a wrapper for the images API.
31
+ def images
32
+ ImagesWrapper.new(openai_client: @openai_client, tracer: @tracer)
33
+ end
34
+
35
+ private
36
+
37
+ # A private wrapper for the OpenAI Images API.
38
+ class ImagesWrapper
39
+ def initialize(openai_client:, tracer:)
40
+ @openai_client = openai_client
41
+ @tracer = tracer
42
+ end
43
+
44
+ def generate(parameters:)
45
+ current_span = OpenTelemetry::Trace.current_span
46
+ unless current_span.context.valid?
47
+ Paid::Tracing::Logging.logger.warn("No active span found, calling OpenAI directly without tracing.")
48
+ return @openai_client.images.generate(parameters: parameters)
49
+ end
50
+
51
+ external_customer_id = Paid::Tracing.get_external_customer_id_from_context
52
+ token = Paid::Tracing.get_token_from_context
53
+ model = parameters[:model] || "dall-e-3"
54
+ span_name = "trace.images #{model}"
55
+
56
+ @tracer.in_span(span_name) do |span|
57
+ attributes = {
58
+ "gen_ai.request.model" => model,
59
+ "gen_ai.system" => "openai",
60
+ "gen_ai.operation.name" => "image_generation"
61
+ }
62
+ attributes["external_customer_id"] = external_customer_id if external_customer_id
63
+ attributes["token"] = token if token
64
+ span.add_attributes(attributes)
65
+
66
+ begin
67
+ response = @openai_client.images.generate(parameters: parameters)
68
+ span.add_attributes({
69
+ "gen_ai.image.count" => parameters[:n] || 1,
70
+ "gen_ai.image.size" => parameters[:size] || "1024x1024",
71
+ "gen_ai.image.quality" => parameters[:quality] || "standard"
72
+ })
73
+ span.status = OpenTelemetry::Trace::Status.ok("Success")
74
+ response
75
+ rescue StandardError => e
76
+ span.record_exception(e)
77
+ span.status = OpenTelemetry::Trace::Status.error("Error: #{e.message}")
78
+ raise e
79
+ end
80
+ end
81
+ end
82
+ end
83
+
84
+ def wrap_call(operation:, model:, &block)
85
+ current_span = OpenTelemetry::Trace.current_span
86
+ unless current_span.context.valid?
87
+ Paid::Tracing::Logging.logger.warn("No active span found, calling OpenAI directly without tracing.")
88
+ return yield
89
+ end
90
+
91
+ external_customer_id = Paid::Tracing.get_external_customer_id_from_context
92
+ token = Paid::Tracing.get_token_from_context
93
+ model_name = model || "unknown"
94
+ span_name = "trace.#{operation} #{model_name}"
95
+
96
+ @tracer.in_span(span_name) do |span|
97
+ attributes = {
98
+ "gen_ai.system" => "openai",
99
+ "gen_ai.operation.name" => operation
100
+ }
101
+ attributes["external_customer_id"] = external_customer_id if external_customer_id
102
+ attributes["token"] = token if token
103
+ span.add_attributes(attributes)
104
+
105
+ begin
106
+ response = yield
107
+ add_response_attributes(span, response)
108
+ span.status = OpenTelemetry::Trace::Status.ok("Success")
109
+ response
110
+ rescue StandardError => e
111
+ span.record_exception(e)
112
+ span.status = OpenTelemetry::Trace::Status.error("Error: #{e.message}")
113
+ raise e
114
+ end
115
+ end
116
+ end
117
+
118
+ def add_response_attributes(span, response)
119
+ return unless response.is_a?(Hash) && response.dig("usage")
120
+
121
+ attributes = {
122
+ "gen_ai.usage.input_tokens" => response.dig("usage", "prompt_tokens"),
123
+ "gen_ai.usage.output_tokens" => response.dig("usage", "completion_tokens"),
124
+ "gen_ai.response.model" => response.dig("model")
125
+ }.compact
126
+
127
+ span.add_attributes(attributes)
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
data/lib/paid_ruby.rb CHANGED
@@ -9,6 +9,8 @@ require_relative "paid_ruby/contacts/client"
9
9
  require_relative "paid_ruby/orders/client"
10
10
  require_relative "paid_ruby/usage/client"
11
11
  require_relative "extensions/batch"
12
+ require_relative "paid_ruby/tracing/tracing"
13
+ require_relative "paid_ruby/tracing/wrappers/open_ai_wrapper"
12
14
 
13
15
  module Paid
14
16
  class Client
@@ -44,6 +46,19 @@ module Paid
44
46
  @orders = Paid::OrdersClient.new(request_client: @request_client)
45
47
  @usage = Paid::BatchUsageClient.new(request_client: @request_client)
46
48
  end
49
+
50
+ def initialize_tracing
51
+ token = @request_client.token
52
+ api_key = token.gsub(/^Bearer /, "")
53
+ Paid::Tracing.initialize_tracing(api_key: api_key)
54
+ end
55
+
56
+ # @param external_customer_id [String]
57
+ # @param args [Array]
58
+ # @param block [Proc]
59
+ def capture(*args, external_customer_id:, &block)
60
+ Paid::Tracing.capture(*args, external_customer_id: external_customer_id, &block)
61
+ end
47
62
  end
48
63
 
49
64
  class AsyncClient
@@ -79,5 +94,18 @@ module Paid
79
94
  @orders = Paid::AsyncOrdersClient.new(request_client: @async_request_client)
80
95
  @usage = Paid::AsyncBatchUsageClient.new(request_client: @async_request_client)
81
96
  end
97
+
98
+ def initialize_tracing
99
+ token = @async_request_client.token
100
+ api_key = token.gsub(/^Bearer /, "")
101
+ Paid::Tracing.initialize_tracing(api_key: api_key)
102
+ end
103
+
104
+ # @param external_customer_id [String]
105
+ # @param args [Array]
106
+ # @param block [Proc]
107
+ def capture(*args, external_customer_id:, &block)
108
+ Paid::Tracing.capture(*args, external_customer_id: external_customer_id, &block)
109
+ end
82
110
  end
83
111
  end
metadata CHANGED
@@ -1,15 +1,35 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: paid_ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.5.0.pre.alpha1
5
5
  platform: ruby
6
6
  authors:
7
7
  - ''
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-06-12 00:00:00.000000000 Z
11
+ date: 2025-06-19 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: async-http-faraday
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0.0'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '1.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '0.0'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '1.0'
13
33
  - !ruby/object:Gem::Dependency
14
34
  name: faraday
15
35
  requirement: !ruby/object:Gem::Requirement
@@ -71,25 +91,89 @@ dependencies:
71
91
  - !ruby/object:Gem::Version
72
92
  version: '3.0'
73
93
  - !ruby/object:Gem::Dependency
74
- name: async-http-faraday
94
+ name: opentelemetry-api
75
95
  requirement: !ruby/object:Gem::Requirement
76
96
  requirements:
77
- - - ">="
97
+ - - "~>"
78
98
  - !ruby/object:Gem::Version
79
- version: '0.0'
80
- - - "<"
99
+ version: '1.5'
100
+ type: :runtime
101
+ prerelease: false
102
+ version_requirements: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - "~>"
81
105
  - !ruby/object:Gem::Version
82
- version: '1.0'
106
+ version: '1.5'
107
+ - !ruby/object:Gem::Dependency
108
+ name: opentelemetry-exporter-otlp
109
+ requirement: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - "~>"
112
+ - !ruby/object:Gem::Version
113
+ version: 0.30.0
83
114
  type: :runtime
84
115
  prerelease: false
85
116
  version_requirements: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - "~>"
119
+ - !ruby/object:Gem::Version
120
+ version: 0.30.0
121
+ - !ruby/object:Gem::Dependency
122
+ name: opentelemetry-sdk
123
+ requirement: !ruby/object:Gem::Requirement
124
+ requirements:
125
+ - - "~>"
126
+ - !ruby/object:Gem::Version
127
+ version: '1.8'
128
+ type: :runtime
129
+ prerelease: false
130
+ version_requirements: !ruby/object:Gem::Requirement
131
+ requirements:
132
+ - - "~>"
133
+ - !ruby/object:Gem::Version
134
+ version: '1.8'
135
+ - !ruby/object:Gem::Dependency
136
+ name: ostruct
137
+ requirement: !ruby/object:Gem::Requirement
86
138
  requirements:
87
139
  - - ">="
88
140
  - !ruby/object:Gem::Version
89
- version: '0.0'
90
- - - "<"
141
+ version: '0'
142
+ type: :runtime
143
+ prerelease: false
144
+ version_requirements: !ruby/object:Gem::Requirement
145
+ requirements:
146
+ - - ">="
91
147
  - !ruby/object:Gem::Version
92
- version: '1.0'
148
+ version: '0'
149
+ - !ruby/object:Gem::Dependency
150
+ name: rake
151
+ requirement: !ruby/object:Gem::Requirement
152
+ requirements:
153
+ - - "~>"
154
+ - !ruby/object:Gem::Version
155
+ version: '13.0'
156
+ type: :runtime
157
+ prerelease: false
158
+ version_requirements: !ruby/object:Gem::Requirement
159
+ requirements:
160
+ - - "~>"
161
+ - !ruby/object:Gem::Version
162
+ version: '13.0'
163
+ - !ruby/object:Gem::Dependency
164
+ name: ruby-openai
165
+ requirement: !ruby/object:Gem::Requirement
166
+ requirements:
167
+ - - "~>"
168
+ - !ruby/object:Gem::Version
169
+ version: '8.1'
170
+ type: :runtime
171
+ prerelease: false
172
+ version_requirements: !ruby/object:Gem::Requirement
173
+ requirements:
174
+ - - "~>"
175
+ - !ruby/object:Gem::Version
176
+ version: '8.1'
93
177
  description: ''
94
178
  email: ''
95
179
  executables: []
@@ -105,6 +189,9 @@ files:
105
189
  - lib/paid_ruby/customers/client.rb
106
190
  - lib/paid_ruby/orders/client.rb
107
191
  - lib/paid_ruby/orders/lines/client.rb
192
+ - lib/paid_ruby/tracing/logging.rb
193
+ - lib/paid_ruby/tracing/tracing.rb
194
+ - lib/paid_ruby/tracing/wrappers/open_ai_wrapper.rb
108
195
  - lib/paid_ruby/types/address.rb
109
196
  - lib/paid_ruby/types/agent.rb
110
197
  - lib/paid_ruby/types/agent_attribute.rb
@@ -149,14 +236,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
149
236
  requirements:
150
237
  - - ">="
151
238
  - !ruby/object:Gem::Version
152
- version: 2.7.0
239
+ version: 3.1.0
153
240
  required_rubygems_version: !ruby/object:Gem::Requirement
154
241
  requirements:
155
- - - ">="
242
+ - - ">"
156
243
  - !ruby/object:Gem::Version
157
- version: '0'
244
+ version: 1.3.1
158
245
  requirements: []
159
- rubygems_version: 3.1.6
246
+ rubygems_version: 3.3.27
160
247
  signing_key:
161
248
  specification_version: 4
162
249
  summary: ''