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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +51 -0
- data/LICENSE +22 -0
- data/README.md +289 -0
- data/exe/lescopr +117 -0
- data/lib/lescopr/core/client.rb +133 -0
- data/lib/lescopr/core/daemon_runner.rb +75 -0
- data/lib/lescopr/core/log_queue.rb +42 -0
- data/lib/lescopr/filesystem/config_manager.rb +42 -0
- data/lib/lescopr/filesystem/project_analyzer.rb +79 -0
- data/lib/lescopr/integrations/rack/middleware.rb +31 -0
- data/lib/lescopr/integrations/rails/railtie.rb +58 -0
- data/lib/lescopr/integrations/sinatra/extension.rb +46 -0
- data/lib/lescopr/monitoring/logger.rb +42 -0
- data/lib/lescopr/transport/http_client.rb +78 -0
- data/lib/lescopr/version.rb +6 -0
- data/lib/lescopr.rb +110 -0
- metadata +149 -0
|
@@ -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
|
+
|
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: []
|