lescopr 0.1.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.
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lescopr
4
+ module Core
5
+ # Thread-safe in-memory log queue with automatic batching.
6
+ class LogQueue
7
+ attr_reader :size
8
+
9
+ def initialize(max_size: 1000)
10
+ @queue = []
11
+ @mutex = Mutex.new
12
+ @max_size = max_size
13
+ end
14
+
15
+ # @param entry [Hash]
16
+ def push(entry)
17
+ @mutex.synchronize do
18
+ @queue.shift if @queue.size >= @max_size # drop oldest if full
19
+ @queue << entry
20
+ end
21
+ end
22
+
23
+ # Drain up to `batch_size` entries atomically.
24
+ # @param batch_size [Integer]
25
+ # @return [Array<Hash>]
26
+ def drain(batch_size = 50)
27
+ @mutex.synchronize do
28
+ @queue.shift(batch_size)
29
+ end
30
+ end
31
+
32
+ def size
33
+ @mutex.synchronize { @queue.size }
34
+ end
35
+
36
+ def empty?
37
+ @mutex.synchronize { @queue.empty? }
38
+ end
39
+ end
40
+ end
41
+ end
42
+
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Lescopr
6
+ module Filesystem
7
+ CONFIG_FILE = ".lescopr.json"
8
+
9
+ # Thread-safe reader/writer for .lescopr.json
10
+ class ConfigManager
11
+ def initialize(path: nil)
12
+ @path = path || File.join(Dir.pwd, CONFIG_FILE)
13
+ @mutex = Mutex.new
14
+ end
15
+
16
+ # @return [Hash, nil]
17
+ def load
18
+ @mutex.synchronize do
19
+ return nil unless File.exist?(@path)
20
+
21
+ JSON.parse(File.read(@path), symbolize_names: true)
22
+ rescue JSON::ParserError, Errno::ENOENT
23
+ nil
24
+ end
25
+ end
26
+
27
+ # @param data [Hash]
28
+ def save(data)
29
+ @mutex.synchronize do
30
+ File.write(@path, JSON.pretty_generate(data))
31
+ end
32
+ end
33
+
34
+ def exists? = File.exist?(@path)
35
+
36
+ def delete!
37
+ @mutex.synchronize { File.delete(@path) if File.exist?(@path) }
38
+ end
39
+ end
40
+ end
41
+ end
42
+
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lescopr
4
+ module Filesystem
5
+ # Analyses the project directory and detects framework / stack.
6
+ class ProjectAnalyzer
7
+ FRAMEWORK_SIGNATURES = {
8
+ "rails" => %w[config/application.rb app/controllers config/routes.rb Gemfile],
9
+ "sinatra" => %w[config.ru],
10
+ "rack" => %w[config.ru Gemfile],
11
+ "hanami" => %w[config/app.rb config/environment.rb],
12
+ "padrino" => %w[config/apps.rb],
13
+ "grape" => %w[Gemfile],
14
+ }.freeze
15
+
16
+ GEMFILE_PATTERNS = {
17
+ "rails" => /gem\s+['"]rails['"]/,
18
+ "sinatra" => /gem\s+['"]sinatra['"]/,
19
+ "hanami" => /gem\s+['"]hanami['"]/,
20
+ "padrino" => /gem\s+['"]padrino['"]/,
21
+ "grape" => /gem\s+['"]grape['"]/,
22
+ "rack" => /gem\s+['"]rack['"]/,
23
+ }.freeze
24
+
25
+ def initialize(root: Dir.pwd)
26
+ @root = root
27
+ end
28
+
29
+ # @return [Hash] payload for POST /api/v1/sdk/verify/
30
+ def analyze
31
+ stack = detect_stack
32
+ ruby_ver = RUBY_VERSION rescue "unknown"
33
+ gemfile = read_gemfile
34
+
35
+ {
36
+ project_name: File.basename(@root),
37
+ project_path: @root,
38
+ project_stack: stack,
39
+ language: "ruby",
40
+ language_version: ruby_ver,
41
+ sdk_type: "ruby",
42
+ gemfile_present: File.exist?(File.join(@root, "Gemfile")),
43
+ dependencies: parse_gemfile_deps(gemfile),
44
+ environment: ENV.fetch("RAILS_ENV", ENV.fetch("RACK_ENV", "development"))
45
+ }
46
+ end
47
+
48
+ private
49
+
50
+ def detect_stack
51
+ gemfile = read_gemfile
52
+ detected = []
53
+
54
+ GEMFILE_PATTERNS.each do |fw, pattern|
55
+ detected << fw if gemfile =~ pattern
56
+ end
57
+
58
+ FRAMEWORK_SIGNATURES.each do |fw, files|
59
+ next if detected.include?(fw)
60
+ detected << fw if files.any? { |f| File.exist?(File.join(@root, f)) }
61
+ end
62
+
63
+ detected.empty? ? ["ruby"] : detected.uniq
64
+ end
65
+
66
+ def read_gemfile
67
+ path = File.join(@root, "Gemfile")
68
+ File.exist?(path) ? File.read(path) : ""
69
+ rescue StandardError
70
+ ""
71
+ end
72
+
73
+ def parse_gemfile_deps(content)
74
+ content.scan(/gem\s+['"]([^'"]+)['"]/).flatten.first(20)
75
+ end
76
+ end
77
+ end
78
+ end
79
+
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lescopr
4
+ module Integrations
5
+ module Rack
6
+ # Rack middleware — captures exceptions from any Rack-compatible app.
7
+ #
8
+ # @example Plain Rack (config.ru)
9
+ # require "lescopr"
10
+ # use Lescopr::Integrations::Rack::Middleware
11
+ #
12
+ class Middleware
13
+ def initialize(app)
14
+ @app = app
15
+ end
16
+
17
+ def call(env)
18
+ @app.call(env)
19
+ rescue StandardError => e
20
+ Lescopr.log("ERROR", "#{e.class}: #{e.message}", {
21
+ rack_path: env["PATH_INFO"],
22
+ rack_method: env["REQUEST_METHOD"],
23
+ backtrace: e.backtrace&.first(10)
24
+ })
25
+ raise
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lescopr
4
+ module Integrations
5
+ module Rails
6
+ # Railtie — auto-registers Lescopr in any Rails app.
7
+ #
8
+ # Add to config/initializers/lescopr.rb:
9
+ #
10
+ # Lescopr.configure do |c|
11
+ # c.sdk_key = ENV["LESCOPR_SDK_KEY"]
12
+ # c.api_key = ENV["LESCOPR_API_KEY"]
13
+ # c.environment = ::Rails.env
14
+ # end
15
+ #
16
+ class Railtie < ::Rails::Railtie
17
+ config.lescopr = ActiveSupport::OrderedOptions.new
18
+
19
+ initializer "lescopr.configure", after: :load_config_initializers do |app|
20
+ # Pick up values set in config/initializers/lescopr.rb
21
+ opts = app.config.lescopr
22
+
23
+ Lescopr.configure do |c|
24
+ c.sdk_key = opts[:sdk_key] || ENV["LESCOPR_SDK_KEY"] || c.sdk_key
25
+ c.api_key = opts[:api_key] || ENV["LESCOPR_API_KEY"] || c.api_key
26
+ c.environment = opts[:environment] || ENV["LESCOPR_ENVIRONMENT"] || ::Rails.env.to_s
27
+ c.debug = opts[:debug] || (ENV["LESCOPR_DEBUG"] == "true")
28
+ end
29
+ end
30
+
31
+ initializer "lescopr.middleware" do |app|
32
+ app.middleware.use Lescopr::Integrations::Rack::Middleware
33
+ end
34
+
35
+ # Hook into ActiveSupport::Notifications to capture exceptions
36
+ config.after_initialize do
37
+ if defined?(ActiveSupport::Notifications)
38
+ ActiveSupport::Notifications.subscribe("process_action.action_controller") do |*args|
39
+ event = ActiveSupport::Notifications::Event.new(*args)
40
+ exception = event.payload[:exception_object]
41
+
42
+ if exception
43
+ Lescopr.log("ERROR", "#{exception.class}: #{exception.message}", {
44
+ controller: event.payload[:controller],
45
+ action: event.payload[:action],
46
+ path: event.payload[:path],
47
+ format: event.payload[:format]&.to_s,
48
+ backtrace: exception.backtrace&.first(10)
49
+ })
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lescopr
4
+ module Integrations
5
+ module Sinatra
6
+ # Sinatra extension — registers a helper and hooks into error handling.
7
+ #
8
+ # @example
9
+ # require "sinatra"
10
+ # require "lescopr"
11
+ #
12
+ # class MyApp < Sinatra::Base
13
+ # register Lescopr::Integrations::Sinatra::Extension
14
+ #
15
+ # get "/" do
16
+ # lescopr_log(:info, "Home page visited")
17
+ # "Hello!"
18
+ # end
19
+ # end
20
+ #
21
+ module Extension
22
+ def self.registered(app)
23
+ app.use Lescopr::Integrations::Rack::Middleware
24
+
25
+ app.helpers Helpers
26
+
27
+ app.error(StandardError) do |e|
28
+ Lescopr.log("ERROR", "#{e.class}: #{e.message}", {
29
+ sinatra_path: request.path_info,
30
+ sinatra_method: request.request_method,
31
+ backtrace: e.backtrace&.first(10)
32
+ })
33
+ raise e
34
+ end
35
+ end
36
+ end
37
+
38
+ module Helpers
39
+ def lescopr_log(level, message, metadata = {})
40
+ Lescopr.log(level, message, metadata.merge(sinatra_route: request.path_info))
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+
5
+ module Lescopr
6
+ module Monitoring
7
+ # Internal SDK logger — writes to .lescopr.log, never pollutes app output.
8
+ class Logger
9
+ LOG_FILE = ".lescopr.log"
10
+ MAX_SIZE = 5 * 1024 * 1024 # 5 MB
11
+ MAX_FILES = 3
12
+
13
+ def initialize(debug: false)
14
+ @debug = debug
15
+ @logger = build_logger
16
+ end
17
+
18
+ def debug(msg) = @logger.debug(format_msg(msg)) if @debug
19
+ def info(msg) = @logger.info(format_msg(msg))
20
+ def warn(msg) = @logger.warn(format_msg(msg))
21
+ def error(msg) = @logger.error(format_msg(msg))
22
+ def fatal(msg) = @logger.fatal(format_msg(msg))
23
+
24
+ private
25
+
26
+ def format_msg(msg) = "[LESCOPR] #{msg}"
27
+
28
+ def build_logger
29
+ log_path = File.join(Dir.pwd, LOG_FILE)
30
+ logger = ::Logger.new(log_path, MAX_FILES, MAX_SIZE)
31
+ logger.level = @debug ? ::Logger::DEBUG : ::Logger::INFO
32
+ logger.formatter = proc do |sev, time, _prog, msg|
33
+ "#{time.strftime('%Y-%m-%dT%H:%M:%S')} #{sev}: #{msg}\n"
34
+ end
35
+ logger
36
+ rescue StandardError
37
+ ::Logger.new($stderr)
38
+ end
39
+ end
40
+ end
41
+ end
42
+
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "uri"
5
+ require "json"
6
+
7
+ module Lescopr
8
+ module Transport
9
+ # HTTPS batch transport to api.lescopr.com
10
+ class HttpClient
11
+ BASE_URL = "https://api.lescopr.com/api/v1"
12
+ TIMEOUT_SEC = 10
13
+ MAX_RETRIES = 3
14
+
15
+ def initialize(api_key:, sdk_key:)
16
+ @api_key = api_key
17
+ @sdk_key = sdk_key
18
+ end
19
+
20
+ # Register / verify the project with the API.
21
+ # @param payload [Hash]
22
+ # @return [Hash, nil]
23
+ def verify_project(payload)
24
+ post("/sdk/verify/", payload)
25
+ end
26
+
27
+ # Send a batch of log entries.
28
+ # @param logs [Array<Hash>]
29
+ # @return [Boolean]
30
+ def send_logs(logs)
31
+ return true if logs.empty?
32
+
33
+ result = post("/sdk/logs/", { logs: logs })
34
+ !result.nil?
35
+ end
36
+
37
+ # Send a single heartbeat.
38
+ def send_heartbeat(sdk_id)
39
+ post("/sdk/heartbeat/", { sdk_id: sdk_id })
40
+ end
41
+
42
+ private
43
+
44
+ def post(path, body)
45
+ uri = URI("#{BASE_URL}#{path}")
46
+ http = build_http(uri)
47
+
48
+ request = Net::HTTP::Post.new(uri)
49
+ request["Content-Type"] = "application/json"
50
+ request["X-API-Key"] = @api_key.to_s
51
+ request["X-SDK-Key"] = @sdk_key.to_s
52
+ request["User-Agent"] = "lescopr-ruby/#{Lescopr::VERSION}"
53
+ request.body = body.to_json
54
+
55
+ retries = 0
56
+ begin
57
+ response = http.request(request)
58
+ return JSON.parse(response.body, symbolize_names: true) if response.is_a?(Net::HTTPSuccess)
59
+
60
+ nil
61
+ rescue Net::OpenTimeout, Net::ReadTimeout, Errno::ECONNREFUSED, SocketError => e
62
+ retries += 1
63
+ retry if retries < MAX_RETRIES
64
+ nil
65
+ end
66
+ end
67
+
68
+ def build_http(uri)
69
+ http = Net::HTTP.new(uri.host, uri.port)
70
+ http.use_ssl = uri.scheme == "https"
71
+ http.open_timeout = TIMEOUT_SEC
72
+ http.read_timeout = TIMEOUT_SEC
73
+ http
74
+ end
75
+ end
76
+ end
77
+ end
78
+
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lescopr
4
+ VERSION = "0.1.0"
5
+ end
6
+
data/lib/lescopr.rb ADDED
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "net/http"
5
+ require "uri"
6
+ require "logger"
7
+ require "singleton"
8
+
9
+ require_relative "lescopr/version"
10
+ require_relative "lescopr/monitoring/logger"
11
+ require_relative "lescopr/filesystem/config_manager"
12
+ require_relative "lescopr/filesystem/project_analyzer"
13
+ require_relative "lescopr/transport/http_client"
14
+ require_relative "lescopr/core/client"
15
+ require_relative "lescopr/core/log_queue"
16
+ require_relative "lescopr/core/daemon_runner"
17
+
18
+ # Framework integrations — loaded lazily based on detected environment
19
+ require_relative "lescopr/integrations/rack/middleware"
20
+ require_relative "lescopr/integrations/sinatra/extension"
21
+
22
+ # Rails Railtie is auto-loaded when Rails is present
23
+ if defined?(Rails)
24
+ require_relative "lescopr/integrations/rails/railtie"
25
+ end
26
+
27
+ ##
28
+ # Lescopr — zero-configuration Ruby monitoring SDK.
29
+ #
30
+ # @example Plain Ruby / Rack
31
+ # require "lescopr"
32
+ # Lescopr.init!(sdk_key: "lsk_xxx", api_key: "lak_xxx")
33
+ #
34
+ # @example Rails (auto-init via Railtie)
35
+ # # config/initializers/lescopr.rb
36
+ # Lescopr.configure do |c|
37
+ # c.sdk_key = ENV["LESCOPR_SDK_KEY"]
38
+ # c.api_key = ENV["LESCOPR_API_KEY"]
39
+ # c.environment = ENV.fetch("LESCOPR_ENVIRONMENT", "production")
40
+ # end
41
+ #
42
+ module Lescopr
43
+ BASE_URL = "https://api.lescopr.com/api/v1"
44
+
45
+ class << self
46
+ # @return [Lescopr::Core::Client] the active SDK client instance
47
+ attr_reader :client
48
+
49
+ # Configure the SDK via a block.
50
+ #
51
+ # @yieldparam config [Lescopr::Configuration]
52
+ def configure
53
+ yield(configuration)
54
+ @client = Core::Client.new(configuration)
55
+ @client.setup_auto_logging!
56
+ @client
57
+ end
58
+
59
+ # Shorthand initialiser — accepts a hash or keyword args.
60
+ #
61
+ # @param opts [Hash]
62
+ # @option opts [String] :sdk_key SDK key (lsk_xxx)
63
+ # @option opts [String] :api_key API key (lak_xxx)
64
+ # @option opts [String] :environment "development" or "production"
65
+ def init!(opts = {})
66
+ configuration.sdk_key = opts[:sdk_key] if opts[:sdk_key]
67
+ configuration.api_key = opts[:api_key] if opts[:api_key]
68
+ configuration.environment = opts[:environment] if opts[:environment]
69
+ configure { |_c| } # trigger setup
70
+ end
71
+
72
+ # Send a log entry manually.
73
+ #
74
+ # @param level [String, Symbol] log level (debug/info/warn/error/fatal)
75
+ # @param message [String]
76
+ # @param metadata [Hash] extra context
77
+ def log(level, message, metadata = {})
78
+ return unless client&.ready?
79
+
80
+ client.send_log(level.to_s.upcase, message, metadata)
81
+ end
82
+
83
+ # @return [Lescopr::Configuration]
84
+ def configuration
85
+ @configuration ||= Configuration.new
86
+ end
87
+
88
+ # Reset the SDK (useful in tests).
89
+ def reset!
90
+ @client&.shutdown!
91
+ @client = nil
92
+ @configuration = nil
93
+ end
94
+ end
95
+
96
+ # ─── Configuration ────────────────────────────────────────────────────────
97
+ class Configuration
98
+ attr_accessor :sdk_key, :api_key, :environment, :debug, :batch_size, :flush_interval
99
+
100
+ def initialize
101
+ @sdk_key = ENV["LESCOPR_SDK_KEY"]
102
+ @api_key = ENV["LESCOPR_API_KEY"]
103
+ @environment = ENV.fetch("LESCOPR_ENVIRONMENT", "development")
104
+ @debug = ENV["LESCOPR_DEBUG"] == "true"
105
+ @batch_size = 50
106
+ @flush_interval = 5 # seconds
107
+ end
108
+ end
109
+ end
110
+
metadata ADDED
@@ -0,0 +1,149 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lescopr
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - SonnaLab
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 2026-03-07 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: json
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '2.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '2.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rspec
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '3.12'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '3.12'
40
+ - !ruby/object:Gem::Dependency
41
+ name: webmock
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '3.18'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '3.18'
54
+ - !ruby/object:Gem::Dependency
55
+ name: rack
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '2.0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '2.0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: rake
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '13.0'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '13.0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: rubocop
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '1.50'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '1.50'
96
+ description: Lescopr automatically captures logs, errors and exceptions from any Ruby
97
+ project and streams them in real-time to the Lescopr dashboard. Works with Rails,
98
+ Sinatra, Rack and plain Ruby.
99
+ email:
100
+ - support@lescopr.com
101
+ executables:
102
+ - lescopr
103
+ extensions: []
104
+ extra_rdoc_files: []
105
+ files:
106
+ - CHANGELOG.md
107
+ - LICENSE
108
+ - README.md
109
+ - exe/lescopr
110
+ - lib/lescopr.rb
111
+ - lib/lescopr/core/client.rb
112
+ - lib/lescopr/core/daemon_runner.rb
113
+ - lib/lescopr/core/log_queue.rb
114
+ - lib/lescopr/filesystem/config_manager.rb
115
+ - lib/lescopr/filesystem/project_analyzer.rb
116
+ - lib/lescopr/integrations/rack/middleware.rb
117
+ - lib/lescopr/integrations/rails/railtie.rb
118
+ - lib/lescopr/integrations/sinatra/extension.rb
119
+ - lib/lescopr/monitoring/logger.rb
120
+ - lib/lescopr/transport/http_client.rb
121
+ - lib/lescopr/version.rb
122
+ homepage: https://lescopr.com
123
+ licenses:
124
+ - MIT
125
+ metadata:
126
+ homepage_uri: https://lescopr.com
127
+ source_code_uri: https://github.com/Lescopr/lescopr-ruby
128
+ changelog_uri: https://github.com/Lescopr/lescopr-ruby/blob/main/CHANGELOG.md
129
+ documentation_uri: https://docs.lescopr.com
130
+ bug_tracker_uri: https://github.com/Lescopr/lescopr-ruby/issues
131
+ rubygems_mfa_required: 'true'
132
+ rdoc_options: []
133
+ require_paths:
134
+ - lib
135
+ required_ruby_version: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - ">="
138
+ - !ruby/object:Gem::Version
139
+ version: 2.7.0
140
+ required_rubygems_version: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: '0'
145
+ requirements: []
146
+ rubygems_version: 3.6.3
147
+ specification_version: 4
148
+ summary: Zero-configuration Ruby monitoring SDK
149
+ test_files: []