1// Copyright 2023 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5// package config provides methods for loading and querying a
6// telemetry upload config file.
7package config
8
9import (
10	"encoding/json"
11	"os"
12	"strings"
13
14	"golang.org/x/telemetry/internal/telemetry"
15)
16
17// Config is a wrapper around telemetry.UploadConfig that provides some
18// convenience methods for checking the contents of a report.
19type Config struct {
20	*telemetry.UploadConfig
21	program         map[string]bool
22	goos            map[string]bool
23	goarch          map[string]bool
24	goversion       map[string]bool
25	pgversion       map[pgkey]bool
26	pgcounter       map[pgkey]bool
27	pgcounterprefix map[pgkey]bool
28	pgstack         map[pgkey]bool
29	rate            map[pgkey]float64
30}
31
32type pgkey struct {
33	program, key string
34}
35
36func ReadConfig(file string) (*Config, error) {
37	data, err := os.ReadFile(file)
38	if err != nil {
39		return nil, err
40	}
41	var cfg telemetry.UploadConfig
42	if err := json.Unmarshal(data, &cfg); err != nil {
43		return nil, err
44	}
45	return NewConfig(&cfg), nil
46}
47
48func NewConfig(cfg *telemetry.UploadConfig) *Config {
49	ucfg := Config{UploadConfig: cfg}
50	ucfg.goos = set(ucfg.GOOS)
51	ucfg.goarch = set(ucfg.GOARCH)
52	ucfg.goversion = set(ucfg.GoVersion)
53	ucfg.program = make(map[string]bool, len(ucfg.Programs))
54	ucfg.pgversion = make(map[pgkey]bool, len(ucfg.Programs))
55	ucfg.pgcounter = make(map[pgkey]bool, len(ucfg.Programs))
56	ucfg.pgcounterprefix = make(map[pgkey]bool, len(ucfg.Programs))
57	ucfg.pgstack = make(map[pgkey]bool, len(ucfg.Programs))
58	ucfg.rate = make(map[pgkey]float64)
59	for _, p := range ucfg.Programs {
60		ucfg.program[p.Name] = true
61		for _, v := range p.Versions {
62			ucfg.pgversion[pgkey{p.Name, v}] = true
63		}
64		for _, c := range p.Counters {
65			for _, e := range Expand(c.Name) {
66				ucfg.pgcounter[pgkey{p.Name, e}] = true
67				ucfg.rate[pgkey{p.Name, e}] = c.Rate
68			}
69			prefix, _, found := strings.Cut(c.Name, ":")
70			if found {
71				ucfg.pgcounterprefix[pgkey{p.Name, prefix}] = true
72			}
73		}
74		for _, s := range p.Stacks {
75			ucfg.pgstack[pgkey{p.Name, s.Name}] = true
76			ucfg.rate[pgkey{p.Name, s.Name}] = s.Rate
77		}
78	}
79	return &ucfg
80}
81
82func (r *Config) HasProgram(s string) bool {
83	return r.program[s]
84}
85
86func (r *Config) HasGOOS(s string) bool {
87	return r.goos[s]
88}
89
90func (r *Config) HasGOARCH(s string) bool {
91	return r.goarch[s]
92}
93
94func (r *Config) HasGoVersion(s string) bool {
95	return r.goversion[s]
96}
97
98func (r *Config) HasVersion(program, version string) bool {
99	return r.pgversion[pgkey{program, version}]
100}
101
102func (r *Config) HasCounter(program, counter string) bool {
103	return r.pgcounter[pgkey{program, counter}]
104}
105
106func (r *Config) HasCounterPrefix(program, prefix string) bool {
107	return r.pgcounterprefix[pgkey{program, prefix}]
108}
109
110func (r *Config) HasStack(program, stack string) bool {
111	return r.pgstack[pgkey{program, stack}]
112}
113
114func (r *Config) Rate(program, name string) float64 {
115	return r.rate[pgkey{program, name}]
116}
117
118func set(slice []string) map[string]bool {
119	s := make(map[string]bool, len(slice))
120	for _, v := range slice {
121		s[v] = true
122	}
123	return s
124}
125
126// Expand takes a counter defined with buckets and expands it into distinct
127// strings for each bucket
128func Expand(counter string) []string {
129	prefix, rest, hasBuckets := strings.Cut(counter, "{")
130	var counters []string
131	if hasBuckets {
132		buckets := strings.Split(strings.TrimSuffix(rest, "}"), ",")
133		for _, b := range buckets {
134			counters = append(counters, prefix+b)
135		}
136	} else {
137		counters = append(counters, prefix)
138	}
139	return counters
140}
141