rivulet-rb 0.2.2 → 0.3.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 +4 -4
- data/docs/AGENTS.md +6 -1
- data/docs/telemetry.md +54 -0
- data/lib/rivulet/application.rb +2 -1
- data/lib/rivulet/cli/new.rb +3 -1
- data/lib/rivulet/container.rb +1 -0
- data/lib/rivulet/operations/run_console.rb +8 -6
- data/lib/rivulet/operations/startup.rb +7 -5
- data/lib/rivulet/steps/build_config.rb +8 -1
- data/lib/rivulet/steps/load_db.rb +5 -0
- data/lib/rivulet/steps/load_initializers.rb +15 -0
- data/lib/rivulet/steps/load_settings.rb +1 -1
- data/lib/rivulet/telemetry/sink.rb +19 -0
- data/lib/rivulet/telemetry.rb +12 -1
- data/lib/rivulet/version.rb +1 -1
- data/lib/rivulet.rb +2 -0
- metadata +22 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 622289122f59a270997655c87af3287273f2f16f820f7713dd95092a32cb102b
|
|
4
|
+
data.tar.gz: 99896962459012b47744e0e8466a1b691bf5bc04f5337ef44ab3039b12938296
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '081156052069871b24b394ef91db773c57a9c3c1225fb52b76499f944e7d834edb0319f87211e0c53bc5d57d24e43c6d744df0455f3206a850c427c72ef0db87'
|
|
7
|
+
data.tar.gz: 25b0d8cdb73570dd48c16d5e15d8bfb371f2721e47ffff66b12d9f87c9057957629c4fc2b005c85295fd7e1c4fa2d460b0522aea2b80da67aef5ddb8c6253038
|
data/docs/AGENTS.md
CHANGED
|
@@ -84,7 +84,7 @@ app/
|
|
|
84
84
|
utils/
|
|
85
85
|
models/ # Sequel models
|
|
86
86
|
config/
|
|
87
|
-
application.rb # App configuration (DSN, logger, sendfile)
|
|
87
|
+
application.rb # App configuration (DSN, logger, sendfile, telemetry)
|
|
88
88
|
routes.rb # Route definitions
|
|
89
89
|
db/
|
|
90
90
|
migrations/ # SQL migration files
|
|
@@ -739,3 +739,8 @@ must be self-contained SQL; there is no `drop`/`alter` safety net.
|
|
|
739
739
|
them in the container instead.
|
|
740
740
|
- Keep nesting in routes shallow and meaningful — express context, not the
|
|
741
741
|
full domain hierarchy.
|
|
742
|
+
- Every request is automatically timed through `Rivulet::Telemetry` (one node
|
|
743
|
+
per operation and step, plus DB timing). The tree is logged at the end of
|
|
744
|
+
each request. If `config.telemetry.sink` is set (e.g. to
|
|
745
|
+
`Rivulet::OTel::Sink.new` via `rivulet-otel setup`), the sink receives
|
|
746
|
+
callbacks for each lifecycle event. The default sink is a no-op.
|
data/docs/telemetry.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Telemetry
|
|
2
|
+
|
|
3
|
+
Rivulet includes a built-in, fiber-local telemetry system that records the
|
|
4
|
+
execution flow of every request. Each `Rivulet::Operation` and `Rivulet::Step`
|
|
5
|
+
is automatically timed via `Rivulet::Telemetry::TimingWrapper`, which is
|
|
6
|
+
prepended into every subclass. Database queries are timed through
|
|
7
|
+
`Rivulet::Telemetry::SequelExtension`. The resulting tree of nodes is logged
|
|
8
|
+
at the end of each request:
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
Completed total_ms=42.3 db_ms=8.1 flow:
|
|
12
|
+
Handlers::Posts::Operations::Show (2.1) =>
|
|
13
|
+
Services::Posts::Steps::LoadPost (35.4) =>
|
|
14
|
+
Services::Posts::Steps::Authorize (1.2)
|
|
15
|
+
Handlers::Posts::Steps::BuildResponse (1.1)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Sink Protocol
|
|
19
|
+
|
|
20
|
+
The telemetry system is extensible through a sink protocol
|
|
21
|
+
(`Rivulet::Telemetry::Sink`). A sink receives callbacks for each lifecycle
|
|
22
|
+
event:
|
|
23
|
+
|
|
24
|
+
- `on_start(node, parent)` — called when an operation or step begins.
|
|
25
|
+
- `on_stop(node)` — called when it ends, with `duration_ms` and `self_ms`
|
|
26
|
+
already computed.
|
|
27
|
+
- `on_db(elapsed_ms)` — called after each database query.
|
|
28
|
+
- `on_root(node, total_ms)` — called at the end of the request, after the
|
|
29
|
+
full tree is built.
|
|
30
|
+
|
|
31
|
+
The default sink is `Rivulet::Telemetry::Sink::Null` (no-op). Apps configure
|
|
32
|
+
a custom sink via `config.telemetry.sink` in `config/application.rb`.
|
|
33
|
+
|
|
34
|
+
## OpenTelemetry
|
|
35
|
+
|
|
36
|
+
Add the `rivulet-opentelemetry` gem to your application and run
|
|
37
|
+
`bundle exec rivulet-otel setup` to wire in OpenTelemetry. The gem provides:
|
|
38
|
+
|
|
39
|
+
- `Rivulet::OTel::Sink` — implements the sink protocol, emitting one span
|
|
40
|
+
per operation and per step, with correct parent-child nesting via a
|
|
41
|
+
fiber-local span stack. Also records metrics (request count/duration,
|
|
42
|
+
DB queries/duration, step duration) on each callback.
|
|
43
|
+
- `Rivulet::OTel::Logger` — replaces the default dry-logger. Each log
|
|
44
|
+
record is emitted as a real OTel log with the current span context
|
|
45
|
+
attached (trace_id, span_id), enabling trace-to-log correlation in
|
|
46
|
+
Grafana. Logs are also written to stdout for local visibility.
|
|
47
|
+
- `Rivulet::OTel.configure(service_name:)` — boots the OTel SDK with
|
|
48
|
+
traces, logs, and metrics exporters, gated on
|
|
49
|
+
`OTEL_EXPORTER_OTLP_ENDPOINT` (no-op when unset).
|
|
50
|
+
|
|
51
|
+
The setup command adds a single `grafana/otel-lgtm` service to
|
|
52
|
+
`docker-compose.yml` and wires in the initializer. Grafana is available at
|
|
53
|
+
`http://localhost:3000` (admin/admin) with Tempo, Loki, and Prometheus
|
|
54
|
+
pre-configured.
|
data/lib/rivulet/application.rb
CHANGED
|
@@ -74,7 +74,7 @@ module Rivulet
|
|
|
74
74
|
private
|
|
75
75
|
|
|
76
76
|
def with_telemetry
|
|
77
|
-
t = Telemetry.new
|
|
77
|
+
t = Telemetry.new(sink: config.telemetry.sink)
|
|
78
78
|
Fiber[:rivulet_telemetry] = t
|
|
79
79
|
|
|
80
80
|
result = yield
|
|
@@ -84,6 +84,7 @@ module Rivulet
|
|
|
84
84
|
)
|
|
85
85
|
result
|
|
86
86
|
ensure
|
|
87
|
+
t&.finish
|
|
87
88
|
Fiber[:rivulet_telemetry] = nil
|
|
88
89
|
end
|
|
89
90
|
end
|
data/lib/rivulet/cli/new.rb
CHANGED
|
@@ -19,6 +19,7 @@ module Rivulet
|
|
|
19
19
|
app/services/shared/utils
|
|
20
20
|
app/models
|
|
21
21
|
config
|
|
22
|
+
config/initializers
|
|
22
23
|
db/migrations
|
|
23
24
|
].freeze
|
|
24
25
|
|
|
@@ -100,13 +101,14 @@ module Rivulet
|
|
|
100
101
|
|
|
101
102
|
<<~RUBY
|
|
102
103
|
Rivulet.configure do |config|
|
|
104
|
+
config.app.name = name
|
|
105
|
+
|
|
103
106
|
#{dsn_line}
|
|
104
107
|
|
|
105
108
|
# config.sendfile.enabled = true
|
|
106
109
|
# config.sendfile.variation = 'x-accel-redirect'
|
|
107
110
|
# config.sendfile.mappings = [['/var/www/', '/files/']]
|
|
108
111
|
|
|
109
|
-
config.logger.name = :#{name}
|
|
110
112
|
config.logger.level = :info
|
|
111
113
|
end
|
|
112
114
|
RUBY
|
data/lib/rivulet/container.rb
CHANGED
|
@@ -20,6 +20,7 @@ module Rivulet
|
|
|
20
20
|
register('load_settings') { Rivulet::Steps::LoadSettings.new }
|
|
21
21
|
register('load_db') { Rivulet::Steps::LoadDb.new }
|
|
22
22
|
register('load_routes') { Rivulet::Steps::LoadRoutes.new }
|
|
23
|
+
register('load_initializers') { Rivulet::Steps::LoadInitializers.new }
|
|
23
24
|
register('run_migrations') { Rivulet::Steps::RunMigrations.new }
|
|
24
25
|
register('run_console') { Rivulet::Steps::RunConsole.new }
|
|
25
26
|
register('print_routes') { Rivulet::Steps::PrintRoutes.new }
|
|
@@ -2,12 +2,13 @@ module Rivulet
|
|
|
2
2
|
module Operations
|
|
3
3
|
class RunConsole < Rivulet::Operation
|
|
4
4
|
include Import[
|
|
5
|
-
build_config:
|
|
6
|
-
load_settings:
|
|
7
|
-
load_app:
|
|
8
|
-
load_db:
|
|
9
|
-
load_routes:
|
|
10
|
-
|
|
5
|
+
build_config: 'steps.build_config',
|
|
6
|
+
load_settings: 'steps.load_settings',
|
|
7
|
+
load_app: 'steps.load_app',
|
|
8
|
+
load_db: 'steps.load_db',
|
|
9
|
+
load_routes: 'steps.load_routes',
|
|
10
|
+
load_initializers: 'steps.load_initializers',
|
|
11
|
+
run_console: 'steps.run_console'
|
|
11
12
|
]
|
|
12
13
|
|
|
13
14
|
def call(input = {})
|
|
@@ -16,6 +17,7 @@ module Rivulet
|
|
|
16
17
|
result = step load_db.(result)
|
|
17
18
|
result = step load_app.(result)
|
|
18
19
|
result = step load_routes.(result)
|
|
20
|
+
result = step load_initializers.(result)
|
|
19
21
|
result = step run_console.(result)
|
|
20
22
|
|
|
21
23
|
result
|
|
@@ -2,11 +2,12 @@ module Rivulet
|
|
|
2
2
|
module Operations
|
|
3
3
|
class Startup < Rivulet::Operation
|
|
4
4
|
include Import[
|
|
5
|
-
build_config:
|
|
6
|
-
load_settings:
|
|
7
|
-
load_app:
|
|
8
|
-
load_db:
|
|
9
|
-
load_routes:
|
|
5
|
+
build_config: 'steps.build_config',
|
|
6
|
+
load_settings: 'steps.load_settings',
|
|
7
|
+
load_app: 'steps.load_app',
|
|
8
|
+
load_db: 'steps.load_db',
|
|
9
|
+
load_routes: 'steps.load_routes',
|
|
10
|
+
load_initializers: 'steps.load_initializers'
|
|
10
11
|
]
|
|
11
12
|
|
|
12
13
|
def call(input = {})
|
|
@@ -15,6 +16,7 @@ module Rivulet
|
|
|
15
16
|
result = step load_db.(result)
|
|
16
17
|
result = step load_app.(result)
|
|
17
18
|
result = step load_routes.(result)
|
|
19
|
+
result = step load_initializers.(result)
|
|
18
20
|
|
|
19
21
|
result
|
|
20
22
|
end
|
|
@@ -2,6 +2,10 @@ module Rivulet
|
|
|
2
2
|
module Steps
|
|
3
3
|
class BuildConfig < Rivulet::Step
|
|
4
4
|
def call(input)
|
|
5
|
+
Rivulet::Application.setting :app do
|
|
6
|
+
setting :name
|
|
7
|
+
end
|
|
8
|
+
|
|
5
9
|
Rivulet::Application.setting :database do
|
|
6
10
|
setting :dsn
|
|
7
11
|
setting :pool
|
|
@@ -9,7 +13,6 @@ module Rivulet
|
|
|
9
13
|
|
|
10
14
|
Rivulet::Application.setting :logger, reader: true do
|
|
11
15
|
setting :engine
|
|
12
|
-
setting :name
|
|
13
16
|
setting :level
|
|
14
17
|
end
|
|
15
18
|
|
|
@@ -19,6 +22,10 @@ module Rivulet
|
|
|
19
22
|
setting :mappings, default: []
|
|
20
23
|
end
|
|
21
24
|
|
|
25
|
+
Rivulet::Application.setting :telemetry do
|
|
26
|
+
setting :sink, default: Rivulet::Telemetry::Sink::Null.new
|
|
27
|
+
end
|
|
28
|
+
|
|
22
29
|
Success(input)
|
|
23
30
|
end
|
|
24
31
|
end
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
require 'sequel'
|
|
2
2
|
require_relative '../telemetry/sequel_extension'
|
|
3
3
|
|
|
4
|
+
# Load globally BEFORE connecting — fiber_concurrency is a Sequel-wide extension,
|
|
5
|
+
# not a per-database extension. It replaces the default connection pool with a
|
|
6
|
+
# fiber-aware one that gives each fiber its own connection.
|
|
7
|
+
Sequel.extension(:fiber_concurrency)
|
|
8
|
+
|
|
4
9
|
module Rivulet
|
|
5
10
|
module Steps
|
|
6
11
|
class LoadDb < Rivulet::Step
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Rivulet
|
|
2
|
+
module Steps
|
|
3
|
+
class LoadInitializers < Rivulet::Step
|
|
4
|
+
def call(input)
|
|
5
|
+
init_dir = Dir[File.expand_path('config/initializers/**/*.rb')]
|
|
6
|
+
|
|
7
|
+
init_dir.each do |init_file|
|
|
8
|
+
require init_file
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
Success(input)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -17,7 +17,7 @@ module Rivulet
|
|
|
17
17
|
private
|
|
18
18
|
|
|
19
19
|
def default_logger(app)
|
|
20
|
-
Dry.Logger(app.config.
|
|
20
|
+
Dry.Logger(app.config.app.name, level: app.config.logger.level) do |setup|
|
|
21
21
|
setup.add_backend(
|
|
22
22
|
stream: $stdout,
|
|
23
23
|
log_if: :debug?,
|
data/lib/rivulet/telemetry.rb
CHANGED
|
@@ -2,11 +2,12 @@ module Rivulet
|
|
|
2
2
|
class Telemetry
|
|
3
3
|
attr_reader :db_ms
|
|
4
4
|
|
|
5
|
-
def initialize
|
|
5
|
+
def initialize(sink:)
|
|
6
6
|
@started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
7
7
|
@root = nil
|
|
8
8
|
@stack = []
|
|
9
9
|
@db_ms = 0.0
|
|
10
|
+
@sink = sink
|
|
10
11
|
end
|
|
11
12
|
|
|
12
13
|
def start_recording(activity)
|
|
@@ -16,12 +17,15 @@ module Rivulet
|
|
|
16
17
|
children: []
|
|
17
18
|
)
|
|
18
19
|
|
|
20
|
+
parent = @stack.last
|
|
21
|
+
|
|
19
22
|
if @stack.empty?
|
|
20
23
|
@root = node
|
|
21
24
|
else
|
|
22
25
|
@stack.last[:children] << node
|
|
23
26
|
end
|
|
24
27
|
|
|
28
|
+
@sink.on_start(node, parent)
|
|
25
29
|
@stack << node
|
|
26
30
|
end
|
|
27
31
|
|
|
@@ -32,10 +36,13 @@ module Rivulet
|
|
|
32
36
|
((node.ended_at - node.started_at) * 1000.0).round(3)
|
|
33
37
|
node.self_ms =
|
|
34
38
|
(node.duration_ms - node.children.sum(&:duration_ms)).round(3)
|
|
39
|
+
|
|
40
|
+
@sink.on_stop(node)
|
|
35
41
|
end
|
|
36
42
|
|
|
37
43
|
def record_db(ms)
|
|
38
44
|
@db_ms = (@db_ms + ms).round(3)
|
|
45
|
+
@sink.on_db(ms)
|
|
39
46
|
end
|
|
40
47
|
|
|
41
48
|
def total_ms
|
|
@@ -58,5 +65,9 @@ module Rivulet
|
|
|
58
65
|
|
|
59
66
|
entry
|
|
60
67
|
end
|
|
68
|
+
|
|
69
|
+
def finish
|
|
70
|
+
@sink.on_root(@root, total_ms)
|
|
71
|
+
end
|
|
61
72
|
end
|
|
62
73
|
end
|
data/lib/rivulet/version.rb
CHANGED
data/lib/rivulet.rb
CHANGED
|
@@ -17,6 +17,7 @@ require 'sequel'
|
|
|
17
17
|
|
|
18
18
|
require_relative 'rivulet/version'
|
|
19
19
|
require_relative 'rivulet/telemetry'
|
|
20
|
+
require_relative 'rivulet/telemetry/sink'
|
|
20
21
|
require_relative 'rivulet/telemetry/node'
|
|
21
22
|
require_relative 'rivulet/telemetry/sequel_extension'
|
|
22
23
|
require_relative 'rivulet/telemetry/timing_wrapper'
|
|
@@ -37,6 +38,7 @@ require_relative 'rivulet/steps/load_app'
|
|
|
37
38
|
require_relative 'rivulet/steps/load_db'
|
|
38
39
|
require_relative 'rivulet/steps/load_routes'
|
|
39
40
|
require_relative 'rivulet/steps/load_settings'
|
|
41
|
+
require_relative 'rivulet/steps/load_initializers'
|
|
40
42
|
require_relative 'rivulet/steps/print_routes'
|
|
41
43
|
require_relative 'rivulet/steps/run_migrations'
|
|
42
44
|
require_relative 'rivulet/steps/run_console'
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rivulet-rb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Vladimir Dombrovskiy <vold@fastmail.com>
|
|
@@ -219,6 +219,20 @@ dependencies:
|
|
|
219
219
|
- - ">="
|
|
220
220
|
- !ruby/object:Gem::Version
|
|
221
221
|
version: '0'
|
|
222
|
+
- !ruby/object:Gem::Dependency
|
|
223
|
+
name: pry
|
|
224
|
+
requirement: !ruby/object:Gem::Requirement
|
|
225
|
+
requirements:
|
|
226
|
+
- - ">="
|
|
227
|
+
- !ruby/object:Gem::Version
|
|
228
|
+
version: '0'
|
|
229
|
+
type: :development
|
|
230
|
+
prerelease: false
|
|
231
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
232
|
+
requirements:
|
|
233
|
+
- - ">="
|
|
234
|
+
- !ruby/object:Gem::Version
|
|
235
|
+
version: '0'
|
|
222
236
|
- !ruby/object:Gem::Dependency
|
|
223
237
|
name: rake
|
|
224
238
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -269,6 +283,7 @@ files:
|
|
|
269
283
|
- bin/rivulet
|
|
270
284
|
- docs/AGENTS.md
|
|
271
285
|
- docs/architecture.md
|
|
286
|
+
- docs/telemetry.md
|
|
272
287
|
- lib/rivulet.rb
|
|
273
288
|
- lib/rivulet/application.rb
|
|
274
289
|
- lib/rivulet/cli.rb
|
|
@@ -305,6 +320,7 @@ files:
|
|
|
305
320
|
- lib/rivulet/steps/dispatch.rb
|
|
306
321
|
- lib/rivulet/steps/load_app.rb
|
|
307
322
|
- lib/rivulet/steps/load_db.rb
|
|
323
|
+
- lib/rivulet/steps/load_initializers.rb
|
|
308
324
|
- lib/rivulet/steps/load_routes.rb
|
|
309
325
|
- lib/rivulet/steps/load_settings.rb
|
|
310
326
|
- lib/rivulet/steps/print_routes.rb
|
|
@@ -314,9 +330,11 @@ files:
|
|
|
314
330
|
- lib/rivulet/telemetry.rb
|
|
315
331
|
- lib/rivulet/telemetry/node.rb
|
|
316
332
|
- lib/rivulet/telemetry/sequel_extension.rb
|
|
333
|
+
- lib/rivulet/telemetry/sink.rb
|
|
317
334
|
- lib/rivulet/telemetry/timing_wrapper.rb
|
|
318
335
|
- lib/rivulet/version.rb
|
|
319
|
-
licenses:
|
|
336
|
+
licenses:
|
|
337
|
+
- Apache-2.0
|
|
320
338
|
metadata: {}
|
|
321
339
|
rdoc_options: []
|
|
322
340
|
require_paths:
|
|
@@ -325,14 +343,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
325
343
|
requirements:
|
|
326
344
|
- - ">="
|
|
327
345
|
- !ruby/object:Gem::Version
|
|
328
|
-
version: '3.
|
|
346
|
+
version: '3.3'
|
|
329
347
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
330
348
|
requirements:
|
|
331
349
|
- - ">="
|
|
332
350
|
- !ruby/object:Gem::Version
|
|
333
351
|
version: '0'
|
|
334
352
|
requirements: []
|
|
335
|
-
rubygems_version:
|
|
353
|
+
rubygems_version: 4.0.19
|
|
336
354
|
specification_version: 4
|
|
337
355
|
summary: A small Rack web framework built on dry-rb, falcon and forced layering architecture
|
|
338
356
|
test_files: []
|