ditliti 0.1.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 +21 -0
- data/lib/ditliti/db.rb +99 -0
- data/lib/ditliti.rb +297 -0
- metadata +48 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: '05886e9711c1cbe6a5c8d27fa42a2663ef98d57e04a76f557f16b21b6db59b5c'
|
|
4
|
+
data.tar.gz: f8b4dd43af968bc050a58e370a243e1280434213acc53c80898d2886c0f446c1
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 033ec9d232d630eabdd803000d2f5527f4cd63280e0cec0c56ec462eecc8fbc8c352411bb91edf63f38b7b2c6e52422c8c44ed052d5eb5ef7a6dc490caeadcdd
|
|
7
|
+
data.tar.gz: 33f4b8de25f65db24de77362d88e44005be1511a1d0f6982976c262b34a8e7495df503a82cac58546ad76e4ae1fa14477649b6ffc22fbadad116459d2664d0e7
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 candrabasic
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/lib/ditliti/db.rb
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
require_relative "../ditliti"
|
|
2
|
+
|
|
3
|
+
module Ditliti
|
|
4
|
+
# Database query auto-instrumentation.
|
|
5
|
+
#
|
|
6
|
+
# Wraps a query execution with a `db.query` span carrying OpenTelemetry-
|
|
7
|
+
# shaped database attributes. The backend (ingestion-api) parameterizes
|
|
8
|
+
# and sanitizes `db.query.text` server-side and derives
|
|
9
|
+
# `db.query.summary` / `db.operation.name` / `db.collection.name` /
|
|
10
|
+
# `ditliti.query_hash` from it - callers here only need to supply the
|
|
11
|
+
# real SQL text, exactly like every other span already sent by this SDK
|
|
12
|
+
# travels over the same trust boundary to the user's own ingestion-api.
|
|
13
|
+
#
|
|
14
|
+
# This never changes the wrapped call's exception flow: span-recording
|
|
15
|
+
# failures are swallowed, and the original exception (if any) is always
|
|
16
|
+
# re-raised unchanged.
|
|
17
|
+
#
|
|
18
|
+
# Note: this environment has no Ruby runtime available, so this file has
|
|
19
|
+
# been carefully written (mirroring the already-tested Python/PHP/Elixir
|
|
20
|
+
# adapters' generic-wrapper + ORM-notification shape) but has not itself
|
|
21
|
+
# been executed - review before relying on it in production.
|
|
22
|
+
module DbTracing
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
def trace_query(client, system:, text:, collection: nil, operation: nil)
|
|
26
|
+
tags = { "db.system.name" => system, "db.query.text" => text }
|
|
27
|
+
tags["db.operation.name"] = operation if operation
|
|
28
|
+
tags["db.collection.name"] = collection if collection
|
|
29
|
+
|
|
30
|
+
span = client.start_span("db.query", tags: tags)
|
|
31
|
+
begin
|
|
32
|
+
result = yield
|
|
33
|
+
count = extract_row_count(result)
|
|
34
|
+
safe_finish(span, count ? { "db.response.returned_rows" => count.to_s } : {})
|
|
35
|
+
result
|
|
36
|
+
rescue => error
|
|
37
|
+
safe_finish(span, { "error.type" => error.class.name })
|
|
38
|
+
raise
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Subscribes to ActiveRecord's `sql.active_record` notification - no
|
|
43
|
+
# query wrapping required, works for the query builder, Active Record
|
|
44
|
+
# models, and raw `connection.execute` alike. Requires ActiveSupport to
|
|
45
|
+
# already be loaded by the host application (this SDK has no Rails
|
|
46
|
+
# dependency of its own).
|
|
47
|
+
def subscribe_active_record(client, system: "postgresql")
|
|
48
|
+
ActiveSupport::Notifications.subscribe("sql.active_record") do |*args|
|
|
49
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
50
|
+
next if event.payload[:name] == "SCHEMA" # migrations/introspection queries, not app queries
|
|
51
|
+
|
|
52
|
+
record_completed_span(
|
|
53
|
+
client,
|
|
54
|
+
system: system,
|
|
55
|
+
text: event.payload[:sql],
|
|
56
|
+
duration_ms: event.duration,
|
|
57
|
+
collection: event.payload[:name]
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Records a span whose duration is already known (e.g. from
|
|
63
|
+
# ActiveRecord's notification, which reports a query's elapsed time
|
|
64
|
+
# only after it has already completed) - unlike `trace_query`, there's
|
|
65
|
+
# no separate "finish" step since the operation is already over.
|
|
66
|
+
def record_completed_span(client, system:, text:, duration_ms:, collection: nil, operation: nil, error_type: nil)
|
|
67
|
+
client.start_trace if client.trace.nil?
|
|
68
|
+
parent_span_id = client.trace.span_id
|
|
69
|
+
ctx = client.trace.child
|
|
70
|
+
|
|
71
|
+
tags = { "db.system.name" => system, "db.query.text" => text }
|
|
72
|
+
tags["db.operation.name"] = operation if operation
|
|
73
|
+
tags["db.collection.name"] = collection if collection
|
|
74
|
+
tags["error.type"] = error_type if error_type
|
|
75
|
+
|
|
76
|
+
client.record_span(
|
|
77
|
+
trace_id: ctx.trace_id,
|
|
78
|
+
span_id: ctx.span_id,
|
|
79
|
+
parent_span_id: parent_span_id,
|
|
80
|
+
operation_name: "db.query",
|
|
81
|
+
duration_ms: duration_ms,
|
|
82
|
+
tags: tags
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def safe_finish(span, extra_tags)
|
|
87
|
+
span.finish(extra_tags: extra_tags)
|
|
88
|
+
rescue StandardError
|
|
89
|
+
# Instrumentation must never break the caller's query path.
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def extract_row_count(result)
|
|
93
|
+
return result if result.is_a?(Integer) && result >= 0
|
|
94
|
+
return result.length if result.respond_to?(:length)
|
|
95
|
+
|
|
96
|
+
nil
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
data/lib/ditliti.rb
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
require "digest"
|
|
2
|
+
require "json"
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
require "time"
|
|
6
|
+
require "uri"
|
|
7
|
+
|
|
8
|
+
module Ditliti
|
|
9
|
+
# A W3C trace-context (https://www.w3.org/TR/trace-context/) carried
|
|
10
|
+
# across service boundaries via the `traceparent` header, so a trace
|
|
11
|
+
# started in one process is continued - not restarted - in the next.
|
|
12
|
+
class TraceContext
|
|
13
|
+
attr_reader :trace_id, :span_id, :sampled
|
|
14
|
+
|
|
15
|
+
def self.generate_trace_id
|
|
16
|
+
SecureRandom.hex(16)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.generate_span_id
|
|
20
|
+
SecureRandom.hex(8)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.start
|
|
24
|
+
new(generate_trace_id, generate_span_id, true)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.from_traceparent(header)
|
|
28
|
+
return nil if header.nil? || header.strip.empty?
|
|
29
|
+
|
|
30
|
+
parts = header.strip.split("-")
|
|
31
|
+
return nil unless parts.length == 4
|
|
32
|
+
|
|
33
|
+
_version, trace_id, span_id, flags = parts
|
|
34
|
+
return nil unless trace_id =~ /\A[0-9a-f]{32}\z/ && trace_id != "0" * 32
|
|
35
|
+
return nil unless span_id =~ /\A[0-9a-f]{16}\z/ && span_id != "0" * 16
|
|
36
|
+
|
|
37
|
+
flags_value = Integer(flags, 16) rescue nil
|
|
38
|
+
return nil if flags_value.nil?
|
|
39
|
+
|
|
40
|
+
new(trace_id, span_id, (flags_value & 1) == 1)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def initialize(trace_id, span_id, sampled = true)
|
|
44
|
+
@trace_id = trace_id
|
|
45
|
+
@span_id = span_id
|
|
46
|
+
@sampled = sampled
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def child
|
|
50
|
+
self.class.new(trace_id, self.class.generate_span_id, sampled)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def to_traceparent
|
|
54
|
+
"00-#{trace_id}-#{span_id}-#{sampled ? "01" : "00"}"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Returned by Client#start_span; call #finish when the operation completes
|
|
59
|
+
# to record and send the span.
|
|
60
|
+
class Span
|
|
61
|
+
def initialize(client, operation_name, parent_span_id, ctx, tags)
|
|
62
|
+
@client = client
|
|
63
|
+
@operation_name = operation_name
|
|
64
|
+
@parent_span_id = parent_span_id
|
|
65
|
+
@ctx = ctx
|
|
66
|
+
@tags = tags
|
|
67
|
+
@started_at = Time.now.utc
|
|
68
|
+
@started_monotonic = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def trace_id
|
|
72
|
+
@ctx.trace_id
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def span_id
|
|
76
|
+
@ctx.span_id
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def traceparent
|
|
80
|
+
@ctx.to_traceparent
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def finish(extra_tags: {})
|
|
84
|
+
duration_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - @started_monotonic) * 1000
|
|
85
|
+
@client.record_span(
|
|
86
|
+
trace_id: @ctx.trace_id,
|
|
87
|
+
span_id: @ctx.span_id,
|
|
88
|
+
parent_span_id: @parent_span_id,
|
|
89
|
+
operation_name: @operation_name,
|
|
90
|
+
duration_ms: duration_ms,
|
|
91
|
+
started_at: @started_at,
|
|
92
|
+
tags: @tags.merge(extra_tags)
|
|
93
|
+
)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
class Client
|
|
98
|
+
attr_accessor :user, :session_id
|
|
99
|
+
attr_reader :trace
|
|
100
|
+
|
|
101
|
+
def initialize(endpoint:, api_key:, project_id:, release: nil, environment: nil, timeout: 1.5)
|
|
102
|
+
@endpoint = endpoint.sub(%r{/$}, "")
|
|
103
|
+
@api_key = api_key
|
|
104
|
+
@project_id = project_id
|
|
105
|
+
@release = release
|
|
106
|
+
@environment = environment
|
|
107
|
+
@timeout = timeout
|
|
108
|
+
@breadcrumbs = []
|
|
109
|
+
@trace = nil
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Start (or continue, if an inbound `traceparent` header is given) the
|
|
113
|
+
# active trace. Downstream calls should read #get_traceparent and
|
|
114
|
+
# forward it as the `traceparent` header of any outgoing HTTP request to
|
|
115
|
+
# propagate the trace into the next service.
|
|
116
|
+
def start_trace(incoming_traceparent = nil)
|
|
117
|
+
parent = TraceContext.from_traceparent(incoming_traceparent)
|
|
118
|
+
@trace = parent ? parent.child : TraceContext.start
|
|
119
|
+
@trace
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def get_traceparent
|
|
123
|
+
@trace&.to_traceparent
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Start a child span under the active trace (starting one implicitly if
|
|
127
|
+
# none is active yet).
|
|
128
|
+
def start_span(operation_name, tags: {})
|
|
129
|
+
start_trace if @trace.nil?
|
|
130
|
+
parent_span_id = @trace.span_id
|
|
131
|
+
ctx = @trace.child
|
|
132
|
+
Span.new(self, operation_name, parent_span_id, ctx, { runtime: "ruby" }.merge(tags))
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def record_span(trace_id:, span_id:, operation_name:, duration_ms:, parent_span_id: nil, started_at: nil, tags: {})
|
|
136
|
+
base_tags = { runtime: "ruby" }
|
|
137
|
+
base_tags[:environment] = @environment if @environment
|
|
138
|
+
base_tags[:release] = @release if @release
|
|
139
|
+
send_envelope({
|
|
140
|
+
spans: [{
|
|
141
|
+
trace_id: trace_id,
|
|
142
|
+
span_id: span_id,
|
|
143
|
+
parent_span_id: parent_span_id,
|
|
144
|
+
project_id: @project_id,
|
|
145
|
+
operation_name: operation_name,
|
|
146
|
+
start_time: (started_at || Time.now.utc).iso8601(3),
|
|
147
|
+
duration_ms: duration_ms,
|
|
148
|
+
tags: base_tags.merge(tags)
|
|
149
|
+
}]
|
|
150
|
+
})
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
private
|
|
154
|
+
|
|
155
|
+
def current_trace_tags
|
|
156
|
+
@trace ? { trace_id: @trace.trace_id } : {}
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
public
|
|
160
|
+
|
|
161
|
+
def add_breadcrumb(message:, category: nil, level: "info", data: {})
|
|
162
|
+
@breadcrumbs << {
|
|
163
|
+
timestamp: Time.now.utc.iso8601,
|
|
164
|
+
category: category,
|
|
165
|
+
message: message.to_s,
|
|
166
|
+
level: level,
|
|
167
|
+
data: data || {}
|
|
168
|
+
}
|
|
169
|
+
@breadcrumbs.shift while @breadcrumbs.length > 100
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def capture_exception(error, context: {}, tags: {}, transaction: nil, user_agent: nil)
|
|
173
|
+
payload = {
|
|
174
|
+
id: SecureRandom.uuid,
|
|
175
|
+
project_id: @project_id,
|
|
176
|
+
level: "error",
|
|
177
|
+
message: error.message.to_s,
|
|
178
|
+
exception_type: error.class.name,
|
|
179
|
+
stack_trace: stack_frames(error),
|
|
180
|
+
tags: { runtime: "ruby" }.merge(current_trace_tags).merge(tags || {}),
|
|
181
|
+
context: context || {},
|
|
182
|
+
user_agent: user_agent,
|
|
183
|
+
release: @release,
|
|
184
|
+
environment: @environment,
|
|
185
|
+
transaction: transaction,
|
|
186
|
+
session_id: @session_id,
|
|
187
|
+
breadcrumbs: @breadcrumbs,
|
|
188
|
+
user: @user
|
|
189
|
+
}
|
|
190
|
+
post("/api/v1/ingest", payload, stable_id(payload))
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def capture_rack_exception(error, env)
|
|
194
|
+
request_method = env["REQUEST_METHOD"]
|
|
195
|
+
path = env["PATH_INFO"] || env["REQUEST_URI"]
|
|
196
|
+
capture_exception(
|
|
197
|
+
error,
|
|
198
|
+
context: {
|
|
199
|
+
framework: "rack",
|
|
200
|
+
method: request_method,
|
|
201
|
+
path: path,
|
|
202
|
+
query: env["QUERY_STRING"]
|
|
203
|
+
},
|
|
204
|
+
tags: { framework: "rack" },
|
|
205
|
+
transaction: "#{request_method || "HTTP"} #{path || "/"}",
|
|
206
|
+
user_agent: env["HTTP_USER_AGENT"]
|
|
207
|
+
)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def send_envelope(envelope)
|
|
211
|
+
post("/api/v1/envelope", {
|
|
212
|
+
project_id: @project_id,
|
|
213
|
+
events: envelope.fetch(:events, []),
|
|
214
|
+
spans: envelope.fetch(:spans, []),
|
|
215
|
+
logs: envelope.fetch(:logs, []),
|
|
216
|
+
replays: envelope.fetch(:replays, []),
|
|
217
|
+
profiles: envelope.fetch(:profiles, [])
|
|
218
|
+
})
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def capture_replay_segment(duration_ms:, session_id: @session_id, segment_index: 0, events: [], tags: {}, user: @user, release: @release, environment: @environment)
|
|
222
|
+
raise ArgumentError, "replay session_id is required" if session_id.nil? || session_id.to_s.empty?
|
|
223
|
+
|
|
224
|
+
send_envelope({
|
|
225
|
+
replays: [{
|
|
226
|
+
project_id: @project_id,
|
|
227
|
+
session_id: session_id,
|
|
228
|
+
started_at: Time.now.utc.iso8601,
|
|
229
|
+
duration_ms: duration_ms,
|
|
230
|
+
segment_index: segment_index,
|
|
231
|
+
events: events || [],
|
|
232
|
+
tags: { runtime: "ruby" }.merge(tags || {}),
|
|
233
|
+
user: user,
|
|
234
|
+
release: release,
|
|
235
|
+
environment: environment
|
|
236
|
+
}]
|
|
237
|
+
})
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def capture_profile(duration_ms:, trace_id: nil, transaction: nil, platform: "ruby", samples: [], tags: {}, release: @release, environment: @environment)
|
|
241
|
+
send_envelope({
|
|
242
|
+
profiles: [{
|
|
243
|
+
project_id: @project_id,
|
|
244
|
+
trace_id: trace_id || @trace&.trace_id,
|
|
245
|
+
transaction: transaction,
|
|
246
|
+
started_at: Time.now.utc.iso8601,
|
|
247
|
+
duration_ms: duration_ms,
|
|
248
|
+
platform: platform,
|
|
249
|
+
samples: samples || [],
|
|
250
|
+
tags: { runtime: "ruby" }.merge(tags || {}),
|
|
251
|
+
release: release,
|
|
252
|
+
environment: environment
|
|
253
|
+
}]
|
|
254
|
+
})
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
private
|
|
258
|
+
|
|
259
|
+
def post(path, payload, event_id = nil)
|
|
260
|
+
uri = URI("#{@endpoint}#{path}")
|
|
261
|
+
request = Net::HTTP::Post.new(uri)
|
|
262
|
+
request["content-type"] = "application/json"
|
|
263
|
+
request["x-api-key"] = @api_key
|
|
264
|
+
request["x-event-id"] = event_id if event_id
|
|
265
|
+
request.body = JSON.generate(payload)
|
|
266
|
+
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", read_timeout: @timeout, open_timeout: @timeout) do |http|
|
|
267
|
+
http.request(request)
|
|
268
|
+
end
|
|
269
|
+
raise "ditliti ingestion failed with status #{response.code}" unless response.is_a?(Net::HTTPSuccess)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def stack_frames(error)
|
|
273
|
+
Array(error.backtrace_locations).map do |frame|
|
|
274
|
+
{ function: frame.label, file: frame.path, line: frame.lineno, column: nil }
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def stable_id(payload)
|
|
279
|
+
Digest::SHA256.hexdigest("#{payload[:project_id]}|#{payload[:exception_type]}|#{payload[:message]}")
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
class RackMiddleware
|
|
284
|
+
def initialize(app, client)
|
|
285
|
+
@app = app
|
|
286
|
+
@client = client
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def call(env)
|
|
290
|
+
@client.start_trace(env["HTTP_TRACEPARENT"])
|
|
291
|
+
@app.call(env)
|
|
292
|
+
rescue => error
|
|
293
|
+
@client.capture_rack_exception(error, env)
|
|
294
|
+
raise
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: ditliti
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.2
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- candrabasic
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-07-15 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: Error tracking, tracing, replay, and profiling ingestion client for a
|
|
14
|
+
self-hosted Ditliti instance.
|
|
15
|
+
email:
|
|
16
|
+
executables: []
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- LICENSE
|
|
21
|
+
- lib/ditliti.rb
|
|
22
|
+
- lib/ditliti/db.rb
|
|
23
|
+
homepage: https://github.com/kangartha/inspectora/tree/main/sdk/ruby
|
|
24
|
+
licenses:
|
|
25
|
+
- MIT
|
|
26
|
+
metadata:
|
|
27
|
+
source_code_uri: https://github.com/kangartha/inspectora
|
|
28
|
+
bug_tracker_uri: https://github.com/kangartha/inspectora/issues
|
|
29
|
+
post_install_message:
|
|
30
|
+
rdoc_options: []
|
|
31
|
+
require_paths:
|
|
32
|
+
- lib
|
|
33
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
34
|
+
requirements:
|
|
35
|
+
- - ">="
|
|
36
|
+
- !ruby/object:Gem::Version
|
|
37
|
+
version: '3.0'
|
|
38
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
39
|
+
requirements:
|
|
40
|
+
- - ">="
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: '0'
|
|
43
|
+
requirements: []
|
|
44
|
+
rubygems_version: 3.5.22
|
|
45
|
+
signing_key:
|
|
46
|
+
specification_version: 4
|
|
47
|
+
summary: Official Ruby SDK for Ditliti
|
|
48
|
+
test_files: []
|