fluyenta-ruby 0.1.14
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/CHANGELOG.md +68 -0
- data/LICENSE +11 -0
- data/README.md +571 -0
- data/lib/brainzlab/beacon/client.rb +227 -0
- data/lib/brainzlab/beacon/provisioner.rb +44 -0
- data/lib/brainzlab/beacon.rb +215 -0
- data/lib/brainzlab/configuration.rb +676 -0
- data/lib/brainzlab/context.rb +90 -0
- data/lib/brainzlab/cortex/cache.rb +59 -0
- data/lib/brainzlab/cortex/client.rb +159 -0
- data/lib/brainzlab/cortex/provisioner.rb +49 -0
- data/lib/brainzlab/cortex.rb +223 -0
- data/lib/brainzlab/debug.rb +305 -0
- data/lib/brainzlab/dendrite/client.rb +250 -0
- data/lib/brainzlab/dendrite/provisioner.rb +44 -0
- data/lib/brainzlab/dendrite.rb +195 -0
- data/lib/brainzlab/development/logger.rb +150 -0
- data/lib/brainzlab/development/store.rb +121 -0
- data/lib/brainzlab/development.rb +72 -0
- data/lib/brainzlab/devtools/assets/devtools.css +1329 -0
- data/lib/brainzlab/devtools/assets/devtools.js +396 -0
- data/lib/brainzlab/devtools/assets/logo.svg +6 -0
- data/lib/brainzlab/devtools/assets/templates/debug_panel.html.erb +511 -0
- data/lib/brainzlab/devtools/assets/templates/error_page.html.erb +1086 -0
- data/lib/brainzlab/devtools/data/collector.rb +248 -0
- data/lib/brainzlab/devtools/middleware/asset_server.rb +63 -0
- data/lib/brainzlab/devtools/middleware/database_handler.rb +177 -0
- data/lib/brainzlab/devtools/middleware/debug_panel.rb +126 -0
- data/lib/brainzlab/devtools/middleware/error_page.rb +377 -0
- data/lib/brainzlab/devtools/renderers/debug_panel_renderer.rb +159 -0
- data/lib/brainzlab/devtools/renderers/error_page_renderer.rb +98 -0
- data/lib/brainzlab/devtools.rb +75 -0
- data/lib/brainzlab/errors.rb +490 -0
- data/lib/brainzlab/flux/buffer.rb +96 -0
- data/lib/brainzlab/flux/client.rb +68 -0
- data/lib/brainzlab/flux/provisioner.rb +124 -0
- data/lib/brainzlab/flux.rb +184 -0
- data/lib/brainzlab/instrumentation/action_cable.rb +351 -0
- data/lib/brainzlab/instrumentation/action_controller.rb +649 -0
- data/lib/brainzlab/instrumentation/action_dispatch.rb +259 -0
- data/lib/brainzlab/instrumentation/action_mailbox.rb +197 -0
- data/lib/brainzlab/instrumentation/action_mailer.rb +182 -0
- data/lib/brainzlab/instrumentation/action_view.rb +380 -0
- data/lib/brainzlab/instrumentation/active_job.rb +569 -0
- data/lib/brainzlab/instrumentation/active_record.rb +559 -0
- data/lib/brainzlab/instrumentation/active_storage.rb +541 -0
- data/lib/brainzlab/instrumentation/active_support_cache.rb +730 -0
- data/lib/brainzlab/instrumentation/aws.rb +183 -0
- data/lib/brainzlab/instrumentation/dalli.rb +108 -0
- data/lib/brainzlab/instrumentation/delayed_job.rb +234 -0
- data/lib/brainzlab/instrumentation/elasticsearch.rb +209 -0
- data/lib/brainzlab/instrumentation/excon.rb +152 -0
- data/lib/brainzlab/instrumentation/faraday.rb +181 -0
- data/lib/brainzlab/instrumentation/good_job.rb +102 -0
- data/lib/brainzlab/instrumentation/grape.rb +293 -0
- data/lib/brainzlab/instrumentation/graphql.rb +252 -0
- data/lib/brainzlab/instrumentation/httparty.rb +193 -0
- data/lib/brainzlab/instrumentation/mongodb.rb +187 -0
- data/lib/brainzlab/instrumentation/net_http.rb +114 -0
- data/lib/brainzlab/instrumentation/rails_deprecation.rb +139 -0
- data/lib/brainzlab/instrumentation/railties.rb +134 -0
- data/lib/brainzlab/instrumentation/redis.rb +324 -0
- data/lib/brainzlab/instrumentation/resque.rb +114 -0
- data/lib/brainzlab/instrumentation/sidekiq.rb +265 -0
- data/lib/brainzlab/instrumentation/solid_queue.rb +194 -0
- data/lib/brainzlab/instrumentation/stripe.rb +163 -0
- data/lib/brainzlab/instrumentation/typhoeus.rb +106 -0
- data/lib/brainzlab/instrumentation.rb +360 -0
- data/lib/brainzlab/nerve/client.rb +235 -0
- data/lib/brainzlab/nerve/provisioner.rb +44 -0
- data/lib/brainzlab/nerve.rb +219 -0
- data/lib/brainzlab/pulse/client.rb +203 -0
- data/lib/brainzlab/pulse/instrumentation.rb +401 -0
- data/lib/brainzlab/pulse/propagation.rb +241 -0
- data/lib/brainzlab/pulse/provisioner.rb +114 -0
- data/lib/brainzlab/pulse/tracer.rb +111 -0
- data/lib/brainzlab/pulse.rb +294 -0
- data/lib/brainzlab/rails/log_formatter.rb +807 -0
- data/lib/brainzlab/rails/log_subscriber.rb +334 -0
- data/lib/brainzlab/rails/railtie.rb +606 -0
- data/lib/brainzlab/recall/buffer.rb +66 -0
- data/lib/brainzlab/recall/client.rb +158 -0
- data/lib/brainzlab/recall/logger.rb +116 -0
- data/lib/brainzlab/recall/provisioner.rb +130 -0
- data/lib/brainzlab/recall.rb +175 -0
- data/lib/brainzlab/reflex/breadcrumbs.rb +55 -0
- data/lib/brainzlab/reflex/client.rb +150 -0
- data/lib/brainzlab/reflex/provisioner.rb +116 -0
- data/lib/brainzlab/reflex.rb +421 -0
- data/lib/brainzlab/sentinel/client.rb +236 -0
- data/lib/brainzlab/sentinel/provisioner.rb +44 -0
- data/lib/brainzlab/sentinel.rb +165 -0
- data/lib/brainzlab/signal/client.rb +60 -0
- data/lib/brainzlab/signal/provisioner.rb +115 -0
- data/lib/brainzlab/signal.rb +136 -0
- data/lib/brainzlab/synapse/client.rb +308 -0
- data/lib/brainzlab/synapse/provisioner.rb +44 -0
- data/lib/brainzlab/synapse.rb +270 -0
- data/lib/brainzlab/testing/event_store.rb +377 -0
- data/lib/brainzlab/testing/helpers.rb +650 -0
- data/lib/brainzlab/testing/matchers.rb +391 -0
- data/lib/brainzlab/testing.rb +327 -0
- data/lib/brainzlab/utilities/circuit_breaker.rb +290 -0
- data/lib/brainzlab/utilities/health_check.rb +294 -0
- data/lib/brainzlab/utilities/log_formatter.rb +254 -0
- data/lib/brainzlab/utilities/rate_limiter.rb +230 -0
- data/lib/brainzlab/utilities.rb +17 -0
- data/lib/brainzlab/vault/cache.rb +80 -0
- data/lib/brainzlab/vault/client.rb +216 -0
- data/lib/brainzlab/vault/provisioner.rb +49 -0
- data/lib/brainzlab/vault.rb +262 -0
- data/lib/brainzlab/version.rb +5 -0
- data/lib/brainzlab/vision/client.rb +175 -0
- data/lib/brainzlab/vision/provisioner.rb +136 -0
- data/lib/brainzlab/vision.rb +155 -0
- data/lib/brainzlab-sdk.rb +3 -0
- data/lib/brainzlab.rb +306 -0
- data/lib/generators/brainzlab/install/install_generator.rb +63 -0
- data/lib/generators/brainzlab/install/templates/brainzlab.rb.tt +77 -0
- metadata +251 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'dendrite/client'
|
|
4
|
+
require_relative 'dendrite/provisioner'
|
|
5
|
+
|
|
6
|
+
module BrainzLab
|
|
7
|
+
module Dendrite
|
|
8
|
+
class << self
|
|
9
|
+
# Connect a Git repository for documentation
|
|
10
|
+
# @param url [String] Git repository URL
|
|
11
|
+
# @param name [String] Optional display name
|
|
12
|
+
# @param branch [String] Branch to track (default: main)
|
|
13
|
+
# @return [Hash, nil] Repository info
|
|
14
|
+
#
|
|
15
|
+
# @example
|
|
16
|
+
# BrainzLab::Dendrite.connect(
|
|
17
|
+
# "https://github.com/org/repo",
|
|
18
|
+
# name: "My API",
|
|
19
|
+
# branch: "main"
|
|
20
|
+
# )
|
|
21
|
+
#
|
|
22
|
+
def connect(url, name: nil, branch: 'main', **)
|
|
23
|
+
return nil unless enabled?
|
|
24
|
+
|
|
25
|
+
ensure_provisioned!
|
|
26
|
+
return nil unless BrainzLab.configuration.dendrite_valid?
|
|
27
|
+
|
|
28
|
+
client.connect_repository(url: url, name: name, branch: branch, **)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Trigger documentation sync for a repository
|
|
32
|
+
# @param repo_id [String] Repository ID
|
|
33
|
+
# @return [Boolean] True if sync started
|
|
34
|
+
def sync(repo_id)
|
|
35
|
+
return false unless enabled?
|
|
36
|
+
|
|
37
|
+
ensure_provisioned!
|
|
38
|
+
return false unless BrainzLab.configuration.dendrite_valid?
|
|
39
|
+
|
|
40
|
+
client.sync_repository(repo_id)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Get repository info
|
|
44
|
+
# @param repo_id [String] Repository ID
|
|
45
|
+
# @return [Hash, nil] Repository details
|
|
46
|
+
def repository(repo_id)
|
|
47
|
+
return nil unless enabled?
|
|
48
|
+
|
|
49
|
+
ensure_provisioned!
|
|
50
|
+
return nil unless BrainzLab.configuration.dendrite_valid?
|
|
51
|
+
|
|
52
|
+
client.get_repository(repo_id)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# List all connected repositories
|
|
56
|
+
# @return [Array<Hash>] List of repositories
|
|
57
|
+
def repositories
|
|
58
|
+
return [] unless enabled?
|
|
59
|
+
|
|
60
|
+
ensure_provisioned!
|
|
61
|
+
return [] unless BrainzLab.configuration.dendrite_valid?
|
|
62
|
+
|
|
63
|
+
client.list_repositories
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Get wiki for a repository
|
|
67
|
+
# @param repo_id [String] Repository ID
|
|
68
|
+
# @return [Hash, nil] Wiki structure
|
|
69
|
+
def wiki(repo_id)
|
|
70
|
+
return nil unless enabled?
|
|
71
|
+
|
|
72
|
+
ensure_provisioned!
|
|
73
|
+
return nil unless BrainzLab.configuration.dendrite_valid?
|
|
74
|
+
|
|
75
|
+
client.get_wiki(repo_id)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Get a specific wiki page
|
|
79
|
+
# @param repo_id [String] Repository ID
|
|
80
|
+
# @param page [String] Page slug (e.g., "models/user")
|
|
81
|
+
# @return [Hash, nil] Page content
|
|
82
|
+
def page(repo_id, page_slug)
|
|
83
|
+
return nil unless enabled?
|
|
84
|
+
|
|
85
|
+
ensure_provisioned!
|
|
86
|
+
return nil unless BrainzLab.configuration.dendrite_valid?
|
|
87
|
+
|
|
88
|
+
client.get_wiki_page(repo_id, page_slug)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Semantic search across the codebase
|
|
92
|
+
# @param repo_id [String] Repository ID
|
|
93
|
+
# @param query [String] Search query
|
|
94
|
+
# @param limit [Integer] Max results (default: 10)
|
|
95
|
+
# @return [Array<Hash>] Search results
|
|
96
|
+
#
|
|
97
|
+
# @example
|
|
98
|
+
# results = BrainzLab::Dendrite.search(repo_id, "authentication flow")
|
|
99
|
+
#
|
|
100
|
+
def search(repo_id, query, limit: 10)
|
|
101
|
+
return [] unless enabled?
|
|
102
|
+
|
|
103
|
+
ensure_provisioned!
|
|
104
|
+
return [] unless BrainzLab.configuration.dendrite_valid?
|
|
105
|
+
|
|
106
|
+
client.search(repo_id, query, limit: limit)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Ask a question about the codebase
|
|
110
|
+
# @param repo_id [String] Repository ID
|
|
111
|
+
# @param question [String] Question to ask
|
|
112
|
+
# @param session_id [String] Optional session for follow-up questions
|
|
113
|
+
# @return [Hash, nil] AI response with answer
|
|
114
|
+
#
|
|
115
|
+
# @example
|
|
116
|
+
# response = BrainzLab::Dendrite.ask(repo_id, "How does the payment flow work?")
|
|
117
|
+
# puts response[:answer]
|
|
118
|
+
#
|
|
119
|
+
def ask(repo_id, question, session_id: nil)
|
|
120
|
+
return nil unless enabled?
|
|
121
|
+
|
|
122
|
+
ensure_provisioned!
|
|
123
|
+
return nil unless BrainzLab.configuration.dendrite_valid?
|
|
124
|
+
|
|
125
|
+
client.ask(repo_id, question, session_id: session_id)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Explain a file or code symbol
|
|
129
|
+
# @param repo_id [String] Repository ID
|
|
130
|
+
# @param path [String] File path
|
|
131
|
+
# @param symbol [String] Optional specific symbol (class, method)
|
|
132
|
+
# @return [Hash, nil] Explanation
|
|
133
|
+
#
|
|
134
|
+
# @example
|
|
135
|
+
# explanation = BrainzLab::Dendrite.explain(repo_id, "app/models/user.rb", symbol: "authenticate")
|
|
136
|
+
#
|
|
137
|
+
def explain(repo_id, path, symbol: nil)
|
|
138
|
+
return nil unless enabled?
|
|
139
|
+
|
|
140
|
+
ensure_provisioned!
|
|
141
|
+
return nil unless BrainzLab.configuration.dendrite_valid?
|
|
142
|
+
|
|
143
|
+
client.explain(repo_id, path, symbol: symbol)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Generate a diagram
|
|
147
|
+
# @param repo_id [String] Repository ID
|
|
148
|
+
# @param type [Symbol] Diagram type (:class, :er, :sequence, :architecture)
|
|
149
|
+
# @param scope [String] Optional scope (module, class name)
|
|
150
|
+
# @return [Hash, nil] Mermaid diagram
|
|
151
|
+
#
|
|
152
|
+
# @example
|
|
153
|
+
# diagram = BrainzLab::Dendrite.diagram(repo_id, :er)
|
|
154
|
+
# puts diagram[:mermaid]
|
|
155
|
+
#
|
|
156
|
+
def diagram(repo_id, type:, scope: nil)
|
|
157
|
+
return nil unless enabled?
|
|
158
|
+
|
|
159
|
+
ensure_provisioned!
|
|
160
|
+
return nil unless BrainzLab.configuration.dendrite_valid?
|
|
161
|
+
|
|
162
|
+
client.generate_diagram(repo_id, type: type, scope: scope)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# === INTERNAL ===
|
|
166
|
+
|
|
167
|
+
def ensure_provisioned!
|
|
168
|
+
return if @provisioned
|
|
169
|
+
|
|
170
|
+
@provisioned = true
|
|
171
|
+
provisioner.ensure_project!
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def provisioner
|
|
175
|
+
@provisioner ||= Provisioner.new(BrainzLab.configuration)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def client
|
|
179
|
+
@client ||= Client.new(BrainzLab.configuration)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def reset!
|
|
183
|
+
@client = nil
|
|
184
|
+
@provisioner = nil
|
|
185
|
+
@provisioned = false
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
private
|
|
189
|
+
|
|
190
|
+
def enabled?
|
|
191
|
+
BrainzLab.configuration.dendrite_enabled
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module BrainzLab
|
|
6
|
+
module Development
|
|
7
|
+
# Pretty-prints development mode events to stdout
|
|
8
|
+
class Logger
|
|
9
|
+
# ANSI color codes
|
|
10
|
+
COLORS = {
|
|
11
|
+
reset: "\e[0m",
|
|
12
|
+
bold: "\e[1m",
|
|
13
|
+
dim: "\e[2m",
|
|
14
|
+
# Services
|
|
15
|
+
recall: "\e[36m", # Cyan
|
|
16
|
+
reflex: "\e[31m", # Red
|
|
17
|
+
pulse: "\e[33m", # Yellow
|
|
18
|
+
flux: "\e[35m", # Magenta
|
|
19
|
+
signal: "\e[32m", # Green
|
|
20
|
+
vault: "\e[34m", # Blue
|
|
21
|
+
vision: "\e[95m", # Light magenta
|
|
22
|
+
cortex: "\e[96m", # Light cyan
|
|
23
|
+
beacon: "\e[92m", # Light green
|
|
24
|
+
nerve: "\e[93m", # Light yellow
|
|
25
|
+
dendrite: "\e[94m", # Light blue
|
|
26
|
+
sentinel: "\e[91m", # Light red
|
|
27
|
+
synapse: "\e[97m", # White
|
|
28
|
+
# Log levels
|
|
29
|
+
debug: "\e[37m", # Gray
|
|
30
|
+
info: "\e[32m", # Green
|
|
31
|
+
warn: "\e[33m", # Yellow
|
|
32
|
+
error: "\e[31m", # Red
|
|
33
|
+
fatal: "\e[35m" # Magenta
|
|
34
|
+
}.freeze
|
|
35
|
+
|
|
36
|
+
def initialize(output: $stdout, colors: nil)
|
|
37
|
+
@output = output
|
|
38
|
+
@colors = colors.nil? ? tty? : colors
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Log an event to stdout in a readable format
|
|
42
|
+
# @param service [Symbol] :recall, :reflex, :pulse, etc.
|
|
43
|
+
# @param event_type [String] type of event
|
|
44
|
+
# @param payload [Hash] event data
|
|
45
|
+
def log(service:, event_type:, payload:)
|
|
46
|
+
timestamp = Time.now.strftime('%H:%M:%S.%L')
|
|
47
|
+
service_color = COLORS[service] || COLORS[:reset]
|
|
48
|
+
|
|
49
|
+
# Build the log line
|
|
50
|
+
parts = []
|
|
51
|
+
parts << colorize("[#{timestamp}]", :dim)
|
|
52
|
+
parts << colorize("[#{service.to_s.upcase}]", service_color, bold: true)
|
|
53
|
+
parts << colorize(event_type, :bold)
|
|
54
|
+
|
|
55
|
+
# Add message or name depending on event type
|
|
56
|
+
message = extract_message(payload, event_type)
|
|
57
|
+
parts << message if message
|
|
58
|
+
|
|
59
|
+
# Print the main line
|
|
60
|
+
@output.puts parts.join(' ')
|
|
61
|
+
|
|
62
|
+
# Print additional details indented
|
|
63
|
+
print_details(payload, event_type)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def tty?
|
|
69
|
+
@output.respond_to?(:tty?) && @output.tty?
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def colorize(text, color, bold: false)
|
|
73
|
+
return text unless @colors
|
|
74
|
+
|
|
75
|
+
color_code = color.is_a?(Symbol) ? COLORS[color] : color
|
|
76
|
+
prefix = bold ? "#{COLORS[:bold]}#{color_code}" : color_code.to_s
|
|
77
|
+
"#{prefix}#{text}#{COLORS[:reset]}"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def extract_message(payload, event_type)
|
|
81
|
+
case event_type
|
|
82
|
+
when 'log'
|
|
83
|
+
level = payload[:level]&.to_sym
|
|
84
|
+
level_color = COLORS[level] || COLORS[:info]
|
|
85
|
+
msg = "#{colorize("[#{level&.upcase}]", level_color)} #{payload[:message]}"
|
|
86
|
+
msg
|
|
87
|
+
when 'error'
|
|
88
|
+
"#{payload[:error_class]}: #{payload[:message]}"
|
|
89
|
+
when 'trace'
|
|
90
|
+
duration = payload[:duration_ms] ? "(#{payload[:duration_ms]}ms)" : ''
|
|
91
|
+
"#{payload[:name]} #{duration}"
|
|
92
|
+
when 'metric'
|
|
93
|
+
"#{payload[:name]} = #{payload[:value]}"
|
|
94
|
+
when 'span'
|
|
95
|
+
duration = payload[:duration_ms] ? "(#{payload[:duration_ms]}ms)" : ''
|
|
96
|
+
"#{payload[:name]} #{duration}"
|
|
97
|
+
else
|
|
98
|
+
payload[:message] || payload[:name]
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def print_details(payload, event_type)
|
|
103
|
+
details = extract_details(payload, event_type)
|
|
104
|
+
return if details.empty?
|
|
105
|
+
|
|
106
|
+
details.each do |key, value|
|
|
107
|
+
formatted_value = format_value(value)
|
|
108
|
+
@output.puts " #{colorize(key.to_s, :dim)}: #{formatted_value}"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def extract_details(payload, event_type)
|
|
113
|
+
# Fields to exclude from details (already shown in main line)
|
|
114
|
+
excluded = %i[timestamp message level name kind]
|
|
115
|
+
|
|
116
|
+
case event_type
|
|
117
|
+
when 'log'
|
|
118
|
+
payload.except(*excluded, :environment, :service, :host)
|
|
119
|
+
when 'error'
|
|
120
|
+
payload.slice(:error_class, :environment, :request_id, :user, :tags).compact
|
|
121
|
+
when 'trace'
|
|
122
|
+
payload.slice(:request_method, :request_path, :status, :db_ms, :view_ms, :spans).compact
|
|
123
|
+
when 'metric'
|
|
124
|
+
payload.slice(:kind, :tags).compact
|
|
125
|
+
else
|
|
126
|
+
payload.except(*excluded)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def format_value(value)
|
|
131
|
+
case value
|
|
132
|
+
when Hash
|
|
133
|
+
if value.size <= 3
|
|
134
|
+
value.map { |k, v| "#{k}=#{v.inspect}" }.join(', ')
|
|
135
|
+
else
|
|
136
|
+
"\n #{JSON.pretty_generate(value).gsub("\n", "\n ")}"
|
|
137
|
+
end
|
|
138
|
+
when Array
|
|
139
|
+
if value.size <= 3 && value.all? { |v| v.is_a?(String) || v.is_a?(Numeric) }
|
|
140
|
+
value.inspect
|
|
141
|
+
else
|
|
142
|
+
"\n #{JSON.pretty_generate(value).gsub("\n", "\n ")}"
|
|
143
|
+
end
|
|
144
|
+
else
|
|
145
|
+
value.inspect
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'sqlite3'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'fileutils'
|
|
6
|
+
|
|
7
|
+
module BrainzLab
|
|
8
|
+
module Development
|
|
9
|
+
# SQLite-backed store for development mode events
|
|
10
|
+
class Store
|
|
11
|
+
DEFAULT_PATH = 'tmp/brainzlab.sqlite3'
|
|
12
|
+
|
|
13
|
+
def initialize(config)
|
|
14
|
+
@config = config
|
|
15
|
+
@db_path = config.development_db_path || DEFAULT_PATH
|
|
16
|
+
@db = nil
|
|
17
|
+
ensure_database!
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Insert an event into the store
|
|
21
|
+
# @param service [Symbol] :recall, :reflex, :pulse, etc.
|
|
22
|
+
# @param event_type [String] type of event
|
|
23
|
+
# @param payload [Hash] event data
|
|
24
|
+
def insert(service:, event_type:, payload:)
|
|
25
|
+
db.execute(
|
|
26
|
+
'INSERT INTO events (service, event_type, payload, created_at) VALUES (?, ?, ?, ?)',
|
|
27
|
+
[service.to_s, event_type.to_s, JSON.generate(payload), Time.now.utc.iso8601(3)]
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Query events from the store
|
|
32
|
+
# @param service [Symbol, nil] filter by service
|
|
33
|
+
# @param event_type [String, nil] filter by event type
|
|
34
|
+
# @param since [Time, nil] filter events after this time
|
|
35
|
+
# @param limit [Integer] max number of events to return
|
|
36
|
+
# @return [Array<Hash>] matching events
|
|
37
|
+
def query(service: nil, event_type: nil, since: nil, limit: 100)
|
|
38
|
+
conditions = []
|
|
39
|
+
params = []
|
|
40
|
+
|
|
41
|
+
if service
|
|
42
|
+
conditions << 'service = ?'
|
|
43
|
+
params << service.to_s
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
if event_type
|
|
47
|
+
conditions << 'event_type = ?'
|
|
48
|
+
params << event_type.to_s
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
if since
|
|
52
|
+
conditions << 'created_at >= ?'
|
|
53
|
+
params << since.utc.iso8601(3)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
where_clause = conditions.empty? ? '' : "WHERE #{conditions.join(' AND ')}"
|
|
57
|
+
params << limit
|
|
58
|
+
|
|
59
|
+
sql = "SELECT id, service, event_type, payload, created_at FROM events #{where_clause} ORDER BY created_at DESC LIMIT ?"
|
|
60
|
+
|
|
61
|
+
db.execute(sql, params).map do |row|
|
|
62
|
+
{
|
|
63
|
+
id: row[0],
|
|
64
|
+
service: row[1].to_sym,
|
|
65
|
+
event_type: row[2],
|
|
66
|
+
payload: JSON.parse(row[3], symbolize_names: true),
|
|
67
|
+
created_at: Time.parse(row[4])
|
|
68
|
+
}
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Get event counts by service
|
|
73
|
+
def stats
|
|
74
|
+
results = db.execute('SELECT service, COUNT(*) as count FROM events GROUP BY service')
|
|
75
|
+
results.to_h { |row| [row[0].to_sym, row[1]] }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Clear all events
|
|
79
|
+
def clear!
|
|
80
|
+
db.execute('DELETE FROM events')
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Close the database connection
|
|
84
|
+
def close
|
|
85
|
+
@db&.close
|
|
86
|
+
@db = nil
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def db
|
|
92
|
+
@db ||= begin
|
|
93
|
+
SQLite3::Database.new(@db_path).tap do |database|
|
|
94
|
+
database.results_as_hash = false
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def ensure_database!
|
|
100
|
+
# Ensure the directory exists
|
|
101
|
+
FileUtils.mkdir_p(File.dirname(@db_path))
|
|
102
|
+
|
|
103
|
+
# Create the events table if it doesn't exist
|
|
104
|
+
db.execute(<<~SQL)
|
|
105
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
106
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
107
|
+
service TEXT NOT NULL,
|
|
108
|
+
event_type TEXT NOT NULL,
|
|
109
|
+
payload TEXT NOT NULL,
|
|
110
|
+
created_at TEXT NOT NULL
|
|
111
|
+
)
|
|
112
|
+
SQL
|
|
113
|
+
|
|
114
|
+
# Create indexes for common queries
|
|
115
|
+
db.execute('CREATE INDEX IF NOT EXISTS idx_events_service ON events(service)')
|
|
116
|
+
db.execute('CREATE INDEX IF NOT EXISTS idx_events_event_type ON events(event_type)')
|
|
117
|
+
db.execute('CREATE INDEX IF NOT EXISTS idx_events_created_at ON events(created_at)')
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'development/store'
|
|
4
|
+
require_relative 'development/logger'
|
|
5
|
+
|
|
6
|
+
module BrainzLab
|
|
7
|
+
# Development mode support for offline SDK usage
|
|
8
|
+
# Logs all events to stdout and stores them locally in SQLite
|
|
9
|
+
module Development
|
|
10
|
+
class << self
|
|
11
|
+
# Check if development mode is enabled
|
|
12
|
+
def enabled?
|
|
13
|
+
BrainzLab.configuration.mode == :development
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Get the store instance
|
|
17
|
+
def store
|
|
18
|
+
@store ||= Store.new(BrainzLab.configuration)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Get the development logger
|
|
22
|
+
def logger
|
|
23
|
+
@logger ||= Logger.new(output: BrainzLab.configuration.development_log_output || $stdout)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Record an event from any service
|
|
27
|
+
# @param service [Symbol] :recall, :reflex, :pulse, etc.
|
|
28
|
+
# @param event_type [String] type of event (log, error, trace, metric, etc.)
|
|
29
|
+
# @param payload [Hash] event data
|
|
30
|
+
def record(service:, event_type:, payload:)
|
|
31
|
+
return unless enabled?
|
|
32
|
+
|
|
33
|
+
# Log to stdout
|
|
34
|
+
logger.log(service: service, event_type: event_type, payload: payload)
|
|
35
|
+
|
|
36
|
+
# Store in SQLite
|
|
37
|
+
store.insert(service: service, event_type: event_type, payload: payload)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Query stored events
|
|
41
|
+
# @param service [Symbol, nil] filter by service
|
|
42
|
+
# @param event_type [String, nil] filter by event type
|
|
43
|
+
# @param since [Time, nil] filter events after this time
|
|
44
|
+
# @param limit [Integer] max number of events to return (default: 100)
|
|
45
|
+
# @return [Array<Hash>] matching events
|
|
46
|
+
def events(service: nil, event_type: nil, since: nil, limit: 100)
|
|
47
|
+
return [] unless enabled?
|
|
48
|
+
|
|
49
|
+
store.query(service: service, event_type: event_type, since: since, limit: limit)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Clear all stored events
|
|
53
|
+
def clear!
|
|
54
|
+
store.clear! if enabled?
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Reset the development module (for testing)
|
|
58
|
+
def reset!
|
|
59
|
+
@store&.close
|
|
60
|
+
@store = nil
|
|
61
|
+
@logger = nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Get event counts by service
|
|
65
|
+
def stats
|
|
66
|
+
return {} unless enabled?
|
|
67
|
+
|
|
68
|
+
store.stats
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|