lescopr 0.1.1 → 1.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 17e96cbe7ffa8dd500efe4c22fff59e4098b55ba8c172e3571b6dfc93e754f4f
4
- data.tar.gz: c1c3486176a778b133d4fc5bbaa54fbe236b54ee62e4a1bc43eb9a1043f4148c
3
+ metadata.gz: 3efeda7d85c0e939043d1e0c9d4537977d37152832209a96e486e2abed28a2b7
4
+ data.tar.gz: 05310bf63b8dd2fb3c0e0da98e6c5b5181c751482c8652a647ffd2b92c6067ee
5
5
  SHA512:
6
- metadata.gz: 685527b7a1d399d79a8066feafeeb83868db264c2a625158210196df359fb9cd8a95161d305fa0219489d68758f74be2c27096aab135dac0b5cfa71df489d80d
7
- data.tar.gz: 0e122b2d31270aa68b09347b0409423dbb4a0a3e2873d60b54d52eb15e4b3d04cd5a2d46d05d04fa6f9c83a9e187ea3d96e606ddd31073482789af21c59b648f
6
+ metadata.gz: ce9a7fc494e0946721ae00bc07e4ac82ebedec432f27423f27671b773d78bd1d6f1de4ce67b5dea83559bfca210f45dd60fce4d9b190f5437555bef25ecb6440
7
+ data.tar.gz: 77788f5050abf55f0d5dc62dbba9a2dbe867bea75e7dc8d110841858f9bb036617d40cbb476d847144870870dd17b0782e0706b031df50d29fc035f7d1e41d1f
data/CHANGELOG.md CHANGED
@@ -11,6 +11,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
11
11
 
12
12
  ---
13
13
 
14
+ ## [1.0.0] — 2026-04-17
15
+
16
+ ### Added
17
+ - **Modes** — nouveau module `lib/lescopr/modes/` pour configurer finement le comportement du SDK (silent, verbose, strict)
18
+ - **Makefile** — workflow de release simplifié (`bump-patch`, `bump-minor`, `bump-major`, `release V=x.y.z`, `test`, `build`, `tag`, `push`)
19
+
20
+ ### Changed
21
+ - `lib/lescopr.rb` — API publique alignée avec les nouvelles capacités de configuration
22
+ - `lib/lescopr/core/client.rb` — intégration des modes
23
+ - `lib/lescopr/transport/http_client.rb` — retry logic renforcée
24
+
25
+ ### Fixed
26
+ - Script de release : vérification de l'arbre git limitée au répertoire Ruby (`git status --porcelain .`) pour éviter les faux positifs dans les setups monorepo
27
+
28
+ ---
29
+
30
+ ## [0.1.2] — 2026-03-08
31
+
32
+ ### Fixed
33
+ - CI: use `GEM_HOST_API_KEY` env var on `gem push` to avoid interactive MFA prompt
34
+ - CI: remove `rake release` dependency, build gem directly in workflow
35
+
36
+ ---
37
+
14
38
  ## [0.1.1] — 2026-03-08
15
39
 
16
40
  ### Fixed
@@ -52,7 +76,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
52
76
 
53
77
  ---
54
78
 
55
- [Unreleased]: https://github.com/Lescopr/lescopr-ruby/compare/v0.1.1...HEAD
79
+ [Unreleased]: https://github.com/Lescopr/lescopr-ruby/compare/v1.0.0...HEAD
80
+ [1.0.0]: https://github.com/Lescopr/lescopr-ruby/releases/tag/v1.0.0
81
+ [0.1.2]: https://github.com/Lescopr/lescopr-ruby/compare/v0.1.1...v0.1.2
56
82
  [0.1.1]: https://github.com/Lescopr/lescopr-ruby/compare/v0.1.0...v0.1.1
57
83
  [0.1.0]: https://github.com/Lescopr/lescopr-ruby/releases/tag/v0.1.0
58
84
  [0.1.0]: https://github.com/Lescopr/lescopr-ruby/releases/tag/v0.1.0
data/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  [![Ruby versions](https://img.shields.io/badge/ruby-2.7%20%7C%203.0%20%7C%203.1%20%7C%203.2%20%7C%203.3-ruby?cacheSeconds=300)](https://rubygems.org/gems/lescopr)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
7
7
 
8
- **Lescopr** is a zero-configuration Ruby monitoring SDK that automatically captures logs, errors, and exceptions from any Ruby project and streams them in real-time to the [Lescopr dashboard](https://app.lescopr.com).
8
+ **Lescopr** is a zero-configuration Ruby monitoring SDK that automatically captures logs, errors, and exceptions from any Ruby project and streams them in real-time to the [Lescopr app](https://app.lescopr.com).
9
9
 
10
10
  Works out of the box with **Rails**, **Sinatra**, **Rack**, **Hanami**, **Grape**, and **plain Ruby**.
11
11
 
@@ -87,7 +87,7 @@ This detects your framework, registers the project with the Lescopr API, and wri
87
87
 
88
88
  **Step 2 — Integrate into your application** (see [Framework Integration](#framework-integration) below).
89
89
 
90
- **That's it.** All logs and exceptions are forwarded to the Lescopr dashboard automatically.
90
+ **That's it.** All logs and exceptions are forwarded to the Lescopr app automatically.
91
91
 
92
92
  ---
93
93
 
@@ -195,7 +195,7 @@ Your Ruby Application
195
195
  https://api.lescopr.com
196
196
 
197
197
 
198
- Lescopr Dashboard
198
+ Lescopr App
199
199
  https://app.lescopr.com
200
200
  ```
201
201
 
@@ -276,8 +276,8 @@ To publish a new release:
276
276
 
277
277
  | Channel | Link |
278
278
  |---|---|
279
- | 📖 Documentation | <https://docs.lescopr.com> |
280
- | 🌐 Dashboard | <https://app.lescopr.com> |
279
+ | 📖 Documentation | <https://lescopr.com/docs> |
280
+ | 🌐 App | <https://app.lescopr.com> |
281
281
  | 📧 Email | <support@lescopr.com> |
282
282
  | 🐛 Bug reports | <https://github.com/Lescopr/lescopr-ruby/issues> |
283
283
 
@@ -5,13 +5,16 @@ module Lescopr
5
5
  # Central SDK client — manages configuration, log queue, daemon lifecycle
6
6
  # and the Ruby logger hook.
7
7
  class Client
8
- attr_reader :configuration, :sdk_id, :log_queue, :http_client, :sdk_logger
8
+ attr_reader :configuration, :sdk_id, :log_queue, :http_client, :sdk_logger,
9
+ :mode, :direct_mode
9
10
 
10
11
  def initialize(configuration)
11
12
  @configuration = configuration
12
13
  @sdk_id = nil
13
14
  @ready = false
14
15
  @mutex = Mutex.new
16
+ @mode = nil # 'daemon' | 'embedded' | 'direct'
17
+ @direct_mode = nil # DirectMode or EmbeddedMode instance
15
18
 
16
19
  @sdk_logger = Monitoring::Logger.new(debug: configuration.debug)
17
20
  @log_queue = LogQueue.new
@@ -60,7 +63,20 @@ module Lescopr
60
63
  @daemon.start
61
64
  @ready = true
62
65
 
63
- sdk_logger.info("SDK initialised project: #{payload[:project_name]}, sdk_id: #{@sdk_id}")
66
+ # Determine and start mode after being ready
67
+ @mode = Modes::Detector.detect
68
+ sdk_logger.info("SDK initialised — mode: #{@mode}, project: #{payload[:project_name]}, sdk_id: #{@sdk_id}")
69
+
70
+ case @mode
71
+ when 'direct'
72
+ @direct_mode = Modes::DirectMode.new(http_client)
73
+ @direct_mode.start
74
+ when 'embedded'
75
+ @direct_mode = Modes::EmbeddedMode.new(http_client)
76
+ @direct_mode.start
77
+ end
78
+ # 'daemon' uses existing DaemonRunner — no extra setup needed
79
+
64
80
  true
65
81
  rescue StandardError => e
66
82
  sdk_logger.error("setup! failed: #{e.message}")
@@ -87,12 +103,22 @@ module Lescopr
87
103
  environment: configuration.environment,
88
104
  metadata: metadata
89
105
  }
90
- log_queue.push(entry)
106
+
107
+ # Route to mode-specific transport
108
+ if @direct_mode
109
+ @direct_mode.add_log(entry)
110
+ else
111
+ log_queue.push(entry)
112
+ end
91
113
  end
92
114
 
93
115
  # Graceful shutdown — flush queue then stop daemon.
94
116
  def shutdown!
95
- @daemon.stop
117
+ if @direct_mode
118
+ @direct_mode.stop
119
+ else
120
+ @daemon.stop
121
+ end
96
122
  @ready = false
97
123
  sdk_logger.info("SDK shut down")
98
124
  end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lescopr
4
+ module Modes
5
+ ##
6
+ # Auto-detect the optimal transport mode.
7
+ #
8
+ # Priority:
9
+ # 1. LESCOPR_MODE env var (daemon | embedded | direct)
10
+ # 2. Serverless / short-lived signals → direct
11
+ # 3. Multi-worker forked process → embedded
12
+ # 4. Container (Docker / K8s) → embedded
13
+ # 5. Daemon local active (checked by caller) → daemon
14
+ # 6. Fallback → embedded
15
+ #
16
+ # @return ['daemon', 'embedded', 'direct']
17
+ module Detector
18
+ SERVERLESS_SIGNALS = %w[
19
+ AWS_LAMBDA_FUNCTION_NAME
20
+ AWS_LAMBDA_RUNTIME_API
21
+ LAMBDA_TASK_ROOT
22
+ VERCEL
23
+ VERCEL_ENV
24
+ NETLIFY
25
+ NETLIFY_DEV
26
+ K_SERVICE
27
+ FUNCTION_NAME
28
+ FUNCTIONS_WORKER_RUNTIME
29
+ AZURE_FUNCTIONS_ENVIRONMENT
30
+ ].freeze
31
+
32
+ def self.detect
33
+ forced = ENV.fetch("LESCOPR_MODE", "").downcase
34
+ return forced if %w[daemon embedded direct].include?(forced)
35
+
36
+ return "direct" if serverless?
37
+ return "embedded" if multiworker_child?
38
+ return "embedded" if container?
39
+
40
+ # Caller checks whether the daemon is actually running
41
+ "daemon"
42
+ end
43
+
44
+ def self.serverless?
45
+ SERVERLESS_SIGNALS.any? { |s| ENV.key?(s) }
46
+ end
47
+
48
+ def self.multiworker_child?
49
+ # Unicorn / Puma forked workers expose the master PID
50
+ return true if ENV["UNICORN_FD"] || ($0 && $0.include?("unicorn"))
51
+ return true if ENV["PUMA_WORKER"] || (defined?(Puma) && Puma.respond_to?(:cli_config))
52
+ return true if ENV["SIDEKIQ_WORKERS"] || (defined?(Sidekiq) && Sidekiq.server?)
53
+ false
54
+ end
55
+
56
+ def self.container?
57
+ return true if File.exist?("/.dockerenv")
58
+ return true if ENV["KUBERNETES_SERVICE_HOST"]
59
+
60
+ begin
61
+ content = File.read("/proc/1/cgroup")
62
+ return true if content.include?("docker") || content.include?("kubepods")
63
+ rescue Errno::ENOENT, Errno::EACCES
64
+ # Not Linux or no access — not a container
65
+ end
66
+
67
+ false
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thread"
4
+
5
+ module Lescopr
6
+ module Modes
7
+ ##
8
+ # DirectMode — in-memory queue + HTTPS batch flush to /logs/ingest.
9
+ #
10
+ # Designed for serverless / ephemeral Ruby processes:
11
+ # - AWS Lambda (ruby runtime)
12
+ # - Heroku one-off dynos
13
+ # - Short-lived Rake tasks or scripts
14
+ #
15
+ # Guarantees:
16
+ # - Periodic flush every FLUSH_INTERVAL seconds (background thread)
17
+ # - Guaranteed flush on process exit via at_exit hook
18
+ # - Re-queue on network error (up to MAX_QUEUE entries)
19
+ class DirectMode
20
+ FLUSH_INTERVAL = 15 # seconds
21
+ BATCH_SIZE = 100
22
+ MAX_QUEUE = 500
23
+
24
+ def initialize(http_client)
25
+ @http_client = http_client
26
+ @queue = []
27
+ @mutex = Mutex.new
28
+ @stop_event = false
29
+ @thread = nil
30
+ end
31
+
32
+ # ── Lifecycle ──────────────────────────────────────────────────────────
33
+
34
+ def start
35
+ install_exit_hook!
36
+ start_flush_thread!
37
+
38
+ Lescopr::Monitoring::Logger.new.info("[DIRECT] Mode direct actif (HTTPS batch)")
39
+ self
40
+ end
41
+
42
+ def stop
43
+ @stop_event = true
44
+ @thread&.kill
45
+ flush_sync
46
+ end
47
+
48
+ # ── Public API ─────────────────────────────────────────────────────────
49
+
50
+ def add_log(entry)
51
+ @mutex.synchronize do
52
+ @queue << entry
53
+ if @queue.size > MAX_QUEUE
54
+ @queue = @queue.last(MAX_QUEUE / 2)
55
+ end
56
+ end
57
+ end
58
+
59
+ # ── Flush ──────────────────────────────────────────────────────────────
60
+
61
+ def flush_sync
62
+ loop do
63
+ batch = @mutex.synchronize { @queue.shift(BATCH_SIZE) }
64
+ break if batch.empty?
65
+
66
+ send_batch(batch)
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ def start_flush_thread!
73
+ @thread = Thread.new do
74
+ Thread.current.name = "lescopr-direct" rescue nil
75
+ until @stop_event
76
+ sleep(FLUSH_INTERVAL)
77
+ flush_sync
78
+ end
79
+ end
80
+ @thread.abort_on_exception = false
81
+ end
82
+
83
+ def install_exit_hook!
84
+ at_exit { flush_sync }
85
+ end
86
+
87
+ def send_batch(batch)
88
+ return if batch.empty?
89
+
90
+ result = @http_client.ingest_logs(batch)
91
+
92
+ if result
93
+ Lescopr::Monitoring::Logger.new.debug("[DIRECT] #{batch.size} log(s) envoyé(s)")
94
+ else
95
+ Lescopr::Monitoring::Logger.new.warn("[DIRECT] Flush échoué — remis en queue")
96
+ @mutex.synchronize do
97
+ @queue = batch + @queue
98
+ if @queue.size > MAX_QUEUE
99
+ @queue = @queue.last(MAX_QUEUE / 2)
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thread"
4
+
5
+ module Lescopr
6
+ module Modes
7
+ ##
8
+ # EmbeddedMode — gRPC-less in-process transport for long-running Ruby apps.
9
+ #
10
+ # Designed for:
11
+ # - Docker / Kubernetes containers (no sidecar daemon)
12
+ # - Gunicorn-style forked multi-worker apps (Puma, Unicorn)
13
+ # - Any environment where running a separate daemon is impractical
14
+ #
15
+ # Behaviour:
16
+ # - Reuses the existing HTTP transport (no gRPC required)
17
+ # - Background thread flushes queue via HTTPS batch every FLUSH_INTERVAL s
18
+ # - Exponential backoff on connection errors (up to 12 attempts)
19
+ # - Exits cleanly via at_exit hook
20
+ class EmbeddedMode
21
+ FLUSH_INTERVAL = 30 # seconds between normal flushes
22
+ BATCH_SIZE = 100
23
+ MAX_QUEUE = 500
24
+ MAX_CONNECT_ATTEMPTS = 12
25
+
26
+ def initialize(http_client)
27
+ @http_client = http_client
28
+ @queue = []
29
+ @mutex = Mutex.new
30
+ @stop_event = false
31
+ @thread = nil
32
+ @connected = false
33
+ @logger = Lescopr::Monitoring::Logger.new
34
+ end
35
+
36
+ # ── Lifecycle ──────────────────────────────────────────────────────────
37
+
38
+ def start
39
+ install_exit_hook!
40
+ start_worker_thread!
41
+
42
+ @logger.info("[EMBEDDED] Mode embedded actif (thread HTTP)")
43
+ self
44
+ end
45
+
46
+ def stop
47
+ @stop_event = true
48
+ @thread&.join(5)
49
+ flush_sync
50
+ end
51
+
52
+ # ── Public API ─────────────────────────────────────────────────────────
53
+
54
+ def add_log(entry)
55
+ @mutex.synchronize do
56
+ @queue << entry
57
+ if @queue.size > MAX_QUEUE
58
+ @queue = @queue.last(MAX_QUEUE / 2)
59
+ end
60
+ end
61
+ end
62
+
63
+ # ── Flush ──────────────────────────────────────────────────────────────
64
+
65
+ def flush_sync
66
+ loop do
67
+ batch = @mutex.synchronize { @queue.shift(BATCH_SIZE) }
68
+ break if batch.empty?
69
+
70
+ send_batch(batch)
71
+ end
72
+ end
73
+
74
+ private
75
+
76
+ def start_worker_thread!
77
+ @thread = Thread.new do
78
+ Thread.current.name = "lescopr-embedded" rescue nil
79
+ _connect_with_backoff
80
+ end
81
+ @thread.abort_on_exception = false
82
+ end
83
+
84
+ def _connect_with_backoff
85
+ delay = 2
86
+ attempts = 0
87
+
88
+ until @stop_event
89
+ sleep(FLUSH_INTERVAL) unless attempts.zero?
90
+
91
+ begin
92
+ flush_sync
93
+ @connected = true
94
+ @logger.info("[EMBEDDED] ✅ Flush réussi (tentative #{attempts + 1})") if attempts > 0
95
+ attempts = 0
96
+ delay = 2
97
+ rescue StandardError => e
98
+ attempts += 1
99
+ @logger.debug("[EMBEDDED] Tentative #{attempts} échouée: #{e.message}")
100
+
101
+ if attempts >= MAX_CONNECT_ATTEMPTS
102
+ @logger.warn("[EMBEDDED] Trop d'échecs — pause de 60s avant retry")
103
+ sleep(60)
104
+ attempts = 0
105
+ delay = 2
106
+ else
107
+ sleep(delay)
108
+ delay = [delay * 2, 60].min
109
+ end
110
+ end
111
+ end
112
+ end
113
+
114
+ def install_exit_hook!
115
+ at_exit { flush_sync }
116
+ end
117
+
118
+ def send_batch(batch)
119
+ return if batch.empty?
120
+
121
+ result = @http_client.ingest_logs(batch)
122
+
123
+ unless result
124
+ @mutex.synchronize do
125
+ @queue = batch + @queue
126
+ if @queue.size > MAX_QUEUE
127
+ @queue = @queue.last(MAX_QUEUE / 2)
128
+ end
129
+ end
130
+ raise "ingest_logs returned nil"
131
+ end
132
+
133
+ @logger.debug("[EMBEDDED] #{batch.size} log(s) envoyé(s)")
134
+ end
135
+ end
136
+ end
137
+ end
@@ -34,6 +34,18 @@ module Lescopr
34
34
  !result.nil?
35
35
  end
36
36
 
37
+ # Batch ingest via the new /logs/ingest endpoint (Direct / Embedded mode).
38
+ # Authenticated by X-SDK-Key header — no user session required.
39
+ #
40
+ # @param logs [Array<Hash>]
41
+ # @return [Boolean]
42
+ def ingest_logs(logs)
43
+ return true if logs.empty?
44
+
45
+ result = post("/logs/ingest", { logs: logs })
46
+ !result.nil?
47
+ end
48
+
37
49
  # Send a single heartbeat.
38
50
  def send_heartbeat(sdk_id)
39
51
  post("/sdk/heartbeat/", { sdk_id: sdk_id })
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Lescopr
4
- VERSION = "0.1.1"
4
+ VERSION = "1.0.0"
5
5
  end
6
6
 
data/lib/lescopr.rb CHANGED
@@ -19,6 +19,11 @@ require_relative "lescopr/core/daemon_runner"
19
19
  require_relative "lescopr/integrations/rack/middleware"
20
20
  require_relative "lescopr/integrations/sinatra/extension"
21
21
 
22
+ # Modes package
23
+ require_relative "lescopr/modes/detector"
24
+ require_relative "lescopr/modes/direct"
25
+ require_relative "lescopr/modes/embedded"
26
+
22
27
  # Rails Railtie is auto-loaded when Rails is present
23
28
  if defined?(Rails)
24
29
  require_relative "lescopr/integrations/rails/railtie"
@@ -56,6 +61,29 @@ module Lescopr
56
61
  @client
57
62
  end
58
63
 
64
+ # Zero-config init — loads SDK keys from .lescopr/config.json and
65
+ # auto-detects the best transport mode (daemon / embedded / direct).
66
+ #
67
+ # Usage (e.g. config/initializers/lescopr.rb or top of application.rb):
68
+ # Lescopr.logs
69
+ #
70
+ # @return [Lescopr::Core::Client, nil]
71
+ def logs
72
+ return @client if @client
73
+
74
+ config_mgr = Filesystem::ConfigManager.new
75
+ saved = config_mgr.load
76
+ return nil unless saved && saved[:sdk_key]
77
+
78
+ configuration.sdk_key = saved[:sdk_key]
79
+ configuration.api_key = saved[:api_key]
80
+ configuration.environment = saved[:environment] || "development"
81
+
82
+ @client = Core::Client.new(configuration)
83
+ @client.setup_auto_logging!
84
+ @client
85
+ end
86
+
59
87
  # Shorthand initialiser — accepts a hash or keyword args.
60
88
  #
61
89
  # @param opts [Hash]
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lescopr
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - SonnaLab
8
+ autorequire:
8
9
  bindir: exe
9
10
  cert_chain: []
10
- date: 2026-03-07 00:00:00.000000000 Z
11
+ date: 2026-04-17 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: json
@@ -116,6 +117,9 @@ files:
116
117
  - lib/lescopr/integrations/rack/middleware.rb
117
118
  - lib/lescopr/integrations/rails/railtie.rb
118
119
  - lib/lescopr/integrations/sinatra/extension.rb
120
+ - lib/lescopr/modes/detector.rb
121
+ - lib/lescopr/modes/direct.rb
122
+ - lib/lescopr/modes/embedded.rb
119
123
  - lib/lescopr/monitoring/logger.rb
120
124
  - lib/lescopr/transport/http_client.rb
121
125
  - lib/lescopr/version.rb
@@ -129,6 +133,7 @@ metadata:
129
133
  documentation_uri: https://docs.lescopr.com
130
134
  bug_tracker_uri: https://github.com/Lescopr/lescopr-ruby/issues
131
135
  rubygems_mfa_required: 'true'
136
+ post_install_message:
132
137
  rdoc_options: []
133
138
  require_paths:
134
139
  - lib
@@ -143,7 +148,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
143
148
  - !ruby/object:Gem::Version
144
149
  version: '0'
145
150
  requirements: []
146
- rubygems_version: 3.6.3
151
+ rubygems_version: 3.0.3.1
152
+ signing_key:
147
153
  specification_version: 4
148
154
  summary: Zero-configuration Ruby monitoring SDK
149
155
  test_files: []