Skip to content

Commit 4befd22

Browse files
authored
feat(dataobj-explorer): Add stream distribution info to dataobj explorer UI (#16525)
1 parent a57a80e commit 4befd22

File tree

3 files changed

+94
-3
lines changed

3 files changed

+94
-3
lines changed

‎pkg/dataobj/explorer/inspect.go

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@ import (
1111
"github.com/thanos-io/objstore"
1212
"golang.org/x/sync/errgroup"
1313

14+
"github.com/grafana/loki/v3/pkg/dataobj/internal/dataset"
1415
"github.com/grafana/loki/v3/pkg/dataobj/internal/encoding"
1516
"github.com/grafana/loki/v3/pkg/dataobj/internal/metadata/datasetmd"
1617
"github.com/grafana/loki/v3/pkg/dataobj/internal/metadata/filemd"
1718
"github.com/grafana/loki/v3/pkg/dataobj/internal/metadata/logsmd"
1819
"github.com/grafana/loki/v3/pkg/dataobj/internal/metadata/streamsmd"
1920
"github.com/grafana/loki/v3/pkg/dataobj/internal/result"
21+
"github.com/grafana/loki/v3/pkg/dataobj/internal/sections/streams"
2022
)
2123

2224
type FileMetadata struct {
@@ -68,6 +70,9 @@ type SectionMetadata struct {
6870
TotalUncompressedSize uint64 `json:"totalUncompressedSize"`
6971
ColumnCount int `json:"columnCount"`
7072
Columns []ColumnWithPages `json:"columns"`
73+
Distribution []uint64 `json:"distribution"`
74+
MinTimestamp time.Time `json:"minTimestamp"`
75+
MaxTimestamp time.Time `json:"maxTimestamp"`
7176
}
7277

7378
func (s *Service) handleInspect(w http.ResponseWriter, r *http.Request) {
@@ -90,6 +95,10 @@ func (s *Service) handleInspect(w http.ResponseWriter, r *http.Request) {
9095

9196
metadata := inspectFile(r.Context(), s.bucket, filename)
9297
metadata.LastModified = attrs.LastModified.UTC()
98+
for _, section := range metadata.Sections {
99+
section.MinTimestamp = section.MinTimestamp.UTC().Truncate(time.Second)
100+
section.MaxTimestamp = section.MaxTimestamp.UTC().Truncate(time.Second)
101+
}
93102

94103
w.Header().Set("Content-Type", "application/json")
95104
if err := json.NewEncoder(w).Encode(metadata); err != nil {
@@ -147,14 +156,16 @@ func inspectFile(ctx context.Context, bucket objstore.BucketReader, path string)
147156
sectionMeta, err = inspectLogsSection(ctx, reader, section)
148157
if err != nil {
149158
return FileMetadata{
150-
Error: fmt.Sprintf("failed to inspect logs section: %v", err),
159+
Sections: make([]SectionMetadata, 0, len(sections)),
160+
Error: fmt.Sprintf("failed to inspect logs section: %v", err),
151161
}
152162
}
153163
case filemd.SECTION_TYPE_STREAMS:
154164
sectionMeta, err = inspectStreamsSection(ctx, reader, section)
155165
if err != nil {
156166
return FileMetadata{
157-
Error: fmt.Sprintf("failed to inspect streams section: %v", err),
167+
Sections: make([]SectionMetadata, 0, len(sections)),
168+
Error: fmt.Sprintf("failed to inspect streams section: %v", err),
158169
}
159170
}
160171
}
@@ -254,8 +265,10 @@ func inspectStreamsSection(ctx context.Context, reader encoding.Decoder, section
254265
meta.ColumnCount = len(cols)
255266

256267
// Create error group for parallel execution
257-
g, ctx := errgroup.WithContext(ctx)
268+
g, _ := errgroup.WithContext(ctx)
258269

270+
globalMaxTimestamp := time.Time{}
271+
globalMinTimestamp := time.Time{}
259272
// Process each column in parallel
260273
for i, col := range cols {
261274
meta.TotalCompressedSize += col.Info.CompressedSize
@@ -268,6 +281,18 @@ func inspectStreamsSection(ctx context.Context, reader encoding.Decoder, section
268281
return err
269282
}
270283

284+
if col.Type == streamsmd.COLUMN_TYPE_MAX_TIMESTAMP && col.Info.Statistics != nil {
285+
var ts dataset.Value
286+
_ = ts.UnmarshalBinary(col.Info.Statistics.MaxValue)
287+
globalMaxTimestamp = time.Unix(0, ts.Int64()).UTC()
288+
}
289+
290+
if col.Type == streamsmd.COLUMN_TYPE_MIN_TIMESTAMP && col.Info.Statistics != nil {
291+
var ts dataset.Value
292+
_ = ts.UnmarshalBinary(col.Info.Statistics.MinValue)
293+
globalMinTimestamp = time.Unix(0, ts.Int64()).UTC()
294+
}
295+
271296
var pageInfos []PageInfo
272297
for _, pages := range pageSets {
273298
for _, page := range pages {
@@ -309,5 +334,27 @@ func inspectStreamsSection(ctx context.Context, reader encoding.Decoder, section
309334
return meta, err
310335
}
311336

337+
if globalMaxTimestamp.IsZero() || globalMinTimestamp.IsZero() {
338+
// Short circuit if we don't have any timestamps
339+
return meta, nil
340+
}
341+
342+
width := int(globalMaxTimestamp.Add(1 * time.Hour).Truncate(time.Hour).Sub(globalMinTimestamp.Truncate(time.Hour)).Hours())
343+
counts := make([]uint64, width)
344+
for streamVal := range streams.Iter(ctx, reader) {
345+
stream, err := streamVal.Value()
346+
if err != nil {
347+
return meta, err
348+
}
349+
for i := stream.MinTimestamp; !i.After(stream.MaxTimestamp); i = i.Add(time.Hour) {
350+
hoursBeforeMax := int(globalMaxTimestamp.Sub(i).Hours())
351+
counts[hoursBeforeMax]++
352+
}
353+
}
354+
355+
meta.MinTimestamp = globalMinTimestamp
356+
meta.MaxTimestamp = globalMaxTimestamp
357+
meta.Distribution = counts
358+
312359
return meta, nil
313360
}

‎pkg/ui/frontend/src/components/explorer/file-metadata.tsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ function HeadlineStats({
207207
</div>
208208
</div>
209209
)}
210+
210211
{logCount && (
211212
<div className="rounded-lg bg-muted/50 p-6 shadow-sm">
212213
<div className="text-sm text-muted-foreground mb-2">Log Count</div>
@@ -339,6 +340,43 @@ function SectionStats({ section }: SectionStatsProps) {
339340
</Badge>
340341
</div>
341342
</div>
343+
{section.distribution && (
344+
<div className="rounded-lg bg-muted/50 p-6 shadow-sm">
345+
<div className="text-sm text-muted-foreground mb-2">Stream age distribution</div>
346+
<div className="space-y-2">
347+
348+
<div className="text-sm">
349+
Spanning {Math.ceil((new Date(section.maxTimestamp).getTime() - new Date(section.minTimestamp).getTime()) / (1000 * 60 * 60)+1)} hours
350+
<span className="text-sm text-muted-foreground">
351+
<span> from </span><DateHover date={new Date(section.minTimestamp)} /> to <DateHover date={new Date(section.maxTimestamp)} />
352+
</span>
353+
</div>
354+
355+
<div className="text-sm">
356+
Age within object
357+
</div>
358+
<div className="mt-4 space-y-1">
359+
{section.distribution.map((count, i) => {
360+
const maxCount = Math.max(...section.distribution);
361+
const percentage = (count / maxCount) * 100;
362+
const hours = i+1;
363+
return (
364+
<div key={i} className="flex items-center gap-2">
365+
<div className="w-26 text-xs text-right">{`${hours}h`}</div>
366+
<div className="flex-1 h-4 bg-muted rounded-full overflow-hidden">
367+
<div
368+
className="h-full bg-primary/50 rounded-full transition-all"
369+
style={{width: `${percentage}%`}}
370+
/>
371+
</div>
372+
<div className="w-20 text-xs">{count.toLocaleString()} streams</div>
373+
</div>
374+
);
375+
})}
376+
</div>
377+
</div>
378+
</div>
379+
)}
342380
</div>
343381
);
344382
}

‎pkg/ui/frontend/src/types/explorer.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,18 @@ export interface SectionMetadata {
4343
totalUncompressedSize: number;
4444
columnCount: number;
4545
columns: ColumnInfo[];
46+
maxTimestamp: string;
47+
minTimestamp: string;
48+
distribution: number[];
4649
}
4750

4851
export interface FileMetadataResponse {
4952
sections: SectionMetadata[];
5053
error?: string;
5154
lastModified: string;
55+
minTimestamp: string;
56+
maxTimestamp: string;
57+
distribution: number[];
5258
}
5359

5460
interface ColumnStatistics {

0 commit comments

Comments
 (0)