conservation-guardian 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/README.md +84 -0
- data/lib/conservation_guardian/budget.rb +33 -0
- data/lib/conservation_guardian/detector.rb +57 -0
- data/lib/conservation_guardian/profiler.rb +54 -0
- data/lib/conservation_guardian/report.rb +52 -0
- data/lib/conservation_guardian.rb +19 -0
- metadata +50 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 017bb06291f703c2bbb5d8d07d5efac74460f64c2eba4a4ff56b7312453d8f73
|
|
4
|
+
data.tar.gz: 59d2a23f363cfb005b8f8c6e785cbc1faf25d9d69a7fa938a8875de6428157de
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: cbbdcec8e4a8b22a0d9b1468a9948ada0818a9974a533a64cb065369e710180d738d6b9b22b1bd550b9405e0f0557375b642887f6b7cef955b038542c9b1ad70
|
|
7
|
+
data.tar.gz: 74831bf3304f302668035688043adb4224d69b2f631484610bc81ea80005a4e46c9853aa874f7af783ebf9834e246c42c66a8fa07e7c02ab7e3918ada032a035
|
data/README.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Conservation Guardian
|
|
2
|
+
|
|
3
|
+
Universal **Budget → Profile → Detect → Report** pattern for resource conservation in any Ruby project.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
gem install conservation-guardian
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
require "conservation_guardian"
|
|
15
|
+
|
|
16
|
+
# Define a budget
|
|
17
|
+
budget = ConservationGuardian::Budget.new(
|
|
18
|
+
name: "server_resources",
|
|
19
|
+
limits: { memory_mb: 512, cpu_percent: 80 }
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
# Profile usage
|
|
23
|
+
profiler = ConservationGuardian::Profiler.new(budget)
|
|
24
|
+
profiler.ingest([
|
|
25
|
+
{ category: :memory_mb, value: 200, label: "worker_1" },
|
|
26
|
+
{ category: :memory_mb, value: 600, label: "worker_2" },
|
|
27
|
+
{ category: :cpu_percent, value: 45, label: "worker_1" },
|
|
28
|
+
{ category: :cpu_percent, value: 95, label: "worker_2" }
|
|
29
|
+
])
|
|
30
|
+
|
|
31
|
+
# Detect waste
|
|
32
|
+
detector = ConservationGuardian::Detector.new(budget, profiler)
|
|
33
|
+
findings = detector.detect
|
|
34
|
+
|
|
35
|
+
# Generate report
|
|
36
|
+
report = ConservationGuardian::Report.new(budget, profiler, findings)
|
|
37
|
+
puts report
|
|
38
|
+
|
|
39
|
+
# Or use the convenience method
|
|
40
|
+
report = ConservationGuardian.analyze(budget: budget, samples: samples)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## API
|
|
44
|
+
|
|
45
|
+
### Budget
|
|
46
|
+
|
|
47
|
+
Defines resource limits by category.
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
budget = ConservationGuardian::Budget.new(name: "limits", limits: { memory: 512 })
|
|
51
|
+
budget.exceeded?(:memory, 600) # => true
|
|
52
|
+
budget.limit_for(:memory) # => 512
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Profiler
|
|
56
|
+
|
|
57
|
+
Tracks usage samples per category.
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
profiler.ingest({ category: :memory, value: 200, label: "process" })
|
|
61
|
+
profiler.stats_for(:memory) # => { count:, min:, max:, sum:, avg: }
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Detector
|
|
65
|
+
|
|
66
|
+
Finds over-budget and anomalous usage.
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
detector.detect # => [{ type: :over_budget, severity: :high, ... }]
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Report
|
|
73
|
+
|
|
74
|
+
Human-readable and machine-parseable output.
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
report.to_h # => Hash with budget, stats, findings, summary
|
|
78
|
+
report.to_s # => formatted text
|
|
79
|
+
report.summary # => { total_samples:, total_findings:, ... }
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## License
|
|
83
|
+
|
|
84
|
+
MIT
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ConservationGuardian
|
|
4
|
+
class Budget
|
|
5
|
+
attr_reader :limits, :name
|
|
6
|
+
|
|
7
|
+
# limits: Hash of { category_name => max_value }
|
|
8
|
+
def initialize(name: "default", limits: {})
|
|
9
|
+
@name = name
|
|
10
|
+
@limits = limits.transform_keys(&:to_sym)
|
|
11
|
+
freeze
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def limit_for(category)
|
|
15
|
+
@limits[category.to_sym]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def exceeded?(category, value)
|
|
19
|
+
max = limit_for(category)
|
|
20
|
+
return false if max.nil?
|
|
21
|
+
|
|
22
|
+
value > max
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def categories
|
|
26
|
+
@limits.keys
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def to_h
|
|
30
|
+
{ name: @name, limits: @limits.dup }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ConservationGuardian
|
|
4
|
+
class Detector
|
|
5
|
+
attr_reader :budget, :profiler
|
|
6
|
+
|
|
7
|
+
def initialize(budget, profiler)
|
|
8
|
+
@budget = budget
|
|
9
|
+
@profiler = profiler
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def detect
|
|
13
|
+
findings = []
|
|
14
|
+
findings.concat(detect_over_budget)
|
|
15
|
+
findings.concat(detect_anomalies)
|
|
16
|
+
findings
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def detect_over_budget
|
|
22
|
+
profiler.over_budget_samples.map do |sample|
|
|
23
|
+
{
|
|
24
|
+
type: :over_budget,
|
|
25
|
+
severity: :high,
|
|
26
|
+
category: sample[:category],
|
|
27
|
+
value: sample[:value],
|
|
28
|
+
limit: budget.limit_for(sample[:category]),
|
|
29
|
+
label: sample[:label],
|
|
30
|
+
message: "#{sample[:label]} (#{sample[:value]}) exceeds limit #{budget.limit_for(sample[:category])} for #{sample[:category]}"
|
|
31
|
+
}
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def detect_anomalies
|
|
36
|
+
findings = []
|
|
37
|
+
profiler.samples.map { |s| s[:category] }.uniq.each do |cat|
|
|
38
|
+
stats = profiler.stats_for(cat)
|
|
39
|
+
next unless stats && stats[:count] >= 2
|
|
40
|
+
|
|
41
|
+
threshold = stats[:avg] * 2
|
|
42
|
+
profiler.samples.select { |s| s[:category] == cat && s[:value] > threshold }.each do |sample|
|
|
43
|
+
findings << {
|
|
44
|
+
type: :anomaly,
|
|
45
|
+
severity: :medium,
|
|
46
|
+
category: cat,
|
|
47
|
+
value: sample[:value],
|
|
48
|
+
avg: stats[:avg],
|
|
49
|
+
label: sample[:label],
|
|
50
|
+
message: "#{sample[:label]} (#{sample[:value]}) is >2x the average (#{stats[:avg].round(2)}) for #{cat}"
|
|
51
|
+
}
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
findings
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ConservationGuardian
|
|
4
|
+
class Profiler
|
|
5
|
+
attr_reader :samples, :budget
|
|
6
|
+
|
|
7
|
+
def initialize(budget)
|
|
8
|
+
@budget = budget
|
|
9
|
+
@samples = []
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# samples: Array of { category:, value:, label: }
|
|
13
|
+
# or pass a single sample at a time
|
|
14
|
+
def ingest(samples)
|
|
15
|
+
samples = [samples] if samples.is_a?(Hash)
|
|
16
|
+
samples.each do |s|
|
|
17
|
+
@samples << {
|
|
18
|
+
category: s[:category].to_sym,
|
|
19
|
+
value: s[:value].to_f,
|
|
20
|
+
label: s[:label].to_s,
|
|
21
|
+
timestamp: Time.now
|
|
22
|
+
}
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def stats_for(category)
|
|
27
|
+
cat = category.to_sym
|
|
28
|
+
vals = @samples.select { |s| s[:category] == cat }.map { |s| s[:value] }
|
|
29
|
+
return nil if vals.empty?
|
|
30
|
+
|
|
31
|
+
{
|
|
32
|
+
count: vals.size,
|
|
33
|
+
min: vals.min,
|
|
34
|
+
max: vals.max,
|
|
35
|
+
sum: vals.sum,
|
|
36
|
+
avg: vals.sum / vals.size
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def all_stats
|
|
41
|
+
@samples.map { |s| s[:category] }.uniq.each_with_object({}) do |cat, h|
|
|
42
|
+
h[cat] = stats_for(cat)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def over_budget_samples
|
|
47
|
+
@samples.select { |s| @budget.exceeded?(s[:category], s[:value]) }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def clear!
|
|
51
|
+
@samples.clear
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ConservationGuardian
|
|
4
|
+
class Report
|
|
5
|
+
attr_reader :budget, :profiler, :findings
|
|
6
|
+
|
|
7
|
+
def initialize(budget, profiler, findings)
|
|
8
|
+
@budget = budget
|
|
9
|
+
@profiler = profiler
|
|
10
|
+
@findings = findings
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def to_h
|
|
14
|
+
{
|
|
15
|
+
budget: budget.to_h,
|
|
16
|
+
stats: profiler.all_stats,
|
|
17
|
+
findings: findings,
|
|
18
|
+
summary: summary
|
|
19
|
+
}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def summary
|
|
23
|
+
{
|
|
24
|
+
total_samples: profiler.samples.size,
|
|
25
|
+
total_findings: findings.size,
|
|
26
|
+
over_budget: findings.count { |f| f[:type] == :over_budget },
|
|
27
|
+
anomalies: findings.count { |f| f[:type] == :anomaly },
|
|
28
|
+
categories_checked: budget.categories.size
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def to_s
|
|
33
|
+
lines = []
|
|
34
|
+
lines << "=== Conservation Guardian Report ==="
|
|
35
|
+
lines << "Budget: #{budget.name}"
|
|
36
|
+
lines << "Samples analyzed: #{summary[:total_samples]}"
|
|
37
|
+
lines << "Findings: #{summary[:total_findings]} (#{summary[:over_budget]} over budget, #{summary[:anomalies]} anomalies)"
|
|
38
|
+
lines << ""
|
|
39
|
+
|
|
40
|
+
if findings.any?
|
|
41
|
+
lines << "--- Findings ---"
|
|
42
|
+
findings.each do |f|
|
|
43
|
+
lines << "[#{f[:severity].upcase}] #{f[:message]}"
|
|
44
|
+
end
|
|
45
|
+
else
|
|
46
|
+
lines << "✅ No waste detected. All clear!"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
lines.join("\n")
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "conservation_guardian/budget"
|
|
4
|
+
require_relative "conservation_guardian/profiler"
|
|
5
|
+
require_relative "conservation_guardian/detector"
|
|
6
|
+
require_relative "conservation_guardian/report"
|
|
7
|
+
|
|
8
|
+
module ConservationGuardian
|
|
9
|
+
class Error < StandardError; end
|
|
10
|
+
|
|
11
|
+
# Convenience: run the full pipeline and return a Report
|
|
12
|
+
def self.analyze(budget:, samples:)
|
|
13
|
+
profiler = Profiler.new(budget)
|
|
14
|
+
profiler.ingest(samples)
|
|
15
|
+
detector = Detector.new(budget, profiler)
|
|
16
|
+
findings = detector.detect
|
|
17
|
+
Report.new(budget, profiler, findings)
|
|
18
|
+
end
|
|
19
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: conservation-guardian
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- SuperInstance
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-06-02 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: A guardian pattern library that provides budget tracking, usage profiling,
|
|
14
|
+
waste detection, and conservation reporting for any Ruby project.
|
|
15
|
+
email:
|
|
16
|
+
- team@superinstance.com
|
|
17
|
+
executables: []
|
|
18
|
+
extensions: []
|
|
19
|
+
extra_rdoc_files: []
|
|
20
|
+
files:
|
|
21
|
+
- README.md
|
|
22
|
+
- lib/conservation_guardian.rb
|
|
23
|
+
- lib/conservation_guardian/budget.rb
|
|
24
|
+
- lib/conservation_guardian/detector.rb
|
|
25
|
+
- lib/conservation_guardian/profiler.rb
|
|
26
|
+
- lib/conservation_guardian/report.rb
|
|
27
|
+
homepage: https://github.com/SuperInstance/gem-conservation-guardian
|
|
28
|
+
licenses:
|
|
29
|
+
- MIT
|
|
30
|
+
metadata: {}
|
|
31
|
+
post_install_message:
|
|
32
|
+
rdoc_options: []
|
|
33
|
+
require_paths:
|
|
34
|
+
- lib
|
|
35
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '2.7'
|
|
40
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
41
|
+
requirements:
|
|
42
|
+
- - ">="
|
|
43
|
+
- !ruby/object:Gem::Version
|
|
44
|
+
version: '0'
|
|
45
|
+
requirements: []
|
|
46
|
+
rubygems_version: 3.3.5
|
|
47
|
+
signing_key:
|
|
48
|
+
specification_version: 4
|
|
49
|
+
summary: Universal Budget → Profile → Detect → Report pattern for resource conservation
|
|
50
|
+
test_files: []
|