legionio 1.6.18 → 1.6.21
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/CHANGELOG.md +33 -0
- data/README.md +1 -1
- data/legionio.gemspec +1 -1
- data/lib/legion/api/hooks.rb +1 -2
- data/lib/legion/api/lex.rb +1 -2
- data/lib/legion/api.rb +1 -2
- data/lib/legion/cli/knowledge_command.rb +123 -1
- data/lib/legion/cli/setup_command.rb +6 -6
- data/lib/legion/events.rb +1 -2
- data/lib/legion/extensions/actors/base.rb +2 -4
- data/lib/legion/extensions/actors/every.rb +3 -6
- data/lib/legion/extensions/actors/loop.rb +1 -2
- data/lib/legion/extensions/actors/poll.rb +4 -7
- data/lib/legion/extensions/actors/subscription.rb +7 -9
- data/lib/legion/extensions/core.rb +1 -3
- data/lib/legion/extensions/helpers/logger.rb +56 -3
- data/lib/legion/extensions/helpers/task.rb +2 -4
- data/lib/legion/extensions/transport.rb +2 -5
- data/lib/legion/extensions.rb +1 -2
- data/lib/legion/runner/status.rb +5 -7
- data/lib/legion/service.rb +75 -29
- data/lib/legion/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4d50e1efc9398c0e2d380c7a921e2ccc8b6e6db3a142d4f4a700ff536555a2e4
|
|
4
|
+
data.tar.gz: e44d47e0c4d04362d9a35c6ad6953d0b4a42a5f0f2225dd0420e7f2d1c544ad4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f4cc63f869d21abc423f7836a25b91f62a815d01d1972004b85728be0b969e332a5f95c9172ab6177f3759e837b6e40bbb30afba1ecd3a817cd5088fff4c675b
|
|
7
|
+
data.tar.gz: 719f9a8e3d316937be26ed610b7ba86c653317b476b271d821591fecf5ff8157c02788b03df1468c7fe3af19d76ec1a55cf48312d911a502e30873c51d378bb0
|
data/CHANGELOG.md
CHANGED
|
@@ -2,8 +2,41 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [1.6.21] - 2026-03-27
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- `legionio knowledge capture transcript` CLI command: ingests Claude Code session transcripts into Apollo knowledge store
|
|
9
|
+
- Stop hook for automatic transcript capture at session end (installed via `legion setup claude-code`)
|
|
10
|
+
|
|
11
|
+
## [1.6.20] - 2026-03-27
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- Bump `legion-logging` dependency to `>= 1.4.0` (required for `log_exception`, writer lambdas)
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- `subscription.rb` (both `on_delivery` and `subscribe` blocks): initialize `fn = nil` before `process_message` so the rescue interpolation never raises `NameError` if message processing fails before `fn` is assigned
|
|
18
|
+
- `Helpers::Logger#lex_name` removed to avoid overriding `Helpers::Base#lex_name` (underscore contract used by settings/routing); renamed to private `log_lex_name` used only within this module for gem name derivation
|
|
19
|
+
- `Helpers::Logger#handle_exception`: use `spec&.version&.to_s` so nil spec version produces `nil` rather than `""` in structured log output
|
|
20
|
+
- README: update version badge from `v1.6.18` to `v1.6.20`
|
|
21
|
+
|
|
22
|
+
## [1.6.19] - 2026-03-27
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
- `teardown_logging_transport`: rescue block in `setup_logging_transport` now calls `teardown_logging_transport` to clean up any partially-created `@log_session` on failure
|
|
26
|
+
- `teardown_logging_transport`: guard `open?` call with `respond_to?(:open?)` check to avoid `NoMethodError` on session objects that do not implement the method
|
|
27
|
+
- `service_logging_transport_spec`: early-return specs now assert `create_dedicated_session` was not called and `@log_session` remains nil, rather than the vacuous `respond_to(:call)` check
|
|
28
|
+
- `service_logging_transport_spec`: replaced vacuous `not_to eq(owner)` assertion with `have_received(:create_dedicated_session)` to verify the dedicated session was actually created
|
|
29
|
+
|
|
5
30
|
## [1.6.18] - 2026-03-27
|
|
6
31
|
|
|
32
|
+
### Added
|
|
33
|
+
- `setup_logging_transport`: dedicated AMQP session for log and exception forwarding, replacing the previous `register_logging_hooks` approach; writer lambda wiring is gated by `Settings[:logging][:transport]` feature flags
|
|
34
|
+
- `teardown_logging_transport`: cleanly shuts down the dedicated logging AMQP session during the shutdown sequence
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
- Split `log.error(e.message); log.error(e.backtrace)` patterns replaced with `log.log_exception` across 14 files for structured, single-call exception logging
|
|
38
|
+
- `Extensions::Helpers::Logger#handle_exception` rewritten to use `log.log_exception` with full lex context
|
|
39
|
+
|
|
7
40
|
### Fixed
|
|
8
41
|
- `legionio pipeline image analyze`: `call_llm` no longer passes unsupported `messages:` keyword to `Legion::LLM.chat`; now creates a chat object and sends multimodal content via `chat.ask`, returning a plain hash with `:content` and `:usage` keys
|
|
9
42
|
- `legionio ai trace search/summarize`: both commands now call `setup_connection` before invoking `TraceSearch`, ensuring `Legion::LLM` is booted so `TraceSearch.generate_filter` can use structured LLM output instead of returning "no filter generated"; added `class_option :config_dir` and `class_option :verbose` to `TraceCommand`
|
data/README.md
CHANGED
|
@@ -14,7 +14,7 @@ Schedule tasks, chain services into dependency graphs, run them concurrently via
|
|
|
14
14
|
╰──────────────────────────────────────╯
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
**Ruby >= 3.4** | **v1.
|
|
17
|
+
**Ruby >= 3.4** | **v1.6.20** | **Apache-2.0** | [@Esity](https://github.com/Esity)
|
|
18
18
|
|
|
19
19
|
---
|
|
20
20
|
|
data/legionio.gemspec
CHANGED
|
@@ -56,7 +56,7 @@ Gem::Specification.new do |spec|
|
|
|
56
56
|
spec.add_dependency 'legion-crypt', '>= 1.4.17'
|
|
57
57
|
spec.add_dependency 'legion-data', '>= 1.6.7'
|
|
58
58
|
spec.add_dependency 'legion-json', '>= 1.2.1'
|
|
59
|
-
spec.add_dependency 'legion-logging', '>= 1.
|
|
59
|
+
spec.add_dependency 'legion-logging', '>= 1.4.0'
|
|
60
60
|
spec.add_dependency 'legion-settings', '>= 1.3.19'
|
|
61
61
|
spec.add_dependency 'legion-transport', '>= 1.4.4'
|
|
62
62
|
|
data/lib/legion/api/hooks.rb
CHANGED
|
@@ -66,8 +66,7 @@ module Legion
|
|
|
66
66
|
|
|
67
67
|
dispatch_hook(context, payload: payload, runner: runner, function: function)
|
|
68
68
|
rescue StandardError => e
|
|
69
|
-
Legion::Logging.
|
|
70
|
-
Legion::Logging.error e.backtrace&.first(5)
|
|
69
|
+
Legion::Logging.log_exception(e, payload_summary: "API #{request.request_method} #{request.path_info}", component_type: :api)
|
|
71
70
|
context.json_error('internal_error', e.message, status_code: 500)
|
|
72
71
|
end
|
|
73
72
|
|
data/lib/legion/api/lex.rb
CHANGED
|
@@ -53,8 +53,7 @@ module Legion
|
|
|
53
53
|
context.json_response({ task_id: result[:task_id], status: result[:status],
|
|
54
54
|
result: result[:result] }.compact)
|
|
55
55
|
rescue StandardError => e
|
|
56
|
-
Legion::Logging.
|
|
57
|
-
Legion::Logging.error e.backtrace&.first(5)
|
|
56
|
+
Legion::Logging.log_exception(e, payload_summary: "API POST /api/lex/#{request.path_info.sub(%r{^/api/lex/}, '')}", component_type: :api)
|
|
58
57
|
context.json_error('internal_error', e.message, status_code: 500)
|
|
59
58
|
end
|
|
60
59
|
|
data/lib/legion/api.rb
CHANGED
|
@@ -95,8 +95,7 @@ module Legion
|
|
|
95
95
|
error do
|
|
96
96
|
content_type :json
|
|
97
97
|
err = env['sinatra.error']
|
|
98
|
-
Legion::Logging.
|
|
99
|
-
Legion::Logging.error err.backtrace&.first(10)
|
|
98
|
+
Legion::Logging.log_exception(err, payload_summary: "API #{request.request_method} #{request.path_info} returned 500", component_type: :api)
|
|
100
99
|
Legion::JSON.dump({
|
|
101
100
|
error: { code: 'internal_error', message: err.message },
|
|
102
101
|
meta: { timestamp: Time.now.utc.iso8601, node: Legion::Settings[:client][:name] }
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'shellwords'
|
|
4
|
+
|
|
3
5
|
module Legion
|
|
4
6
|
module CLI
|
|
5
7
|
class MonitorCommand < Thor
|
|
@@ -173,10 +175,130 @@ module Legion
|
|
|
173
175
|
end
|
|
174
176
|
end
|
|
175
177
|
|
|
176
|
-
|
|
178
|
+
desc 'transcript', 'Capture a Claude Code session transcript as knowledge'
|
|
179
|
+
option :session_id, type: :string, desc: 'Session ID (defaults to CLAUDE_SESSION_ID env)'
|
|
180
|
+
option :cwd, type: :string, desc: 'Working directory (defaults to CLAUDE_CWD env)'
|
|
181
|
+
option :max_chunks, type: :numeric, default: 50, desc: 'Max conversation turn chunks to ingest'
|
|
182
|
+
def transcript
|
|
183
|
+
session_id = options[:session_id] || ENV.fetch('CLAUDE_SESSION_ID', nil)
|
|
184
|
+
cwd = options[:cwd] || ENV.fetch('CLAUDE_CWD', nil) || ::Dir.pwd
|
|
185
|
+
|
|
186
|
+
unless session_id
|
|
187
|
+
formatter.warn('No session ID provided (set CLAUDE_SESSION_ID or --session-id)')
|
|
188
|
+
return
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
jsonl_path = resolve_transcript_path(session_id, cwd)
|
|
192
|
+
unless jsonl_path && ::File.exist?(jsonl_path)
|
|
193
|
+
formatter.warn("Transcript not found: #{jsonl_path || 'could not resolve path'}")
|
|
194
|
+
return
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
turns = extract_turns(jsonl_path)
|
|
198
|
+
if turns.empty?
|
|
199
|
+
formatter.warn('No conversation turns found in transcript')
|
|
200
|
+
return
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
repo = `git -C #{Shellwords.escape(cwd)} rev-parse --show-toplevel 2>/dev/null`.strip.split('/').last
|
|
204
|
+
base_tags = ['claude-code', 'transcript', "session:#{session_id}", ::Time.now.strftime('%Y-%m-%d')]
|
|
205
|
+
base_tags << "repo:#{repo}" unless repo.to_s.empty?
|
|
206
|
+
|
|
207
|
+
ingested = 0
|
|
208
|
+
turns.first(options[:max_chunks]).each_with_index do |turn, idx|
|
|
209
|
+
content = format_turn(turn, idx + 1)
|
|
210
|
+
next if content.strip.empty?
|
|
211
|
+
|
|
212
|
+
result = ingest_content(
|
|
213
|
+
content: content,
|
|
214
|
+
tags: base_tags + ["turn:#{idx + 1}"],
|
|
215
|
+
source: "claude-code:#{session_id}:turn-#{idx + 1}"
|
|
216
|
+
)
|
|
217
|
+
ingested += 1 if result[:success]
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
out = formatter
|
|
221
|
+
if options[:json]
|
|
222
|
+
out.json(success: true, session_id: session_id, turns: turns.size, ingested: ingested)
|
|
223
|
+
else
|
|
224
|
+
out.success("Captured #{ingested}/#{[turns.size, options[:max_chunks]].min} turns from session #{session_id[0, 8]}")
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
no_commands do # rubocop:disable Metrics/BlockLength
|
|
177
229
|
def formatter
|
|
178
230
|
@formatter ||= Output::Formatter.new(json: options[:json], color: !options[:no_color])
|
|
179
231
|
end
|
|
232
|
+
|
|
233
|
+
def resolve_transcript_path(session_id, cwd)
|
|
234
|
+
project_dir = cwd.gsub('/', '-')
|
|
235
|
+
::File.expand_path("~/.claude/projects/#{project_dir}/#{session_id}.jsonl")
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def extract_turns(path)
|
|
239
|
+
turns = []
|
|
240
|
+
current_turn = nil
|
|
241
|
+
|
|
242
|
+
::File.foreach(path) do |line|
|
|
243
|
+
entry = ::JSON.parse(line, symbolize_names: true)
|
|
244
|
+
type = entry[:type]
|
|
245
|
+
|
|
246
|
+
case type
|
|
247
|
+
when 'user'
|
|
248
|
+
turns << current_turn if current_turn
|
|
249
|
+
current_turn = { user: extract_message_text(entry), assistant: +'', timestamp: entry[:timestamp] }
|
|
250
|
+
when 'assistant'
|
|
251
|
+
next unless current_turn
|
|
252
|
+
|
|
253
|
+
text = extract_message_text(entry)
|
|
254
|
+
current_turn[:assistant] << text unless text.empty?
|
|
255
|
+
end
|
|
256
|
+
rescue ::JSON::ParserError
|
|
257
|
+
next
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
turns << current_turn if current_turn
|
|
261
|
+
turns
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def extract_message_text(entry)
|
|
265
|
+
msg = entry[:message]
|
|
266
|
+
return '' unless msg
|
|
267
|
+
|
|
268
|
+
content = msg[:content]
|
|
269
|
+
case content
|
|
270
|
+
when String then content
|
|
271
|
+
when Array
|
|
272
|
+
content.filter_map { |block| block[:text] if block[:type] == 'text' }.join("\n")
|
|
273
|
+
else ''
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def format_turn(turn, number)
|
|
278
|
+
text = "## Turn #{number}\n"
|
|
279
|
+
text << "Timestamp: #{turn[:timestamp]}\n\n" if turn[:timestamp]
|
|
280
|
+
text << "### User\n#{truncate_text(turn[:user], 4096)}\n\n"
|
|
281
|
+
text << "### Assistant\n#{truncate_text(turn[:assistant], 4096)}\n"
|
|
282
|
+
text
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def truncate_text(text, max_bytes)
|
|
286
|
+
return text if text.bytesize <= max_bytes
|
|
287
|
+
|
|
288
|
+
"#{text.byteslice(0, max_bytes - 20)}\n\n[truncated]"
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def ingest_content(content:, tags:, source:)
|
|
292
|
+
if defined?(Legion::Extensions::Knowledge::Runners::Ingest)
|
|
293
|
+
Legion::Extensions::Knowledge::Runners::Ingest.ingest_file(
|
|
294
|
+
content: content, tags: tags, source: source
|
|
295
|
+
)
|
|
296
|
+
elsif defined?(Legion::Apollo)
|
|
297
|
+
Legion::Apollo.ingest(content: content, tags: tags, source: source)
|
|
298
|
+
else
|
|
299
|
+
{ success: false, error: 'neither lex-knowledge nor legion-apollo available' }
|
|
300
|
+
end
|
|
301
|
+
end
|
|
180
302
|
end
|
|
181
303
|
end
|
|
182
304
|
|
|
@@ -350,9 +350,9 @@ module Legion
|
|
|
350
350
|
|
|
351
351
|
hooks = existing['hooks'] || {}
|
|
352
352
|
|
|
353
|
-
has_commit
|
|
354
|
-
|
|
355
|
-
if has_commit &&
|
|
353
|
+
has_commit = Array(hooks['PostToolUse']).any? { |h| h['command']&.include?('knowledge capture commit') }
|
|
354
|
+
has_transcript = Array(hooks['Stop']).any? { |h| h['command']&.include?('knowledge capture transcript') }
|
|
355
|
+
if has_commit && has_transcript && !options[:force]
|
|
356
356
|
puts ' Write-back hooks already present (use --force to overwrite)' unless options[:json]
|
|
357
357
|
return
|
|
358
358
|
end
|
|
@@ -368,10 +368,10 @@ module Legion
|
|
|
368
368
|
}
|
|
369
369
|
end
|
|
370
370
|
|
|
371
|
-
unless
|
|
371
|
+
unless has_transcript
|
|
372
372
|
hooks['Stop'] << {
|
|
373
|
-
'command' => 'legionio knowledge capture
|
|
374
|
-
'timeout' =>
|
|
373
|
+
'command' => 'legionio knowledge capture transcript',
|
|
374
|
+
'timeout' => 30_000
|
|
375
375
|
}
|
|
376
376
|
end
|
|
377
377
|
|
data/lib/legion/events.rb
CHANGED
|
@@ -31,8 +31,7 @@ module Legion
|
|
|
31
31
|
listeners[event_name.to_s].each do |listener|
|
|
32
32
|
listener.call(event)
|
|
33
33
|
rescue StandardError => e
|
|
34
|
-
Legion::Logging.
|
|
35
|
-
Legion::Logging.error e.backtrace&.first(5)
|
|
34
|
+
Legion::Logging.log_exception(e, payload_summary: "[Events] listener error on #{event_name}", component_type: :event)
|
|
36
35
|
end
|
|
37
36
|
|
|
38
37
|
# Also fire wildcard listeners
|
|
@@ -9,8 +9,7 @@ module Legion
|
|
|
9
9
|
def runner
|
|
10
10
|
Legion::Runner.run(runner_class: runner_class, function: function, check_subtask: check_subtask?, generate_task: generate_task?)
|
|
11
11
|
rescue StandardError => e
|
|
12
|
-
Legion::Logging.
|
|
13
|
-
Legion::Logging.error e.backtrace
|
|
12
|
+
Legion::Logging.log_exception(e, component_type: :actor)
|
|
14
13
|
end
|
|
15
14
|
|
|
16
15
|
def manual
|
|
@@ -23,8 +22,7 @@ module Legion
|
|
|
23
22
|
klass.send(func, **args)
|
|
24
23
|
end
|
|
25
24
|
rescue StandardError => e
|
|
26
|
-
Legion::Logging.
|
|
27
|
-
Legion::Logging.error e.backtrace
|
|
25
|
+
Legion::Logging.log_exception(e, component_type: :actor)
|
|
28
26
|
end
|
|
29
27
|
|
|
30
28
|
def function
|
|
@@ -16,15 +16,13 @@ module Legion
|
|
|
16
16
|
begin
|
|
17
17
|
skip_or_run { use_runner? ? runner : manual }
|
|
18
18
|
rescue StandardError => e
|
|
19
|
-
log.
|
|
20
|
-
log.error e.backtrace if defined?(log)
|
|
19
|
+
log.log_exception(e, payload_summary: "[Every] tick failed for #{self.class}", component_type: :actor) if defined?(log)
|
|
21
20
|
end
|
|
22
21
|
end
|
|
23
22
|
|
|
24
23
|
@timer.execute
|
|
25
24
|
rescue StandardError => e
|
|
26
|
-
log.
|
|
27
|
-
log.error e.backtrace
|
|
25
|
+
log.log_exception(e, component_type: :actor)
|
|
28
26
|
end
|
|
29
27
|
|
|
30
28
|
def time
|
|
@@ -49,8 +47,7 @@ module Legion
|
|
|
49
47
|
|
|
50
48
|
@timer.shutdown
|
|
51
49
|
rescue StandardError => e
|
|
52
|
-
log.
|
|
53
|
-
log.error e.backtrace
|
|
50
|
+
log.log_exception(e, component_type: :actor)
|
|
54
51
|
end
|
|
55
52
|
end
|
|
56
53
|
end
|
|
@@ -17,13 +17,11 @@ check_subtask: check_subtask? }}"
|
|
|
17
17
|
@timer = Concurrent::TimerTask.new(execution_interval: time, run_now: run_now?) do
|
|
18
18
|
skip_or_run { poll_cycle }
|
|
19
19
|
rescue StandardError => e
|
|
20
|
-
Legion::Logging.fatal
|
|
21
|
-
Legion::Logging.fatal e.backtrace
|
|
20
|
+
Legion::Logging.log_exception(e, level: :fatal, component_type: :actor)
|
|
22
21
|
end
|
|
23
22
|
@timer.execute
|
|
24
23
|
rescue StandardError => e
|
|
25
|
-
Legion::Logging.
|
|
26
|
-
Legion::Logging.error e.backtrace
|
|
24
|
+
Legion::Logging.log_exception(e, component_type: :actor)
|
|
27
25
|
end
|
|
28
26
|
|
|
29
27
|
def poll_cycle
|
|
@@ -55,8 +53,7 @@ check_subtask: check_subtask? }}"
|
|
|
55
53
|
log.debug("#{self.class} result: #{results}")
|
|
56
54
|
results
|
|
57
55
|
rescue StandardError => e
|
|
58
|
-
Legion::Logging.fatal
|
|
59
|
-
Legion::Logging.fatal e.backtrace
|
|
56
|
+
Legion::Logging.log_exception(e, level: :fatal, component_type: :actor)
|
|
60
57
|
end
|
|
61
58
|
|
|
62
59
|
def cache_name
|
|
@@ -91,7 +88,7 @@ check_subtask: check_subtask? }}"
|
|
|
91
88
|
Legion::Logging.debug 'Cancelling Legion Poller'
|
|
92
89
|
@timer.shutdown
|
|
93
90
|
rescue StandardError => e
|
|
94
|
-
Legion::Logging.
|
|
91
|
+
Legion::Logging.log_exception(e, component_type: :actor)
|
|
95
92
|
end
|
|
96
93
|
end
|
|
97
94
|
end
|
|
@@ -17,8 +17,7 @@ module Legion
|
|
|
17
17
|
@queue = queue.new
|
|
18
18
|
@queue.channel.prefetch(prefetch) if defined? prefetch
|
|
19
19
|
rescue StandardError => e
|
|
20
|
-
log.fatal
|
|
21
|
-
log.fatal e.backtrace
|
|
20
|
+
log.log_exception(e, level: :fatal, component_type: :actor)
|
|
22
21
|
end
|
|
23
22
|
|
|
24
23
|
def create_queue
|
|
@@ -48,12 +47,13 @@ module Legion
|
|
|
48
47
|
true
|
|
49
48
|
end
|
|
50
49
|
|
|
51
|
-
def prepare
|
|
50
|
+
def prepare
|
|
52
51
|
@queue = queue.new
|
|
53
52
|
@queue.channel.prefetch(prefetch) if defined? prefetch
|
|
54
53
|
consumer_tag = "#{Legion::Settings[:client][:name]}_#{lex_name}_#{runner_name}_#{SecureRandom.uuid}"
|
|
55
54
|
@consumer = Bunny::Consumer.new(@queue.channel, @queue, consumer_tag, false, false)
|
|
56
55
|
@consumer.on_delivery do |delivery_info, metadata, payload|
|
|
56
|
+
fn = nil
|
|
57
57
|
message = process_message(payload, metadata, delivery_info)
|
|
58
58
|
fn = find_function(message)
|
|
59
59
|
log.debug "[Subscription] message received: #{lex_name}/#{fn}" if defined?(log)
|
|
@@ -76,14 +76,12 @@ module Legion
|
|
|
76
76
|
|
|
77
77
|
cancel if Legion::Settings[:client][:shutting_down]
|
|
78
78
|
rescue StandardError => e
|
|
79
|
-
log.
|
|
80
|
-
log.error e.backtrace
|
|
79
|
+
log.log_exception(e, payload_summary: "[Subscription] message processing failed: #{lex_name}/#{fn}", component_type: :actor)
|
|
81
80
|
@queue.reject(delivery_info.delivery_tag) if manual_ack
|
|
82
81
|
end
|
|
83
82
|
log.info "[Subscription] prepared: #{lex_name}/#{runner_name}"
|
|
84
83
|
rescue StandardError => e
|
|
85
|
-
log.fatal
|
|
86
|
-
log.fatal e.backtrace
|
|
84
|
+
log.log_exception(e, level: :fatal, payload_summary: 'Subscription#prepare failed', component_type: :actor)
|
|
87
85
|
end
|
|
88
86
|
|
|
89
87
|
def activate
|
|
@@ -159,6 +157,7 @@ module Legion
|
|
|
159
157
|
metadata = rmq_message.last
|
|
160
158
|
delivery_info = rmq_message.first
|
|
161
159
|
|
|
160
|
+
fn = nil
|
|
162
161
|
message = process_message(payload, metadata, delivery_info)
|
|
163
162
|
fn = find_function(message)
|
|
164
163
|
log.debug "[Subscription] message received: #{lex_name}/#{fn}" if defined?(log)
|
|
@@ -185,8 +184,7 @@ module Legion
|
|
|
185
184
|
|
|
186
185
|
cancel if Legion::Settings[:client][:shutting_down]
|
|
187
186
|
rescue StandardError => e
|
|
188
|
-
log.
|
|
189
|
-
log.error e.backtrace
|
|
187
|
+
log.log_exception(e, payload_summary: "[Subscription] message processing failed: #{lex_name}/#{fn}", component_type: :actor)
|
|
190
188
|
log.warn "[Subscription] nacking message for #{lex_name}/#{fn}"
|
|
191
189
|
@queue.reject(delivery_info.delivery_tag) if manual_ack
|
|
192
190
|
end
|
|
@@ -220,9 +220,7 @@ module Legion
|
|
|
220
220
|
lex_class.const_set(:Data, Module.new { extend Legion::Extensions::Data })
|
|
221
221
|
end
|
|
222
222
|
rescue StandardError => e
|
|
223
|
-
|
|
224
|
-
log.error e.message
|
|
225
|
-
log.error e.backtrace
|
|
223
|
+
log.log_exception(e, payload_summary: "[Core] auto_generate_data failed for #{name}", component_type: :builder)
|
|
226
224
|
end
|
|
227
225
|
end
|
|
228
226
|
end
|
|
@@ -7,9 +7,17 @@ module Legion
|
|
|
7
7
|
include Legion::Logging::Helper
|
|
8
8
|
|
|
9
9
|
def handle_exception(exception, task_id: nil, **opts)
|
|
10
|
-
|
|
11
|
-
log.
|
|
12
|
-
|
|
10
|
+
spec = gem_spec_for_lex
|
|
11
|
+
log.log_exception(exception,
|
|
12
|
+
lex: log_lex_name,
|
|
13
|
+
component_type: derive_component_type,
|
|
14
|
+
gem_name: lex_gem_name,
|
|
15
|
+
lex_version: spec&.version&.to_s,
|
|
16
|
+
gem_path: spec&.full_gem_path,
|
|
17
|
+
source_code_uri: spec&.metadata&.[]('source_code_uri'),
|
|
18
|
+
handled: true,
|
|
19
|
+
payload_summary: opts.empty? ? nil : opts,
|
|
20
|
+
task_id: task_id)
|
|
13
21
|
|
|
14
22
|
unless task_id.nil?
|
|
15
23
|
Legion::Transport::Messages::TaskLog.new(
|
|
@@ -25,6 +33,51 @@ module Legion
|
|
|
25
33
|
|
|
26
34
|
raise Legion::Exception::HandledTask
|
|
27
35
|
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def derive_component_type
|
|
40
|
+
parts = respond_to?(:calling_class_array) ? calling_class_array : self.class.to_s.split('::')
|
|
41
|
+
match = parts.find { |p| Legion::Extensions::Helpers::Base::NAMESPACE_BOUNDARIES.include?(p) }
|
|
42
|
+
case match
|
|
43
|
+
when 'Runners' then :runner
|
|
44
|
+
when 'Actor', 'Actors' then :actor
|
|
45
|
+
when 'Transport' then :transport
|
|
46
|
+
when 'Helpers' then :helper
|
|
47
|
+
when 'Data' then :data
|
|
48
|
+
else :unknown
|
|
49
|
+
end
|
|
50
|
+
rescue StandardError
|
|
51
|
+
:unknown
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def lex_gem_name
|
|
55
|
+
base_name = log_lex_name
|
|
56
|
+
return nil unless base_name
|
|
57
|
+
|
|
58
|
+
"lex-#{base_name}"
|
|
59
|
+
rescue StandardError
|
|
60
|
+
nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def gem_spec_for_lex
|
|
64
|
+
name = lex_gem_name
|
|
65
|
+
return nil unless name
|
|
66
|
+
|
|
67
|
+
Gem::Specification.find_by_name(name)
|
|
68
|
+
rescue Gem::MissingSpecError
|
|
69
|
+
nil
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def log_lex_name
|
|
73
|
+
if respond_to?(:segments)
|
|
74
|
+
segments.join('-')
|
|
75
|
+
else
|
|
76
|
+
derive_log_tag
|
|
77
|
+
end
|
|
78
|
+
rescue StandardError
|
|
79
|
+
nil
|
|
80
|
+
end
|
|
28
81
|
end
|
|
29
82
|
end
|
|
30
83
|
end
|
|
@@ -16,8 +16,7 @@ module Legion
|
|
|
16
16
|
return true if Legion::Data::Model::TaskLog.insert(task_id: task_id, function_id: function_id, entry: Legion::JSON.dump(payload))
|
|
17
17
|
end
|
|
18
18
|
rescue StandardError => e
|
|
19
|
-
log.warn
|
|
20
|
-
log.warn("generate_task_log failed, reverting to rmq message, e: #{e.message}")
|
|
19
|
+
log.log_exception(e, level: :warn, payload_summary: 'generate_task_log failed, reverting to rmq message', component_type: :helper)
|
|
21
20
|
end
|
|
22
21
|
Legion::Transport::Messages::TaskLog.new(task_id: task_id, runner_class: runner_class, function: function, entry: payload).publish
|
|
23
22
|
end
|
|
@@ -41,8 +40,7 @@ module Legion
|
|
|
41
40
|
end
|
|
42
41
|
Legion::Transport::Messages::TaskUpdate.new(**update_hash).publish
|
|
43
42
|
rescue StandardError => e
|
|
44
|
-
log.fatal
|
|
45
|
-
log.fatal e.backtrace
|
|
43
|
+
log.log_exception(e, level: :fatal, component_type: :helper)
|
|
46
44
|
raise e
|
|
47
45
|
end
|
|
48
46
|
|
|
@@ -24,8 +24,7 @@ module Legion
|
|
|
24
24
|
auto_create_dlx_queue
|
|
25
25
|
log.info "[Transport] built exchanges=#{@exchanges.count} queues=#{@queues.count} for #{lex_name}"
|
|
26
26
|
rescue StandardError => e
|
|
27
|
-
log.
|
|
28
|
-
log.error e.backtrace
|
|
27
|
+
log.log_exception(e, payload_summary: "[Transport] build failed for #{lex_name}", component_type: :transport)
|
|
29
28
|
end
|
|
30
29
|
|
|
31
30
|
def generate_base_modules
|
|
@@ -140,9 +139,7 @@ module Legion
|
|
|
140
139
|
to = to.is_a?(String) ? Kernel.const_get(to).new : to.new
|
|
141
140
|
to.bind(from, routing_key: routing_key)
|
|
142
141
|
rescue StandardError => e
|
|
143
|
-
log.fatal
|
|
144
|
-
log.fatal e.backtrace
|
|
145
|
-
log.fatal({ from: from, to: to, routing_key: routing_key })
|
|
142
|
+
log.log_exception(e, level: :fatal, payload_summary: { from: from, to: to, routing_key: routing_key }, component_type: :transport)
|
|
146
143
|
end
|
|
147
144
|
|
|
148
145
|
def e_to_q
|
data/lib/legion/extensions.rb
CHANGED
data/lib/legion/runner/status.rb
CHANGED
|
@@ -21,8 +21,7 @@ module Legion
|
|
|
21
21
|
Legion::Transport::Messages::TaskUpdate.new(task_id: task_id, status: status, **).publish
|
|
22
22
|
rescue StandardError => e
|
|
23
23
|
retries += 1
|
|
24
|
-
Legion::Logging.
|
|
25
|
-
Legion::Logging.fatal e.backtrace
|
|
24
|
+
Legion::Logging.log_exception(e, level: :fatal, payload_summary: "[Status] update_rmq failed (attempt #{retries}/3)", component_type: :runner)
|
|
26
25
|
retry if retries < 3
|
|
27
26
|
end
|
|
28
27
|
|
|
@@ -32,9 +31,9 @@ module Legion
|
|
|
32
31
|
task = Legion::Data::Model::Task[task_id]
|
|
33
32
|
task.update(status: status)
|
|
34
33
|
rescue StandardError => e
|
|
35
|
-
Legion::Logging.
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
Legion::Logging.log_exception(e, level: :warn,
|
|
35
|
+
payload_summary: "[Status] update_db failed for task_id=#{task_id}, falling back to RabbitMQ update",
|
|
36
|
+
component_type: :runner)
|
|
38
37
|
update_rmq(task_id: task_id, status: status, **)
|
|
39
38
|
end
|
|
40
39
|
|
|
@@ -61,8 +60,7 @@ module Legion
|
|
|
61
60
|
|
|
62
61
|
{ success: true, task_id: Legion::Data::Model::Task.insert(insert), **insert }
|
|
63
62
|
rescue StandardError => e
|
|
64
|
-
Legion::Logging.
|
|
65
|
-
Legion::Logging.error e.backtrace
|
|
63
|
+
Legion::Logging.log_exception(e, component_type: :runner)
|
|
66
64
|
raise(e)
|
|
67
65
|
end
|
|
68
66
|
end
|
data/lib/legion/service.rb
CHANGED
|
@@ -48,7 +48,7 @@ module Legion
|
|
|
48
48
|
if transport
|
|
49
49
|
setup_transport
|
|
50
50
|
Legion::Readiness.mark_ready(:transport)
|
|
51
|
-
|
|
51
|
+
setup_logging_transport
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
setup_dispatch
|
|
@@ -248,14 +248,18 @@ module Legion
|
|
|
248
248
|
Legion::Logging.setup(log_level: log_level, level: log_level, trace: true)
|
|
249
249
|
end
|
|
250
250
|
|
|
251
|
-
def reconfigure_logging(cli_level)
|
|
252
|
-
|
|
253
|
-
level = cli_level ||
|
|
251
|
+
def reconfigure_logging(cli_level = nil)
|
|
252
|
+
ls = Legion::Settings[:logging] || {}
|
|
253
|
+
level = cli_level || ls[:level] || 'info'
|
|
254
|
+
|
|
254
255
|
Legion::Logging.setup(
|
|
255
|
-
level:
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
256
|
+
level: level,
|
|
257
|
+
format: (ls[:format] || 'text').to_sym,
|
|
258
|
+
log_file: ls[:log_file],
|
|
259
|
+
log_stdout: ls.fetch(:log_stdout, true),
|
|
260
|
+
trace: ls.fetch(:trace, true),
|
|
261
|
+
async: ls.fetch(:async, true),
|
|
262
|
+
include_pid: ls.fetch(:include_pid, false)
|
|
259
263
|
)
|
|
260
264
|
end
|
|
261
265
|
|
|
@@ -365,34 +369,73 @@ module Legion
|
|
|
365
369
|
Legion::Logging.info 'Legion::Transport connected'
|
|
366
370
|
end
|
|
367
371
|
|
|
368
|
-
def
|
|
372
|
+
def setup_logging_transport # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
|
369
373
|
return unless defined?(Legion::Transport::Connection)
|
|
370
374
|
return unless Legion::Transport::Connection.session_open?
|
|
371
|
-
return unless Legion::Transport::Connection.respond_to?(:log_channel)
|
|
372
375
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
376
|
+
lt_settings = begin
|
|
377
|
+
Legion::Settings.dig(:logging, :transport) || {}
|
|
378
|
+
rescue StandardError
|
|
379
|
+
{}
|
|
377
380
|
end
|
|
381
|
+
return unless lt_settings[:enabled] == true
|
|
378
382
|
|
|
379
|
-
|
|
380
|
-
|
|
383
|
+
forward_logs = lt_settings.fetch(:forward_logs, true)
|
|
384
|
+
forward_exceptions = lt_settings.fetch(:forward_exceptions, true)
|
|
385
|
+
return unless forward_logs || forward_exceptions
|
|
381
386
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
387
|
+
log_session = Legion::Transport::Connection.create_dedicated_session(name: 'legion-logging')
|
|
388
|
+
@log_session = log_session
|
|
389
|
+
log_channel = log_session.create_channel
|
|
390
|
+
log_channel.prefetch(1)
|
|
391
|
+
exchange = log_channel.topic('legion.logging', durable: true)
|
|
385
392
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
393
|
+
if forward_logs
|
|
394
|
+
Legion::Logging.log_writer = lambda { |event, routing_key:|
|
|
395
|
+
begin
|
|
396
|
+
next unless log_channel&.open?
|
|
397
|
+
|
|
398
|
+
exchange.publish(Legion::JSON.dump(event), routing_key: routing_key)
|
|
399
|
+
rescue StandardError
|
|
400
|
+
nil
|
|
401
|
+
end
|
|
402
|
+
}
|
|
392
403
|
end
|
|
393
404
|
|
|
394
|
-
|
|
395
|
-
|
|
405
|
+
if forward_exceptions
|
|
406
|
+
Legion::Logging.exception_writer = lambda { |event, routing_key:, headers:, properties:|
|
|
407
|
+
begin
|
|
408
|
+
next unless log_channel&.open?
|
|
409
|
+
|
|
410
|
+
exchange.publish(
|
|
411
|
+
Legion::JSON.dump(event),
|
|
412
|
+
routing_key: routing_key,
|
|
413
|
+
headers: headers,
|
|
414
|
+
**properties
|
|
415
|
+
)
|
|
416
|
+
rescue StandardError
|
|
417
|
+
nil
|
|
418
|
+
end
|
|
419
|
+
}
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
modes = []
|
|
423
|
+
modes << 'logs' if forward_logs
|
|
424
|
+
modes << 'exceptions' if forward_exceptions
|
|
425
|
+
Legion::Logging.info("Logging transport wired: #{modes.join(' + ')} (dedicated session)")
|
|
426
|
+
rescue StandardError => e
|
|
427
|
+
Legion::Logging.warn "Logging transport setup failed: #{e.message}"
|
|
428
|
+
teardown_logging_transport
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
def teardown_logging_transport
|
|
432
|
+
Legion::Logging.log_writer = nil
|
|
433
|
+
Legion::Logging.exception_writer = nil
|
|
434
|
+
@log_session&.close if @log_session.respond_to?(:close) &&
|
|
435
|
+
(!@log_session.respond_to?(:open?) || @log_session.open?)
|
|
436
|
+
@log_session = nil
|
|
437
|
+
rescue StandardError
|
|
438
|
+
nil
|
|
396
439
|
end
|
|
397
440
|
|
|
398
441
|
def setup_alerts
|
|
@@ -547,6 +590,7 @@ module Legion
|
|
|
547
590
|
shutdown_component('Cache') { Legion::Cache.shutdown }
|
|
548
591
|
Legion::Readiness.mark_not_ready(:cache)
|
|
549
592
|
|
|
593
|
+
teardown_logging_transport
|
|
550
594
|
shutdown_component('Transport') { Legion::Transport::Connection.shutdown }
|
|
551
595
|
Legion::Readiness.mark_not_ready(:transport)
|
|
552
596
|
|
|
@@ -558,7 +602,7 @@ module Legion
|
|
|
558
602
|
Legion::Events.emit('service.shutdown')
|
|
559
603
|
end
|
|
560
604
|
|
|
561
|
-
def reload
|
|
605
|
+
def reload # rubocop:disable Metrics/MethodLength
|
|
562
606
|
return if @reloading
|
|
563
607
|
|
|
564
608
|
@reloading = true
|
|
@@ -583,6 +627,7 @@ module Legion
|
|
|
583
627
|
shutdown_component('Cache') { Legion::Cache.shutdown }
|
|
584
628
|
Legion::Readiness.mark_not_ready(:cache)
|
|
585
629
|
|
|
630
|
+
teardown_logging_transport
|
|
586
631
|
shutdown_component('Transport') { Legion::Transport::Connection.shutdown }
|
|
587
632
|
Legion::Readiness.mark_not_ready(:transport)
|
|
588
633
|
|
|
@@ -599,7 +644,8 @@ module Legion
|
|
|
599
644
|
|
|
600
645
|
setup_transport
|
|
601
646
|
Legion::Readiness.mark_ready(:transport)
|
|
602
|
-
|
|
647
|
+
teardown_logging_transport
|
|
648
|
+
setup_logging_transport
|
|
603
649
|
|
|
604
650
|
require 'legion/cache' unless defined?(Legion::Cache)
|
|
605
651
|
Legion::Cache.setup
|
data/lib/legion/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: legionio
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.6.
|
|
4
|
+
version: 1.6.21
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -281,14 +281,14 @@ dependencies:
|
|
|
281
281
|
requirements:
|
|
282
282
|
- - ">="
|
|
283
283
|
- !ruby/object:Gem::Version
|
|
284
|
-
version: 1.
|
|
284
|
+
version: 1.4.0
|
|
285
285
|
type: :runtime
|
|
286
286
|
prerelease: false
|
|
287
287
|
version_requirements: !ruby/object:Gem::Requirement
|
|
288
288
|
requirements:
|
|
289
289
|
- - ">="
|
|
290
290
|
- !ruby/object:Gem::Version
|
|
291
|
-
version: 1.
|
|
291
|
+
version: 1.4.0
|
|
292
292
|
- !ruby/object:Gem::Dependency
|
|
293
293
|
name: legion-settings
|
|
294
294
|
requirement: !ruby/object:Gem::Requirement
|