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 +4 -4
- data/lib/paid_ruby/tracing/logging.rb +32 -0
- data/lib/paid_ruby/tracing/tracing.rb +93 -0
- data/lib/paid_ruby/tracing/wrappers/open_ai_wrapper.rb +132 -0
- data/lib/paid_ruby.rb +28 -0
- metadata +101 -14
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a2dcc5d043c51e405cbfaf7218d192091d7ee650616596eef5c26ed790b52a78
|
4
|
+
data.tar.gz: 8e0462f0215cbdb94d810ed7c04309a4d1581bcba0efd02d9d49220451625ebc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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-
|
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:
|
94
|
+
name: opentelemetry-api
|
75
95
|
requirement: !ruby/object:Gem::Requirement
|
76
96
|
requirements:
|
77
|
-
- - "
|
97
|
+
- - "~>"
|
78
98
|
- !ruby/object:Gem::Version
|
79
|
-
version: '
|
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.
|
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
|
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: '
|
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:
|
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:
|
244
|
+
version: 1.3.1
|
158
245
|
requirements: []
|
159
|
-
rubygems_version: 3.
|
246
|
+
rubygems_version: 3.3.27
|
160
247
|
signing_key:
|
161
248
|
specification_version: 4
|
162
249
|
summary: ''
|