@@ -12,6 +12,7 @@ import (
1212 "time"
1313
1414 "github.com/kernel/kernel-images/server/lib/events"
15+ "github.com/kernel/kernel-images/server/lib/logger"
1516 oapi "github.com/kernel/kernel-images/server/lib/oapi"
1617)
1718
@@ -123,6 +124,115 @@ func (s *ApiService) StreamTelemetryEvents(ctx context.Context, req oapi.StreamT
123124 return oapi.StreamTelemetryEvents200TexteventStreamResponse {Body : pr , Headers : headers }, nil
124125}
125126
127+ // defaultReadWindow bounds a read that supplies no since/until.
128+ const defaultReadWindow = 5 * time .Minute
129+
130+ // maxReadLimit caps the number of envelopes a single read returns.
131+ const maxReadLimit = 1000
132+
133+ // ReadTelemetryEvents handles GET /telemetry/events.
134+ // Reads archived telemetry envelopes for the current session from durable S2
135+ // storage, applies category and limit filters, and returns them in ascending
136+ // sequence order. Returns an empty list when S2 storage is not configured.
137+ func (s * ApiService ) ReadTelemetryEvents (ctx context.Context , req oapi.ReadTelemetryEventsRequestObject ) (oapi.ReadTelemetryEventsResponseObject , error ) {
138+ log := logger .FromContext (ctx )
139+
140+ if ! s .s2Enabled () {
141+ return readTelemetryEventsOKResponse {}, nil
142+ }
143+
144+ startSeq := s .telemetrySession .SessionStartSeq ()
145+ envs , err := events .Read (ctx , s .s2Basin , s .s2AccessToken , s .s2Stream , buildReadOptions (req .Params ), log )
146+ if err != nil {
147+ log .Error ("failed to read telemetry events from S2" , "err" , err )
148+ return oapi.ReadTelemetryEvents500JSONResponse {InternalErrorJSONResponse : oapi.InternalErrorJSONResponse {Message : "failed to read telemetry events" }}, nil
149+ }
150+
151+ envs = dropPriorSessions (envs , startSeq )
152+ envs = filterByCategory (envs , req .Params .Category )
153+ envs = capLimit (envs , req .Params .Limit )
154+
155+ return readTelemetryEventsOKResponse {envs : envs }, nil
156+ }
157+
158+ // buildReadOptions maps query params to a bounded read window. since/until are
159+ // the start/end of the window; the window defaults to the last defaultReadWindow.
160+ // limit is applied after category filtering, not pushed into the S2 read.
161+ func buildReadOptions (p oapi.ReadTelemetryEventsParams ) events.ReadOptions {
162+ var opts events.ReadOptions
163+ if p .Since != nil {
164+ start := uint64 (* p .Since )
165+ opts .Timestamp = & start
166+ } else {
167+ start := uint64 (time .Now ().Add (- defaultReadWindow ).UnixMilli ())
168+ opts .Timestamp = & start
169+ }
170+ if p .Until != nil {
171+ until := uint64 (* p .Until )
172+ opts .Until = & until
173+ }
174+ return opts
175+ }
176+
177+ // dropPriorSessions removes envelopes from before the current session's start.
178+ // startSeq is 0 when no session has run, in which case nothing is dropped.
179+ func dropPriorSessions (envs []events.Envelope , startSeq uint64 ) []events.Envelope {
180+ if startSeq == 0 {
181+ return envs
182+ }
183+ out := make ([]events.Envelope , 0 , len (envs ))
184+ for _ , e := range envs {
185+ if e .Seq >= startSeq {
186+ out = append (out , e )
187+ }
188+ }
189+ return out
190+ }
191+
192+ func filterByCategory (envs []events.Envelope , cats * []oapi.TelemetryEventCategory ) []events.Envelope {
193+ if cats == nil || len (* cats ) == 0 {
194+ return envs
195+ }
196+ want := make (map [oapi.TelemetryEventCategory ]struct {}, len (* cats ))
197+ for _ , c := range * cats {
198+ want [c ] = struct {}{}
199+ }
200+ out := make ([]events.Envelope , 0 , len (envs ))
201+ for _ , e := range envs {
202+ if _ , ok := want [e .Event .Category ]; ok {
203+ out = append (out , e )
204+ }
205+ }
206+ return out
207+ }
208+
209+ func capLimit (envs []events.Envelope , limit * int ) []events.Envelope {
210+ n := maxReadLimit
211+ if limit != nil && * limit > 0 && * limit < n {
212+ n = * limit
213+ }
214+ if len (envs ) > n {
215+ return envs [:n ]
216+ }
217+ return envs
218+ }
219+
220+ // readTelemetryEventsOKResponse serializes events.Envelope directly so the
221+ // response shape matches the SSE stream frames and the publish endpoint.
222+ type readTelemetryEventsOKResponse struct { envs []events.Envelope }
223+
224+ func (r readTelemetryEventsOKResponse ) VisitReadTelemetryEventsResponse (w http.ResponseWriter ) error {
225+ w .Header ().Set ("Content-Type" , "application/json" )
226+ w .WriteHeader (http .StatusOK )
227+ envs := r .envs
228+ if envs == nil {
229+ envs = []events.Envelope {}
230+ }
231+ return json .NewEncoder (w ).Encode (struct {
232+ Events []events.Envelope `json:"events"`
233+ }{Events : envs })
234+ }
235+
126236// publishTelemetryEventOKResponse serializes events.Envelope directly so the response
127237// is identical in shape to the SSE stream frames.
128238type publishTelemetryEventOKResponse struct { env events.Envelope }
0 commit comments