11package events
22
33import (
4- "fmt"
5- "log/slog"
64 "sync"
75 "time"
86)
97
108// CaptureConfig holds caller-supplied capture preferences. All fields are
119// optional; zero values mean "use server defaults" (all categories).
1210type CaptureConfig struct {
13- // Categories limits which event categories are captured
14- // nil represents all categories.
11+ // Categories limits which event categories are captured.
12+ // nil or empty includes all categories.
1513 Categories []EventCategory
1614}
1715
18- // CaptureSession wraps events in envelopes and fans them out to a fileWriter
19- // Reusable: call Start with a new ID to begin a new session; call Stop to end
20- // the current session without closing the underlying writers. Close tears down
21- // file descriptors and should only be called during server shutdown.
16+ // CaptureSession manages a capture session against a shared EventStream.
17+ // It is responsible for (a) category-filtering Publish calls and (b) tracking
18+ // session-scoped metadata (ID, config, timestamps).
2219type CaptureSession struct {
20+ es * EventStream
2321 mu sync.Mutex
24- ring * ringBuffer
25- files * fileWriter
26- seq uint64
27- sessionStartSeq uint64 // seq at the time the current session started
2822 captureSessionID string
23+ sessionStartSeq uint64
2924 categories map [EventCategory ]struct {}
3025 createdAt time.Time
3126}
3227
33- // CaptureSessionConfig holds the parameters for creating a CaptureSession.
34- type CaptureSessionConfig struct {
35- LogDir string
36- // RingCapacity is the number of envelopes the in-memory ring buffer holds.
37- RingCapacity int
38- }
39-
40- func NewCaptureSession (cfg CaptureSessionConfig ) (* CaptureSession , error ) {
41- rb , err := newRingBuffer (cfg .RingCapacity )
42- if err != nil {
43- return nil , fmt .Errorf ("capture session: %w" , err )
44- }
45- fw , err := newFileWriter (cfg .LogDir )
46- if err != nil {
47- return nil , fmt .Errorf ("capture session: %w" , err )
48- }
28+ func NewCaptureSession (es * EventStream ) * CaptureSession {
4929 cats := make (map [EventCategory ]struct {}, len (allCategories ))
5030 for _ , c := range allCategories {
5131 cats [c ] = struct {}{}
5232 }
53- return & CaptureSession {
54- ring : rb ,
55- files : fw ,
56- categories : cats ,
57- }, nil
33+ return & CaptureSession {es : es , categories : cats }
5834}
5935
60- // Start sets the capture session ID and applies the given config. Sequence
36+ // Start begins a new capture session with the given ID and config. Sequence
6137// numbers are process-monotonic and do not reset between sessions; a
62- // Last-Event-ID from any previous session is valid for resuming the stream.
63- // The fileWriter is intentionally not rotated: events from different sessions
64- // are interleaved in the same per-category JSONL files and distinguished by
65- // their envelope's capture_session_id.
38+ // Last-Event-ID from any previous session is valid for resuming the SSE stream.
39+ // Events from different sessions are interleaved in the same per-category JSONL
40+ // files and distinguished by their envelope's captureSessionID.
6641func (s * CaptureSession ) Start (captureSessionID string , cfg CaptureConfig ) {
6742 s .mu .Lock ()
6843 defer s .mu .Unlock ()
6944 s .captureSessionID = captureSessionID
70- s .sessionStartSeq = s .seq
45+ s .sessionStartSeq = s .es . Seq ()
7146 s .createdAt = time .Now ()
7247 cats := cfg .Categories
7348 if len (cats ) == 0 {
@@ -79,54 +54,33 @@ func (s *CaptureSession) Start(captureSessionID string, cfg CaptureConfig) {
7954 }
8055}
8156
82- // publishLocked is the core publish path. Requires s.mu held and a captureSessionID.
57+ // publishLocked builds an envelope and forwards it to the EventStream.
58+ // Requires s.mu to be held.
8359func (s * CaptureSession ) publishLocked (ev Event ) Envelope {
8460 if ev .Ts == 0 {
8561 ev .Ts = time .Now ().UnixMicro ()
8662 }
87- s .seq ++
88- env := Envelope {
63+ return s .es .publish (Envelope {
8964 CaptureSessionID : s .captureSessionID ,
90- Seq : s .seq ,
9165 Event : ev ,
92- }
93- env , data := truncateIfNeeded (env )
94- if data == nil {
95- slog .Error ("capture_session: marshal failed, skipping file write" , "seq" , env .Seq , "category" , env .Event .Category )
96- } else {
97- filename := string (env .Event .Category ) + ".log"
98- if err := s .files .Write (filename , data ); err != nil {
99- slog .Error ("capture_session: file write failed" , "seq" , env .Seq , "category" , env .Event .Category , "err" , err )
100- }
101- }
102- s .ring .publish (env )
103- return env
66+ })
10467}
10568
106- // Publish wraps ev in an Envelope, truncates if needed, then writes to
107- // fileWriter (durable) before RingBuffer (in-memory fan-out).
69+ // Publish applies the category filter then forwards ev to the EventStream.
10870func (s * CaptureSession ) Publish (ev Event ) {
10971 s .mu .Lock ()
11072 defer s .mu .Unlock ()
111-
112- // No active session, drop silently. This can happen when events
113- // arrive between Stop() and producers noticing, or before Start().
11473 if s .captureSessionID == "" {
11574 return
11675 }
117-
118- // Drop events whose category is outside the configured set.
11976 if _ , ok := s .categories [ev .Category ]; ! ok {
12077 return
12178 }
122-
12379 s .publishLocked (ev )
12480}
12581
126- // PublishUnfiltered publishes ev without applying the category filter. Use for
127- // externally-initiated events (e.g. API callers) that must not be silently
128- // dropped by capture preferences set by the session owner.
129- // Returns the assigned Envelope, or a zero Envelope if no session is active.
82+ // PublishUnfiltered forwards ev to the EventStream without applying the category
83+ // filter. Returns the assigned Envelope, or a zero Envelope if no session is active.
13084func (s * CaptureSession ) PublishUnfiltered (ev Event ) Envelope {
13185 s .mu .Lock ()
13286 defer s .mu .Unlock ()
@@ -136,9 +90,9 @@ func (s *CaptureSession) PublishUnfiltered(ev Event) Envelope {
13690 return s .publishLocked (ev )
13791}
13892
139- // NewReader returns a Reader positioned at the start of the ring buffer .
93+ // NewReader returns a Reader from the EventStream positioned after afterSeq .
14094func (s * CaptureSession ) NewReader (afterSeq uint64 ) * Reader {
141- return s .ring . newReader (afterSeq )
95+ return s .es . NewReader (afterSeq )
14296}
14397
14498// ID returns the current capture session ID, or "" if no session is active.
@@ -148,16 +102,13 @@ func (s *CaptureSession) ID() string {
148102 return s .captureSessionID
149103}
150104
151- // Seq returns the current sequence number ( last published) .
105+ // Seq returns the sequence number of the last published event .
152106func (s * CaptureSession ) Seq () uint64 {
153- s .mu .Lock ()
154- defer s .mu .Unlock ()
155- return s .seq
107+ return s .es .Seq ()
156108}
157109
158110// SessionStartSeq returns the sequence number at which the current session
159- // started. Fresh SSE connections with no Last-Event-ID should begin here so
160- // they see only the current session's events.
111+ // started. Fresh SSE connections with no Last-Event-ID should begin here.
161112func (s * CaptureSession ) SessionStartSeq () uint64 {
162113 s .mu .Lock ()
163114 defer s .mu .Unlock ()
@@ -172,9 +123,7 @@ func (s *CaptureSession) Config() CaptureConfig {
172123 for c := range s .categories {
173124 cats = append (cats , c )
174125 }
175- return CaptureConfig {
176- Categories : cats ,
177- }
126+ return CaptureConfig {Categories : cats }
178127}
179128
180129// CreatedAt returns when the current session was started.
@@ -184,8 +133,7 @@ func (s *CaptureSession) CreatedAt() time.Time {
184133 return s .createdAt
185134}
186135
187- // UpdateConfig applies a new CaptureConfig to the running session without
188- // resetting the sequence counter or ring buffer.
136+ // UpdateConfig applies a new CaptureConfig to the running session.
189137func (s * CaptureSession ) UpdateConfig (cfg CaptureConfig ) {
190138 s .mu .Lock ()
191139 defer s .mu .Unlock ()
@@ -206,11 +154,9 @@ func (s *CaptureSession) Active() bool {
206154 return s .captureSessionID != ""
207155}
208156
209- // Stop ends the current session. It publishes a synthetic session_ended
210- // envelope so open SSE stream connections receive a terminal frame and can
211- // close cleanly, then clears the session ID. The ring buffer is intentionally
212- // left intact so existing readers can finish draining. A new session can be
213- // started by calling Start again.
157+ // Stop ends the current session by publishing a synthetic session_ended event,
158+ // then clears the session ID. The ring buffer is left intact so existing readers
159+ // can finish draining.
214160func (s * CaptureSession ) Stop () {
215161 s .mu .Lock ()
216162 defer s .mu .Unlock ()
@@ -225,7 +171,7 @@ func (s *CaptureSession) Stop() {
225171 s .captureSessionID = ""
226172}
227173
228- // Close flushes and releases all open file descriptors .
174+ // Close releases resources held by the EventStream .
229175func (s * CaptureSession ) Close () error {
230- return s .files .Close ()
176+ return s .es .Close ()
231177}
0 commit comments