rails-flow-map 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/CHANGELOG.md +52 -0
- data/LICENSE +21 -0
- data/README.md +314 -0
- data/README_ja.md +314 -0
- data/README_zh.md +314 -0
- data/lib/rails_flow_map/analyzers/controller_analyzer.rb +124 -0
- data/lib/rails_flow_map/analyzers/model_analyzer.rb +139 -0
- data/lib/rails_flow_map/configuration.rb +22 -0
- data/lib/rails_flow_map/engine.rb +16 -0
- data/lib/rails_flow_map/errors.rb +240 -0
- data/lib/rails_flow_map/formatters/d3js_formatter.rb +488 -0
- data/lib/rails_flow_map/formatters/erd_formatter.rb +64 -0
- data/lib/rails_flow_map/formatters/git_diff_formatter.rb +589 -0
- data/lib/rails_flow_map/formatters/graphviz_formatter.rb +111 -0
- data/lib/rails_flow_map/formatters/mermaid_formatter.rb +91 -0
- data/lib/rails_flow_map/formatters/metrics_formatter.rb +196 -0
- data/lib/rails_flow_map/formatters/openapi_formatter.rb +557 -0
- data/lib/rails_flow_map/formatters/plantuml_formatter.rb +92 -0
- data/lib/rails_flow_map/formatters/sequence_formatter.rb +288 -0
- data/lib/rails_flow_map/generators/install/templates/rails_flow_map.rb +34 -0
- data/lib/rails_flow_map/generators/install_generator.rb +32 -0
- data/lib/rails_flow_map/logging.rb +215 -0
- data/lib/rails_flow_map/models/flow_edge.rb +31 -0
- data/lib/rails_flow_map/models/flow_graph.rb +58 -0
- data/lib/rails_flow_map/models/flow_node.rb +37 -0
- data/lib/rails_flow_map/version.rb +3 -0
- data/lib/rails_flow_map.rb +310 -0
- data/lib/tasks/rails_flow_map.rake +70 -0
- metadata +156 -0
@@ -0,0 +1,288 @@
|
|
1
|
+
module RailsFlowMap
|
2
|
+
# Generates sequence diagrams showing request flow through the application
|
3
|
+
#
|
4
|
+
# This formatter creates Mermaid sequence diagrams that visualize how requests
|
5
|
+
# flow through controllers, services, models, and the database. It can optionally
|
6
|
+
# include middleware, callbacks, and validation steps.
|
7
|
+
#
|
8
|
+
# @example Basic usage
|
9
|
+
# formatter = SequenceFormatter.new(graph, endpoint: '/api/users')
|
10
|
+
# diagram = formatter.format
|
11
|
+
#
|
12
|
+
# @example With all features enabled
|
13
|
+
# formatter = SequenceFormatter.new(graph, {
|
14
|
+
# endpoint: '/api/users',
|
15
|
+
# include_middleware: true,
|
16
|
+
# include_callbacks: true,
|
17
|
+
# include_validations: true,
|
18
|
+
# include_database: true
|
19
|
+
# })
|
20
|
+
#
|
21
|
+
class SequenceFormatter
|
22
|
+
# Creates a new sequence diagram formatter
|
23
|
+
#
|
24
|
+
# @param graph [FlowGraph] The graph to analyze
|
25
|
+
# @param options [Hash] Configuration options
|
26
|
+
# @option options [String] :endpoint The endpoint to analyze (analyzes all if nil)
|
27
|
+
# @option options [Boolean] :include_middleware Include middleware in diagram (default: false)
|
28
|
+
# @option options [Boolean] :include_callbacks Include callbacks in diagram (default: false)
|
29
|
+
# @option options [Boolean] :include_validations Include validations in diagram (default: false)
|
30
|
+
# @option options [Boolean] :include_database Include database queries in diagram (default: true)
|
31
|
+
def initialize(graph, options = {})
|
32
|
+
@graph = graph
|
33
|
+
@endpoint = options[:endpoint]
|
34
|
+
@include_middleware = options[:include_middleware] || false
|
35
|
+
@include_callbacks = options[:include_callbacks] || false
|
36
|
+
@include_validations = options[:include_validations] || false
|
37
|
+
@include_database = options[:include_database] || true
|
38
|
+
end
|
39
|
+
|
40
|
+
# Generates the sequence diagram
|
41
|
+
#
|
42
|
+
# @param graph [FlowGraph] Optional graph to format (uses instance graph by default)
|
43
|
+
# @return [String] Mermaid sequence diagram markup
|
44
|
+
def format(graph = @graph)
|
45
|
+
output = []
|
46
|
+
output << "```mermaid"
|
47
|
+
output << "sequenceDiagram"
|
48
|
+
output << " autonumber"
|
49
|
+
|
50
|
+
# 参加者の定義
|
51
|
+
output.concat(define_participants(graph))
|
52
|
+
|
53
|
+
# エンドポイントに関連するノードを取得
|
54
|
+
route_nodes = filter_route_nodes(graph)
|
55
|
+
|
56
|
+
if route_nodes.empty?
|
57
|
+
output << " Note over Client: No routes found for endpoint: #{@endpoint}"
|
58
|
+
else
|
59
|
+
route_nodes.each do |route|
|
60
|
+
output << ""
|
61
|
+
output << " Note over Client: === #{route.attributes[:verb]} #{route.attributes[:path]} ==="
|
62
|
+
output.concat(generate_sequence_for_route(route, graph))
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
output << "```"
|
67
|
+
output.join("\n")
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def define_participants(graph)
|
73
|
+
participants = [" participant Client", " participant Router"]
|
74
|
+
|
75
|
+
if @include_middleware
|
76
|
+
participants << " participant Middleware"
|
77
|
+
end
|
78
|
+
|
79
|
+
# コントローラーの追加
|
80
|
+
controllers = graph.nodes_by_type(:controller).map(&:name).uniq
|
81
|
+
controllers.each do |controller|
|
82
|
+
participants << " participant #{sanitize_name(controller)}"
|
83
|
+
end
|
84
|
+
|
85
|
+
# サービスの追加
|
86
|
+
services = graph.nodes_by_type(:service).map(&:name).uniq
|
87
|
+
services.each do |service|
|
88
|
+
participants << " participant #{sanitize_name(service)}"
|
89
|
+
end
|
90
|
+
|
91
|
+
# モデルの追加
|
92
|
+
models = graph.nodes_by_type(:model).map(&:name).uniq
|
93
|
+
models.each do |model|
|
94
|
+
participants << " participant #{sanitize_name(model)}"
|
95
|
+
end
|
96
|
+
|
97
|
+
if @include_database
|
98
|
+
participants << " participant Database"
|
99
|
+
end
|
100
|
+
|
101
|
+
participants
|
102
|
+
end
|
103
|
+
|
104
|
+
def filter_route_nodes(graph)
|
105
|
+
route_nodes = graph.nodes_by_type(:route)
|
106
|
+
|
107
|
+
if @endpoint
|
108
|
+
# エンドポイントの正確なマッチング
|
109
|
+
route_nodes = route_nodes.select do |node|
|
110
|
+
path = node.attributes[:path]
|
111
|
+
path == @endpoint || path&.gsub(/:[\w_]+/, '*') == @endpoint.gsub(/\d+/, '*')
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
route_nodes
|
116
|
+
end
|
117
|
+
|
118
|
+
def generate_sequence_for_route(route, graph)
|
119
|
+
lines = []
|
120
|
+
|
121
|
+
# ミドルウェア処理
|
122
|
+
if @include_middleware
|
123
|
+
lines << " Client->>Router: #{route.attributes[:verb]} #{route.attributes[:path]}"
|
124
|
+
lines << " Router->>Middleware: Process request"
|
125
|
+
lines << " activate Middleware"
|
126
|
+
lines << " Note right of Middleware: Authentication<br/>CORS<br/>Rate limiting"
|
127
|
+
lines << " Middleware->>Router: Continue"
|
128
|
+
lines << " deactivate Middleware"
|
129
|
+
else
|
130
|
+
lines << " Client->>+Router: #{route.attributes[:verb]} #{route.attributes[:path]}"
|
131
|
+
end
|
132
|
+
|
133
|
+
# ルートからアクションへの処理
|
134
|
+
route_edges = graph.edges.select { |e| e.from == route.id && e.type == :routes_to }
|
135
|
+
|
136
|
+
route_edges.each do |edge|
|
137
|
+
action = graph.find_node(edge.to)
|
138
|
+
next unless action
|
139
|
+
|
140
|
+
controller_name = sanitize_name(action.attributes[:controller] || "Controller")
|
141
|
+
|
142
|
+
# コントローラーアクションの開始
|
143
|
+
lines << " Router->>+#{controller_name}: #{action.name}(params)"
|
144
|
+
|
145
|
+
# コールバック処理
|
146
|
+
if @include_callbacks
|
147
|
+
lines << " activate #{controller_name}"
|
148
|
+
lines << " Note over #{controller_name}: before_action callbacks"
|
149
|
+
end
|
150
|
+
|
151
|
+
# バリデーション
|
152
|
+
if @include_validations && ['create', 'update'].include?(action.name)
|
153
|
+
lines << " #{controller_name}->>#{controller_name}: validate_params"
|
154
|
+
lines << " alt Invalid parameters"
|
155
|
+
lines << " #{controller_name}-->>Client: 422 Unprocessable Entity"
|
156
|
+
lines << " else Valid parameters"
|
157
|
+
end
|
158
|
+
|
159
|
+
# サービス呼び出し
|
160
|
+
lines.concat(generate_service_calls(action, controller_name, graph))
|
161
|
+
|
162
|
+
# バリデーションの終了
|
163
|
+
if @include_validations && ['create', 'update'].include?(action.name)
|
164
|
+
lines << " end"
|
165
|
+
end
|
166
|
+
|
167
|
+
# レスポンスの生成
|
168
|
+
lines << " #{controller_name}->>#{controller_name}: render_response"
|
169
|
+
|
170
|
+
# コールバック処理の終了
|
171
|
+
if @include_callbacks
|
172
|
+
lines << " Note over #{controller_name}: after_action callbacks"
|
173
|
+
lines << " deactivate #{controller_name}"
|
174
|
+
end
|
175
|
+
|
176
|
+
# クライアントへのレスポンス
|
177
|
+
lines << " #{controller_name}-->>-Router: Response data"
|
178
|
+
lines << " Router-->>-Client: #{generate_response_code(action.name)} {data}"
|
179
|
+
end
|
180
|
+
|
181
|
+
lines
|
182
|
+
end
|
183
|
+
|
184
|
+
def generate_service_calls(action, controller_name, graph)
|
185
|
+
lines = []
|
186
|
+
|
187
|
+
# アクションからサービスへの呼び出し
|
188
|
+
service_edges = graph.edges.select { |e| e.from == action.id && e.type == :calls_service }
|
189
|
+
|
190
|
+
service_edges.each_with_index do |service_edge, index|
|
191
|
+
service = graph.find_node(service_edge.to)
|
192
|
+
next unless service
|
193
|
+
|
194
|
+
service_name = sanitize_name(service.name)
|
195
|
+
method_name = service_edge.label || "process"
|
196
|
+
|
197
|
+
# サービスメソッドの呼び出し
|
198
|
+
lines << " #{controller_name}->>+#{service_name}: #{method_name}(params)"
|
199
|
+
|
200
|
+
# サービス内の処理
|
201
|
+
lines << " activate #{service_name}"
|
202
|
+
lines << " Note over #{service_name}: Business logic"
|
203
|
+
|
204
|
+
# モデルアクセス
|
205
|
+
lines.concat(generate_model_access(service, service_name, graph))
|
206
|
+
|
207
|
+
# サービスからのレスポンス
|
208
|
+
lines << " deactivate #{service_name}"
|
209
|
+
lines << " #{service_name}-->>-#{controller_name}: ServiceResult"
|
210
|
+
|
211
|
+
# エラーハンドリング
|
212
|
+
if index == 0 # 最初のサービス呼び出しのみ
|
213
|
+
lines << " alt Service error"
|
214
|
+
lines << " #{controller_name}-->>Client: 500 Internal Server Error"
|
215
|
+
lines << " else Success"
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
# エラーハンドリングの終了
|
220
|
+
if service_edges.any?
|
221
|
+
lines << " end"
|
222
|
+
end
|
223
|
+
|
224
|
+
lines
|
225
|
+
end
|
226
|
+
|
227
|
+
def generate_model_access(service, service_name, graph)
|
228
|
+
lines = []
|
229
|
+
|
230
|
+
# サービスからモデルへのアクセス
|
231
|
+
model_edges = graph.edges.select { |e| e.from == service.id && e.type == :accesses_model }
|
232
|
+
|
233
|
+
model_edges.each do |model_edge|
|
234
|
+
model = graph.find_node(model_edge.to)
|
235
|
+
next unless model
|
236
|
+
|
237
|
+
model_name = sanitize_name(model.name)
|
238
|
+
query = model_edge.label || "query"
|
239
|
+
|
240
|
+
# モデルへのクエリ
|
241
|
+
lines << " #{service_name}->>+#{model_name}: #{query}"
|
242
|
+
|
243
|
+
# データベースアクセス
|
244
|
+
if @include_database
|
245
|
+
lines << " #{model_name}->>+Database: SQL Query"
|
246
|
+
lines << " Note right of Database: SELECT * FROM #{model_name.downcase}s<br/>WHERE ..."
|
247
|
+
lines << " Database-->>-#{model_name}: ResultSet"
|
248
|
+
else
|
249
|
+
lines << " activate #{model_name}"
|
250
|
+
lines << " Note over #{model_name}: Database query"
|
251
|
+
lines << " deactivate #{model_name}"
|
252
|
+
end
|
253
|
+
|
254
|
+
# モデルからの結果
|
255
|
+
lines << " #{model_name}-->>-#{service_name}: [#{model_name} objects]"
|
256
|
+
end
|
257
|
+
|
258
|
+
# 関連モデルの読み込み
|
259
|
+
if model_edges.any? && @include_database
|
260
|
+
lines << " Note over #{service_name}: Load associations (N+1 prevention)"
|
261
|
+
end
|
262
|
+
|
263
|
+
lines
|
264
|
+
end
|
265
|
+
|
266
|
+
def generate_response_code(action_name)
|
267
|
+
case action_name
|
268
|
+
when 'index', 'show'
|
269
|
+
'200 OK'
|
270
|
+
when 'create'
|
271
|
+
'201 Created'
|
272
|
+
when 'update'
|
273
|
+
'200 OK'
|
274
|
+
when 'destroy'
|
275
|
+
'204 No Content'
|
276
|
+
else
|
277
|
+
'200 OK'
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
def sanitize_name(name)
|
282
|
+
# Mermaidで使用できない文字を置換
|
283
|
+
sanitized = name.to_s.gsub(/[^a-zA-Z0-9_]/, '_').gsub(/^_+|_+$/, '')
|
284
|
+
# 空文字列になった場合はデフォルト値を返す
|
285
|
+
sanitized.empty? ? "node_#{name.hash.abs}" : sanitized
|
286
|
+
end
|
287
|
+
end
|
288
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# RailsFlowMap configuration
|
2
|
+
#
|
3
|
+
# This initializer allows you to configure RailsFlowMap behavior
|
4
|
+
# You can customize the analysis and output options here
|
5
|
+
|
6
|
+
RailsFlowMap.configure do |config|
|
7
|
+
# Directories to exclude from analysis
|
8
|
+
# config.exclude_paths = ['app/models/concerns', 'app/controllers/concerns']
|
9
|
+
|
10
|
+
# Model files pattern (default: app/models/**/*.rb)
|
11
|
+
# config.model_pattern = 'app/models/**/*.rb'
|
12
|
+
|
13
|
+
# Controller files pattern (default: app/controllers/**/*.rb)
|
14
|
+
# config.controller_pattern = 'app/controllers/**/*.rb'
|
15
|
+
|
16
|
+
# Default output format (mermaid, plantuml, graphviz)
|
17
|
+
# config.default_format = :mermaid
|
18
|
+
|
19
|
+
# Default output directory
|
20
|
+
# config.output_directory = 'doc/flow_maps'
|
21
|
+
|
22
|
+
# Include STI (Single Table Inheritance) relationships
|
23
|
+
# config.include_sti = true
|
24
|
+
|
25
|
+
# Include polymorphic relationships
|
26
|
+
# config.include_polymorphic = true
|
27
|
+
|
28
|
+
# Custom node colors (for Mermaid format)
|
29
|
+
# config.node_colors = {
|
30
|
+
# model: '#f9f',
|
31
|
+
# controller: '#bbf',
|
32
|
+
# action: '#bfb'
|
33
|
+
# }
|
34
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'rails/generators/base'
|
2
|
+
|
3
|
+
module RailsFlowMap
|
4
|
+
module Generators
|
5
|
+
class InstallGenerator < Rails::Generators::Base
|
6
|
+
source_root File.expand_path('templates', __dir__)
|
7
|
+
|
8
|
+
desc "Creates a RailsFlowMap initializer"
|
9
|
+
|
10
|
+
def create_initializer_file
|
11
|
+
template "rails_flow_map.rb", "config/initializers/rails_flow_map.rb"
|
12
|
+
end
|
13
|
+
|
14
|
+
def create_output_directory
|
15
|
+
empty_directory "doc/flow_maps"
|
16
|
+
create_file "doc/flow_maps/.gitkeep"
|
17
|
+
end
|
18
|
+
|
19
|
+
def display_post_install_message
|
20
|
+
say "\nRailsFlowMap has been successfully installed!", :green
|
21
|
+
say "\nAvailable rake tasks:"
|
22
|
+
say " rake rails_flow_map:generate # Generate complete flow map"
|
23
|
+
say " rake rails_flow_map:models # Generate model relationships"
|
24
|
+
say " rake rails_flow_map:controllers # Generate controller/action flow"
|
25
|
+
say " rake rails_flow_map:formats # List available formats"
|
26
|
+
say "\nExample usage:"
|
27
|
+
say " rake rails_flow_map:generate[mermaid,doc/flow_maps/app_flow.md]"
|
28
|
+
say "\nFlow maps will be saved in doc/flow_maps/ by default."
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,215 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
module RailsFlowMap
|
6
|
+
# Centralized logging functionality for RailsFlowMap
|
7
|
+
#
|
8
|
+
# This module provides a standardized logging interface across all components
|
9
|
+
# of the RailsFlowMap gem. It supports configurable log levels, structured
|
10
|
+
# logging, and automatic context inclusion.
|
11
|
+
#
|
12
|
+
# @example Basic usage
|
13
|
+
# RailsFlowMap::Logging.logger.info("Processing graph with 10 nodes")
|
14
|
+
#
|
15
|
+
# @example With context
|
16
|
+
# RailsFlowMap::Logging.with_context(formatter: 'MermaidFormatter') do
|
17
|
+
# RailsFlowMap::Logging.logger.debug("Starting format operation")
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# @example Error logging with context
|
21
|
+
# begin
|
22
|
+
# risky_operation
|
23
|
+
# rescue => e
|
24
|
+
# RailsFlowMap::Logging.log_error(e, context: { operation: 'export' })
|
25
|
+
# end
|
26
|
+
module Logging
|
27
|
+
extend self
|
28
|
+
|
29
|
+
# Custom formatter for structured log output
|
30
|
+
class StructuredFormatter < Logger::Formatter
|
31
|
+
def call(severity, time, progname, msg)
|
32
|
+
context = Thread.current[:rails_flow_map_context] || {}
|
33
|
+
context_str = context.empty? ? '' : " [#{context.map { |k, v| "#{k}=#{v}" }.join(', ')}]"
|
34
|
+
|
35
|
+
"[#{time.strftime('%Y-%m-%d %H:%M:%S')}] #{severity}#{context_str}: #{msg}\n"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Custom log levels for domain-specific logging
|
40
|
+
CUSTOM_LEVELS = {
|
41
|
+
security: Logger::WARN,
|
42
|
+
performance: Logger::INFO,
|
43
|
+
graph_analysis: Logger::DEBUG
|
44
|
+
}.freeze
|
45
|
+
|
46
|
+
# @return [Logger] The shared logger instance
|
47
|
+
def logger
|
48
|
+
@logger ||= create_logger
|
49
|
+
end
|
50
|
+
|
51
|
+
# Sets a custom logger instance
|
52
|
+
#
|
53
|
+
# @param custom_logger [Logger] The logger to use
|
54
|
+
def logger=(custom_logger)
|
55
|
+
@logger = custom_logger
|
56
|
+
end
|
57
|
+
|
58
|
+
# Log an error with full context and backtrace
|
59
|
+
#
|
60
|
+
# @param error [Exception] The error to log
|
61
|
+
# @param context [Hash] Additional context information
|
62
|
+
# @param level [Symbol] Log level (:error, :warn, :info, :debug)
|
63
|
+
def log_error(error, context: {}, level: :error)
|
64
|
+
error_context = {
|
65
|
+
error_class: error.class.name,
|
66
|
+
error_message: error.message,
|
67
|
+
**context
|
68
|
+
}
|
69
|
+
|
70
|
+
with_context(error_context) do
|
71
|
+
logger.send(level, "#{error.class}: #{error.message}")
|
72
|
+
|
73
|
+
if error.backtrace && logger.level <= Logger::DEBUG
|
74
|
+
logger.debug("Backtrace:\n#{error.backtrace.join("\n")}")
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Execute a block with additional logging context
|
80
|
+
#
|
81
|
+
# @param context [Hash] Context to add to all log messages in the block
|
82
|
+
# @yield The block to execute with context
|
83
|
+
def with_context(context = {})
|
84
|
+
old_context = Thread.current[:rails_flow_map_context] || {}
|
85
|
+
Thread.current[:rails_flow_map_context] = old_context.merge(context)
|
86
|
+
|
87
|
+
yield
|
88
|
+
ensure
|
89
|
+
Thread.current[:rails_flow_map_context] = old_context
|
90
|
+
end
|
91
|
+
|
92
|
+
# Log a performance metric
|
93
|
+
#
|
94
|
+
# @param operation [String] Name of the operation
|
95
|
+
# @param duration [Float] Duration in seconds
|
96
|
+
# @param additional_metrics [Hash] Additional metrics to log
|
97
|
+
def log_performance(operation, duration, additional_metrics = {})
|
98
|
+
return unless logger.level <= CUSTOM_LEVELS[:performance]
|
99
|
+
|
100
|
+
metrics = {
|
101
|
+
operation: operation,
|
102
|
+
duration_seconds: duration.round(3),
|
103
|
+
**additional_metrics
|
104
|
+
}
|
105
|
+
|
106
|
+
with_context(metrics) do
|
107
|
+
logger.info("Performance: #{operation} completed in #{duration.round(3)}s")
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Log a security event
|
112
|
+
#
|
113
|
+
# @param event [String] Description of the security event
|
114
|
+
# @param severity [Symbol] Severity level (:high, :medium, :low)
|
115
|
+
# @param details [Hash] Additional security-related details
|
116
|
+
def log_security(event, severity: :medium, details: {})
|
117
|
+
return unless logger.level <= CUSTOM_LEVELS[:security]
|
118
|
+
|
119
|
+
security_context = {
|
120
|
+
security_event: event,
|
121
|
+
severity: severity,
|
122
|
+
**details
|
123
|
+
}
|
124
|
+
|
125
|
+
with_context(security_context) do
|
126
|
+
logger.warn("SECURITY: #{event}")
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# Log graph analysis information
|
131
|
+
#
|
132
|
+
# @param analysis_type [String] Type of analysis being performed
|
133
|
+
# @param graph_metrics [Hash] Metrics about the graph being analyzed
|
134
|
+
def log_graph_analysis(analysis_type, graph_metrics = {})
|
135
|
+
return unless logger.level <= CUSTOM_LEVELS[:graph_analysis]
|
136
|
+
|
137
|
+
analysis_context = {
|
138
|
+
analysis_type: analysis_type,
|
139
|
+
**graph_metrics
|
140
|
+
}
|
141
|
+
|
142
|
+
with_context(analysis_context) do
|
143
|
+
logger.debug("Graph Analysis: #{analysis_type}")
|
144
|
+
|
145
|
+
if graph_metrics.any?
|
146
|
+
metrics_str = graph_metrics.map { |k, v| "#{k}=#{v}" }.join(', ')
|
147
|
+
logger.debug("Graph metrics: #{metrics_str}")
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# Time a block and log its performance
|
153
|
+
#
|
154
|
+
# @param operation [String] Name of the operation being timed
|
155
|
+
# @param additional_metrics [Hash] Additional metrics to include
|
156
|
+
# @yield The block to time
|
157
|
+
# @return The result of the block
|
158
|
+
def time_operation(operation, additional_metrics = {})
|
159
|
+
start_time = Time.now
|
160
|
+
|
161
|
+
begin
|
162
|
+
result = yield
|
163
|
+
duration = Time.now - start_time
|
164
|
+
log_performance(operation, duration, additional_metrics)
|
165
|
+
result
|
166
|
+
rescue => e
|
167
|
+
duration = Time.now - start_time
|
168
|
+
log_error(e, context: {
|
169
|
+
operation: operation,
|
170
|
+
duration_seconds: duration.round(3),
|
171
|
+
**additional_metrics
|
172
|
+
})
|
173
|
+
raise
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
# Configure logging for different environments
|
178
|
+
#
|
179
|
+
# @param environment [Symbol] The environment (:development, :test, :production)
|
180
|
+
# @param options [Hash] Additional configuration options
|
181
|
+
def configure_for_environment(environment, options = {})
|
182
|
+
case environment
|
183
|
+
when :development
|
184
|
+
logger.level = Logger::DEBUG
|
185
|
+
logger.formatter = StructuredFormatter.new
|
186
|
+
when :test
|
187
|
+
logger.level = Logger::WARN
|
188
|
+
logger.formatter = Logger::Formatter.new
|
189
|
+
when :production
|
190
|
+
logger.level = Logger::INFO
|
191
|
+
logger.formatter = StructuredFormatter.new
|
192
|
+
end
|
193
|
+
|
194
|
+
# Apply any custom options
|
195
|
+
options.each do |key, value|
|
196
|
+
case key
|
197
|
+
when :level
|
198
|
+
logger.level = value
|
199
|
+
when :formatter
|
200
|
+
logger.formatter = value
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
private
|
206
|
+
|
207
|
+
def create_logger
|
208
|
+
logger = Logger.new(STDOUT)
|
209
|
+
logger.level = Logger::INFO
|
210
|
+
logger.formatter = StructuredFormatter.new
|
211
|
+
logger.progname = 'RailsFlowMap'
|
212
|
+
logger
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module RailsFlowMap
|
2
|
+
class FlowEdge
|
3
|
+
attr_accessor :from, :to, :type, :label, :attributes
|
4
|
+
|
5
|
+
def initialize(from:, to:, type:, label: nil, attributes: {})
|
6
|
+
@from = from
|
7
|
+
@to = to
|
8
|
+
@type = type
|
9
|
+
@label = label
|
10
|
+
@attributes = attributes
|
11
|
+
end
|
12
|
+
|
13
|
+
def association?
|
14
|
+
[:belongs_to, :has_one, :has_many, :has_and_belongs_to_many].include?(type)
|
15
|
+
end
|
16
|
+
|
17
|
+
def action_flow?
|
18
|
+
type == :action_flow
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_h
|
22
|
+
{
|
23
|
+
from: from,
|
24
|
+
to: to,
|
25
|
+
type: type,
|
26
|
+
label: label,
|
27
|
+
attributes: attributes
|
28
|
+
}
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module RailsFlowMap
|
2
|
+
class FlowGraph
|
3
|
+
attr_reader :nodes, :edges
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@nodes = {}
|
7
|
+
@edges = []
|
8
|
+
end
|
9
|
+
|
10
|
+
def add_node(node)
|
11
|
+
@nodes[node.id] = node
|
12
|
+
end
|
13
|
+
|
14
|
+
def add_edge(edge)
|
15
|
+
@edges << edge
|
16
|
+
end
|
17
|
+
|
18
|
+
def find_node(id)
|
19
|
+
@nodes[id]
|
20
|
+
end
|
21
|
+
|
22
|
+
def nodes_by_type(type)
|
23
|
+
@nodes.values.select { |node| node.type == type }
|
24
|
+
end
|
25
|
+
|
26
|
+
def edges_by_type(type)
|
27
|
+
@edges.select { |edge| edge.type == type }
|
28
|
+
end
|
29
|
+
|
30
|
+
def connected_nodes(node_id, direction: :both)
|
31
|
+
case direction
|
32
|
+
when :outgoing
|
33
|
+
@edges.select { |e| e.from == node_id }.map { |e| @nodes[e.to] }.compact
|
34
|
+
when :incoming
|
35
|
+
@edges.select { |e| e.to == node_id }.map { |e| @nodes[e.from] }.compact
|
36
|
+
when :both
|
37
|
+
outgoing = @edges.select { |e| e.from == node_id }.map { |e| @nodes[e.to] }
|
38
|
+
incoming = @edges.select { |e| e.to == node_id }.map { |e| @nodes[e.from] }
|
39
|
+
(outgoing + incoming).compact.uniq
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def node_count
|
44
|
+
@nodes.size
|
45
|
+
end
|
46
|
+
|
47
|
+
def edge_count
|
48
|
+
@edges.size
|
49
|
+
end
|
50
|
+
|
51
|
+
def to_h
|
52
|
+
{
|
53
|
+
nodes: @nodes.transform_values(&:to_h),
|
54
|
+
edges: @edges.map(&:to_h)
|
55
|
+
}
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module RailsFlowMap
|
2
|
+
class FlowNode
|
3
|
+
attr_accessor :id, :name, :type, :attributes, :file_path, :line_number
|
4
|
+
|
5
|
+
def initialize(id:, name:, type:, attributes: {}, file_path: nil, line_number: nil)
|
6
|
+
@id = id
|
7
|
+
@name = name
|
8
|
+
@type = type
|
9
|
+
@attributes = attributes
|
10
|
+
@file_path = file_path
|
11
|
+
@line_number = line_number
|
12
|
+
end
|
13
|
+
|
14
|
+
def model?
|
15
|
+
type == :model
|
16
|
+
end
|
17
|
+
|
18
|
+
def controller?
|
19
|
+
type == :controller
|
20
|
+
end
|
21
|
+
|
22
|
+
def action?
|
23
|
+
type == :action
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_h
|
27
|
+
{
|
28
|
+
id: id,
|
29
|
+
name: name,
|
30
|
+
type: type,
|
31
|
+
attributes: attributes,
|
32
|
+
file_path: file_path,
|
33
|
+
line_number: line_number
|
34
|
+
}
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|