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.
@@ -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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EventSystem
4
+ VERSION = "0.1.0"
5
+ 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
@@ -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