130 lines
3.3 KiB
Go
130 lines
3.3 KiB
Go
// Copyright 2026 Alibaba Group
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package usage
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/json"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// Read loads all usage records from the log. Returns an empty slice when the log
|
|
// does not exist yet. Malformed lines are skipped.
|
|
func Read() ([]Record, error) {
|
|
f, err := os.Open(LogPath())
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
defer f.Close()
|
|
|
|
var recs []Record
|
|
sc := bufio.NewScanner(f)
|
|
sc.Buffer(make([]byte, 0, 64*1024), 1<<20)
|
|
for sc.Scan() {
|
|
line := strings.TrimSpace(sc.Text())
|
|
if line == "" {
|
|
continue
|
|
}
|
|
var r Record
|
|
if json.Unmarshal([]byte(line), &r) == nil {
|
|
recs = append(recs, r)
|
|
}
|
|
}
|
|
return recs, sc.Err()
|
|
}
|
|
|
|
// Group aggregates records that share the same (product, tool, arg_keys) shape —
|
|
// the unit a custom shortcut would capture.
|
|
type Group struct {
|
|
Product string `json:"product"`
|
|
Tool string `json:"tool"`
|
|
ArgKeys []string `json:"arg_keys"`
|
|
Count int `json:"count"`
|
|
OKCount int `json:"ok_count"`
|
|
LastSeen string `json:"last_seen"`
|
|
// FixedArgs are argument keys whose sampled value was identical across every
|
|
// occurrence — strong candidates to bake into a custom shortcut as constants.
|
|
FixedArgs map[string]string `json:"fixed_args,omitempty"`
|
|
}
|
|
|
|
// Aggregate groups records and computes per-group counts and fixed-value args,
|
|
// sorted by descending count.
|
|
func Aggregate(recs []Record) []Group {
|
|
type acc struct {
|
|
g Group
|
|
valueSets map[string]map[string]struct{}
|
|
samples map[string]int
|
|
}
|
|
byKey := map[string]*acc{}
|
|
for _, r := range recs {
|
|
key := r.Product + "\x00" + r.Tool + "\x00" + strings.Join(r.ArgKeys, ",")
|
|
a := byKey[key]
|
|
if a == nil {
|
|
a = &acc{
|
|
g: Group{Product: r.Product, Tool: r.Tool, ArgKeys: r.ArgKeys},
|
|
valueSets: map[string]map[string]struct{}{},
|
|
samples: map[string]int{},
|
|
}
|
|
byKey[key] = a
|
|
}
|
|
a.g.Count++
|
|
if r.OK {
|
|
a.g.OKCount++
|
|
}
|
|
if r.TS > a.g.LastSeen {
|
|
a.g.LastSeen = r.TS
|
|
}
|
|
for k, v := range r.SampleArgs {
|
|
a.samples[k]++
|
|
if a.valueSets[k] == nil {
|
|
a.valueSets[k] = map[string]struct{}{}
|
|
}
|
|
a.valueSets[k][v] = struct{}{}
|
|
}
|
|
}
|
|
|
|
out := make([]Group, 0, len(byKey))
|
|
for _, a := range byKey {
|
|
fixed := map[string]string{}
|
|
for k, set := range a.valueSets {
|
|
// A value is "fixed" only if it appeared in every occurrence and never varied.
|
|
if a.samples[k] == a.g.Count && len(set) == 1 {
|
|
for v := range set {
|
|
fixed[k] = v
|
|
}
|
|
}
|
|
}
|
|
if len(fixed) > 0 {
|
|
a.g.FixedArgs = fixed
|
|
}
|
|
out = append(out, a.g)
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].Count > out[j].Count })
|
|
return out
|
|
}
|
|
|
|
// Purge deletes the usage log. Returns nil when there is nothing to delete.
|
|
func Purge() error {
|
|
err := os.Remove(LogPath())
|
|
if os.IsNotExist(err) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|