event_system 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/.rspec +3 -0
- data/README.md +362 -0
- data/Rakefile +4 -0
- data/docs/event_visualizations/example_timeline.html +142 -0
- data/docs/example_logs/events_20251027_151744.jsonl +3 -0
- data/docs/example_logs/events_20251027_151802.jsonl +3 -0
- data/docs/examples/basic_usage.rb +64 -0
- data/exe/event_system +139 -0
- data/lib/event_system/configuration.rb +125 -0
- data/lib/event_system/event.rb +139 -0
- data/lib/event_system/event_manager.rb +191 -0
- data/lib/event_system/event_subscriber.rb +29 -0
- data/lib/event_system/storage/base.rb +64 -0
- data/lib/event_system/storage/file_store.rb +187 -0
- data/lib/event_system/storage/memory_store.rb +134 -0
- data/lib/event_system/version.rb +5 -0
- data/lib/event_system/visualization/timeline_generator.rb +237 -0
- data/lib/event_system.rb +37 -0
- data/spec/event_system/configuration_spec.rb +197 -0
- data/spec/event_system/event_manager_spec.rb +341 -0
- data/spec/event_system/event_spec.rb +193 -0
- data/spec/event_system/event_subscriber_spec.rb +295 -0
- data/spec/event_system/storage/file_store_spec.rb +341 -0
- data/spec/event_system/storage/memory_store_spec.rb +248 -0
- data/spec/event_system/visualization/timeline_generator_spec.rb +252 -0
- data/spec/event_system_spec.rb +57 -0
- data/spec/integration/readme_examples_spec.rb +447 -0
- data/spec/spec_helper.rb +80 -0
- metadata +171 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'json'
|
|
5
|
+
require_relative 'base'
|
|
6
|
+
|
|
7
|
+
module EventSystem
|
|
8
|
+
module Storage
|
|
9
|
+
# File-based event storage implementation
|
|
10
|
+
# Events are stored in JSONL (JSON Lines) format, one event per line
|
|
11
|
+
class FileStore < Base
|
|
12
|
+
attr_reader :current_session, :storage_path
|
|
13
|
+
|
|
14
|
+
# Initialize a new file-based event store
|
|
15
|
+
# @param directory [String] The directory to store event files in
|
|
16
|
+
# @param session_id [String, nil] Optional session ID, defaults to timestamp
|
|
17
|
+
def initialize(directory = "event_logs", session_id = nil)
|
|
18
|
+
@directory = directory
|
|
19
|
+
@storage_path = directory
|
|
20
|
+
@current_session = session_id || Time.now.strftime("%Y%m%d_%H%M%S")
|
|
21
|
+
@current_file = nil
|
|
22
|
+
|
|
23
|
+
FileUtils.mkdir_p(@directory) unless Dir.exist?(@directory)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Store an event to disk
|
|
27
|
+
# @param event [EventSystem::Event] The event to store
|
|
28
|
+
# @return [void]
|
|
29
|
+
def store(event)
|
|
30
|
+
ensure_file_open
|
|
31
|
+
@current_file.puts(event.to_json)
|
|
32
|
+
@current_file.flush # Ensure data is written immediately
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Query for events based on options
|
|
36
|
+
# This implementation loads the session and filters in memory
|
|
37
|
+
# For more advanced querying needs, consider using a database
|
|
38
|
+
# @param options [Hash] Query options
|
|
39
|
+
# - type: [String] Filter by event type
|
|
40
|
+
# - start_time: [Time] Filter events after this time
|
|
41
|
+
# - end_time: [Time] Filter events before this time
|
|
42
|
+
# - session_id: [String] Filter by session ID
|
|
43
|
+
# - limit: [Integer] Limit number of results
|
|
44
|
+
# @return [Array<EventSystem::Event>] Matching events
|
|
45
|
+
def query(options = {})
|
|
46
|
+
session_id = options[:session_id] || @current_session
|
|
47
|
+
events = load_session(session_id)
|
|
48
|
+
|
|
49
|
+
# Filter by type
|
|
50
|
+
if options[:type]
|
|
51
|
+
events = events.select { |e| e.type == options[:type] }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Filter by time range
|
|
55
|
+
if options[:start_time]
|
|
56
|
+
events = events.select { |e| e.timestamp >= options[:start_time] }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
if options[:end_time]
|
|
60
|
+
events = events.select { |e| e.timestamp <= options[:end_time] }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Sort by timestamp (oldest first)
|
|
64
|
+
events = events.sort_by(&:timestamp)
|
|
65
|
+
|
|
66
|
+
# Limit results
|
|
67
|
+
if options[:limit]
|
|
68
|
+
events = events.last(options[:limit])
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
events
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Load all events from a session
|
|
75
|
+
# @param session_id [String, nil] Session ID to load, or current session if nil
|
|
76
|
+
# @return [Array<EventSystem::Event>] Events from the session
|
|
77
|
+
def load_session(session_id = nil)
|
|
78
|
+
session_id ||= @current_session
|
|
79
|
+
events = []
|
|
80
|
+
|
|
81
|
+
# Try different filename patterns for backward compatibility
|
|
82
|
+
filename = find_session_file(session_id)
|
|
83
|
+
return [] unless filename && File.exist?(filename)
|
|
84
|
+
|
|
85
|
+
File.open(filename, "r") do |file|
|
|
86
|
+
file.each_line do |line|
|
|
87
|
+
next if line.strip.empty?
|
|
88
|
+
|
|
89
|
+
begin
|
|
90
|
+
events << EventSystem::Event.from_json(line)
|
|
91
|
+
rescue JSON::ParserError => e
|
|
92
|
+
# Skip malformed lines but log the error
|
|
93
|
+
warn "Skipping malformed event line: #{e.message}"
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
events
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# List available sessions
|
|
102
|
+
# @return [Array<String>] List of session IDs
|
|
103
|
+
def list_sessions
|
|
104
|
+
Dir.glob(File.join(@directory, "events_*.jsonl")).map do |file|
|
|
105
|
+
File.basename(file).gsub(/^events_/, "").gsub(/\.jsonl$/, "")
|
|
106
|
+
end.sort
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Get the current session ID
|
|
110
|
+
# @return [String] Current session ID
|
|
111
|
+
def current_session
|
|
112
|
+
@current_session
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Switch to a different session
|
|
116
|
+
# @param session_id [String] The session ID to switch to
|
|
117
|
+
# @return [void]
|
|
118
|
+
def switch_session(session_id)
|
|
119
|
+
close_current_file
|
|
120
|
+
@current_session = session_id
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Create a new session
|
|
124
|
+
# @param session_id [String, nil] Optional session ID, defaults to timestamp
|
|
125
|
+
# @return [String] The new session ID
|
|
126
|
+
def create_session(session_id = nil)
|
|
127
|
+
close_current_file
|
|
128
|
+
@current_session = session_id || Time.now.strftime("%Y%m%d_%H%M%S")
|
|
129
|
+
@current_session
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Close the file handle
|
|
133
|
+
# @return [void]
|
|
134
|
+
def close
|
|
135
|
+
close_current_file
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Get storage statistics
|
|
139
|
+
# @return [Hash] Storage statistics
|
|
140
|
+
def stats
|
|
141
|
+
super.merge(
|
|
142
|
+
directory: @directory,
|
|
143
|
+
current_session_file: current_session_file,
|
|
144
|
+
total_sessions: list_sessions.length
|
|
145
|
+
)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
private
|
|
149
|
+
|
|
150
|
+
# Ensure the file is open for writing
|
|
151
|
+
# @return [void]
|
|
152
|
+
def ensure_file_open
|
|
153
|
+
return if @current_file && !@current_file.closed?
|
|
154
|
+
|
|
155
|
+
@current_file = File.open(current_session_file, "a")
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Get the current session file path
|
|
159
|
+
# @return [String] Path to the current session file
|
|
160
|
+
def current_session_file
|
|
161
|
+
File.join(@directory, "events_#{@current_session}.jsonl")
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Close the current file if it's open
|
|
165
|
+
# @return [void]
|
|
166
|
+
def close_current_file
|
|
167
|
+
@current_file&.close
|
|
168
|
+
@current_file = nil
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Find the session file using different naming patterns
|
|
172
|
+
# @param session_id [String] The session ID to find
|
|
173
|
+
# @return [String, nil] Path to the session file or nil if not found
|
|
174
|
+
def find_session_file(session_id)
|
|
175
|
+
# Try with events_ prefix first
|
|
176
|
+
filename = File.join(@directory, "events_#{session_id}.jsonl")
|
|
177
|
+
return filename if File.exist?(filename)
|
|
178
|
+
|
|
179
|
+
# Try without events_ prefix for backward compatibility
|
|
180
|
+
filename = File.join(@directory, "#{session_id}.jsonl")
|
|
181
|
+
return filename if File.exist?(filename)
|
|
182
|
+
|
|
183
|
+
nil
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module EventSystem
|
|
6
|
+
module Storage
|
|
7
|
+
# In-memory event storage implementation
|
|
8
|
+
# Perfect for testing, development, or short-lived applications
|
|
9
|
+
class MemoryStore < Base
|
|
10
|
+
attr_reader :current_session
|
|
11
|
+
|
|
12
|
+
# Initialize a new memory-based event store
|
|
13
|
+
# @param session_id [String, nil] Optional session ID, defaults to timestamp
|
|
14
|
+
def initialize(session_id = nil)
|
|
15
|
+
@events = []
|
|
16
|
+
@current_session = session_id || Time.now.strftime("%Y%m%d_%H%M%S")
|
|
17
|
+
@sessions = { @current_session => [] }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Store an event in memory
|
|
21
|
+
# @param event [EventSystem::Event] The event to store
|
|
22
|
+
# @return [void]
|
|
23
|
+
def store(event)
|
|
24
|
+
@events << event
|
|
25
|
+
@sessions[@current_session] << event
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Query for events based on options
|
|
29
|
+
# @param options [Hash] Query options
|
|
30
|
+
# - type: [String] Filter by event type
|
|
31
|
+
# - start_time: [Time] Filter events after this time
|
|
32
|
+
# - end_time: [Time] Filter events before this time
|
|
33
|
+
# - session_id: [String] Filter by session ID
|
|
34
|
+
# - limit: [Integer] Limit number of results
|
|
35
|
+
# @return [Array<EventSystem::Event>] Matching events
|
|
36
|
+
def query(options = {})
|
|
37
|
+
# Use session-specific events by default, or global events if no session specified
|
|
38
|
+
if options[:session_id]
|
|
39
|
+
events = @sessions[options[:session_id]] || []
|
|
40
|
+
else
|
|
41
|
+
# For backward compatibility, use global events when no session specified
|
|
42
|
+
events = @events.dup
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Filter by type
|
|
46
|
+
if options[:type]
|
|
47
|
+
events = events.select { |e| e.type == options[:type] }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Filter by time range
|
|
51
|
+
if options[:start_time]
|
|
52
|
+
events = events.select { |e| e.timestamp >= options[:start_time] }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
if options[:end_time]
|
|
56
|
+
events = events.select { |e| e.timestamp <= options[:end_time] }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Sort by timestamp (oldest first)
|
|
60
|
+
events = events.sort_by(&:timestamp)
|
|
61
|
+
|
|
62
|
+
# Limit results
|
|
63
|
+
if options[:limit]
|
|
64
|
+
events = events.last(options[:limit])
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
events
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Load all events from a session
|
|
71
|
+
# @param session_id [String, nil] Session ID to load, or current session if nil
|
|
72
|
+
# @return [Array<EventSystem::Event>] Events from the session
|
|
73
|
+
def load_session(session_id = nil)
|
|
74
|
+
session_id ||= @current_session
|
|
75
|
+
@sessions[session_id] || []
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# List available sessions
|
|
79
|
+
# @return [Array<String>] List of session IDs
|
|
80
|
+
def list_sessions
|
|
81
|
+
@sessions.keys.sort
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Get the current session ID
|
|
85
|
+
# @return [String] Current session ID
|
|
86
|
+
def current_session
|
|
87
|
+
@current_session
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Switch to a different session
|
|
91
|
+
# @param session_id [String] The session ID to switch to
|
|
92
|
+
# @return [void]
|
|
93
|
+
def switch_session(session_id)
|
|
94
|
+
@current_session = session_id
|
|
95
|
+
@sessions[session_id] ||= []
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Create a new session
|
|
99
|
+
# @param session_id [String, nil] Optional session ID, defaults to timestamp
|
|
100
|
+
# @return [String] The new session ID
|
|
101
|
+
def create_session(session_id = nil)
|
|
102
|
+
session_id ||= Time.now.strftime("%Y%m%d_%H%M%S")
|
|
103
|
+
@sessions[session_id] = []
|
|
104
|
+
@current_session = session_id
|
|
105
|
+
session_id
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Clear all events from memory
|
|
109
|
+
# @return [void]
|
|
110
|
+
def clear!
|
|
111
|
+
@events.clear
|
|
112
|
+
@sessions.clear
|
|
113
|
+
@current_session = Time.now.strftime("%Y%m%d_%H%M%S")
|
|
114
|
+
@sessions[@current_session] = []
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Get total number of events stored
|
|
118
|
+
# @return [Integer] Total event count
|
|
119
|
+
def size
|
|
120
|
+
@events.length
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Get storage statistics
|
|
124
|
+
# @return [Hash] Storage statistics
|
|
125
|
+
def stats
|
|
126
|
+
super.merge(
|
|
127
|
+
total_events: @events.length,
|
|
128
|
+
sessions: @sessions.keys.length,
|
|
129
|
+
current_session_events: @sessions[@current_session]&.length || 0
|
|
130
|
+
)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'time'
|
|
6
|
+
|
|
7
|
+
module EventSystem
|
|
8
|
+
module Visualization
|
|
9
|
+
# A utility class for generating HTML timeline visualizations of events
|
|
10
|
+
class TimelineGenerator
|
|
11
|
+
attr_reader :storage
|
|
12
|
+
|
|
13
|
+
# Initialize a new timeline generator
|
|
14
|
+
# @param storage [EventSystem::Storage::Base] The storage to read events from
|
|
15
|
+
def initialize(storage)
|
|
16
|
+
@storage = storage
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Generates HTML timeline visualization for a session
|
|
20
|
+
# @param session_id [String, nil] The session ID to visualize, or latest if nil
|
|
21
|
+
# @param output_file [String, nil] The HTML file path to write, or auto-generated if nil
|
|
22
|
+
# @return [String, nil] The path to the generated HTML file, or nil if no events
|
|
23
|
+
def generate_timeline(session_id = nil, output_file = nil)
|
|
24
|
+
session_id ||= latest_session_id
|
|
25
|
+
output_file ||= "event_timeline_#{session_id}.html"
|
|
26
|
+
|
|
27
|
+
events = @storage.load_session(session_id)
|
|
28
|
+
return nil if events.empty?
|
|
29
|
+
|
|
30
|
+
html = generate_html(events, session_id)
|
|
31
|
+
|
|
32
|
+
FileUtils.mkdir_p('event_visualizations')
|
|
33
|
+
output_path = File.join('event_visualizations', output_file)
|
|
34
|
+
File.write(output_path, html)
|
|
35
|
+
|
|
36
|
+
output_path
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Find the most recent session ID
|
|
40
|
+
# @return [String, nil] The latest session ID, or nil if no sessions
|
|
41
|
+
def latest_session_id
|
|
42
|
+
sessions = @storage.list_sessions
|
|
43
|
+
return nil if sessions.empty?
|
|
44
|
+
|
|
45
|
+
sessions.max
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Generate a summary of events by type
|
|
49
|
+
# @param session_id [String, nil] The session ID to analyze, or latest if nil
|
|
50
|
+
# @return [Hash] Summary statistics by event type
|
|
51
|
+
def event_summary(session_id = nil)
|
|
52
|
+
session_id ||= latest_session_id
|
|
53
|
+
events = @storage.load_session(session_id)
|
|
54
|
+
|
|
55
|
+
summary = Hash.new(0)
|
|
56
|
+
events.each do |event|
|
|
57
|
+
summary[event.type] += 1
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
summary
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Generate a timeline data structure (for custom visualizations)
|
|
64
|
+
# @param session_id [String, nil] The session ID to analyze, or latest if nil
|
|
65
|
+
# @return [Array<Hash>] Timeline data structure
|
|
66
|
+
def timeline_data(session_id = nil)
|
|
67
|
+
session_id ||= latest_session_id
|
|
68
|
+
events = @storage.load_session(session_id)
|
|
69
|
+
|
|
70
|
+
events.map do |event|
|
|
71
|
+
{
|
|
72
|
+
id: event.id,
|
|
73
|
+
type: event.type,
|
|
74
|
+
timestamp: event.timestamp.iso8601(3),
|
|
75
|
+
source: event.source_to_string,
|
|
76
|
+
data: event.data,
|
|
77
|
+
duration: calculate_duration(event, events)
|
|
78
|
+
}
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
# Generate HTML visualization
|
|
85
|
+
# @param events [Array<EventSystem::Event>] The events to visualize
|
|
86
|
+
# @param session_id [String] The session ID
|
|
87
|
+
# @return [String] HTML content
|
|
88
|
+
def generate_html(events, session_id)
|
|
89
|
+
timeline_data = events.map do |event|
|
|
90
|
+
{
|
|
91
|
+
id: event.id,
|
|
92
|
+
type: event.type,
|
|
93
|
+
timestamp: event.timestamp.iso8601(3),
|
|
94
|
+
source: event.source_to_string,
|
|
95
|
+
data: event.data
|
|
96
|
+
}
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
<<~HTML
|
|
100
|
+
<!DOCTYPE html>
|
|
101
|
+
<html lang="en">
|
|
102
|
+
<head>
|
|
103
|
+
<meta charset="UTF-8">
|
|
104
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
105
|
+
<title>Event Timeline - #{session_id}</title>
|
|
106
|
+
<style>
|
|
107
|
+
body { font-family: Arial, sans-serif; margin: 20px; background-color: #f5f5f5; }
|
|
108
|
+
.container { max-width: 1200px; margin: 0 auto; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
|
|
109
|
+
.header { text-align: center; margin-bottom: 30px; }
|
|
110
|
+
.stats { display: flex; justify-content: space-around; margin-bottom: 30px; padding: 20px; background: #f8f9fa; border-radius: 5px; }
|
|
111
|
+
.stat { text-align: center; }
|
|
112
|
+
.stat-number { font-size: 2em; font-weight: bold; color: #007bff; }
|
|
113
|
+
.stat-label { color: #666; }
|
|
114
|
+
.timeline { position: relative; }
|
|
115
|
+
.timeline-item { margin: 20px 0; padding: 15px; border-left: 4px solid #007bff; background: #f8f9fa; border-radius: 0 5px 5px 0; }
|
|
116
|
+
.timeline-item:hover { background: #e9ecef; }
|
|
117
|
+
.event-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
|
|
118
|
+
.event-type { font-weight: bold; color: #007bff; font-size: 1.1em; }
|
|
119
|
+
.event-time { color: #666; font-size: 0.9em; }
|
|
120
|
+
.event-source { color: #28a745; font-weight: 500; }
|
|
121
|
+
.event-data { background: white; padding: 10px; border-radius: 3px; margin-top: 10px; font-family: monospace; font-size: 0.9em; }
|
|
122
|
+
.event-id { color: #6c757d; font-size: 0.8em; }
|
|
123
|
+
.filter-controls { margin-bottom: 20px; padding: 15px; background: #e9ecef; border-radius: 5px; }
|
|
124
|
+
.filter-controls input, .filter-controls select { margin: 0 10px; padding: 5px; }
|
|
125
|
+
.no-events { text-align: center; color: #666; font-style: italic; padding: 40px; }
|
|
126
|
+
</style>
|
|
127
|
+
</head>
|
|
128
|
+
<body>
|
|
129
|
+
<div class="container">
|
|
130
|
+
<div class="header">
|
|
131
|
+
<h1>Event Timeline</h1>
|
|
132
|
+
<p>Session: #{session_id}</p>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
<div class="stats">
|
|
136
|
+
<div class="stat">
|
|
137
|
+
<div class="stat-number">#{events.length}</div>
|
|
138
|
+
<div class="stat-label">Total Events</div>
|
|
139
|
+
</div>
|
|
140
|
+
<div class="stat">
|
|
141
|
+
<div class="stat-number">#{events.map(&:type).uniq.length}</div>
|
|
142
|
+
<div class="stat-label">Event Types</div>
|
|
143
|
+
</div>
|
|
144
|
+
<div class="stat">
|
|
145
|
+
<div class="stat-number">#{events.map(&:source).uniq.length}</div>
|
|
146
|
+
<div class="stat-label">Sources</div>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<div class="filter-controls">
|
|
151
|
+
<label>Filter by type:</label>
|
|
152
|
+
<select id="typeFilter">
|
|
153
|
+
<option value="">All Types</option>
|
|
154
|
+
#{events.map(&:type).uniq.map { |type| "<option value=\"#{type}\">#{type}</option>" }.join}
|
|
155
|
+
</select>
|
|
156
|
+
<label>Search:</label>
|
|
157
|
+
<input type="text" id="searchFilter" placeholder="Search events...">
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
<div class="timeline" id="timeline">
|
|
161
|
+
#{events.empty? ? '<div class="no-events">No events found</div>' : events.map { |event| generate_event_html(event) }.join}
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
<script>
|
|
166
|
+
const events = #{timeline_data.to_json};
|
|
167
|
+
|
|
168
|
+
function filterEvents() {
|
|
169
|
+
const typeFilter = document.getElementById('typeFilter').value;
|
|
170
|
+
const searchFilter = document.getElementById('searchFilter').value.toLowerCase();
|
|
171
|
+
const timeline = document.getElementById('timeline');
|
|
172
|
+
|
|
173
|
+
const filteredEvents = events.filter(event => {
|
|
174
|
+
const typeMatch = !typeFilter || event.type === typeFilter;
|
|
175
|
+
const searchMatch = !searchFilter ||
|
|
176
|
+
event.type.toLowerCase().includes(searchFilter) ||
|
|
177
|
+
event.source.toLowerCase().includes(searchFilter) ||
|
|
178
|
+
JSON.stringify(event.data).toLowerCase().includes(searchFilter);
|
|
179
|
+
return typeMatch && searchMatch;
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
timeline.innerHTML = filteredEvents.length === 0 ?
|
|
183
|
+
'<div class="no-events">No events match the current filters</div>' :
|
|
184
|
+
filteredEvents.map(event => generateEventHTML(event)).join('');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function generateEventHTML(event) {
|
|
188
|
+
return `
|
|
189
|
+
<div class="timeline-item">
|
|
190
|
+
<div class="event-header">
|
|
191
|
+
<span class="event-type">${event.type}</span>
|
|
192
|
+
<span class="event-time">${new Date(event.timestamp).toLocaleString()}</span>
|
|
193
|
+
</div>
|
|
194
|
+
<div class="event-source">Source: ${event.source}</div>
|
|
195
|
+
<div class="event-data">${JSON.stringify(event.data, null, 2)}</div>
|
|
196
|
+
<div class="event-id">ID: ${event.id}</div>
|
|
197
|
+
</div>
|
|
198
|
+
`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
document.getElementById('typeFilter').addEventListener('change', filterEvents);
|
|
202
|
+
document.getElementById('searchFilter').addEventListener('input', filterEvents);
|
|
203
|
+
</script>
|
|
204
|
+
</body>
|
|
205
|
+
</html>
|
|
206
|
+
HTML
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Generate HTML for a single event
|
|
210
|
+
# @param event [EventSystem::Event] The event to generate HTML for
|
|
211
|
+
# @return [String] HTML for the event
|
|
212
|
+
def generate_event_html(event)
|
|
213
|
+
<<~HTML
|
|
214
|
+
<div class="timeline-item">
|
|
215
|
+
<div class="event-header">
|
|
216
|
+
<span class="event-type">#{event.type}</span>
|
|
217
|
+
<span class="event-time">#{event.timestamp.strftime('%Y-%m-%d %H:%M:%S.%3N')}</span>
|
|
218
|
+
</div>
|
|
219
|
+
<div class="event-source">Source: #{event.source_to_string}</div>
|
|
220
|
+
<div class="event-data">#{JSON.pretty_generate(event.data)}</div>
|
|
221
|
+
<div class="event-id">ID: #{event.id}</div>
|
|
222
|
+
</div>
|
|
223
|
+
HTML
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Calculate duration between events (for timeline visualization)
|
|
227
|
+
# @param event [EventSystem::Event] The current event
|
|
228
|
+
# @param all_events [Array<EventSystem::Event>] All events for context
|
|
229
|
+
# @return [Float, nil] Duration in seconds, or nil if not applicable
|
|
230
|
+
def calculate_duration(event, all_events)
|
|
231
|
+
# This is a placeholder for more sophisticated duration calculation
|
|
232
|
+
# Could be enhanced to calculate time between related events
|
|
233
|
+
nil
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
data/lib/event_system.rb
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "event_system/version"
|
|
4
|
+
require_relative "event_system/event"
|
|
5
|
+
require_relative "event_system/event_subscriber"
|
|
6
|
+
require_relative "event_system/configuration"
|
|
7
|
+
require_relative "event_system/event_manager"
|
|
8
|
+
require_relative "event_system/storage/base"
|
|
9
|
+
require_relative "event_system/storage/memory_store"
|
|
10
|
+
require_relative "event_system/storage/file_store"
|
|
11
|
+
require_relative "event_system/visualization/timeline_generator"
|
|
12
|
+
|
|
13
|
+
module EventSystem
|
|
14
|
+
class Error < StandardError; end
|
|
15
|
+
|
|
16
|
+
# Create a new event manager with default configuration
|
|
17
|
+
# @param config [Hash, EventSystem::Configuration, nil] Configuration options
|
|
18
|
+
# @return [EventSystem::EventManager] A new event manager instance
|
|
19
|
+
def self.create_manager(config = nil)
|
|
20
|
+
EventManager.new(config)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Create a new event
|
|
24
|
+
# @param type [String, Symbol] The event type
|
|
25
|
+
# @param source [Object] The event source
|
|
26
|
+
# @param data [Hash] Event data
|
|
27
|
+
# @return [EventSystem::Event] A new event instance
|
|
28
|
+
def self.create_event(type, source = nil, data = {})
|
|
29
|
+
Event.new(type, source, data)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Get the current version
|
|
33
|
+
# @return [String] The current version
|
|
34
|
+
def self.version
|
|
35
|
+
VERSION
|
|
36
|
+
end
|
|
37
|
+
end
|