mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-08-16 12:16:36 +00:00
* telemetry: keep the reported version in the daily history The version only ever lived on the instance record, which holds a cluster's latest report, so there was no way to ask what anything ran last Tuesday. Record it per sample, and let the daily axis carry strings as well as counts. State written before this has no version on its samples. The newest sample is the report the instance record itself came from, so fill that one in on load rather than starting a version series a day late. * telemetry: serve the fleet's version make-up over time /api/versions gives how many clusters ran each release per day, on the same axis and hold-forward rule as the cluster sizes. Releases are ordered by number rather than by size: the caller stacks them, and a stack whose order changes with the counts is unreadable over time. The tail past the limit is summed into "other" so the stack still adds up. Days with no version are dropped before the axis is built, so the series spans the days it knows a version for instead of climbing out of blanks. * telemetry: draw version distribution as a stacked growth chart The pie only ever showed today. Stacked over 30 days the height is the confirmed fleet and each band is a release, so one chart carries the growth and the rollouts at once. Newest release on the floor, so the band being read is anchored to the axis instead of riding on everything below it. Eight fixed hues instead of the evenly spaced ones the cluster stacks use: evenly spaced put a green and a cyan close enough to be hard to tell apart, which matters for a set you read rather than a wall of anonymous ids. Versions past the eighth fold into "other", and each band carries its own number so the chart reads without matching colours against the legend.
164 lines
4.9 KiB
Go
164 lines
4.9 KiB
Go
package storage
|
|
|
|
import (
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// VersionCounts is how many clusters ran one version per day, aligned to the
|
|
// shared date axis of the enclosing VersionSeries.
|
|
type VersionCounts struct {
|
|
Version string `json:"version"`
|
|
Clusters []uint64 `json:"clusters"`
|
|
}
|
|
|
|
// OtherVersions is the versions beyond the caller's limit, summed per day so a
|
|
// stacked chart still adds up to the cluster total.
|
|
type OtherVersions struct {
|
|
Count int `json:"count"`
|
|
Clusters []uint64 `json:"clusters"`
|
|
}
|
|
|
|
// VersionSeries is the version make-up of the fleet over time: one cluster
|
|
// count per version per day, oldest release first.
|
|
type VersionSeries struct {
|
|
Dates []string `json:"dates"`
|
|
Versions []VersionCounts `json:"versions"`
|
|
Other *OtherVersions `json:"other,omitempty"`
|
|
TotalClusters uint64 `json:"total_clusters"` // across all versions on the last day
|
|
}
|
|
|
|
// GetVersionSeries returns the last `days` days of per-version cluster counts
|
|
// across confirmed clusters. Versions beyond `limit` are folded into Other,
|
|
// keeping the ones with the most clusters on the last day.
|
|
func (s *PrometheusStorage) GetVersionSeries(days, limit int) VersionSeries {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
|
|
histories := versionedHistories(s.seriesHistories())
|
|
axis := newDailySeries(days, histories)
|
|
activeSince := time.Now().UTC().AddDate(0, 0, -activeDays).Unix()
|
|
last := len(axis.dates) - 1
|
|
series := VersionSeries{Dates: axis.dates}
|
|
|
|
daily := make(map[string][]uint64)
|
|
for _, history := range histories {
|
|
versions, ok := align(axis, history, activeSince, sampleVersion)
|
|
if !ok {
|
|
continue
|
|
}
|
|
for i, v := range versions {
|
|
if v == "" {
|
|
continue // before the cluster's first day in the window
|
|
}
|
|
counts, ok := daily[v]
|
|
if !ok {
|
|
counts = make([]uint64, len(axis.dates))
|
|
daily[v] = counts
|
|
}
|
|
counts[i]++
|
|
}
|
|
}
|
|
|
|
for v, counts := range daily {
|
|
series.Versions = append(series.Versions, VersionCounts{Version: v, Clusters: counts})
|
|
series.TotalClusters += counts[last]
|
|
}
|
|
// Ordered by release rather than by size: the caller stacks them, and a
|
|
// stack whose order changes with the counts is unreadable over time.
|
|
sort.Slice(series.Versions, func(i, j int) bool {
|
|
return versionLess(series.Versions[i].Version, series.Versions[j].Version)
|
|
})
|
|
|
|
if limit > 0 && len(series.Versions) > limit {
|
|
// Old releases are a long tail of one-cluster bands; keep the versions
|
|
// most of the fleet is on now and sum the rest into one band.
|
|
ranked := append([]VersionCounts(nil), series.Versions...)
|
|
sort.Slice(ranked, func(i, j int) bool {
|
|
if ranked[i].Clusters[last] != ranked[j].Clusters[last] {
|
|
return ranked[i].Clusters[last] > ranked[j].Clusters[last]
|
|
}
|
|
return versionLess(ranked[j].Version, ranked[i].Version)
|
|
})
|
|
other := OtherVersions{Count: len(ranked) - limit, Clusters: make([]uint64, len(axis.dates))}
|
|
for _, v := range ranked[limit:] {
|
|
for i, c := range v.Clusters {
|
|
other.Clusters[i] += c
|
|
}
|
|
}
|
|
kept := make(map[string]bool, limit)
|
|
for _, v := range ranked[:limit] {
|
|
kept[v.Version] = true
|
|
}
|
|
versions := series.Versions[:0]
|
|
for _, v := range series.Versions {
|
|
if kept[v.Version] {
|
|
versions = append(versions, v)
|
|
}
|
|
}
|
|
series.Versions = versions
|
|
series.Other = &other
|
|
}
|
|
return series
|
|
}
|
|
|
|
// versionedHistories drops the samples recorded before the reported version was
|
|
// kept in history, so the version series spans the days it actually knows a
|
|
// version for instead of climbing out of a run of blank days.
|
|
func versionedHistories(histories map[string][]HistorySample) map[string][]HistorySample {
|
|
out := make(map[string][]HistorySample, len(histories))
|
|
for id, history := range histories {
|
|
var kept []HistorySample
|
|
for _, sample := range history {
|
|
if sample.Version != "" {
|
|
kept = append(kept, sample)
|
|
}
|
|
}
|
|
if len(kept) > 0 {
|
|
out[id] = kept
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// versionLess orders release strings like "3.97" and "4.40" by their numeric
|
|
// components rather than lexically, which would sort "10.02" before "9.99". A
|
|
// build suffix sits next to the release it was built from. Anything that
|
|
// doesn't parse, such as the "unknown" a client with no version compiled in
|
|
// reports, sorts first.
|
|
func versionLess(a, b string) bool {
|
|
an, aSuffix, aok := versionParts(a)
|
|
bn, bSuffix, bok := versionParts(b)
|
|
if aok != bok {
|
|
return !aok
|
|
}
|
|
if !aok {
|
|
return a < b
|
|
}
|
|
for i := 0; i < len(an) && i < len(bn); i++ {
|
|
if an[i] != bn[i] {
|
|
return an[i] < bn[i]
|
|
}
|
|
}
|
|
if len(an) != len(bn) {
|
|
return len(an) < len(bn)
|
|
}
|
|
return aSuffix < bSuffix
|
|
}
|
|
|
|
func versionParts(v string) ([]int, string, bool) {
|
|
number, suffix, _ := strings.Cut(v, "-")
|
|
fields := strings.Split(number, ".")
|
|
parts := make([]int, 0, len(fields))
|
|
for _, field := range fields {
|
|
n, err := strconv.Atoi(field)
|
|
if err != nil {
|
|
return nil, "", false
|
|
}
|
|
parts = append(parts, n)
|
|
}
|
|
return parts, suffix, len(parts) > 0
|
|
}
|