amlexia 1.0.2
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 +7 -0
- data/LICENSE +38 -0
- data/README.md +23 -0
- data/amlexia.gemspec +21 -0
- data/exe/amlexia +20 -0
- data/lib/amlexia/anthropic.rb +29 -0
- data/lib/amlexia/client.rb +156 -0
- data/lib/amlexia/cost.rb +55 -0
- data/lib/amlexia/diagnostic.rb +5 -0
- data/lib/amlexia/health.rb +25 -0
- data/lib/amlexia/http_wrap.rb +52 -0
- data/lib/amlexia/openai.rb +30 -0
- data/lib/amlexia/sampling.rb +14 -0
- data/lib/amlexia/tracing.rb +38 -0
- data/lib/amlexia/version.rb +5 -0
- data/lib/amlexia.rb +15 -0
- metadata +62 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: d25d8c8ded5e08e6a76090728c11816ba7267fdd2cc05de42ca538561f57c171
|
|
4
|
+
data.tar.gz: 4cbe93f9d3d9a7cb0b1f6204f218a7ae099a53f44cfa33d5404e6a7e48e5ad4c
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 55e69475ca93b746803e3d25affdade613e51b67b09debce0a75020ece7353b9318f71aa183331410b9813b60a6467b9eab93a9c2f5bc32bc1790edf24301ffc
|
|
7
|
+
data.tar.gz: aa4acd6b0c9d2dc8576a63f2a29375f5b855331f4c3e1de8a64918dd3c497a9a7379e29e78e5446fca63d87c0235c15ca64eeaf5fad1065580f01e05de55743b
|
data/LICENSE
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
Amlexia Software License Agreement (SDK)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Amlexia. All rights reserved.
|
|
4
|
+
|
|
5
|
+
IMPORTANT: This software is proprietary and is NOT open source. You may not
|
|
6
|
+
use, copy, modify, merge, publish, distribute, sublicense, or sell copies of
|
|
7
|
+
this software except as expressly permitted below.
|
|
8
|
+
|
|
9
|
+
1. Grant
|
|
10
|
+
Amlexia grants you a limited, non-exclusive, non-transferable, revocable
|
|
11
|
+
license to install and use this SDK solely to send telemetry to and integrate
|
|
12
|
+
with the Amlexia service (https://amlexia.com) in accordance with the
|
|
13
|
+
Amlexia Terms of Service (https://amlexia.com/terms).
|
|
14
|
+
|
|
15
|
+
2. Restrictions
|
|
16
|
+
You may not:
|
|
17
|
+
(a) use the SDK except with an active Amlexia account and valid credentials;
|
|
18
|
+
(b) remove or alter proprietary notices;
|
|
19
|
+
(c) reverse engineer, decompile, or disassemble the SDK except where law
|
|
20
|
+
expressly permits and cannot be waived;
|
|
21
|
+
(d) redistribute, sublicense, or make the SDK available to third parties as
|
|
22
|
+
a standalone product or competing service;
|
|
23
|
+
(e) use the SDK to build a product that substitutes for Amlexia.
|
|
24
|
+
|
|
25
|
+
3. Ownership
|
|
26
|
+
Amlexia and its licensors retain all rights, title, and interest in the SDK.
|
|
27
|
+
|
|
28
|
+
4. Termination
|
|
29
|
+
This license terminates automatically if you breach these terms or your
|
|
30
|
+
Amlexia account is terminated. Upon termination, you must cease use and
|
|
31
|
+
destroy copies in your possession.
|
|
32
|
+
|
|
33
|
+
5. Disclaimer
|
|
34
|
+
THE SDK IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. SEE
|
|
35
|
+
https://amlexia.com/terms FOR LIMITATION OF LIABILITY.
|
|
36
|
+
|
|
37
|
+
6. Contact
|
|
38
|
+
Support: support@amlexia.com
|
data/README.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Amlexia Ruby SDK
|
|
2
|
+
|
|
3
|
+
```ruby
|
|
4
|
+
require "amlexia"
|
|
5
|
+
|
|
6
|
+
client = Amlexia::Client.from_env
|
|
7
|
+
|
|
8
|
+
client.track(
|
|
9
|
+
endpoint: "/v1/chat/completions",
|
|
10
|
+
method: "POST",
|
|
11
|
+
status_code: 200,
|
|
12
|
+
latency_ms: 340,
|
|
13
|
+
provider: "openai",
|
|
14
|
+
model_name: "gpt-4o-mini",
|
|
15
|
+
tokens_input: 120,
|
|
16
|
+
tokens_output: 45
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
Amlexia::OpenAI.track_openai_completion(client, model: "gpt-4o-mini", status_code: 200, latency_ms: 340, usage: { prompt_tokens: 120, completion_tokens: 45 })
|
|
20
|
+
Amlexia::Health.check_ingest_health
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
CLI: `bundle exec amlexia health`
|
data/amlexia.gemspec
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
require_relative "lib/amlexia/version"
|
|
2
|
+
|
|
3
|
+
Gem::Specification.new do |s|
|
|
4
|
+
s.name = "amlexia"
|
|
5
|
+
s.version = Amlexia::VERSION
|
|
6
|
+
s.summary = "Official Amlexia Ruby SDK — API, AI, and infrastructure observability"
|
|
7
|
+
s.description = "Track HTTP APIs, LLM providers, payments, and webhooks with traces, latency, and cost fields."
|
|
8
|
+
s.authors = ["Amlexia"]
|
|
9
|
+
s.email = "support@amlexia.com"
|
|
10
|
+
s.files = Dir["lib/**/*.rb", "exe/amlexia", "README.md", "LICENSE"] + ["amlexia.gemspec"]
|
|
11
|
+
s.executables = ["amlexia"]
|
|
12
|
+
s.bindir = "exe"
|
|
13
|
+
s.homepage = "https://amlexia.com"
|
|
14
|
+
s.metadata = {
|
|
15
|
+
"source_code_uri" => "https://github.com/Amlexia/Amlexia",
|
|
16
|
+
"documentation_uri" => "https://docs.amlexia.com/sdk/ruby",
|
|
17
|
+
"changelog_uri" => "https://docs.amlexia.com/changelog"
|
|
18
|
+
}
|
|
19
|
+
s.license = "Nonstandard"
|
|
20
|
+
s.required_ruby_version = ">= 3.0"
|
|
21
|
+
end
|
data/exe/amlexia
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "json"
|
|
5
|
+
require_relative "../lib/amlexia"
|
|
6
|
+
|
|
7
|
+
ingest = ENV.fetch("AMLEXIA_INGEST_URL", Amlexia::Client::DEFAULT_INGEST_URL)
|
|
8
|
+
cmd = ARGV[0] || "health"
|
|
9
|
+
|
|
10
|
+
case cmd
|
|
11
|
+
when "version"
|
|
12
|
+
puts Amlexia::VERSION
|
|
13
|
+
when "health"
|
|
14
|
+
result = Amlexia::Health.check_ingest_health(ingest_url: ingest)
|
|
15
|
+
puts JSON.pretty_generate({ ingestUrl: ingest, **result })
|
|
16
|
+
exit(result[:ok] ? 0 : 1)
|
|
17
|
+
else
|
|
18
|
+
warn "Usage: amlexia [health|version]"
|
|
19
|
+
exit 1
|
|
20
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Amlexia
|
|
4
|
+
module Anthropic
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def track_anthropic_message(client, model:, status_code:, latency_ms:, usage: {}, cost_usd: nil, **kwargs)
|
|
8
|
+
event = Cost.enrich_event(
|
|
9
|
+
cost_usd: cost_usd,
|
|
10
|
+
model_name: model,
|
|
11
|
+
provider: "anthropic",
|
|
12
|
+
tokens_input: usage[:input_tokens],
|
|
13
|
+
tokens_output: usage[:output_tokens]
|
|
14
|
+
)
|
|
15
|
+
client.track(
|
|
16
|
+
endpoint: "/v1/messages",
|
|
17
|
+
method: "POST",
|
|
18
|
+
status_code: status_code,
|
|
19
|
+
latency_ms: latency_ms,
|
|
20
|
+
provider: "anthropic",
|
|
21
|
+
model_name: model,
|
|
22
|
+
tokens_input: usage[:input_tokens],
|
|
23
|
+
tokens_output: usage[:output_tokens],
|
|
24
|
+
cost_usd: event[:cost_usd],
|
|
25
|
+
**kwargs
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "uri"
|
|
6
|
+
require "thread"
|
|
7
|
+
|
|
8
|
+
module Amlexia
|
|
9
|
+
class Client
|
|
10
|
+
DEFAULT_INGEST_URL = "https://ingest.amlexia.com"
|
|
11
|
+
DEFAULT_FLUSH_SECONDS = 5
|
|
12
|
+
DEFAULT_BATCH_SIZE = 50
|
|
13
|
+
DEFAULT_MAX_RETRIES = 5
|
|
14
|
+
|
|
15
|
+
attr_reader :diagnostic
|
|
16
|
+
|
|
17
|
+
def initialize(
|
|
18
|
+
sdk_key:,
|
|
19
|
+
ingest_url: DEFAULT_INGEST_URL,
|
|
20
|
+
flush_interval_seconds: DEFAULT_FLUSH_SECONDS,
|
|
21
|
+
max_batch_size: DEFAULT_BATCH_SIZE,
|
|
22
|
+
max_retries: DEFAULT_MAX_RETRIES,
|
|
23
|
+
environment: nil,
|
|
24
|
+
release_version: nil,
|
|
25
|
+
default_session_id: nil,
|
|
26
|
+
sample_rate: 1.0,
|
|
27
|
+
diagnostic: false
|
|
28
|
+
)
|
|
29
|
+
@sdk_key = sdk_key
|
|
30
|
+
@ingest_url = ingest_url.chomp("/")
|
|
31
|
+
@flush_interval = flush_interval_seconds
|
|
32
|
+
@max_batch_size = max_batch_size
|
|
33
|
+
@max_retries = max_retries
|
|
34
|
+
@environment = environment
|
|
35
|
+
@release_version = release_version
|
|
36
|
+
@default_session_id = default_session_id
|
|
37
|
+
@sample_rate = sample_rate
|
|
38
|
+
@diagnostic = DiagnosticState.new(enabled: diagnostic, events_buffered: 0)
|
|
39
|
+
@buffer = []
|
|
40
|
+
@mutex = Mutex.new
|
|
41
|
+
@flushing = false
|
|
42
|
+
@stop = false
|
|
43
|
+
@flush_thread = Thread.new { flush_loop }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.from_env
|
|
47
|
+
new(
|
|
48
|
+
sdk_key: ENV.fetch("AMLEXIA_SDK_KEY"),
|
|
49
|
+
ingest_url: ENV.fetch("AMLEXIA_INGEST_URL", DEFAULT_INGEST_URL),
|
|
50
|
+
environment: ENV["AMLEXIA_ENVIRONMENT"],
|
|
51
|
+
release_version: ENV["AMLEXIA_RELEASE"]
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def track(**event)
|
|
56
|
+
return unless Sampling.should_sample(@sample_rate)
|
|
57
|
+
|
|
58
|
+
event = Cost.enrich_event(event)
|
|
59
|
+
event[:timestamp] ||= Time.now.to_i
|
|
60
|
+
event[:environment] ||= @environment
|
|
61
|
+
event[:release_version] ||= @release_version
|
|
62
|
+
event[:session_id] ||= @default_session_id
|
|
63
|
+
event[:method] = event[:method].to_s.upcase
|
|
64
|
+
|
|
65
|
+
should_flush = false
|
|
66
|
+
@mutex.synchronize do
|
|
67
|
+
@buffer << event
|
|
68
|
+
should_flush = @buffer.length >= @max_batch_size
|
|
69
|
+
end
|
|
70
|
+
flush if should_flush
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def get_diagnostic_snapshot
|
|
74
|
+
@mutex.synchronize do
|
|
75
|
+
@diagnostic.events_buffered = @buffer.length
|
|
76
|
+
DiagnosticState.new(
|
|
77
|
+
enabled: @diagnostic.enabled,
|
|
78
|
+
events_buffered: @diagnostic.events_buffered,
|
|
79
|
+
last_flush_at: @diagnostic.last_flush_at,
|
|
80
|
+
last_error: @diagnostic.last_error
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def flush
|
|
86
|
+
events = nil
|
|
87
|
+
@mutex.synchronize do
|
|
88
|
+
return if @flushing || @buffer.empty?
|
|
89
|
+
|
|
90
|
+
@flushing = true
|
|
91
|
+
events = @buffer.shift(@max_batch_size)
|
|
92
|
+
end
|
|
93
|
+
post_events(events)
|
|
94
|
+
ensure
|
|
95
|
+
@mutex.synchronize { @flushing = false }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def shutdown
|
|
99
|
+
@stop = true
|
|
100
|
+
@flush_thread.join
|
|
101
|
+
flush until @mutex.synchronize { @buffer.empty? }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
def flush_loop
|
|
107
|
+
until @stop
|
|
108
|
+
sleep @flush_interval
|
|
109
|
+
flush
|
|
110
|
+
rescue StandardError
|
|
111
|
+
nil
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def post_events(events)
|
|
116
|
+
post("/v1/events", { sdk_key: @sdk_key, events: events })
|
|
117
|
+
@diagnostic.last_flush_at = (Time.now.to_f * 1000).to_i
|
|
118
|
+
@diagnostic.last_error = nil
|
|
119
|
+
warn "[amlexia] flushed #{events.length} events" if @diagnostic.enabled
|
|
120
|
+
rescue StandardError => e
|
|
121
|
+
@diagnostic.last_error = e.message
|
|
122
|
+
warn "[amlexia] flush failed: #{e.message}" if @diagnostic.enabled
|
|
123
|
+
@mutex.synchronize { @buffer = events + @buffer }
|
|
124
|
+
raise
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def post(path, body)
|
|
128
|
+
uri = URI("#{@ingest_url}#{path}")
|
|
129
|
+
data = JSON.generate(body)
|
|
130
|
+
attempt = 0
|
|
131
|
+
last_error = nil
|
|
132
|
+
while attempt < @max_retries
|
|
133
|
+
begin
|
|
134
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
135
|
+
http.use_ssl = uri.scheme == "https"
|
|
136
|
+
req = Net::HTTP::Post.new(uri)
|
|
137
|
+
req["Content-Type"] = "application/json"
|
|
138
|
+
req.body = data
|
|
139
|
+
res = http.request(req)
|
|
140
|
+
return true if res.is_a?(Net::HTTPSuccess)
|
|
141
|
+
raise "Invalid SDK key" if res.code.to_i == 401
|
|
142
|
+
raise "Usage limit exceeded" if res.code.to_i == 402
|
|
143
|
+
raise "Ingest failed: #{res.code} #{res.body}" if res.code.to_i >= 400 && res.code.to_i < 500
|
|
144
|
+
|
|
145
|
+
last_error = "HTTP #{res.code}"
|
|
146
|
+
rescue StandardError => e
|
|
147
|
+
last_error = e.message
|
|
148
|
+
raise e if attempt >= @max_retries - 1
|
|
149
|
+
end
|
|
150
|
+
sleep([2**attempt, 30].min)
|
|
151
|
+
attempt += 1
|
|
152
|
+
end
|
|
153
|
+
raise last_error || "Failed to send events after retries"
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
data/lib/amlexia/cost.rb
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Amlexia
|
|
4
|
+
MODEL_PRICES = {
|
|
5
|
+
"gpt-4o-mini" => { input: 0.15, output: 0.6 },
|
|
6
|
+
"gpt-4o" => { input: 2.5, output: 10 },
|
|
7
|
+
"claude-3-5-haiku" => { input: 0.8, output: 4 },
|
|
8
|
+
"claude-3-5-sonnet" => { input: 3, output: 15 },
|
|
9
|
+
"gemini-2.0-flash" => { input: 0.1, output: 0.4 }
|
|
10
|
+
}.freeze
|
|
11
|
+
|
|
12
|
+
PROVIDER_DEFAULT_MODEL = {
|
|
13
|
+
"openai" => "gpt-4o-mini",
|
|
14
|
+
"anthropic" => "claude-3-5-haiku",
|
|
15
|
+
"google" => "gemini-2.0-flash"
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
module Cost
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
def estimate_cost_usd(cost_usd:, model_name: nil, provider: nil, tokens_input: nil, tokens_output: nil, total_tokens: nil)
|
|
22
|
+
return [cost_usd, "reported"] if cost_usd && cost_usd.positive?
|
|
23
|
+
|
|
24
|
+
key = (model_name || "").downcase
|
|
25
|
+
price = MODEL_PRICES[key]
|
|
26
|
+
price ||= MODEL_PRICES[PROVIDER_DEFAULT_MODEL[(provider || "").downcase]] if provider
|
|
27
|
+
return [cost_usd, nil] unless price
|
|
28
|
+
|
|
29
|
+
inp = tokens_input || 0
|
|
30
|
+
out = tokens_output || 0
|
|
31
|
+
if inp.zero? && out.zero?
|
|
32
|
+
return [cost_usd, nil] unless total_tokens&.positive?
|
|
33
|
+
|
|
34
|
+
blended = (price[:input] + price[:output]) / 2.0
|
|
35
|
+
return [(total_tokens / 1_000_000.0) * blended, "estimated"]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
est = (inp / 1_000_000.0) * price[:input] + (out / 1_000_000.0) * price[:output]
|
|
39
|
+
[est.round(8), "estimated"]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def enrich_event(event)
|
|
43
|
+
cost, = estimate_cost_usd(
|
|
44
|
+
cost_usd: event[:cost_usd],
|
|
45
|
+
model_name: event[:model_name],
|
|
46
|
+
provider: event[:provider] || event[:provider_name],
|
|
47
|
+
tokens_input: event[:tokens_input],
|
|
48
|
+
tokens_output: event[:tokens_output],
|
|
49
|
+
total_tokens: event[:total_tokens]
|
|
50
|
+
)
|
|
51
|
+
event[:cost_usd] = cost if cost
|
|
52
|
+
event
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
6
|
+
module Amlexia
|
|
7
|
+
module Health
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def check_ingest_health(ingest_url: Client::DEFAULT_INGEST_URL, timeout: 5)
|
|
11
|
+
uri = URI("#{ingest_url.to_s.chomp('/')}/health")
|
|
12
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
13
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
14
|
+
http.use_ssl = uri.scheme == "https"
|
|
15
|
+
http.open_timeout = timeout
|
|
16
|
+
http.read_timeout = timeout
|
|
17
|
+
res = http.get(uri.path)
|
|
18
|
+
latency = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000).to_i
|
|
19
|
+
{ ok: res.is_a?(Net::HTTPSuccess), status: res.code.to_i, latency_ms: latency }
|
|
20
|
+
rescue StandardError
|
|
21
|
+
latency = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000).to_i
|
|
22
|
+
{ ok: false, status: 0, latency_ms: latency }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
6
|
+
module Amlexia
|
|
7
|
+
module HttpWrap
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def provider_from_url(url)
|
|
11
|
+
return "openai" if url.include?("openai")
|
|
12
|
+
return "anthropic" if url.include?("anthropic")
|
|
13
|
+
return "stripe" if url.include?("stripe")
|
|
14
|
+
|
|
15
|
+
nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def wrap_net_http(client)
|
|
19
|
+
Net::HTTP.class_eval do
|
|
20
|
+
alias_method :amlexia_original_request, :request unless method_defined?(:amlexia_original_request)
|
|
21
|
+
|
|
22
|
+
define_method(:request) do |req, body = nil, &block|
|
|
23
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
24
|
+
status = 500
|
|
25
|
+
err = nil
|
|
26
|
+
begin
|
|
27
|
+
res = amlexia_original_request(req, body, &block)
|
|
28
|
+
status = res.code.to_i
|
|
29
|
+
res
|
|
30
|
+
rescue StandardError => e
|
|
31
|
+
err = e.message
|
|
32
|
+
raise
|
|
33
|
+
ensure
|
|
34
|
+
latency = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000).to_i
|
|
35
|
+
client.track(
|
|
36
|
+
endpoint: req.uri.to_s,
|
|
37
|
+
method: req.method,
|
|
38
|
+
status_code: status,
|
|
39
|
+
latency_ms: latency,
|
|
40
|
+
provider: Amlexia::HttpWrap.provider_from_url(req.uri.to_s),
|
|
41
|
+
error_message: err
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def track_http_call(client, **event)
|
|
49
|
+
client.track(**event)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Amlexia
|
|
4
|
+
module OpenAI
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def track_openai_completion(client, model:, status_code:, latency_ms:, usage: {}, cost_usd: nil, endpoint: "/v1/chat/completions", **kwargs)
|
|
8
|
+
event = Cost.enrich_event(
|
|
9
|
+
cost_usd: cost_usd,
|
|
10
|
+
model_name: model,
|
|
11
|
+
provider: "openai",
|
|
12
|
+
tokens_input: usage[:prompt_tokens],
|
|
13
|
+
tokens_output: usage[:completion_tokens],
|
|
14
|
+
total_tokens: usage[:total_tokens]
|
|
15
|
+
)
|
|
16
|
+
client.track(
|
|
17
|
+
endpoint: endpoint,
|
|
18
|
+
method: "POST",
|
|
19
|
+
status_code: status_code,
|
|
20
|
+
latency_ms: latency_ms,
|
|
21
|
+
provider: "openai",
|
|
22
|
+
model_name: model,
|
|
23
|
+
tokens_input: usage[:prompt_tokens],
|
|
24
|
+
tokens_output: usage[:completion_tokens],
|
|
25
|
+
cost_usd: event[:cost_usd],
|
|
26
|
+
**kwargs
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module Amlexia
|
|
6
|
+
TraceContext = Struct.new(
|
|
7
|
+
:trace_id, :span_id, :parent_span_id, :session_id, :user_id, :environment, :release_version,
|
|
8
|
+
keyword_init: true
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
module Tracing
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def create_trace_context(**kwargs)
|
|
15
|
+
TraceContext.new(
|
|
16
|
+
trace_id: kwargs[:trace_id] || SecureRandom.hex(16),
|
|
17
|
+
span_id: kwargs[:span_id] || SecureRandom.hex(8),
|
|
18
|
+
parent_span_id: kwargs[:parent_span_id],
|
|
19
|
+
session_id: kwargs[:session_id],
|
|
20
|
+
user_id: kwargs[:user_id],
|
|
21
|
+
environment: kwargs[:environment],
|
|
22
|
+
release_version: kwargs[:release_version]
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def child_span(parent)
|
|
27
|
+
TraceContext.new(
|
|
28
|
+
trace_id: parent.trace_id,
|
|
29
|
+
span_id: SecureRandom.hex(8),
|
|
30
|
+
parent_span_id: parent.span_id,
|
|
31
|
+
session_id: parent.session_id,
|
|
32
|
+
user_id: parent.user_id,
|
|
33
|
+
environment: parent.environment,
|
|
34
|
+
release_version: parent.release_version
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
data/lib/amlexia.rb
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "amlexia/version"
|
|
4
|
+
require_relative "amlexia/cost"
|
|
5
|
+
require_relative "amlexia/sampling"
|
|
6
|
+
require_relative "amlexia/diagnostic"
|
|
7
|
+
require_relative "amlexia/health"
|
|
8
|
+
require_relative "amlexia/tracing"
|
|
9
|
+
require_relative "amlexia/http_wrap"
|
|
10
|
+
require_relative "amlexia/openai"
|
|
11
|
+
require_relative "amlexia/anthropic"
|
|
12
|
+
require_relative "amlexia/client"
|
|
13
|
+
|
|
14
|
+
module Amlexia
|
|
15
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: amlexia
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.2
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Amlexia
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-05-19 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: Track HTTP APIs, LLM providers, payments, and webhooks with traces, latency,
|
|
14
|
+
and cost fields.
|
|
15
|
+
email: support@amlexia.com
|
|
16
|
+
executables:
|
|
17
|
+
- amlexia
|
|
18
|
+
extensions: []
|
|
19
|
+
extra_rdoc_files: []
|
|
20
|
+
files:
|
|
21
|
+
- LICENSE
|
|
22
|
+
- README.md
|
|
23
|
+
- amlexia.gemspec
|
|
24
|
+
- exe/amlexia
|
|
25
|
+
- lib/amlexia.rb
|
|
26
|
+
- lib/amlexia/anthropic.rb
|
|
27
|
+
- lib/amlexia/client.rb
|
|
28
|
+
- lib/amlexia/cost.rb
|
|
29
|
+
- lib/amlexia/diagnostic.rb
|
|
30
|
+
- lib/amlexia/health.rb
|
|
31
|
+
- lib/amlexia/http_wrap.rb
|
|
32
|
+
- lib/amlexia/openai.rb
|
|
33
|
+
- lib/amlexia/sampling.rb
|
|
34
|
+
- lib/amlexia/tracing.rb
|
|
35
|
+
- lib/amlexia/version.rb
|
|
36
|
+
homepage: https://amlexia.com
|
|
37
|
+
licenses:
|
|
38
|
+
- Nonstandard
|
|
39
|
+
metadata:
|
|
40
|
+
source_code_uri: https://github.com/Amlexia/Amlexia
|
|
41
|
+
documentation_uri: https://docs.amlexia.com/sdk/ruby
|
|
42
|
+
changelog_uri: https://docs.amlexia.com/changelog
|
|
43
|
+
post_install_message:
|
|
44
|
+
rdoc_options: []
|
|
45
|
+
require_paths:
|
|
46
|
+
- lib
|
|
47
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
48
|
+
requirements:
|
|
49
|
+
- - ">="
|
|
50
|
+
- !ruby/object:Gem::Version
|
|
51
|
+
version: '3.0'
|
|
52
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
53
|
+
requirements:
|
|
54
|
+
- - ">="
|
|
55
|
+
- !ruby/object:Gem::Version
|
|
56
|
+
version: '0'
|
|
57
|
+
requirements: []
|
|
58
|
+
rubygems_version: 3.5.22
|
|
59
|
+
signing_key:
|
|
60
|
+
specification_version: 4
|
|
61
|
+
summary: Official Amlexia Ruby SDK — API, AI, and infrastructure observability
|
|
62
|
+
test_files: []
|