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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 291f6d817d72c6598f388e7088f36c742d278d58508b74443327bc81a108771e
|
|
4
|
+
data.tar.gz: 31c73668b1e5b2c57461d28fdd6eb085fdc55227de6973597ccd0656bfa45f91
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: a5783011f3c58e03a82c22141ec444f07cfbc4e0e3a93afbe533cbd465c4d235a0c2d8d70679eae06dbf8c392b0761ec8a7385733505465d052b150f61d5f32d
|
|
7
|
+
data.tar.gz: 8c9ee746851f418d710bc4e374d2c5bb03161e88ee58938afcf39b4ebe55f711f7c34135cc8345cd046c56fd22a48664b50886bbc3f76daec63e70957674252c
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `lescopr` (Ruby gem) are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## [Unreleased]
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## [0.1.0] — 2026-03-08
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## [0.1.0] — 2026-03-07
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- **Core SDK** (`Lescopr::Core::Client`) — central object managing config, log queue and daemon lifecycle
|
|
22
|
+
- **Background daemon** (`DaemonRunner`) — Ruby thread flushing logs to `api.lescopr.com` every 5 s, heartbeat every 30 s
|
|
23
|
+
- **HTTP transport** (`Transport::HttpClient`) — HTTPS batch delivery via `net/http`, 3 retries, zero external gems
|
|
24
|
+
- **Framework auto-detection** (`Filesystem::ProjectAnalyzer`) — detects Rails, Sinatra, Hanami, Grape, Padrino and plain Rack from `Gemfile`
|
|
25
|
+
- **Config manager** (`Filesystem::ConfigManager`) — thread-safe read/write of `.lescopr.json`
|
|
26
|
+
- **Rails integration** (`Integrations::Rails::Railtie`) — auto-registers on `require "lescopr"`; hooks `process_action.action_controller` notifications
|
|
27
|
+
- **Rack middleware** (`Integrations::Rack::Middleware`) — captures exceptions from any Rack-compatible app
|
|
28
|
+
- **Sinatra extension** (`Integrations::Sinatra::Extension`) — `register`-based integration with `lescopr_log` helper
|
|
29
|
+
- **CLI** (`exe/lescopr`) — `init`, `status`, `diagnose`, `reset` commands via stdlib `optparse`
|
|
30
|
+
- **Internal logger** (`Monitoring::Logger`) — writes to `.lescopr.log` with rotation, never pollutes app output
|
|
31
|
+
- **Test suite** — RSpec unit tests for LogQueue, ConfigManager, ProjectAnalyzer, Rack middleware
|
|
32
|
+
- **Ruby 2.7+ compatibility** — no Ruby 3.x-only syntax; tested on 2.7, 3.0, 3.1, 3.2, 3.3
|
|
33
|
+
- **Zero external runtime dependencies** — only `json` (stdlib-compatible)
|
|
34
|
+
|
|
35
|
+
### Compatibility matrix
|
|
36
|
+
|
|
37
|
+
| Framework | Version | Ruby |
|
|
38
|
+
|-------------|---------------|---------|
|
|
39
|
+
| Rails | 6, 7, 8 | 2.7–3.3 |
|
|
40
|
+
| Sinatra | 2, 3, 4 | 2.7–3.3 |
|
|
41
|
+
| Hanami | 1, 2 | 2.7–3.3 |
|
|
42
|
+
| Grape | 1.x, 2.x | 2.7–3.3 |
|
|
43
|
+
| Plain Rack | 2, 3 | 2.7–3.3 |
|
|
44
|
+
| Plain Ruby | — | 2.7–3.3 |
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
[Unreleased]: https://github.com/Lescopr/lescopr-ruby/compare/v0.1.0...HEAD
|
|
49
|
+
[0.1.0]: https://github.com/Lescopr/lescopr-ruby/releases/tag/v0.1.0
|
|
50
|
+
[0.1.0]: https://github.com/Lescopr/lescopr-ruby/releases/tag/v0.1.0
|
|
51
|
+
|
data/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-2026 SonnaLab (https://sonnalab.com)
|
|
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.
|
|
22
|
+
|
data/README.md
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
# Lescopr Ruby SDK
|
|
2
|
+
|
|
3
|
+
[](https://rubygems.org/gems/lescopr)
|
|
4
|
+
[](https://rubygems.org/gems/lescopr)
|
|
5
|
+
[](https://rubygems.org/gems/lescopr)
|
|
6
|
+
[](LICENSE)
|
|
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).
|
|
9
|
+
|
|
10
|
+
Works out of the box with **Rails**, **Sinatra**, **Rack**, **Hanami**, **Grape**, and **plain Ruby**.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Table of Contents
|
|
15
|
+
|
|
16
|
+
- [Features](#features)
|
|
17
|
+
- [Requirements](#requirements)
|
|
18
|
+
- [Installation](#installation)
|
|
19
|
+
- [Quick Start](#quick-start)
|
|
20
|
+
- [Framework Integration](#framework-integration)
|
|
21
|
+
- [Rails](#rails)
|
|
22
|
+
- [Sinatra](#sinatra)
|
|
23
|
+
- [Rack](#rack)
|
|
24
|
+
- [Plain Ruby](#plain-ruby)
|
|
25
|
+
- [Architecture](#architecture)
|
|
26
|
+
- [CLI Reference](#cli-reference)
|
|
27
|
+
- [Advanced Configuration](#advanced-configuration)
|
|
28
|
+
- [RubyGems](#rubygems)
|
|
29
|
+
- [License](#license)
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Features
|
|
34
|
+
|
|
35
|
+
- ✅ **Automatic error capture** — hooks into Rails `process_action`, Rack middleware, and `at_exit`
|
|
36
|
+
- ✅ **Framework auto-detection** — detects Rails, Sinatra, Hanami, Grape, Padrino and plain Rack from `Gemfile`
|
|
37
|
+
- ✅ **Background daemon** — Ruby thread, completely non-blocking, flushes every 5 seconds
|
|
38
|
+
- ✅ **HTTP batch transport** — logs are batched and sent via `net/http` (no external gem required)
|
|
39
|
+
- ✅ **Zero runtime dependencies** — only `json` (already in stdlib since Ruby 2.7)
|
|
40
|
+
- ✅ **Works everywhere** — Rails, Sinatra, Rack, scripts, plain Ruby
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Requirements
|
|
45
|
+
|
|
46
|
+
| Requirement | Version |
|
|
47
|
+
|---|---|
|
|
48
|
+
| Ruby | ≥ 2.7 |
|
|
49
|
+
| Bundler | ≥ 2.0 |
|
|
50
|
+
| `json` | bundled with Ruby |
|
|
51
|
+
|
|
52
|
+
> **Note:** No external gem required at runtime. `net/http` and `openssl` are part of Ruby's stdlib.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Installation
|
|
57
|
+
|
|
58
|
+
Add to your `Gemfile`:
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
gem "lescopr"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Then run:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
bundle install
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Or install directly:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
gem install lescopr
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Quick Start
|
|
79
|
+
|
|
80
|
+
**Step 1 — Initialise the SDK in your project directory:**
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
bundle exec lescopr init --sdk-key YOUR_SDK_KEY
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
This detects your framework, registers the project with the Lescopr API, and writes `.lescopr.json`.
|
|
87
|
+
|
|
88
|
+
**Step 2 — Integrate into your application** (see [Framework Integration](#framework-integration) below).
|
|
89
|
+
|
|
90
|
+
**That's it.** All logs and exceptions are forwarded to the Lescopr dashboard automatically.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Framework Integration
|
|
95
|
+
|
|
96
|
+
### Rails
|
|
97
|
+
|
|
98
|
+
The SDK registers automatically via the `Railtie`. Simply add the gem and create an initializer:
|
|
99
|
+
|
|
100
|
+
**`config/initializers/lescopr.rb`:**
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
Lescopr.configure do |c|
|
|
104
|
+
c.sdk_key = ENV["LESCOPR_SDK_KEY"]
|
|
105
|
+
c.api_key = ENV["LESCOPR_API_KEY"]
|
|
106
|
+
c.environment = Rails.env
|
|
107
|
+
end
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**Environment variables (`.env` / credentials):**
|
|
111
|
+
|
|
112
|
+
```dotenv
|
|
113
|
+
LESCOPR_SDK_KEY=lsk_xxxx
|
|
114
|
+
LESCOPR_API_KEY=lak_xxxx
|
|
115
|
+
LESCOPR_ENVIRONMENT=production
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
All controller exceptions are captured automatically via `process_action.action_controller` notification.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
### Sinatra
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
require "sinatra"
|
|
126
|
+
require "lescopr"
|
|
127
|
+
|
|
128
|
+
Lescopr.init!(sdk_key: ENV["LESCOPR_SDK_KEY"], api_key: ENV["LESCOPR_API_KEY"])
|
|
129
|
+
|
|
130
|
+
class MyApp < Sinatra::Base
|
|
131
|
+
register Lescopr::Integrations::Sinatra::Extension
|
|
132
|
+
|
|
133
|
+
get "/" do
|
|
134
|
+
lescopr_log(:info, "Home page visited")
|
|
135
|
+
"Hello!"
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
### Rack
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
# config.ru
|
|
146
|
+
require "lescopr"
|
|
147
|
+
|
|
148
|
+
Lescopr.init!(sdk_key: ENV["LESCOPR_SDK_KEY"], api_key: ENV["LESCOPR_API_KEY"])
|
|
149
|
+
|
|
150
|
+
use Lescopr::Integrations::Rack::Middleware
|
|
151
|
+
|
|
152
|
+
run MyApp
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
### Plain Ruby
|
|
158
|
+
|
|
159
|
+
```ruby
|
|
160
|
+
require "lescopr"
|
|
161
|
+
|
|
162
|
+
Lescopr.init!(
|
|
163
|
+
sdk_key: "lsk_xxxx",
|
|
164
|
+
api_key: "lak_xxxx",
|
|
165
|
+
environment: "production"
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# Manual logging
|
|
169
|
+
Lescopr.log(:info, "Job started", { job: "EmailWorker" })
|
|
170
|
+
|
|
171
|
+
# Exceptions are captured automatically at_exit
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Architecture
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
Your Ruby Application
|
|
180
|
+
│
|
|
181
|
+
│ Rails.logger / raise / Lescopr.log
|
|
182
|
+
▼
|
|
183
|
+
┌─────────────────────────────────┐
|
|
184
|
+
│ Railtie / Rack Middleware │ (framework hooks)
|
|
185
|
+
│ OR at_exit handler │ (plain Ruby)
|
|
186
|
+
└──────────────┬──────────────────┘
|
|
187
|
+
│ push to LogQueue
|
|
188
|
+
▼
|
|
189
|
+
┌─────────────────────────────────┐
|
|
190
|
+
│ DaemonRunner (Thread) │ flushes every 5 s
|
|
191
|
+
│ Heartbeat every 30 s │
|
|
192
|
+
└──────────────┬──────────────────┘
|
|
193
|
+
│ HTTPS batch (net/http)
|
|
194
|
+
▼
|
|
195
|
+
https://api.lescopr.com
|
|
196
|
+
│
|
|
197
|
+
▼
|
|
198
|
+
Lescopr Dashboard
|
|
199
|
+
https://app.lescopr.com
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**Key components:**
|
|
203
|
+
|
|
204
|
+
| Component | Path | Role |
|
|
205
|
+
|---|---|---|
|
|
206
|
+
| `Lescopr` (module) | `lib/lescopr.rb` | Entry point, `configure`, `init!`, `log` |
|
|
207
|
+
| `Core::Client` | `lib/lescopr/core/client.rb` | Central client — config, queue, daemon lifecycle |
|
|
208
|
+
| `Core::DaemonRunner` | `lib/lescopr/core/daemon_runner.rb` | Background thread — flush + heartbeat |
|
|
209
|
+
| `Core::LogQueue` | `lib/lescopr/core/log_queue.rb` | Thread-safe in-memory queue |
|
|
210
|
+
| `Transport::HttpClient` | `lib/lescopr/transport/http_client.rb` | HTTPS delivery via `net/http` |
|
|
211
|
+
| `Filesystem::ProjectAnalyzer` | `lib/lescopr/filesystem/project_analyzer.rb` | Framework detection from `Gemfile` |
|
|
212
|
+
| `Filesystem::ConfigManager` | `lib/lescopr/filesystem/config_manager.rb` | Thread-safe `.lescopr.json` r/w |
|
|
213
|
+
| `Integrations::Rails::Railtie` | `lib/lescopr/integrations/rails/railtie.rb` | Auto-registers in Rails |
|
|
214
|
+
| `Integrations::Rack::Middleware` | `lib/lescopr/integrations/rack/middleware.rb` | Exception capture for Rack apps |
|
|
215
|
+
| `Integrations::Sinatra::Extension` | `lib/lescopr/integrations/sinatra/extension.rb` | `register` for Sinatra |
|
|
216
|
+
| CLI | `exe/lescopr` | `init`, `status`, `diagnose`, `reset` |
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## CLI Reference
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
bundle exec lescopr [COMMAND] [OPTIONS]
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
| Command | Description |
|
|
227
|
+
|---|---|
|
|
228
|
+
| `init --sdk-key KEY` | Initialise the SDK in the current project |
|
|
229
|
+
| `status` | Show SDK configuration and daemon status |
|
|
230
|
+
| `diagnose` | Run a full diagnostic (Ruby env, config, API connectivity) |
|
|
231
|
+
| `reset` | Remove `.lescopr.json`, `.lescopr.pid`, `.lescopr.log` |
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## Advanced Configuration
|
|
236
|
+
|
|
237
|
+
`.lescopr.json` is generated automatically by `lescopr init`:
|
|
238
|
+
|
|
239
|
+
```json
|
|
240
|
+
{
|
|
241
|
+
"sdk_id": "proj_xxxx",
|
|
242
|
+
"sdk_key": "lsk_xxxx",
|
|
243
|
+
"api_key": "lak_xxxx",
|
|
244
|
+
"environment": "production",
|
|
245
|
+
"project_name": "my-app",
|
|
246
|
+
"project_stack": ["rails"]
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
> **Security:** Add `.lescopr.json` to your `.gitignore`.
|
|
251
|
+
|
|
252
|
+
### Environment variables
|
|
253
|
+
|
|
254
|
+
| Variable | Description |
|
|
255
|
+
|---|---|
|
|
256
|
+
| `LESCOPR_SDK_KEY` | SDK key (overrides `.lescopr.json`) |
|
|
257
|
+
| `LESCOPR_API_KEY` | API key (overrides `.lescopr.json`) |
|
|
258
|
+
| `LESCOPR_ENVIRONMENT` | `development` or `production` |
|
|
259
|
+
| `LESCOPR_DEBUG=true` | Enables verbose output to `.lescopr.log` |
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## RubyGems
|
|
264
|
+
|
|
265
|
+
The gem is available on RubyGems: **[lescopr](https://rubygems.org/gems/lescopr)**
|
|
266
|
+
|
|
267
|
+
To publish a new release:
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
./scripts/release.sh 0.2.0
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## Support
|
|
276
|
+
|
|
277
|
+
| Channel | Link |
|
|
278
|
+
|---|---|
|
|
279
|
+
| 📖 Documentation | <https://docs.lescopr.com> |
|
|
280
|
+
| 🌐 Dashboard | <https://app.lescopr.com> |
|
|
281
|
+
| 📧 Email | <support@lescopr.com> |
|
|
282
|
+
| 🐛 Bug reports | <https://github.com/Lescopr/lescopr-ruby/issues> |
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
## License
|
|
287
|
+
|
|
288
|
+
[MIT](LICENSE) © 2024-present [SonnaLab](https://sonnalab.com). All rights reserved.
|
|
289
|
+
|
data/exe/lescopr
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "optparse"
|
|
5
|
+
require "json"
|
|
6
|
+
require_relative "../lib/lescopr"
|
|
7
|
+
|
|
8
|
+
COMMANDS = %w[init start stop status diagnose reset].freeze
|
|
9
|
+
|
|
10
|
+
def usage
|
|
11
|
+
puts <<~USAGE
|
|
12
|
+
Usage: lescopr <command> [options]
|
|
13
|
+
|
|
14
|
+
Commands:
|
|
15
|
+
init Initialise the SDK in the current project
|
|
16
|
+
start Start the monitoring daemon
|
|
17
|
+
stop Stop the monitoring daemon
|
|
18
|
+
status Show daemon and SDK status
|
|
19
|
+
diagnose Run a full diagnostic
|
|
20
|
+
reset Remove SDK configuration and stop daemon
|
|
21
|
+
|
|
22
|
+
Options:
|
|
23
|
+
--sdk-key KEY SDK key (lsk_xxx)
|
|
24
|
+
--api-key KEY API key (lak_xxx)
|
|
25
|
+
--environment ENV development | production (default: development)
|
|
26
|
+
--help Show this message
|
|
27
|
+
USAGE
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
options = {}
|
|
31
|
+
parser = OptionParser.new do |opts|
|
|
32
|
+
opts.on("--sdk-key KEY") { |v| options[:sdk_key] = v }
|
|
33
|
+
opts.on("--api-key KEY") { |v| options[:api_key] = v }
|
|
34
|
+
opts.on("--environment ENV") { |v| options[:environment] = v }
|
|
35
|
+
opts.on("-h", "--help") { usage; exit 0 }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
parser.parse!(ARGV)
|
|
39
|
+
command = ARGV.shift
|
|
40
|
+
|
|
41
|
+
if command.nil? || !COMMANDS.include?(command)
|
|
42
|
+
usage
|
|
43
|
+
exit 1
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
config_path = File.join(Dir.pwd, ".lescopr.json")
|
|
47
|
+
pid_path = File.join(Dir.pwd, ".lescopr.pid")
|
|
48
|
+
|
|
49
|
+
case command
|
|
50
|
+
when "init"
|
|
51
|
+
unless options[:sdk_key]
|
|
52
|
+
puts "❌ --sdk-key is required"
|
|
53
|
+
exit 1
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
puts "⏳ Initialising Lescopr SDK..."
|
|
57
|
+
|
|
58
|
+
Lescopr.configure do |c|
|
|
59
|
+
c.sdk_key = options[:sdk_key]
|
|
60
|
+
c.api_key = options[:api_key]
|
|
61
|
+
c.environment = options[:environment] || "development"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
if Lescopr.client&.ready?
|
|
65
|
+
cfg = JSON.parse(File.read(config_path), symbolize_names: true) rescue {}
|
|
66
|
+
puts "✅ SDK initialised"
|
|
67
|
+
puts " SDK ID : #{cfg[:sdk_id]}"
|
|
68
|
+
puts " Project : #{cfg[:project_name]}"
|
|
69
|
+
puts " Stack : #{Array(cfg[:project_stack]).join(', ')}"
|
|
70
|
+
puts " Config : #{config_path}"
|
|
71
|
+
else
|
|
72
|
+
puts "❌ Initialisation failed — check your keys and connectivity"
|
|
73
|
+
exit 1
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
when "status"
|
|
77
|
+
if File.exist?(config_path)
|
|
78
|
+
cfg = JSON.parse(File.read(config_path), symbolize_names: true) rescue {}
|
|
79
|
+
puts "✅ SDK configured"
|
|
80
|
+
puts " SDK ID : #{cfg[:sdk_id]}"
|
|
81
|
+
puts " Project : #{cfg[:project_name]}"
|
|
82
|
+
puts " Env : #{cfg[:environment]}"
|
|
83
|
+
pid = File.exist?(pid_path) ? File.read(pid_path).strip : nil
|
|
84
|
+
puts " Daemon : #{pid ? "running (PID #{pid})" : "stopped"}"
|
|
85
|
+
else
|
|
86
|
+
puts "⚠️ SDK not initialised. Run: lescopr init --sdk-key YOUR_KEY"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
when "diagnose"
|
|
90
|
+
puts "🔍 Lescopr Diagnostic"
|
|
91
|
+
puts " Ruby version : #{RUBY_VERSION}"
|
|
92
|
+
puts " Platform : #{RUBY_PLATFORM}"
|
|
93
|
+
puts " Config present : #{File.exist?(config_path)}"
|
|
94
|
+
puts " PID file : #{File.exist?(pid_path) ? File.read(pid_path).strip : 'none'}"
|
|
95
|
+
|
|
96
|
+
begin
|
|
97
|
+
require "net/http"
|
|
98
|
+
uri = URI("https://api.lescopr.com/health")
|
|
99
|
+
res = Net::HTTP.get_response(uri)
|
|
100
|
+
puts " API reachable : #{res.is_a?(Net::HTTPSuccess) ? '✅' : "❌ (#{res.code})"}"
|
|
101
|
+
rescue StandardError => e
|
|
102
|
+
puts " API reachable : ❌ (#{e.message})"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
when "reset"
|
|
106
|
+
[config_path, pid_path, File.join(Dir.pwd, ".lescopr.log")].each do |f|
|
|
107
|
+
if File.exist?(f)
|
|
108
|
+
File.delete(f)
|
|
109
|
+
puts "🗑 Deleted #{f}"
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
puts "✅ SDK reset"
|
|
113
|
+
|
|
114
|
+
else
|
|
115
|
+
puts "ℹ️ Command '#{command}' is not yet implemented via CLI. Use the Ruby API directly."
|
|
116
|
+
end
|
|
117
|
+
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lescopr
|
|
4
|
+
module Core
|
|
5
|
+
# Central SDK client — manages configuration, log queue, daemon lifecycle
|
|
6
|
+
# and the Ruby logger hook.
|
|
7
|
+
class Client
|
|
8
|
+
attr_reader :configuration, :sdk_id, :log_queue, :http_client, :sdk_logger
|
|
9
|
+
|
|
10
|
+
def initialize(configuration)
|
|
11
|
+
@configuration = configuration
|
|
12
|
+
@sdk_id = nil
|
|
13
|
+
@ready = false
|
|
14
|
+
@mutex = Mutex.new
|
|
15
|
+
|
|
16
|
+
@sdk_logger = Monitoring::Logger.new(debug: configuration.debug)
|
|
17
|
+
@log_queue = LogQueue.new
|
|
18
|
+
@http_client = Transport::HttpClient.new(
|
|
19
|
+
api_key: configuration.api_key,
|
|
20
|
+
sdk_key: configuration.sdk_key
|
|
21
|
+
)
|
|
22
|
+
@daemon = DaemonRunner.new(self)
|
|
23
|
+
@config_mgr = Filesystem::ConfigManager.new
|
|
24
|
+
|
|
25
|
+
load_config
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @return [Boolean] true if the SDK is fully initialised and ready
|
|
29
|
+
def ready? = @ready
|
|
30
|
+
|
|
31
|
+
# Bootstrap: analyse project, register with API, start daemon.
|
|
32
|
+
# @return [Boolean]
|
|
33
|
+
def setup!
|
|
34
|
+
unless configuration.api_key && configuration.sdk_key
|
|
35
|
+
sdk_logger.warn("sdk_key and api_key are required. SDK inactive.")
|
|
36
|
+
return false
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
analyzer = Filesystem::ProjectAnalyzer.new
|
|
40
|
+
payload = analyzer.analyze
|
|
41
|
+
|
|
42
|
+
response = http_client.verify_project(payload)
|
|
43
|
+
|
|
44
|
+
unless response && response[:sdk_id]
|
|
45
|
+
sdk_logger.warn("API verification failed — check sdk_key / api_key")
|
|
46
|
+
return false
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
@sdk_id = response[:sdk_id]
|
|
50
|
+
config_data = response.merge(
|
|
51
|
+
sdk_key: configuration.sdk_key,
|
|
52
|
+
api_key: configuration.api_key,
|
|
53
|
+
environment: configuration.environment,
|
|
54
|
+
project_name: payload[:project_name]
|
|
55
|
+
)
|
|
56
|
+
@config_mgr.save(config_data)
|
|
57
|
+
|
|
58
|
+
@daemon.start
|
|
59
|
+
@ready = true
|
|
60
|
+
|
|
61
|
+
sdk_logger.info("SDK initialised — project: #{payload[:project_name]}, sdk_id: #{@sdk_id}")
|
|
62
|
+
true
|
|
63
|
+
rescue StandardError => e
|
|
64
|
+
sdk_logger.error("setup! failed: #{e.message}")
|
|
65
|
+
false
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Hook into Ruby's stdlib Logger so existing `Rails.logger.info` etc.
|
|
69
|
+
# calls are automatically forwarded to Lescopr.
|
|
70
|
+
def setup_auto_logging!
|
|
71
|
+
setup!
|
|
72
|
+
install_global_exception_handler!
|
|
73
|
+
install_at_exit_hook!
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Queue a log entry for async delivery.
|
|
77
|
+
def send_log(level, message, metadata = {})
|
|
78
|
+
return unless @ready
|
|
79
|
+
|
|
80
|
+
entry = {
|
|
81
|
+
timestamp: Time.now.utc.iso8601(3),
|
|
82
|
+
level: level.to_s.upcase,
|
|
83
|
+
message: message.to_s,
|
|
84
|
+
sdk_id: @sdk_id,
|
|
85
|
+
environment: configuration.environment,
|
|
86
|
+
metadata: metadata
|
|
87
|
+
}
|
|
88
|
+
log_queue.push(entry)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Graceful shutdown — flush queue then stop daemon.
|
|
92
|
+
def shutdown!
|
|
93
|
+
@daemon.stop
|
|
94
|
+
@ready = false
|
|
95
|
+
sdk_logger.info("SDK shut down")
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
def load_config
|
|
101
|
+
config = @config_mgr.load
|
|
102
|
+
return unless config
|
|
103
|
+
|
|
104
|
+
@sdk_id = config[:sdk_id]
|
|
105
|
+
configuration.api_key ||= config[:api_key]
|
|
106
|
+
configuration.sdk_key ||= config[:sdk_key]
|
|
107
|
+
configuration.environment = config[:environment] || configuration.environment
|
|
108
|
+
|
|
109
|
+
sdk_logger.info("Config loaded from .lescopr.json — project: #{config[:project_name]}")
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def install_global_exception_handler!
|
|
113
|
+
original = Thread.current[:__lescopr_exc_handler__]
|
|
114
|
+
Thread.report_on_exception = true
|
|
115
|
+
|
|
116
|
+
at_exit do
|
|
117
|
+
exc = $ERROR_INFO
|
|
118
|
+
next unless exc && !exc.is_a?(SystemExit)
|
|
119
|
+
|
|
120
|
+
send_log("FATAL", "#{exc.class}: #{exc.message}", {
|
|
121
|
+
backtrace: exc.backtrace&.first(10)
|
|
122
|
+
})
|
|
123
|
+
@daemon.stop
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def install_at_exit_hook!
|
|
128
|
+
at_exit { @daemon.stop }
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lescopr
|
|
4
|
+
module Core
|
|
5
|
+
# Background thread that flushes the log queue every N seconds
|
|
6
|
+
# and sends heartbeats every 30 s.
|
|
7
|
+
class DaemonRunner
|
|
8
|
+
FLUSH_INTERVAL = 5 # seconds
|
|
9
|
+
HEARTBEAT_INTERVAL = 30 # seconds
|
|
10
|
+
|
|
11
|
+
def initialize(client)
|
|
12
|
+
@client = client
|
|
13
|
+
@running = false
|
|
14
|
+
@thread = nil
|
|
15
|
+
@mutex = Mutex.new
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def start
|
|
19
|
+
@mutex.synchronize do
|
|
20
|
+
return if @running
|
|
21
|
+
|
|
22
|
+
@running = true
|
|
23
|
+
@thread = Thread.new { run_loop }
|
|
24
|
+
@thread.name = "lescopr-daemon"
|
|
25
|
+
@thread.abort_on_exception = false
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def stop
|
|
30
|
+
@mutex.synchronize do
|
|
31
|
+
@running = false
|
|
32
|
+
end
|
|
33
|
+
@thread&.join(3)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def running? = @running
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def run_loop
|
|
41
|
+
last_heartbeat = Time.now
|
|
42
|
+
|
|
43
|
+
while @running
|
|
44
|
+
sleep(FLUSH_INTERVAL)
|
|
45
|
+
|
|
46
|
+
flush_logs
|
|
47
|
+
send_heartbeat if (Time.now - last_heartbeat) >= HEARTBEAT_INTERVAL
|
|
48
|
+
last_heartbeat = Time.now if (Time.now - last_heartbeat) >= HEARTBEAT_INTERVAL
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
flush_logs # final flush on shutdown
|
|
52
|
+
rescue StandardError => e
|
|
53
|
+
@client.sdk_logger.error("DaemonRunner crashed: #{e.message}")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def flush_logs
|
|
57
|
+
return if @client.log_queue.empty?
|
|
58
|
+
|
|
59
|
+
batch = @client.log_queue.drain(@client.configuration.batch_size)
|
|
60
|
+
@client.http_client.send_logs(batch)
|
|
61
|
+
rescue StandardError => e
|
|
62
|
+
@client.sdk_logger.error("flush_logs error: #{e.message}")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def send_heartbeat
|
|
66
|
+
return unless @client.sdk_id
|
|
67
|
+
|
|
68
|
+
@client.http_client.send_heartbeat(@client.sdk_id)
|
|
69
|
+
rescue StandardError
|
|
70
|
+
# silent — heartbeat is non-critical
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|