lex-detect 0.1.3 → 0.1.4
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 +13 -1
- data/lib/legion/extensions/detect/actors/observer_tick.rb +24 -0
- data/lib/legion/extensions/detect/local_migrations/20260320000001_create_observer_events.rb +23 -0
- data/lib/legion/extensions/detect/runners/cancel_task.rb +24 -0
- data/lib/legion/extensions/detect/runners/task_observer.rb +100 -1
- data/lib/legion/extensions/detect/version.rb +1 -1
- data/lib/legion/extensions/detect.rb +2 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4be5aba6e8a22792e9f9fcbc2db27f18fe2d51f176044e7a18bdcc7db534becf
|
|
4
|
+
data.tar.gz: de631309f69e478a59d46d7ea9d8f0ff90f4ceff01e198424813e5bece7c00a1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 18ef38241cf694a8244900735878fe5e5f49a70e3cedf50db94eb2e902aec8c84ecd3074e79a98320accea445e10b9145eca6590fc5fda4b2efce07e5338f737
|
|
7
|
+
data.tar.gz: af5e47f0566ca56b6b1eee75afd6620a5ec6201363e2c1d68ca89d47681221a0634979a52d9e941764204b3cdeb6745a4d9e33305537f548044c148425b137b0
|
data/CHANGELOG.md
CHANGED
|
@@ -1,12 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.4] - 2026-03-20
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- Version bump for deployment (0.1.3 was released before task observer and cancel task changes landed)
|
|
7
|
+
|
|
3
8
|
## [0.1.3] - 2026-03-20
|
|
4
9
|
|
|
5
10
|
### Added
|
|
6
|
-
- `Runners::TaskObserver` for monitoring task failure patterns
|
|
11
|
+
- `Runners::TaskObserver` for monitoring task failure patterns; supports DB-based `observe(since:)` and array-based `observe(tasks:)` calling styles
|
|
12
|
+
- `check_timeout_risk` detects tasks running beyond 2x expected duration and emits `timeout_risk` alert
|
|
13
|
+
- `check_repeated_failure` detects >=3 failures in 10 minutes per runner class
|
|
14
|
+
- `publish_alerts` forwards structured alerts to AMQP when `Legion::Transport` is available
|
|
15
|
+
- `record_observations` persists per-task observation records to `Legion::Data::Local` observer_events table
|
|
7
16
|
- `check_and_publish_failure_patterns` publishes failure pattern events to AMQP for self-healing pipeline integration
|
|
8
17
|
- `extract_gem_name` helper derives gem name from runner class string
|
|
9
18
|
- `build_failure_pattern` creates structured failure pattern hash
|
|
19
|
+
- `Runners::CancelTask` sets `cancelled_at` timestamp on tasks, guarded by `Legion::Data` availability
|
|
20
|
+
- `Actors::ObserverTick` runs `TaskObserver#observe` every 60 seconds, passing `since:` for incremental DB scans
|
|
21
|
+
- Local migration `20260320000001_create_observer_events` creates observer_events table for cognitive observation history
|
|
10
22
|
|
|
11
23
|
## [0.1.2] - 2026-03-19
|
|
12
24
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Detect
|
|
6
|
+
module Actors
|
|
7
|
+
class ObserverTick < Legion::Extensions::Actors::Every
|
|
8
|
+
def time = 60
|
|
9
|
+
def run_now? = false
|
|
10
|
+
def use_runner? = false
|
|
11
|
+
def check_subtask? = false
|
|
12
|
+
def generate_task? = false
|
|
13
|
+
|
|
14
|
+
def action(**_opts)
|
|
15
|
+
observer = Object.new.extend(Runners::TaskObserver)
|
|
16
|
+
observer.observe(since: @last_tick || (Time.now - 60))
|
|
17
|
+
ensure
|
|
18
|
+
@last_tick = Time.now
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Sequel.migration do
|
|
4
|
+
up do
|
|
5
|
+
create_table?(:observer_events) do
|
|
6
|
+
primary_key :id
|
|
7
|
+
String :task_id, size: 64
|
|
8
|
+
String :runner, size: 255
|
|
9
|
+
String :rule, size: 64, null: true
|
|
10
|
+
String :severity, size: 16, null: true
|
|
11
|
+
Float :duration, null: true
|
|
12
|
+
Float :token_cost, null: true
|
|
13
|
+
DateTime :observed_at, null: false, default: Sequel::CURRENT_TIMESTAMP
|
|
14
|
+
index :task_id
|
|
15
|
+
index :rule
|
|
16
|
+
index :observed_at
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
down do
|
|
21
|
+
drop_table?(:observer_events)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Detect
|
|
6
|
+
module Runners
|
|
7
|
+
module CancelTask
|
|
8
|
+
def cancel_task(task_id:, **)
|
|
9
|
+
return { success: false, reason: :data_unavailable } unless defined?(Legion::Data)
|
|
10
|
+
|
|
11
|
+
task = Legion::Data::Model::Task[task_id]
|
|
12
|
+
return { success: false, reason: :not_found } unless task
|
|
13
|
+
return { success: false, reason: :already_cancelled } if task.respond_to?(:cancelled?) && task.cancelled?
|
|
14
|
+
|
|
15
|
+
task.update(cancelled_at: Time.now.utc)
|
|
16
|
+
{ success: true, task_id: task_id, cancelled_at: task.cancelled_at }
|
|
17
|
+
rescue StandardError => e
|
|
18
|
+
{ success: false, reason: :error, message: e.message }
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -7,7 +7,9 @@ module Legion
|
|
|
7
7
|
module TaskObserver
|
|
8
8
|
extend self
|
|
9
9
|
|
|
10
|
-
def observe(tasks
|
|
10
|
+
def observe(tasks: nil, since: nil, **)
|
|
11
|
+
return observe_db(since: since) if tasks.nil?
|
|
12
|
+
|
|
11
13
|
return { success: true, alerts: [] } unless tasks.is_a?(Array)
|
|
12
14
|
|
|
13
15
|
alerts = generate_alerts(tasks)
|
|
@@ -19,6 +21,103 @@ module Legion
|
|
|
19
21
|
|
|
20
22
|
private
|
|
21
23
|
|
|
24
|
+
def observe_db(since: nil)
|
|
25
|
+
return { alerts: [], observed: 0 } unless defined?(Legion::Data)
|
|
26
|
+
|
|
27
|
+
since ||= Time.now - 60
|
|
28
|
+
db_tasks = Legion::Data.connection[:tasks]
|
|
29
|
+
.where { started_at > since }
|
|
30
|
+
.all
|
|
31
|
+
|
|
32
|
+
alerts = db_tasks.filter_map { |task| evaluate_rules(task) }
|
|
33
|
+
|
|
34
|
+
publish_alerts(alerts) if alerts.any?
|
|
35
|
+
record_observations(db_tasks, alerts)
|
|
36
|
+
|
|
37
|
+
{ alerts: alerts, observed: db_tasks.size }
|
|
38
|
+
rescue StandardError => e
|
|
39
|
+
{ alerts: [], observed: 0, error: e.message }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def evaluate_rules(task)
|
|
43
|
+
check_timeout_risk(task) ||
|
|
44
|
+
check_repeated_failure(task) ||
|
|
45
|
+
check_cost_spike(task)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def check_timeout_risk(task, expected_duration: 120)
|
|
49
|
+
return nil unless task[:status] == 'running' && task[:started_at]
|
|
50
|
+
|
|
51
|
+
elapsed = Time.now - task[:started_at]
|
|
52
|
+
return nil unless elapsed > (expected_duration * 2)
|
|
53
|
+
|
|
54
|
+
{
|
|
55
|
+
rule: 'timeout_risk',
|
|
56
|
+
runner: task[:runner_class],
|
|
57
|
+
task_id: task[:id],
|
|
58
|
+
severity: 'warn',
|
|
59
|
+
detail: "Running for #{elapsed.round}s (expected #{expected_duration}s)",
|
|
60
|
+
observed_at: Time.now.utc
|
|
61
|
+
}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def check_repeated_failure(task)
|
|
65
|
+
return nil unless defined?(Legion::Data) && task[:runner_class]
|
|
66
|
+
|
|
67
|
+
count = Legion::Data.connection[:tasks]
|
|
68
|
+
.where(runner_class: task[:runner_class], status: 'failed')
|
|
69
|
+
.where { started_at > Time.now - 600 }
|
|
70
|
+
.count
|
|
71
|
+
return nil unless count >= 3
|
|
72
|
+
|
|
73
|
+
{
|
|
74
|
+
rule: 'repeated_failure',
|
|
75
|
+
runner: task[:runner_class],
|
|
76
|
+
task_id: task[:id],
|
|
77
|
+
severity: 'critical',
|
|
78
|
+
detail: "#{count} failures in last 10 minutes",
|
|
79
|
+
observed_at: Time.now.utc
|
|
80
|
+
}
|
|
81
|
+
rescue StandardError
|
|
82
|
+
nil
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def check_cost_spike(_task)
|
|
86
|
+
nil
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def publish_alerts(alerts)
|
|
90
|
+
return unless defined?(Legion::Transport)
|
|
91
|
+
|
|
92
|
+
alerts.each do |alert|
|
|
93
|
+
Legion::Transport::Messages::Dynamic.new(
|
|
94
|
+
function: 'observer_alert',
|
|
95
|
+
payload: alert
|
|
96
|
+
).publish
|
|
97
|
+
end
|
|
98
|
+
rescue StandardError
|
|
99
|
+
nil
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def record_observations(db_tasks, alerts)
|
|
103
|
+
return unless defined?(Legion::Data::Local)
|
|
104
|
+
|
|
105
|
+
db_tasks.each do |task|
|
|
106
|
+
alert = alerts.find { |a| a[:task_id] == task[:id] }
|
|
107
|
+
Legion::Data::Local.connection[:observer_events].insert(
|
|
108
|
+
task_id: task[:id],
|
|
109
|
+
runner: task[:runner_class],
|
|
110
|
+
rule: alert&.dig(:rule),
|
|
111
|
+
severity: alert&.dig(:severity),
|
|
112
|
+
duration: task[:started_at] ? (Time.now - task[:started_at]).round(2) : nil,
|
|
113
|
+
token_cost: nil,
|
|
114
|
+
observed_at: Time.now.utc
|
|
115
|
+
)
|
|
116
|
+
end
|
|
117
|
+
rescue StandardError
|
|
118
|
+
nil
|
|
119
|
+
end
|
|
120
|
+
|
|
22
121
|
def generate_alerts(tasks)
|
|
23
122
|
failed = tasks.select { |t| t[:status] == 'failed' }
|
|
24
123
|
return [] if failed.empty?
|
|
@@ -5,6 +5,7 @@ require 'legion/extensions/detect/catalog'
|
|
|
5
5
|
require 'legion/extensions/detect/scanner'
|
|
6
6
|
require 'legion/extensions/detect/installer'
|
|
7
7
|
require_relative 'detect/runners/task_observer'
|
|
8
|
+
require_relative 'detect/runners/cancel_task'
|
|
8
9
|
|
|
9
10
|
module Legion
|
|
10
11
|
module Extensions
|
|
@@ -44,6 +45,7 @@ module Legion
|
|
|
44
45
|
|
|
45
46
|
require_relative 'detect/actors/full_scan' if defined?(Legion::Extensions::Actors::Once)
|
|
46
47
|
require_relative 'detect/actors/delta_scan' if defined?(Legion::Extensions::Actors::Every)
|
|
48
|
+
require_relative 'detect/actors/observer_tick' if defined?(Legion::Extensions::Actors::Every)
|
|
47
49
|
|
|
48
50
|
if defined?(Legion::Data::Local)
|
|
49
51
|
Legion::Data::Local.register_migrations(
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lex-detect
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -29,9 +29,12 @@ files:
|
|
|
29
29
|
- lib/legion/extensions/detect.rb
|
|
30
30
|
- lib/legion/extensions/detect/actors/delta_scan.rb
|
|
31
31
|
- lib/legion/extensions/detect/actors/full_scan.rb
|
|
32
|
+
- lib/legion/extensions/detect/actors/observer_tick.rb
|
|
32
33
|
- lib/legion/extensions/detect/catalog.rb
|
|
33
34
|
- lib/legion/extensions/detect/installer.rb
|
|
34
35
|
- lib/legion/extensions/detect/local_migrations/20260319000001_create_detect_results.rb
|
|
36
|
+
- lib/legion/extensions/detect/local_migrations/20260320000001_create_observer_events.rb
|
|
37
|
+
- lib/legion/extensions/detect/runners/cancel_task.rb
|
|
35
38
|
- lib/legion/extensions/detect/runners/task_observer.rb
|
|
36
39
|
- lib/legion/extensions/detect/scanner.rb
|
|
37
40
|
- lib/legion/extensions/detect/version.rb
|