fast_exists 1.0.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/LICENSE.txt +21 -0
- data/README.md +242 -0
- data/benchmarks/benchmark_suite.rb +58 -0
- data/bin/fast_exists +5 -0
- data/docs/ADR/0001_probabilistic_data_structures.md +12 -0
- data/docs/ADR/0002_backend_abstraction.md +11 -0
- data/docs/ADR/0003_active_record_hooks.md +11 -0
- data/docs/ARCHITECTURE.md +36 -0
- data/docs/BENCHMARKS.md +9 -0
- data/docs/DEPLOYMENT.md +11 -0
- data/docs/FAQ.md +10 -0
- data/docs/MEMORY_SIZING.md +22 -0
- data/docs/PERFORMANCE.md +9 -0
- data/docs/PERFORMANCE_INTELLIGENCE.md +114 -0
- data/docs/REDIS_GUIDE.md +15 -0
- data/docs/SCALING.md +74 -0
- data/docs/TROUBLESHOOTING.md +9 -0
- data/lib/fast_exists/active_record/extension.rb +36 -0
- data/lib/fast_exists/active_record/hooks.rb +25 -0
- data/lib/fast_exists/active_record/model_methods.rb +109 -0
- data/lib/fast_exists/backends/base.rb +60 -0
- data/lib/fast_exists/backends/file.rb +70 -0
- data/lib/fast_exists/backends/memory.rb +41 -0
- data/lib/fast_exists/backends/null.rb +31 -0
- data/lib/fast_exists/backends/redis.rb +98 -0
- data/lib/fast_exists/backends/redis_bloom.rb +83 -0
- data/lib/fast_exists/backends/registry.rb +38 -0
- data/lib/fast_exists/bit_array.rb +90 -0
- data/lib/fast_exists/bloom/counting.rb +85 -0
- data/lib/fast_exists/bloom/filter.rb +133 -0
- data/lib/fast_exists/bloom/scalable.rb +79 -0
- data/lib/fast_exists/cli.rb +117 -0
- data/lib/fast_exists/configuration.rb +45 -0
- data/lib/fast_exists/engine.rb +66 -0
- data/lib/fast_exists/errors.rb +10 -0
- data/lib/fast_exists/generators/install_generator.rb +15 -0
- data/lib/fast_exists/generators/templates/fast_exists_initializer.rb +24 -0
- data/lib/fast_exists/instrumentation/event_subscriber.rb +35 -0
- data/lib/fast_exists/instrumentation/open_telemetry.rb +18 -0
- data/lib/fast_exists/instrumentation/prometheus.rb +32 -0
- data/lib/fast_exists/intelligence/analyzer.rb +135 -0
- data/lib/fast_exists/intelligence/auditor.rb +116 -0
- data/lib/fast_exists/intelligence/data_model.rb +57 -0
- data/lib/fast_exists/intelligence/doctor.rb +190 -0
- data/lib/fast_exists/intelligence/health.rb +109 -0
- data/lib/fast_exists/intelligence/report/builder.rb +53 -0
- data/lib/fast_exists/intelligence/report/console.rb +40 -0
- data/lib/fast_exists/intelligence/report/csv.rb +37 -0
- data/lib/fast_exists/intelligence/report/html.rb +118 -0
- data/lib/fast_exists/intelligence/report/json.rb +17 -0
- data/lib/fast_exists/intelligence/report/markdown.rb +41 -0
- data/lib/fast_exists/intelligence/report/pdf.rb +43 -0
- data/lib/fast_exists/intelligence/report/yaml.rb +17 -0
- data/lib/fast_exists/intelligence/stats.rb +78 -0
- data/lib/fast_exists/multi_tenancy/resolver.rb +24 -0
- data/lib/fast_exists/multi_tenant/allocator.rb +43 -0
- data/lib/fast_exists/multi_tenant/base.rb +25 -0
- data/lib/fast_exists/multi_tenant/key_layout.rb +19 -0
- data/lib/fast_exists/multi_tenant/pool.rb +42 -0
- data/lib/fast_exists/multi_tenant/recommendation_engine.rb +76 -0
- data/lib/fast_exists/multi_tenant/strategies/adaptive_strategy.rb +81 -0
- data/lib/fast_exists/multi_tenant/strategies/base_strategy.rb +31 -0
- data/lib/fast_exists/multi_tenant/strategies/global_strategy.rb +37 -0
- data/lib/fast_exists/multi_tenant/strategies/per_tenant_strategy.rb +54 -0
- data/lib/fast_exists/multi_tenant/strategies/shared_strategy.rb +52 -0
- data/lib/fast_exists/optimizer/ai_advisor.rb +52 -0
- data/lib/fast_exists/probabilistic/count_min_sketch.rb +67 -0
- data/lib/fast_exists/probabilistic/cuckoo.rb +128 -0
- data/lib/fast_exists/probabilistic/hyper_log_log.rb +84 -0
- data/lib/fast_exists/railtie.rb +17 -0
- data/lib/fast_exists/statistics/tracker.rb +83 -0
- data/lib/fast_exists/tasks/fast_exists.rake +117 -0
- data/lib/fast_exists/version.rb +5 -0
- data/lib/fast_exists.rb +131 -0
- metadata +218 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module FastExists
|
|
6
|
+
module Intelligence
|
|
7
|
+
module Report
|
|
8
|
+
class Builder
|
|
9
|
+
def self.build(format: :console, output: nil, include: nil, compare: nil)
|
|
10
|
+
data = FastExists::Intelligence::DataModel.new
|
|
11
|
+
data.health = FastExists::Intelligence::Health.check
|
|
12
|
+
data.analysis = FastExists::Intelligence::Analyzer.analyze
|
|
13
|
+
data.audit = FastExists::Intelligence::Auditor.audit
|
|
14
|
+
data.doctor = FastExists::Intelligence::Doctor.diagnose(format: :json)
|
|
15
|
+
|
|
16
|
+
comparison_data = load_comparison(compare) if compare
|
|
17
|
+
|
|
18
|
+
rendered_content = case format.to_sym
|
|
19
|
+
when :json
|
|
20
|
+
FastExists::Intelligence::Report::Json.render(data, comparison: comparison_data)
|
|
21
|
+
when :yaml
|
|
22
|
+
FastExists::Intelligence::Report::Yaml.render(data, comparison: comparison_data)
|
|
23
|
+
when :markdown
|
|
24
|
+
FastExists::Intelligence::Report::Markdown.render(data, comparison: comparison_data)
|
|
25
|
+
when :html
|
|
26
|
+
FastExists::Intelligence::Report::Html.render(data, comparison: comparison_data)
|
|
27
|
+
when :csv
|
|
28
|
+
FastExists::Intelligence::Report::Csv.render(data, comparison: comparison_data)
|
|
29
|
+
when :pdf
|
|
30
|
+
FastExists::Intelligence::Report::Pdf.render(data, comparison: comparison_data)
|
|
31
|
+
else
|
|
32
|
+
FastExists::Intelligence::Report::Console.render(data, comparison: comparison_data)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
if output
|
|
36
|
+
FileUtils.mkdir_p(File.dirname(output))
|
|
37
|
+
File.write(output, rendered_content)
|
|
38
|
+
puts "Report generated successfully at #{output}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
rendered_content
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def self.load_comparison(file_path)
|
|
47
|
+
return nil unless file_path && File.exist?(file_path)
|
|
48
|
+
JSON.parse(File.read(file_path), symbolize_names: true) rescue nil
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FastExists
|
|
4
|
+
module Intelligence
|
|
5
|
+
module Report
|
|
6
|
+
class Console
|
|
7
|
+
def self.render(data, comparison: nil)
|
|
8
|
+
lines = []
|
|
9
|
+
lines << "=========================================================="
|
|
10
|
+
lines << "⚡ FAST_EXISTS PERFORMANCE & ARCHITECTURE REPORT"
|
|
11
|
+
lines << "=========================================================="
|
|
12
|
+
lines << "Timestamp: #{data.timestamp}"
|
|
13
|
+
lines << "Environment: Ruby #{data.environment[:ruby_version]} | Rails #{data.environment[:rails_version]} | Adapter #{data.environment[:database_adapter]}"
|
|
14
|
+
lines << "Active Backend: #{data.environment[:backend]}"
|
|
15
|
+
lines << ""
|
|
16
|
+
lines << "1. OPERATIONAL HEALTH: #{data.health[:overall_status].to_s.upcase}"
|
|
17
|
+
data.health[:checks].each do |check|
|
|
18
|
+
mark = check[:status] == :pass ? "✓" : "⚠"
|
|
19
|
+
lines << " #{mark} #{check[:name]}: #{check[:message]}"
|
|
20
|
+
end
|
|
21
|
+
lines << ""
|
|
22
|
+
lines << "2. ARCHITECTURAL AUDIT & GRADE"
|
|
23
|
+
lines << " Audit Score: #{data.audit[:audit_score]}/100 (Grade: #{data.audit[:grade]})"
|
|
24
|
+
data.audit[:findings].each do |f|
|
|
25
|
+
lines << " - [#{f[:severity].to_s.upcase}] #{f[:location]}: #{f[:problem]}"
|
|
26
|
+
end
|
|
27
|
+
lines << ""
|
|
28
|
+
lines << "3. RUNTIME STATISTICS"
|
|
29
|
+
lines << " Queries Avoided: #{data.stats[:queries_avoided]}"
|
|
30
|
+
lines << " Database Lookups: #{data.stats[:database_lookups]}"
|
|
31
|
+
lines << " Bloom Hits: #{data.stats[:bloom_hits]}"
|
|
32
|
+
lines << " Bloom Misses: #{data.stats[:bloom_misses]}"
|
|
33
|
+
lines << " Hit Ratio: #{(data.stats[:hit_ratio] * 100).round(1)}%"
|
|
34
|
+
lines << "=========================================================="
|
|
35
|
+
lines.join("\n")
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "csv"
|
|
4
|
+
|
|
5
|
+
module FastExists
|
|
6
|
+
module Intelligence
|
|
7
|
+
module Report
|
|
8
|
+
class Csv
|
|
9
|
+
def self.render(data, comparison: nil)
|
|
10
|
+
CSV.generate(headers: true) do |csv|
|
|
11
|
+
csv << ["Category", "Metric / Location", "Value / Message", "Status / Severity"]
|
|
12
|
+
|
|
13
|
+
csv << ["Environment", "Ruby Version", data.environment[:ruby_version], "info"]
|
|
14
|
+
csv << ["Environment", "Rails Version", data.environment[:rails_version], "info"]
|
|
15
|
+
csv << ["Environment", "Backend", data.environment[:backend], "info"]
|
|
16
|
+
|
|
17
|
+
csv << ["Health", "Overall Status", data.health[:overall_status], data.health[:overall_status]]
|
|
18
|
+
data.health[:checks].each do |c|
|
|
19
|
+
csv << ["Health Check", c[:name], c[:message], c[:status]]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
csv << ["Stats", "Queries Avoided", data.stats[:queries_avoided], "info"]
|
|
23
|
+
csv << ["Stats", "Database Lookups", data.stats[:database_lookups], "info"]
|
|
24
|
+
csv << ["Stats", "Bloom Hits", data.stats[:bloom_hits], "info"]
|
|
25
|
+
csv << ["Stats", "False Positives", data.stats[:false_positives], "info"]
|
|
26
|
+
csv << ["Stats", "Hit Ratio", "#{(data.stats[:hit_ratio] * 100).round(1)}%", "info"]
|
|
27
|
+
|
|
28
|
+
csv << ["Audit", "Score", data.audit[:audit_score], data.audit[:grade]]
|
|
29
|
+
data.audit[:findings].each do |f|
|
|
30
|
+
csv << ["Audit Finding", f[:location], f[:problem], f[:severity]]
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FastExists
|
|
4
|
+
module Intelligence
|
|
5
|
+
module Report
|
|
6
|
+
class Html
|
|
7
|
+
def self.render(data, comparison: nil)
|
|
8
|
+
hit_pct = (data.stats[:hit_ratio] * 100).round(1)
|
|
9
|
+
miss_pct = (data.stats[:miss_ratio] * 100).round(1)
|
|
10
|
+
|
|
11
|
+
<<-HTML
|
|
12
|
+
<!DOCTYPE html>
|
|
13
|
+
<html lang="en">
|
|
14
|
+
<head>
|
|
15
|
+
<meta charset="UTF-8">
|
|
16
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
17
|
+
<title>FastExists Performance & Architecture Report</title>
|
|
18
|
+
<style>
|
|
19
|
+
:root {
|
|
20
|
+
--bg: #0f172a;
|
|
21
|
+
--card-bg: #1e293b;
|
|
22
|
+
--text: #f8fafc;
|
|
23
|
+
--text-muted: #94a3b8;
|
|
24
|
+
--accent: #38bdf8;
|
|
25
|
+
--border: #334155;
|
|
26
|
+
--pass: #22c55e;
|
|
27
|
+
--warn: #eab308;
|
|
28
|
+
--fail: #ef4444;
|
|
29
|
+
}
|
|
30
|
+
@media print {
|
|
31
|
+
body { background: #fff !important; color: #000 !important; }
|
|
32
|
+
.card { border: 1px solid #ccc !important; background: #fff !important; color: #000 !important; }
|
|
33
|
+
}
|
|
34
|
+
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: var(--bg); color: var(--text); margin: 0; padding: 2.5rem; line-height: 1.5; }
|
|
35
|
+
.header { border-bottom: 1px solid var(--border); padding-bottom: 1.5rem; margin-bottom: 2rem; }
|
|
36
|
+
.title { font-size: 2.25rem; font-weight: 800; color: var(--accent); margin: 0 0 0.5rem 0; display: flex; align-items: center; gap: 0.5rem; }
|
|
37
|
+
.subtitle { color: var(--text-muted); font-size: 1rem; margin: 0; }
|
|
38
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1.5rem; margin-bottom: 2.5rem; }
|
|
39
|
+
.card { background: var(--card-bg); border-radius: 0.75rem; padding: 1.5rem; border: 1px solid var(--border); box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1); }
|
|
40
|
+
.card-title { color: var(--text-muted); font-size: 0.875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 0.5rem; }
|
|
41
|
+
.card-val { font-size: 2.25rem; font-weight: 800; color: var(--accent); }
|
|
42
|
+
.badge { display: inline-block; padding: 0.25rem 0.75rem; border-radius: 9999px; font-weight: 700; font-size: 0.875rem; text-transform: uppercase; }
|
|
43
|
+
.badge-pass { background: rgba(34, 197, 94, 0.2); color: var(--pass); border: 1px solid var(--pass); }
|
|
44
|
+
.badge-warn { background: rgba(234, 179, 8, 0.2); color: var(--warn); border: 1px solid var(--warn); }
|
|
45
|
+
.badge-fail { background: rgba(239, 68, 68, 0.2); color: var(--fail); border: 1px solid var(--fail); }
|
|
46
|
+
section { margin-bottom: 3rem; }
|
|
47
|
+
h2 { font-size: 1.5rem; border-bottom: 1px solid var(--border); padding-bottom: 0.5rem; margin-bottom: 1.5rem; color: var(--text); }
|
|
48
|
+
table { width: 100%; border-collapse: collapse; margin-top: 1rem; }
|
|
49
|
+
th, td { padding: 0.75rem 1rem; text-align: left; border-bottom: 1px solid var(--border); }
|
|
50
|
+
th { background: rgba(255,255,255,0.03); color: var(--text-muted); font-weight: 600; }
|
|
51
|
+
pre { background: #090d16; padding: 1rem; border-radius: 0.5rem; overflow-x: auto; color: #a5f3fc; border: 1px solid var(--border); font-family: monospace; }
|
|
52
|
+
.chart-bar { background: var(--border); height: 1.25rem; border-radius: 0.25rem; overflow: hidden; display: flex; margin-top: 0.5rem; }
|
|
53
|
+
.chart-fill-hit { background: var(--accent); height: 100%; width: #{hit_pct}%; }
|
|
54
|
+
.chart-fill-miss { background: #64748b; height: 100%; width: #{miss_pct}%; }
|
|
55
|
+
</style>
|
|
56
|
+
</head>
|
|
57
|
+
<body>
|
|
58
|
+
<div class="header">
|
|
59
|
+
<h1 class="title">⚡ FastExists Performance Intelligence Report</h1>
|
|
60
|
+
<p class="subtitle">Generated on #{data.timestamp} • Ruby #{data.environment[:ruby_version]} • Rails #{data.environment[:rails_version]} • Backend: <strong>#{data.environment[:backend]}</strong></p>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<div class="grid">
|
|
64
|
+
<div class="card">
|
|
65
|
+
<div class="card-title">Overall Health</div>
|
|
66
|
+
<div class="card-val"><span class="badge badge-#{data.health[:overall_status] == :healthy ? 'pass' : 'warn'}">#{data.health[:overall_status]}</span></div>
|
|
67
|
+
</div>
|
|
68
|
+
<div class="card">
|
|
69
|
+
<div class="card-title">Architecture Grade</div>
|
|
70
|
+
<div class="card-val">#{data.audit[:grade]} <span style="font-size:1.1rem; color:var(--text-muted)">( #{data.audit[:audit_score]}/100 )</span></div>
|
|
71
|
+
</div>
|
|
72
|
+
<div class="card">
|
|
73
|
+
<div class="card-title">Queries Avoided</div>
|
|
74
|
+
<div class="card-val">#{data.stats[:queries_avoided]}</div>
|
|
75
|
+
</div>
|
|
76
|
+
<div class="card">
|
|
77
|
+
<div class="card-title">Database Lookups</div>
|
|
78
|
+
<div class="card-val">#{data.stats[:database_lookups]}</div>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<section>
|
|
83
|
+
<h2>📊 Query Distribution & Bloom Hit Ratio</h2>
|
|
84
|
+
<p>Hit Ratio: <strong>#{hit_pct}%</strong> | Miss Ratio: <strong>#{miss_pct}%</strong></p>
|
|
85
|
+
<div class="chart-bar">
|
|
86
|
+
<div class="chart-fill-hit" title="Hits: #{hit_pct}%"></div>
|
|
87
|
+
<div class="chart-fill-miss" title="Avoided: #{miss_pct}%"></div>
|
|
88
|
+
</div>
|
|
89
|
+
</section>
|
|
90
|
+
|
|
91
|
+
<section>
|
|
92
|
+
<h2>🩺 Operational Health Checks</h2>
|
|
93
|
+
<table>
|
|
94
|
+
<thead>
|
|
95
|
+
<tr><th>Check Name</th><th>Status</th><th>Message</th></tr>
|
|
96
|
+
</thead>
|
|
97
|
+
<tbody>
|
|
98
|
+
#{data.health[:checks].map { |c| "<tr><td><strong>#{c[:name]}</strong></td><td><span class=\"badge badge-#{c[:status] == :pass ? 'pass' : 'warn'}\">#{c[:status]}</span></td><td>#{c[:message]}</td></tr>" }.join("\n")}
|
|
99
|
+
</tbody>
|
|
100
|
+
</table>
|
|
101
|
+
</section>
|
|
102
|
+
|
|
103
|
+
<section>
|
|
104
|
+
<h2>🔍 Architectural Audit Findings</h2>
|
|
105
|
+
#{data.audit[:findings].map { |f| "<div class=\"card\" style=\"margin-bottom:1rem;\"><div style=\"display:flex; justify-between; align-items:center;\"><strong>[#{f[:severity].to_s.upcase}] #{f[:location]}</strong></div><p style=\"color:var(--text-muted)\">#{f[:problem]}</p><p><strong>Recommendation:</strong> #{f[:recommendation]}</p></div>" }.join("\n")}
|
|
106
|
+
</section>
|
|
107
|
+
</body>
|
|
108
|
+
</html>
|
|
109
|
+
HTML
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def self.render_doctor(recs)
|
|
113
|
+
render(FastExists::Intelligence::DataModel.new)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module FastExists
|
|
6
|
+
module Intelligence
|
|
7
|
+
module Report
|
|
8
|
+
class Json
|
|
9
|
+
def self.render(data, comparison: nil)
|
|
10
|
+
payload = data.to_h
|
|
11
|
+
payload[:comparison] = comparison if comparison
|
|
12
|
+
JSON.pretty_generate(payload)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FastExists
|
|
4
|
+
module Intelligence
|
|
5
|
+
module Report
|
|
6
|
+
class Markdown
|
|
7
|
+
def self.render(data, comparison: nil)
|
|
8
|
+
lines = []
|
|
9
|
+
lines << "# ⚡ FastExists Performance & Architecture Report"
|
|
10
|
+
lines << ""
|
|
11
|
+
lines << "> **Generated**: `#{data.timestamp}` "
|
|
12
|
+
lines << "> **Environment**: Ruby #{data.environment[:ruby_version]} | Rails #{data.environment[:rails_version]} | DB: `#{data.environment[:database_adapter]}` "
|
|
13
|
+
lines << "> **Active Backend**: `#{data.environment[:backend]}`"
|
|
14
|
+
lines << ""
|
|
15
|
+
lines << "## Executive Summary"
|
|
16
|
+
lines << "- **Health Status**: `#{data.health[:overall_status].to_s.upcase}`"
|
|
17
|
+
lines << "- **Architecture Grade**: **#{data.audit[:grade]}** (#{data.audit[:audit_score]}/100)"
|
|
18
|
+
lines << "- **Queries Avoided**: **#{data.stats[:queries_avoided]}**"
|
|
19
|
+
lines << "- **Database Lookups**: #{data.stats[:database_lookups]}"
|
|
20
|
+
lines << "- **Hit Ratio**: #{(data.stats[:hit_ratio] * 100).round(1)}%"
|
|
21
|
+
lines << ""
|
|
22
|
+
lines << "## Health Check Findings"
|
|
23
|
+
data.health[:checks].each do |check|
|
|
24
|
+
badge = check[:status] == :pass ? "✅" : "⚠️"
|
|
25
|
+
lines << "- #{badge} **#{check[:name]}**: #{check[:message]}"
|
|
26
|
+
end
|
|
27
|
+
lines << ""
|
|
28
|
+
lines << "## Audit & Optimizations"
|
|
29
|
+
data.audit[:findings].each do |f|
|
|
30
|
+
lines << "### [#{f[:severity].to_s.upcase}] #{f[:location]}"
|
|
31
|
+
lines << "- **Problem**: #{f[:problem]}"
|
|
32
|
+
lines << "- **Recommendation**: #{f[:recommendation]}"
|
|
33
|
+
lines << "- **Impact**: #{f[:estimated_impact]}"
|
|
34
|
+
lines << ""
|
|
35
|
+
end
|
|
36
|
+
lines.join("\n")
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FastExists
|
|
4
|
+
module Intelligence
|
|
5
|
+
module Report
|
|
6
|
+
class Pdf
|
|
7
|
+
def self.render(data, comparison: nil)
|
|
8
|
+
# Formats executive PDF-compatible markdown/text report
|
|
9
|
+
lines = []
|
|
10
|
+
lines << "% FAST_EXISTS PERFORMANCE & ARCHITECTURE REPORT"
|
|
11
|
+
lines << "% Executive Summary"
|
|
12
|
+
lines << "% Date: #{data.timestamp}"
|
|
13
|
+
lines << ""
|
|
14
|
+
lines << "================================================================================"
|
|
15
|
+
lines << "OVERALL OPERATIONAL HEALTH: #{data.health[:overall_status].to_s.upcase}"
|
|
16
|
+
lines << "ARCHITECTURAL GRADE: #{data.audit[:grade]} (Score: #{data.audit[:audit_score]}/100)"
|
|
17
|
+
lines << "QUERIES AVOIDED BY BLOOM FILTERS: #{data.stats[:queries_avoided]}"
|
|
18
|
+
lines << "================================================================================"
|
|
19
|
+
lines << ""
|
|
20
|
+
lines << "1. APPLICATION & ENVIRONMENT"
|
|
21
|
+
lines << " - Ruby Version: #{data.environment[:ruby_version]}"
|
|
22
|
+
lines << " - Rails Version: #{data.environment[:rails_version]}"
|
|
23
|
+
lines << " - DB Adapter: #{data.environment[:database_adapter]}"
|
|
24
|
+
lines << " - Backend Storage: #{data.environment[:backend]}"
|
|
25
|
+
lines << ""
|
|
26
|
+
lines << "2. HEALTH DIAGNOSTICS"
|
|
27
|
+
data.health[:checks].each do |c|
|
|
28
|
+
lines << " - [#{c[:status].to_s.upcase}] #{c[:name]}: #{c[:message]}"
|
|
29
|
+
end
|
|
30
|
+
lines << ""
|
|
31
|
+
lines << "3. AUDIT & DOCTOR RECOMMENDATIONS"
|
|
32
|
+
data.audit[:findings].each do |f|
|
|
33
|
+
lines << " * Location: #{f[:location]}"
|
|
34
|
+
lines << " Severity: #{f[:severity].to_s.upcase}"
|
|
35
|
+
lines << " Problem: #{f[:problem]}"
|
|
36
|
+
lines << " Recommendation: #{f[:recommendation]}"
|
|
37
|
+
end
|
|
38
|
+
lines.join("\n")
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module FastExists
|
|
6
|
+
module Intelligence
|
|
7
|
+
module Report
|
|
8
|
+
class Yaml
|
|
9
|
+
def self.render(data, comparison: nil)
|
|
10
|
+
payload = data.to_h
|
|
11
|
+
payload[:comparison] = comparison if comparison
|
|
12
|
+
payload.transform_keys(&:to_s).to_yaml
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "yaml"
|
|
5
|
+
|
|
6
|
+
module FastExists
|
|
7
|
+
module Intelligence
|
|
8
|
+
class Stats
|
|
9
|
+
def self.render(format: :console)
|
|
10
|
+
raw_stats = FastExists.stats.merge(
|
|
11
|
+
active_backend: FastExists.configuration.backend,
|
|
12
|
+
uptime_seconds: (FastExists.uptime rescue 0),
|
|
13
|
+
sync_status: FastExists.configuration.auto_sync ? :active : :disabled
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
case format.to_sym
|
|
17
|
+
when :json
|
|
18
|
+
JSON.pretty_generate(raw_stats)
|
|
19
|
+
when :yaml
|
|
20
|
+
raw_stats.transform_keys(&:to_s).to_yaml
|
|
21
|
+
when :markdown
|
|
22
|
+
to_markdown(raw_stats)
|
|
23
|
+
else
|
|
24
|
+
to_console(raw_stats)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def self.to_console(s)
|
|
31
|
+
<<~CONSOLE
|
|
32
|
+
==================================================
|
|
33
|
+
⚡ FastExists Runtime Statistics
|
|
34
|
+
==================================================
|
|
35
|
+
Active Backend: #{s[:active_backend]}
|
|
36
|
+
Memory Usage: #{s[:memory_usage] || 'N/A'}
|
|
37
|
+
Capacity: #{s[:capacity] || 'N/A'}
|
|
38
|
+
Occupancy: #{s[:occupancy] || 'N/A'}
|
|
39
|
+
Bit Array Size: #{s[:bit_size] || 'N/A'}
|
|
40
|
+
Hash Count: #{s[:hash_count] || 'N/A'}
|
|
41
|
+
Inserted Elements: #{s[:inserted_items] || 0}
|
|
42
|
+
Expected Elements: #{s[:expected_elements] || 0}
|
|
43
|
+
Est. False Positive Rate: #{s[:estimated_false_positive_rate] || 0.0}%
|
|
44
|
+
Actual False Positives: #{s[:false_positives]}
|
|
45
|
+
Bloom Hits: #{s[:bloom_hits]}
|
|
46
|
+
Bloom Misses: #{s[:bloom_misses]}
|
|
47
|
+
Queries Avoided: #{s[:queries_avoided]}
|
|
48
|
+
Database Lookups: #{s[:database_lookups]}
|
|
49
|
+
Hit Ratio: #{(s[:hit_ratio] * 100).round(2)}%
|
|
50
|
+
Miss Ratio: #{(s[:miss_ratio] * 100).round(2)}%
|
|
51
|
+
Synchronization Status: #{s[:sync_status]}
|
|
52
|
+
==================================================
|
|
53
|
+
CONSOLE
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.to_markdown(s)
|
|
57
|
+
<<~MARKDOWN
|
|
58
|
+
# ⚡ FastExists Runtime Statistics
|
|
59
|
+
|
|
60
|
+
| Metric | Value |
|
|
61
|
+
|:---|:---|
|
|
62
|
+
| **Active Backend** | `#{s[:active_backend]}` |
|
|
63
|
+
| **Capacity** | #{s[:capacity] || 'N/A'} |
|
|
64
|
+
| **Inserted Elements** | #{s[:inserted_items] || 0} |
|
|
65
|
+
| **Queries Avoided** | **#{s[:queries_avoided]}** |
|
|
66
|
+
| **Database Lookups** | #{s[:database_lookups]} |
|
|
67
|
+
| **Bloom Hits** | #{s[:bloom_hits]} |
|
|
68
|
+
| **Bloom Misses** | #{s[:bloom_misses]} |
|
|
69
|
+
| **False Positives** | #{s[:false_positives]} |
|
|
70
|
+
| **Hit Ratio** | #{(s[:hit_ratio] * 100).round(2)}% |
|
|
71
|
+
| **Miss Ratio** | #{(s[:miss_ratio] * 100).round(2)}% |
|
|
72
|
+
| **Est. FP Rate** | #{s[:estimated_false_positive_rate] || 0.0}% |
|
|
73
|
+
| **Sync Status** | `#{s[:sync_status]}` |
|
|
74
|
+
MARKDOWN
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FastExists
|
|
4
|
+
module MultiTenancy
|
|
5
|
+
class Resolver
|
|
6
|
+
def self.resolve(namespace_spec, record_or_context = nil)
|
|
7
|
+
case namespace_spec
|
|
8
|
+
when Proc
|
|
9
|
+
namespace_spec.call(record_or_context).to_s
|
|
10
|
+
when Symbol
|
|
11
|
+
if record_or_context && record_or_context.respond_to?(namespace_spec)
|
|
12
|
+
record_or_context.send(namespace_spec).to_s
|
|
13
|
+
else
|
|
14
|
+
namespace_spec.to_s
|
|
15
|
+
end
|
|
16
|
+
when String
|
|
17
|
+
namespace_spec
|
|
18
|
+
else
|
|
19
|
+
"default"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FastExists
|
|
4
|
+
module MultiTenant
|
|
5
|
+
class Allocator
|
|
6
|
+
attr_reader :strategy
|
|
7
|
+
|
|
8
|
+
def initialize(strategy_type = FastExists.configuration.tenant_strategy, options = {})
|
|
9
|
+
@strategy_type = strategy_type.to_sym
|
|
10
|
+
@strategy = build_strategy(@strategy_type, options)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def add(tenant_id, attribute, value)
|
|
14
|
+
@strategy.add(tenant_id, attribute, value)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def contains?(tenant_id, attribute, value)
|
|
18
|
+
@strategy.contains?(tenant_id, attribute, value)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def stats
|
|
22
|
+
@strategy.stats.merge(strategy_type: @strategy_type)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def build_strategy(type, options)
|
|
28
|
+
case type
|
|
29
|
+
when :global
|
|
30
|
+
FastExists::MultiTenant::Strategies::GlobalStrategy.new(options)
|
|
31
|
+
when :per_tenant
|
|
32
|
+
FastExists::MultiTenant::Strategies::PerTenantStrategy.new(options)
|
|
33
|
+
when :shared
|
|
34
|
+
FastExists::MultiTenant::Strategies::SharedStrategy.new(options)
|
|
35
|
+
when :adaptive
|
|
36
|
+
FastExists::MultiTenant::Strategies::AdaptiveStrategy.new(options)
|
|
37
|
+
else
|
|
38
|
+
raise UnsupportedBackendError, "Unknown tenant strategy: #{type}"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "key_layout"
|
|
4
|
+
require_relative "pool"
|
|
5
|
+
require_relative "strategies/base_strategy"
|
|
6
|
+
require_relative "strategies/global_strategy"
|
|
7
|
+
require_relative "strategies/per_tenant_strategy"
|
|
8
|
+
require_relative "strategies/shared_strategy"
|
|
9
|
+
require_relative "strategies/adaptive_strategy"
|
|
10
|
+
require_relative "allocator"
|
|
11
|
+
require_relative "recommendation_engine"
|
|
12
|
+
|
|
13
|
+
module FastExists
|
|
14
|
+
module MultiTenant
|
|
15
|
+
class << self
|
|
16
|
+
def allocator
|
|
17
|
+
@allocator ||= FastExists::MultiTenant::Allocator.new
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def reset!
|
|
21
|
+
@allocator = nil
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FastExists
|
|
4
|
+
module MultiTenant
|
|
5
|
+
class KeyLayout
|
|
6
|
+
def self.global_key(prefix = FastExists.configuration.key_prefix)
|
|
7
|
+
"#{prefix}:global"
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def self.pool_key(pool_name, prefix = FastExists.configuration.key_prefix)
|
|
11
|
+
"#{prefix}:pool:#{pool_name}"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.tenant_key(tenant_id, prefix = FastExists.configuration.key_prefix)
|
|
15
|
+
"#{prefix}:tenant:#{tenant_id}"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FastExists
|
|
4
|
+
module MultiTenant
|
|
5
|
+
class Pool
|
|
6
|
+
attr_reader :name, :expected_elements, :false_positive_rate, :backend
|
|
7
|
+
|
|
8
|
+
def initialize(name:, expected_elements:, false_positive_rate: 0.001, backend_type: nil)
|
|
9
|
+
@name = name
|
|
10
|
+
@expected_elements = expected_elements
|
|
11
|
+
@false_positive_rate = false_positive_rate
|
|
12
|
+
@backend_type = backend_type || FastExists.configuration.backend
|
|
13
|
+
@key = FastExists::MultiTenant::KeyLayout.pool_key(name)
|
|
14
|
+
|
|
15
|
+
@backend = FastExists.backends.fetch(
|
|
16
|
+
@backend_type,
|
|
17
|
+
namespace: "pool:#{name}",
|
|
18
|
+
expected_elements: expected_elements,
|
|
19
|
+
false_positive_rate: false_positive_rate
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def add(tenant_id, attribute, value)
|
|
24
|
+
composite_key = "#{tenant_id}:#{attribute}:#{value}"
|
|
25
|
+
@backend.add(composite_key)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def contains?(tenant_id, attribute, value)
|
|
29
|
+
composite_key = "#{tenant_id}:#{attribute}:#{value}"
|
|
30
|
+
@backend.contains?(composite_key)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def clear
|
|
34
|
+
@backend.clear
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def stats
|
|
38
|
+
@backend.stats.merge(pool_name: @name, pool_key: @key)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|