visionary-sdk 0.4.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +19 -0
- data/LICENSE +202 -0
- data/README.md +198 -0
- data/lib/visionary/agent.rb +174 -0
- data/lib/visionary/body.rb +162 -0
- data/lib/visionary/config.rb +151 -0
- data/lib/visionary/context.rb +97 -0
- data/lib/visionary/fork.rb +16 -0
- data/lib/visionary/middleware.rb +169 -0
- data/lib/visionary/queue.rb +57 -0
- data/lib/visionary/railtie.rb +48 -0
- data/lib/visionary/redactor.rb +94 -0
- data/lib/visionary/retention.rb +90 -0
- data/lib/visionary/schema/mysql/requests.sql +37 -0
- data/lib/visionary/schema/mysql/system.sql +76 -0
- data/lib/visionary/schema/mysql/telemetry.sql +52 -0
- data/lib/visionary/schema/postgres/requests.sql +37 -0
- data/lib/visionary/schema/postgres/system.sql +76 -0
- data/lib/visionary/schema/postgres/telemetry.sql +52 -0
- data/lib/visionary/schema/sqlite/requests.sql +37 -0
- data/lib/visionary/schema/sqlite/system.sql +76 -0
- data/lib/visionary/schema/sqlite/telemetry.sql +52 -0
- data/lib/visionary/schema.rb +163 -0
- data/lib/visionary/schemas.rb +64 -0
- data/lib/visionary/store/active_record.rb +103 -0
- data/lib/visionary/store/d1.rb +129 -0
- data/lib/visionary/store/sql.rb +272 -0
- data/lib/visionary/store.rb +53 -0
- data/lib/visionary/system/recorder.rb +353 -0
- data/lib/visionary/system/sources.rb +191 -0
- data/lib/visionary/telemetry/errors.rb +118 -0
- data/lib/visionary/telemetry/hooks.rb +115 -0
- data/lib/visionary/telemetry/sql_text.rb +44 -0
- data/lib/visionary/telemetry.rb +116 -0
- data/lib/visionary/version.rb +5 -0
- data/lib/visionary/writer.rb +185 -0
- data/lib/visionary.rb +157 -0
- metadata +90 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Visionary
|
|
4
|
+
# The raw capture of one request. Redaction, error-field extraction and
|
|
5
|
+
# user lookup happen on the writer thread, off the request path.
|
|
6
|
+
Entry = Struct.new(
|
|
7
|
+
:request_id, :started_at, :method, :path, :route, :query, :status, :duration_ms, :ip, :user_agent,
|
|
8
|
+
:user_id, :user_handle, :user_role, :headers, :request_content_type, :request_body, :request_truncated,
|
|
9
|
+
:request_size, :response_content_type, :response_body, :response_truncated, :response_size,
|
|
10
|
+
:totals, :span_rows, :error_rows,
|
|
11
|
+
keyword_init: true
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
# The single background thread that batches api_logs, spans, errors and
|
|
15
|
+
# releases rows. Queue items are [:request, Entry], [:span, row],
|
|
16
|
+
# [:error, row] or [:release, row].
|
|
17
|
+
class Writer
|
|
18
|
+
INSERTS = { request: "api_logs", span: "spans", error: "errors", release: "releases" }.freeze
|
|
19
|
+
|
|
20
|
+
def initialize(cfg, store, schemas, logger)
|
|
21
|
+
@cfg = cfg
|
|
22
|
+
@store = store
|
|
23
|
+
@schemas = schemas
|
|
24
|
+
@logger = logger
|
|
25
|
+
@redactor = Redactor.new(cfg.redact_headers, cfg.redact_fields, cfg.redact_request_fields)
|
|
26
|
+
@batch_size = cfg.batch_size.between?(1, 50) ? cfg.batch_size : 50
|
|
27
|
+
@flush_interval = cfg.flush_interval.to_f.positive? ? cfg.flush_interval.to_f : 3.0
|
|
28
|
+
@queue_size = cfg.queue_size.positive? ? cfg.queue_size : 5000
|
|
29
|
+
@sql = INSERTS.transform_values { |t| Schema.insert_sql(t, store.string_params?) }
|
|
30
|
+
@groups = []
|
|
31
|
+
@groups << :requests if cfg.log_requests
|
|
32
|
+
@groups << :telemetry if cfg.spans || cfg.errors || cfg.version
|
|
33
|
+
reset
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def start
|
|
37
|
+
@thread = Thread.new { run }
|
|
38
|
+
@thread.name = "visionary-writer" if @thread.respond_to?(:name=)
|
|
39
|
+
self
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Threads don't survive fork; the child starts over with an empty queue.
|
|
43
|
+
def after_fork
|
|
44
|
+
reset
|
|
45
|
+
start
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Never blocks; a full queue drops and counts the item.
|
|
49
|
+
def enqueue(item)
|
|
50
|
+
@queue.push(item)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Stops the thread after it has drained the queue, waiting at most
|
|
54
|
+
# timeout seconds. False if it was still busy then.
|
|
55
|
+
def close(timeout)
|
|
56
|
+
@queue.close
|
|
57
|
+
return true unless @thread
|
|
58
|
+
return true if @thread.join(timeout)
|
|
59
|
+
@logger.warn("visionary: log flush timed out; #{@queue.size} pending")
|
|
60
|
+
false
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def reset
|
|
66
|
+
@queue = BoundedQueue.new(@queue_size)
|
|
67
|
+
@thread = nil
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def run
|
|
71
|
+
Telemetry.internal!
|
|
72
|
+
@groups.each { |g| @schemas.ensure(g) }
|
|
73
|
+
next_flush = mono + @flush_interval
|
|
74
|
+
batch = []
|
|
75
|
+
loop do
|
|
76
|
+
items = @queue.take(@batch_size - batch.size, next_flush)
|
|
77
|
+
batch.concat(items)
|
|
78
|
+
closed = @queue.closed? && @queue.size.zero?
|
|
79
|
+
if batch.size >= @batch_size || mono >= next_flush || closed
|
|
80
|
+
write(batch) unless batch.empty?
|
|
81
|
+
batch = []
|
|
82
|
+
report_drops
|
|
83
|
+
next_flush = mono + @flush_interval
|
|
84
|
+
end
|
|
85
|
+
break if closed
|
|
86
|
+
end
|
|
87
|
+
rescue StandardError => e
|
|
88
|
+
@logger.error("visionary: writer stopped: #{e.class}: #{e.message}")
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def report_drops
|
|
92
|
+
n = @queue.take_dropped
|
|
93
|
+
@logger.warn("visionary: queue full; #{n} entries dropped") if n.positive?
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def write(batch)
|
|
97
|
+
stmts = begin
|
|
98
|
+
users = lookup_users(batch)
|
|
99
|
+
batch.flat_map { |kind, item| statements(kind, item, users) }
|
|
100
|
+
rescue StandardError => e
|
|
101
|
+
@logger.error("visionary: building rows failed; #{batch.size} entries dropped: #{e.class}: #{e.message}")
|
|
102
|
+
return
|
|
103
|
+
end
|
|
104
|
+
err = nil
|
|
105
|
+
3.times do |attempt|
|
|
106
|
+
@groups.each { |g| @schemas.ensure(g) }
|
|
107
|
+
begin
|
|
108
|
+
@store.batch(stmts)
|
|
109
|
+
return
|
|
110
|
+
rescue StandardError => e
|
|
111
|
+
err = e
|
|
112
|
+
end
|
|
113
|
+
sleep(attempt + 1) if attempt < 2
|
|
114
|
+
end
|
|
115
|
+
@logger.error("visionary: batch write failed; #{batch.size} entries dropped: #{err.message}")
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def statements(kind, item, users)
|
|
119
|
+
if kind == :request
|
|
120
|
+
out = [stmt(:request, request_row(item, users))]
|
|
121
|
+
item.span_rows&.each { |r| out << stmt(:span, r) }
|
|
122
|
+
item.error_rows&.each { |r| out << stmt(:error, r) }
|
|
123
|
+
out
|
|
124
|
+
else
|
|
125
|
+
[stmt(kind, item)]
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def stmt(kind, row)
|
|
130
|
+
[@sql.fetch(kind), Store.fit(@store.dialect, INSERTS.fetch(kind), row)]
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Resolves each distinct user missing a handle or role once per batch.
|
|
134
|
+
def lookup_users(batch)
|
|
135
|
+
out = {}
|
|
136
|
+
lookup = @cfg.lookup_user
|
|
137
|
+
return out unless lookup
|
|
138
|
+
batch.each do |kind, e|
|
|
139
|
+
next unless kind == :request
|
|
140
|
+
id = e.user_id
|
|
141
|
+
next if id.empty? || (!e.user_handle.empty? && !e.user_role.empty?) || out.key?(id)
|
|
142
|
+
found = begin
|
|
143
|
+
lookup.call(id)
|
|
144
|
+
rescue StandardError => ex
|
|
145
|
+
@logger.warn("visionary: lookup_user(#{id.inspect}) failed: #{ex.message}")
|
|
146
|
+
nil
|
|
147
|
+
end
|
|
148
|
+
out[id] = found ? [found[0].to_s, found[1].to_s] : ["", ""]
|
|
149
|
+
end
|
|
150
|
+
out
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def request_row(e, users)
|
|
154
|
+
handle = e.user_handle
|
|
155
|
+
role = e.user_role
|
|
156
|
+
if (found = users[e.user_id])
|
|
157
|
+
handle = found[0] if handle.empty?
|
|
158
|
+
role = found[1] if role.empty?
|
|
159
|
+
end
|
|
160
|
+
req_raw = Visionary.utf8(e.request_body)
|
|
161
|
+
resp_raw = Visionary.utf8(e.response_body)
|
|
162
|
+
code = key = ""
|
|
163
|
+
code, key = Redactor.error_fields(resp_raw, @cfg.error_code_field, @cfg.error_key_field) if e.status >= 400
|
|
164
|
+
req_body = resp_body = ""
|
|
165
|
+
if @cfg.capture_bodies
|
|
166
|
+
req_body = @redactor.request_body(req_raw, e.request_content_type)
|
|
167
|
+
resp_body = @redactor.response_body(resp_raw)
|
|
168
|
+
end
|
|
169
|
+
db_count, db_ms, http_count, http_ms = e.totals
|
|
170
|
+
[
|
|
171
|
+
e.request_id, Visionary.timestamp(e.started_at), e.method, e.path, nz(e.route), nz(e.query),
|
|
172
|
+
e.status, e.duration_ms, nz(e.ip), nz(e.user_agent), nz(e.user_id), nz(handle), nz(role),
|
|
173
|
+
nz(@redactor.headers(e.headers)), nz(e.request_content_type), nz(req_body), e.request_truncated ? 1 : 0,
|
|
174
|
+
e.request_size, nz(e.response_content_type), nz(resp_body), e.response_truncated ? 1 : 0, e.response_size,
|
|
175
|
+
nz(code), nz(key), @cfg.service, @cfg.version, db_count, db_ms, http_count, http_ms
|
|
176
|
+
]
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def nz(s)
|
|
180
|
+
s.nil? || s.empty? ? nil : s
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def mono = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
184
|
+
end
|
|
185
|
+
end
|
data/lib/visionary.rb
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "visionary/version"
|
|
4
|
+
require_relative "visionary/config"
|
|
5
|
+
require_relative "visionary/schema"
|
|
6
|
+
require_relative "visionary/store"
|
|
7
|
+
require_relative "visionary/schemas"
|
|
8
|
+
require_relative "visionary/redactor"
|
|
9
|
+
require_relative "visionary/context"
|
|
10
|
+
require_relative "visionary/queue"
|
|
11
|
+
require_relative "visionary/writer"
|
|
12
|
+
require_relative "visionary/retention"
|
|
13
|
+
require_relative "visionary/system/sources"
|
|
14
|
+
require_relative "visionary/system/recorder"
|
|
15
|
+
require_relative "visionary/telemetry"
|
|
16
|
+
require_relative "visionary/telemetry/hooks"
|
|
17
|
+
require_relative "visionary/body"
|
|
18
|
+
require_relative "visionary/middleware"
|
|
19
|
+
require_relative "visionary/agent"
|
|
20
|
+
require_relative "visionary/fork"
|
|
21
|
+
|
|
22
|
+
# Visionary collects what the Visionary server analyses, straight from a Ruby
|
|
23
|
+
# service: one api_logs row per HTTP request, spans for database and outbound
|
|
24
|
+
# HTTP calls, errors, deploy markers and periodic system_metrics samples.
|
|
25
|
+
#
|
|
26
|
+
# # config.ru (Sinatra, Roda, Hanami, any Rack app)
|
|
27
|
+
# require "visionary"
|
|
28
|
+
# use Visionary::Middleware
|
|
29
|
+
#
|
|
30
|
+
# In Rails the Railtie inserts the middleware. Everything is configured
|
|
31
|
+
# through VISIONARY_* environment variables, or Visionary.configure.
|
|
32
|
+
module Visionary
|
|
33
|
+
@lock = Mutex.new
|
|
34
|
+
@agent = nil
|
|
35
|
+
@config = nil
|
|
36
|
+
|
|
37
|
+
class << self
|
|
38
|
+
# The configuration the agent starts with (from the environment unless
|
|
39
|
+
# set). Change it before the first request, e.g. in an initializer.
|
|
40
|
+
def config
|
|
41
|
+
@config ||= Config.from_env
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
attr_writer :config
|
|
45
|
+
|
|
46
|
+
def configure
|
|
47
|
+
yield config
|
|
48
|
+
config
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Builds and starts the process-wide agent; later calls return it. Never
|
|
52
|
+
# raises: on any problem the agent is a no-op.
|
|
53
|
+
def start(cfg = nil)
|
|
54
|
+
return @agent if @agent
|
|
55
|
+
@lock.synchronize do
|
|
56
|
+
return @agent if @agent
|
|
57
|
+
@config = cfg if cfg
|
|
58
|
+
agent = begin
|
|
59
|
+
Agent.new(config).start
|
|
60
|
+
rescue StandardError => e
|
|
61
|
+
warn("visionary: start failed (#{e.class}: #{e.message}); collection is off")
|
|
62
|
+
Agent.new(Config.new.tap { |c| c.log_requests = c.system_metrics = c.spans = c.errors = false })
|
|
63
|
+
end
|
|
64
|
+
at_exit { agent.close }
|
|
65
|
+
@agent = agent
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# The running agent, started on first use.
|
|
70
|
+
def agent = @agent || start
|
|
71
|
+
|
|
72
|
+
# The agent's span and error recorder, or nil when collection is off.
|
|
73
|
+
def telemetry = @agent&.telemetry
|
|
74
|
+
|
|
75
|
+
# Flushes and stops collection, waiting at most timeout seconds.
|
|
76
|
+
def close(timeout = 10)
|
|
77
|
+
@agent&.close(timeout)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Attaches the signed-in user to the request being logged. Call it from
|
|
81
|
+
# authentication code (a before_action, a Warden callback, a Rack
|
|
82
|
+
# middleware) once the user is known. Only id is required. No-op outside a
|
|
83
|
+
# request seen by Visionary::Middleware.
|
|
84
|
+
def set_user(id, handle: nil, role: nil)
|
|
85
|
+
RequestContext.current&.set_user(id, handle, role)
|
|
86
|
+
nil
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Records the route pattern (e.g. "/users/:id") for routers Visionary
|
|
90
|
+
# can't see. No-op outside a request.
|
|
91
|
+
def set_route(pattern)
|
|
92
|
+
ctx = RequestContext.current
|
|
93
|
+
ctx.route = pattern if ctx
|
|
94
|
+
nil
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Measures the block as a span and returns its value. kind is "custom",
|
|
98
|
+
# "cache", "queue", "db" or "http"; name should be low-cardinality (the
|
|
99
|
+
# operation, not its arguments).
|
|
100
|
+
#
|
|
101
|
+
# Visionary.span("charge card", kind: "http", target: "api.stripe.com") { stripe.charge(...) }
|
|
102
|
+
def span(name, kind: "custom", target: nil, detail: nil)
|
|
103
|
+
rec = telemetry
|
|
104
|
+
return yield unless rec&.spans_on
|
|
105
|
+
|
|
106
|
+
t0 = Telemetry.now_ms
|
|
107
|
+
status = "ok"
|
|
108
|
+
error = nil
|
|
109
|
+
begin
|
|
110
|
+
yield
|
|
111
|
+
rescue Exception => e # re-raised; only observed here
|
|
112
|
+
status = "error"
|
|
113
|
+
error = "#{e.class}: #{e.message}"
|
|
114
|
+
raise
|
|
115
|
+
ensure
|
|
116
|
+
rec.span(kind: kind, name: name, target: target, detail: detail, start_mono: t0,
|
|
117
|
+
duration_ms: Telemetry.now_ms - t0, status: status, error: error)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Records a handled error (handled = 1). Inside a request it links to it.
|
|
122
|
+
#
|
|
123
|
+
# rescue PaymentError => e
|
|
124
|
+
# Visionary.capture_exception(e)
|
|
125
|
+
def capture_exception(exception = $!)
|
|
126
|
+
telemetry&.exception(exception, handled: true)
|
|
127
|
+
nil
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Restarts the background threads in a forked child. Called
|
|
131
|
+
# automatically through Process._fork; Puma's on_worker_boot may call it
|
|
132
|
+
# too (it is idempotent per process).
|
|
133
|
+
def after_fork
|
|
134
|
+
agent = @agent
|
|
135
|
+
return unless agent
|
|
136
|
+
@lock = Mutex.new
|
|
137
|
+
agent.after_fork
|
|
138
|
+
rescue StandardError => e
|
|
139
|
+
warn("visionary: restart after fork failed: #{e.message}")
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# created_at format for every table: UTC, milliseconds, literal Z.
|
|
143
|
+
def timestamp(time)
|
|
144
|
+
time.getutc.strftime("%Y-%m-%dT%H:%M:%S.%LZ")
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Bytes as UTF-8 text, invalid sequences replaced.
|
|
148
|
+
def utf8(s)
|
|
149
|
+
return "" if s.nil?
|
|
150
|
+
s = s.to_s
|
|
151
|
+
return s if s.encoding == Encoding::UTF_8 && s.valid_encoding?
|
|
152
|
+
s.dup.force_encoding(Encoding::UTF_8).scrub
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
require_relative "visionary/railtie" if defined?(Rails::Railtie)
|
metadata
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: visionary-sdk
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.4.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- styropyr0
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-10-18 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: |
|
|
14
|
+
The Ruby collector for Visionary. A Rack middleware (with a Railtie for Rails)
|
|
15
|
+
writes one row per HTTP request, database and outbound HTTP spans, errors,
|
|
16
|
+
deploy markers and periodic host/process samples to PostgreSQL, MySQL,
|
|
17
|
+
SQLite or Cloudflare D1, on background threads.
|
|
18
|
+
email:
|
|
19
|
+
executables: []
|
|
20
|
+
extensions: []
|
|
21
|
+
extra_rdoc_files: []
|
|
22
|
+
files:
|
|
23
|
+
- CHANGELOG.md
|
|
24
|
+
- LICENSE
|
|
25
|
+
- README.md
|
|
26
|
+
- lib/visionary.rb
|
|
27
|
+
- lib/visionary/agent.rb
|
|
28
|
+
- lib/visionary/body.rb
|
|
29
|
+
- lib/visionary/config.rb
|
|
30
|
+
- lib/visionary/context.rb
|
|
31
|
+
- lib/visionary/fork.rb
|
|
32
|
+
- lib/visionary/middleware.rb
|
|
33
|
+
- lib/visionary/queue.rb
|
|
34
|
+
- lib/visionary/railtie.rb
|
|
35
|
+
- lib/visionary/redactor.rb
|
|
36
|
+
- lib/visionary/retention.rb
|
|
37
|
+
- lib/visionary/schema.rb
|
|
38
|
+
- lib/visionary/schema/mysql/requests.sql
|
|
39
|
+
- lib/visionary/schema/mysql/system.sql
|
|
40
|
+
- lib/visionary/schema/mysql/telemetry.sql
|
|
41
|
+
- lib/visionary/schema/postgres/requests.sql
|
|
42
|
+
- lib/visionary/schema/postgres/system.sql
|
|
43
|
+
- lib/visionary/schema/postgres/telemetry.sql
|
|
44
|
+
- lib/visionary/schema/sqlite/requests.sql
|
|
45
|
+
- lib/visionary/schema/sqlite/system.sql
|
|
46
|
+
- lib/visionary/schema/sqlite/telemetry.sql
|
|
47
|
+
- lib/visionary/schemas.rb
|
|
48
|
+
- lib/visionary/store.rb
|
|
49
|
+
- lib/visionary/store/active_record.rb
|
|
50
|
+
- lib/visionary/store/d1.rb
|
|
51
|
+
- lib/visionary/store/sql.rb
|
|
52
|
+
- lib/visionary/system/recorder.rb
|
|
53
|
+
- lib/visionary/system/sources.rb
|
|
54
|
+
- lib/visionary/telemetry.rb
|
|
55
|
+
- lib/visionary/telemetry/errors.rb
|
|
56
|
+
- lib/visionary/telemetry/hooks.rb
|
|
57
|
+
- lib/visionary/telemetry/sql_text.rb
|
|
58
|
+
- lib/visionary/version.rb
|
|
59
|
+
- lib/visionary/writer.rb
|
|
60
|
+
homepage: https://github.com/styropyr0/Visionary
|
|
61
|
+
licenses:
|
|
62
|
+
- Apache-2.0
|
|
63
|
+
metadata:
|
|
64
|
+
homepage_uri: https://github.com/styropyr0/Visionary
|
|
65
|
+
source_code_uri: https://github.com/styropyr0/Visionary/tree/main/sdks/ruby
|
|
66
|
+
changelog_uri: https://github.com/styropyr0/Visionary/blob/main/sdks/ruby/CHANGELOG.md
|
|
67
|
+
bug_tracker_uri: https://github.com/styropyr0/Visionary/issues
|
|
68
|
+
documentation_uri: https://github.com/styropyr0/Visionary/tree/main/sdks/ruby#readme
|
|
69
|
+
rubygems_mfa_required: 'true'
|
|
70
|
+
post_install_message:
|
|
71
|
+
rdoc_options: []
|
|
72
|
+
require_paths:
|
|
73
|
+
- lib
|
|
74
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
75
|
+
requirements:
|
|
76
|
+
- - ">="
|
|
77
|
+
- !ruby/object:Gem::Version
|
|
78
|
+
version: '3.1'
|
|
79
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
80
|
+
requirements:
|
|
81
|
+
- - ">="
|
|
82
|
+
- !ruby/object:Gem::Version
|
|
83
|
+
version: '0'
|
|
84
|
+
requirements: []
|
|
85
|
+
rubygems_version: 3.5.22
|
|
86
|
+
signing_key:
|
|
87
|
+
specification_version: 4
|
|
88
|
+
summary: Request logs, spans, errors and system metrics for Visionary, from Rack and
|
|
89
|
+
Rails apps
|
|
90
|
+
test_files: []
|