@@ -2,6 +2,7 @@ package stats
22
33import (
44 "context"
5+ "fmt"
56 "sort"
67 "time"
78
@@ -33,6 +34,11 @@ type CommandStat struct {
3334 FailCount int
3435}
3536
37+ type TimelineBucket struct {
38+ Label string
39+ Count int
40+ }
41+
3642type StatsData struct {
3743 TotalEvents int
3844 TotalSessions int
@@ -48,7 +54,7 @@ type StatsData struct {
4854
4955 Agents []AgentStat
5056
51- HourlyBuckets [ 24 ] int
57+ TimelineBuckets [] TimelineBucket
5258
5359 LinesAdded int
5460 LinesRemoved int
@@ -69,18 +75,22 @@ type StatsData struct {
6975 AvgActionsPerSess float64
7076 LongestSession time.Duration
7177 ShortestSession time.Duration
78+ PeakConcurrent int
7279
7380 TimeSpanStart time.Time
7481 TimeSpanEnd time.Time
7582}
7683
77- func computeStats (ctx context.Context , store storage.Store , since * time.Time , agentFilter string ) (* StatsData , error ) {
84+ func computeStats (ctx context.Context , store storage.Store , since , until * time.Time , agentFilter string ) (* StatsData , error ) {
7885 data := & StatsData {}
7986
8087 sessionFilter := session .NewSessionFilter ().WithLimit (10000 )
8188 if since != nil {
8289 sessionFilter = sessionFilter .WithSince (* since )
8390 }
91+ if until != nil {
92+ sessionFilter = sessionFilter .WithUntil (* until )
93+ }
8494 if agentFilter != "" {
8595 sessionFilter = sessionFilter .WithAgent (agentFilter )
8696 }
@@ -135,6 +145,8 @@ func computeStats(ctx context.Context, store storage.Store, since *time.Time, ag
135145 data .AvgDuration = totalDuration / time .Duration (sessionCount )
136146 }
137147
148+ data .PeakConcurrent = computePeakConcurrent (sessions )
149+
138150 data .UniqueAgents = len (agentMap )
139151 for _ , as := range agentMap {
140152 data .Agents = append (data .Agents , * as )
@@ -147,6 +159,9 @@ func computeStats(ctx context.Context, store storage.Store, since *time.Time, ag
147159 if since != nil {
148160 eventFilter = eventFilter .WithSince (* since )
149161 }
162+ if until != nil {
163+ eventFilter = eventFilter .WithUntil (* until )
164+ }
150165 if agentFilter != "" {
151166 eventFilter = eventFilter .WithAgents (agentFilter )
152167 }
@@ -177,9 +192,6 @@ func computeStats(ctx context.Context, store storage.Store, since *time.Time, ag
177192 data .TimeSpanEnd = e .Timestamp
178193 }
179194
180- hour := e .Timestamp .Local ().Hour ()
181- data .HourlyBuckets [hour ]++
182-
183195 switch e .ResultStatus {
184196 case events .ResultError :
185197 data .TotalErrors ++
@@ -269,5 +281,113 @@ func computeStats(ctx context.Context, store storage.Store, since *time.Time, ag
269281 data .TopCommands = data .TopCommands [:10 ]
270282 }
271283
284+ data .TimelineBuckets = computeTimelineBuckets (evts , since , until )
285+
272286 return data , nil
273287}
288+
289+ func computeTimelineBuckets (evts []* events.Event , since , until * time.Time ) []TimelineBucket {
290+ now := time .Now ()
291+ effectiveSince := now .Add (- 24 * time .Hour )
292+ effectiveUntil := now
293+ if since != nil {
294+ effectiveSince = * since
295+ }
296+ if until != nil {
297+ effectiveUntil = * until
298+ }
299+
300+ span := effectiveUntil .Sub (effectiveSince )
301+
302+ if span <= 24 * time .Hour {
303+ return computeHourlyBuckets (evts )
304+ }
305+ return computeDailyBuckets (evts , effectiveSince , effectiveUntil )
306+ }
307+
308+ func computeHourlyBuckets (evts []* events.Event ) []TimelineBucket {
309+ counts := [24 ]int {}
310+ for _ , e := range evts {
311+ h := e .Timestamp .Local ().Hour ()
312+ counts [h ]++
313+ }
314+ buckets := make ([]TimelineBucket , 24 )
315+ for h := 0 ; h < 24 ; h ++ {
316+ buckets [h ] = TimelineBucket {
317+ Label : fmt .Sprintf ("%02d" , h ),
318+ Count : counts [h ],
319+ }
320+ }
321+ return buckets
322+ }
323+
324+ func computeDailyBuckets (evts []* events.Event , since , until time.Time ) []TimelineBucket {
325+ sinceLocal := since .Local ()
326+ untilLocal := until .Local ()
327+ startDay := time .Date (sinceLocal .Year (), sinceLocal .Month (), sinceLocal .Day (), 0 , 0 , 0 , 0 , sinceLocal .Location ())
328+ endDay := time .Date (untilLocal .Year (), untilLocal .Month (), untilLocal .Day (), 0 , 0 , 0 , 0 , untilLocal .Location ())
329+
330+ days := int (endDay .Sub (startDay ).Hours ()/ 24 ) + 1
331+ if days < 1 {
332+ days = 1
333+ }
334+
335+ buckets := make ([]TimelineBucket , days )
336+ for i := 0 ; i < days ; i ++ {
337+ day := startDay .AddDate (0 , 0 , i )
338+ if days <= 14 {
339+ buckets [i ].Label = day .Format ("Mon" )
340+ } else {
341+ buckets [i ].Label = day .Format ("Jan 2" )
342+ }
343+ }
344+
345+ for _ , e := range evts {
346+ local := e .Timestamp .Local ()
347+ idx := int (time .Date (local .Year (), local .Month (), local .Day (), 0 , 0 , 0 , 0 , local .Location ()).Sub (startDay ).Hours () / 24 )
348+ if idx >= 0 && idx < days {
349+ buckets [idx ].Count ++
350+ }
351+ }
352+
353+ return buckets
354+ }
355+
356+ func computePeakConcurrent (sessions []* session.Session ) int {
357+ if len (sessions ) == 0 {
358+ return 0
359+ }
360+
361+ type endpoint struct {
362+ t time.Time
363+ delta int
364+ }
365+
366+ var points []endpoint
367+ for _ , s := range sessions {
368+ if s .EndedAt .IsZero () {
369+ continue
370+ }
371+ points = append (points ,
372+ endpoint {t : s .StartedAt , delta : 1 },
373+ endpoint {t : s .EndedAt , delta : - 1 },
374+ )
375+ }
376+
377+ sort .Slice (points , func (i , j int ) bool {
378+ if points [i ].t .Equal (points [j ].t ) {
379+ return points [i ].delta > points [j ].delta
380+ }
381+ return points [i ].t .Before (points [j ].t )
382+ })
383+
384+ peak := 0
385+ current := 0
386+ for _ , p := range points {
387+ current += p .delta
388+ if current > peak {
389+ peak = current
390+ }
391+ }
392+ return peak
393+ }
0 commit comments