Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
aos
GitHub Repository: aos/grafana-agent
Path: blob/main/cmd/grafana-agentctl/main.go
4094 views
1
// Command grafana-agentctl provides utilities for interacting with Grafana
2
// Agent.
3
package main
4
5
import (
6
"context"
7
"fmt"
8
"os"
9
"os/signal"
10
"path/filepath"
11
"sort"
12
"strings"
13
"syscall"
14
15
"github.com/grafana/agent/pkg/build"
16
"github.com/grafana/agent/pkg/config"
17
"github.com/grafana/agent/pkg/logs"
18
"github.com/olekukonko/tablewriter"
19
"github.com/prometheus/client_golang/prometheus"
20
21
"github.com/go-kit/log"
22
"github.com/go-kit/log/level"
23
"github.com/grafana/agent/pkg/agentctl"
24
"github.com/grafana/agent/pkg/client"
25
"github.com/spf13/cobra"
26
27
// Register Prometheus SD components
28
_ "github.com/prometheus/prometheus/discovery/install"
29
30
// Register integrations
31
_ "github.com/grafana/agent/pkg/integrations/install"
32
33
// Needed for operator-detach
34
"k8s.io/apimachinery/pkg/fields"
35
"k8s.io/apimachinery/pkg/labels"
36
"k8s.io/apimachinery/pkg/runtime"
37
_ "k8s.io/client-go/plugin/pkg/client/auth"
38
39
apps_v1 "k8s.io/api/apps/v1"
40
core_v1 "k8s.io/api/core/v1"
41
"k8s.io/apimachinery/pkg/api/meta"
42
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
43
kclient "sigs.k8s.io/controller-runtime/pkg/client"
44
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
45
kconfig "sigs.k8s.io/controller-runtime/pkg/client/config"
46
)
47
48
func main() {
49
cmd := &cobra.Command{
50
Use: "agentctl",
51
Short: "Tools for interacting with the Grafana Agent",
52
Version: build.Print("agentctl"),
53
}
54
cmd.SetVersionTemplate("{{ .Version }}\n")
55
56
cmd.AddCommand(
57
configSyncCmd(),
58
configCheckCmd(),
59
walStatsCmd(),
60
targetStatsCmd(),
61
samplesCmd(),
62
operatorDetachCmd(),
63
testLogs(),
64
)
65
66
_ = cmd.Execute()
67
}
68
69
func configSyncCmd() *cobra.Command {
70
var (
71
agentAddr string
72
dryRun bool
73
)
74
75
cmd := &cobra.Command{
76
Use: "config-sync [directory]",
77
Short: "Sync config files from a directory to an Agent's config management API",
78
Long: `config-sync loads all files ending with .yml or .yaml from the specified
79
directory and uploads them through the config management API. The name of the config
80
uploaded will be the base name of the file (e.g., the name of the file without
81
its extension).
82
83
The directory is used as the source-of-truth for the entire set of configs that
84
should be present in the API. config-sync will delete all existing configs from the API
85
that do not match any of the names of the configs that were uploaded from the
86
source-of-truth directory.`,
87
Args: cobra.ExactArgs(1),
88
89
Run: func(_ *cobra.Command, args []string) {
90
logger := log.NewLogfmtLogger(log.NewSyncWriter(os.Stdout))
91
92
if agentAddr == "" {
93
level.Error(logger).Log("msg", "-addr must not be an empty string")
94
os.Exit(1)
95
}
96
97
directory := args[0]
98
cli := client.New(agentAddr)
99
100
err := agentctl.ConfigSync(logger, cli.PrometheusClient, directory, dryRun)
101
if err != nil {
102
level.Error(logger).Log("msg", "failed to sync config", "err", err)
103
os.Exit(1)
104
}
105
},
106
}
107
108
cmd.Flags().StringVarP(&agentAddr, "addr", "a", "http://localhost:12345", "address of the agent to connect to")
109
cmd.Flags().BoolVarP(&dryRun, "dry-run", "d", false, "use the dry run option to validate config files without attempting to upload")
110
return cmd
111
}
112
113
func configCheckCmd() *cobra.Command {
114
var expandEnv bool
115
116
cmd := &cobra.Command{
117
Use: "config-check [config file]",
118
Short: "Perform basic validation of the given Agent configuration file",
119
Long: `config-check performs basic syntactic validation of the given Agent configuration
120
file. The file is checked to ensure the types match the expected configuration types. Optionally,
121
${var} style substitutions can be expanded based on the values of the environmental variables.
122
123
If the configuration file is valid the exit code will be 0. If the configuration file is invalid
124
the exit code will be 1.`,
125
Args: cobra.ExactArgs(1),
126
Run: func(_ *cobra.Command, args []string) {
127
file := args[0]
128
129
cfg := config.Config{}
130
err := config.LoadFile(file, expandEnv, &cfg)
131
if err != nil {
132
fmt.Fprintf(os.Stderr, "failed to validate config: %s\n", err)
133
os.Exit(1)
134
} else {
135
fmt.Fprintln(os.Stdout, "config valid")
136
}
137
},
138
}
139
140
cmd.Flags().BoolVarP(&expandEnv, "expand-env", "e", false, "expands ${var} in config according to the values of the environment variables")
141
return cmd
142
}
143
144
func samplesCmd() *cobra.Command {
145
var selector string
146
147
cmd := &cobra.Command{
148
Use: "sample-stats [WAL directory]",
149
Short: "Discover sample statistics for series matching a label selector within the WAL",
150
Long: `sample-stats reads a WAL directory and collects information on the series and
151
samples within it. A label selector can be used to filter the series that should be targeted.
152
153
Examples:
154
155
Show sample stats for all series in the WAL:
156
157
$ agentctl sample-stats /tmp/wal
158
159
160
Show sample stats for the 'up' series:
161
162
$ agentctl sample-stats -s up /tmp/wal
163
164
165
Show sample stats for all series within 'job=a':
166
167
$ agentctl sample-stats -s '{job="a"}' /tmp/wal
168
`,
169
Args: cobra.ExactArgs(1),
170
Run: func(_ *cobra.Command, args []string) {
171
directory := args[0]
172
if _, err := os.Stat(directory); os.IsNotExist(err) {
173
fmt.Printf("%s does not exist\n", directory)
174
os.Exit(1)
175
} else if err != nil {
176
fmt.Printf("error getting wal: %v\n", err)
177
os.Exit(1)
178
}
179
180
// Check if ./wal is a subdirectory, use that instead.
181
if _, err := os.Stat(filepath.Join(directory, "wal")); err == nil {
182
directory = filepath.Join(directory, "wal")
183
}
184
185
stats, err := agentctl.FindSamples(directory, selector)
186
if err != nil {
187
fmt.Printf("failed to get sample stats: %v\n", err)
188
os.Exit(1)
189
}
190
191
for _, series := range stats {
192
fmt.Print(series.Labels.String(), "\n")
193
fmt.Printf(" Oldest Sample: %s\n", series.From)
194
fmt.Printf(" Newest Sample: %s\n", series.To)
195
fmt.Printf(" Total Samples: %d\n", series.Samples)
196
}
197
},
198
}
199
200
cmd.Flags().StringVarP(&selector, "selector", "s", "{}", "label selector to search for")
201
return cmd
202
}
203
204
func targetStatsCmd() *cobra.Command {
205
var (
206
jobLabel string
207
instanceLabel string
208
)
209
210
cmd := &cobra.Command{
211
Use: "target-stats [WAL directory]",
212
Short: "Discover statistics on a specific target within the WAL.",
213
Long: `target-stats computes statistics on a specific target within the WAL at
214
greater detail than the general wal-stats. The statistics computed is the
215
cardinality of all series within that target.
216
217
The cardinality for a series is defined as the total number of unique
218
combinations of label names and values that a given metric has. The result of
219
this operation can be used to define metric_relabel_rules and drop
220
high-cardinality series that you do not want to send.`,
221
Args: cobra.ExactArgs(1),
222
223
Run: func(_ *cobra.Command, args []string) {
224
directory := args[0]
225
if _, err := os.Stat(directory); os.IsNotExist(err) {
226
fmt.Printf("%s does not exist\n", directory)
227
os.Exit(1)
228
} else if err != nil {
229
fmt.Printf("error getting wal: %v\n", err)
230
os.Exit(1)
231
}
232
233
// Check if ./wal is a subdirectory, use that instead.
234
if _, err := os.Stat(filepath.Join(directory, "wal")); err == nil {
235
directory = filepath.Join(directory, "wal")
236
}
237
238
cardinality, err := agentctl.FindCardinality(directory, jobLabel, instanceLabel)
239
if err != nil {
240
fmt.Printf("failed to get cardinality: %v\n", err)
241
os.Exit(1)
242
}
243
244
sort.Slice(cardinality, func(i, j int) bool {
245
return cardinality[i].Instances > cardinality[j].Instances
246
})
247
248
fmt.Printf("Metric cardinality:\n\n")
249
250
for _, metric := range cardinality {
251
fmt.Printf("%s: %d\n", metric.Metric, metric.Instances)
252
}
253
},
254
}
255
256
cmd.Flags().StringVarP(&jobLabel, "job", "j", "", "job label to search for")
257
cmd.Flags().StringVarP(&instanceLabel, "instance", "i", "", "instance label to search for")
258
must(cmd.MarkFlagRequired("job"))
259
must(cmd.MarkFlagRequired("instance"))
260
return cmd
261
}
262
263
func walStatsCmd() *cobra.Command {
264
return &cobra.Command{
265
Use: "wal-stats [WAL directory]",
266
Short: "Collect stats on the WAL",
267
Long: `wal-stats reads a WAL directory and collects information on the series and
268
samples within it.
269
270
The "Hash Collisions" value refers to the number of ref IDs a label's hash was
271
assigned to. A non-zero amount of collisions has no negative effect on the data
272
sent to the Remote Write endpoint, but may have an impact on memory usage. Labels
273
may collide with multiple ref IDs normally if a series flaps (i.e., gets marked for
274
deletion but then comes back at some point).`,
275
Args: cobra.ExactArgs(1),
276
277
Run: func(_ *cobra.Command, args []string) {
278
directory := args[0]
279
if _, err := os.Stat(directory); os.IsNotExist(err) {
280
fmt.Printf("%s does not exist\n", directory)
281
os.Exit(1)
282
} else if err != nil {
283
fmt.Printf("error getting wal: %v\n", err)
284
os.Exit(1)
285
}
286
287
// Check if ./wal is a subdirectory, use that instead.
288
if _, err := os.Stat(filepath.Join(directory, "wal")); err == nil {
289
directory = filepath.Join(directory, "wal")
290
}
291
292
stats, err := agentctl.CalculateStats(directory)
293
if err != nil {
294
fmt.Printf("failed to get WAL stats: %v\n", err)
295
os.Exit(1)
296
}
297
298
fmt.Printf("Oldest Sample: %s\n", stats.From)
299
fmt.Printf("Newest Sample: %s\n", stats.To)
300
fmt.Printf("Total Series: %d\n", stats.Series())
301
fmt.Printf("Total Samples: %d\n", stats.Samples())
302
fmt.Printf("Hash Collisions: %d\n", stats.HashCollisions)
303
fmt.Printf("Invalid Refs: %d\n", stats.InvalidRefs)
304
fmt.Printf("Checkpoint Segment: %d\n", stats.CheckpointNumber)
305
fmt.Printf("First Segment: %d\n", stats.FirstSegment)
306
fmt.Printf("Latest Segment: %d\n", stats.LastSegment)
307
308
fmt.Printf("\nPer-target stats:\n")
309
310
table := tablewriter.NewWriter(os.Stdout)
311
defer table.Render()
312
313
table.SetHeader([]string{"Job", "Instance", "Series", "Samples"})
314
315
sort.Sort(agentctl.BySeriesCount(stats.Targets))
316
317
for _, t := range stats.Targets {
318
seriesStr := fmt.Sprintf("%d", t.Series)
319
samplesStr := fmt.Sprintf("%d", t.Samples)
320
table.Append([]string{t.Job, t.Instance, seriesStr, samplesStr})
321
}
322
},
323
}
324
}
325
326
func operatorDetachCmd() *cobra.Command {
327
cmd := &cobra.Command{
328
Use: "operator-detach",
329
Short: "Detaches any Operator-Managed resource so CRDs can temporarily be deleted",
330
Long: `operator-detach will find Grafana Agent Operator-Managed resources across the cluster and edit them to remove the OwnerReferences tying them to a GrafanaAgent CRD. This allows the CRDs to be modified without losing the deployment of Grafana Agents.`,
331
Args: cobra.ExactArgs(0),
332
333
RunE: func(_ *cobra.Command, args []string) error {
334
logger := log.NewLogfmtLogger(log.NewSyncWriter(os.Stdout))
335
scheme := runtime.NewScheme()
336
hadErrors := false
337
338
for _, add := range []func(*runtime.Scheme) error{
339
core_v1.AddToScheme,
340
apps_v1.AddToScheme,
341
} {
342
if err := add(scheme); err != nil {
343
return fmt.Errorf("unable to register scheme: %w", err)
344
}
345
}
346
347
cli, err := kclient.New(kconfig.GetConfigOrDie(), kclient.Options{
348
Scheme: scheme,
349
Mapper: nil,
350
})
351
if err != nil {
352
return fmt.Errorf("unable to generate Kubernetes client: %w", err)
353
}
354
355
// Resources to list
356
lists := []kclient.ObjectList{
357
&apps_v1.StatefulSetList{},
358
&apps_v1.DaemonSetList{},
359
&core_v1.SecretList{},
360
&core_v1.ServiceList{},
361
}
362
for _, l := range lists {
363
gvk, err := apiutil.GVKForObject(l, scheme)
364
if err != nil {
365
return fmt.Errorf("failed to get GroupVersionKind: %w", err)
366
}
367
level.Info(logger).Log("msg", "getting objects for resource", "resource", gvk.Kind)
368
369
err = cli.List(context.Background(), l, &kclient.ListOptions{
370
LabelSelector: labels.Everything(),
371
FieldSelector: fields.Everything(),
372
Namespace: "",
373
})
374
if err != nil {
375
level.Error(logger).Log("msg", "failed to list resource", "resource", gvk.Kind, "err", err)
376
hadErrors = true
377
continue
378
}
379
380
elements, err := meta.ExtractList(l)
381
if err != nil {
382
level.Error(logger).Log("msg", "failed to get elements for resource", "resource", gvk.Kind, "err", err)
383
hadErrors = true
384
continue
385
}
386
for _, e := range elements {
387
obj := e.(kclient.Object)
388
389
filtered, changed := filterAgentOwners(obj.GetOwnerReferences())
390
if !changed {
391
continue
392
}
393
394
level.Info(logger).Log("msg", "detaching ownerreferences for object", "resource", gvk.Kind, "namespace", obj.GetNamespace(), "name", obj.GetName())
395
obj.SetOwnerReferences(filtered)
396
397
if err := cli.Update(context.Background(), obj); err != nil {
398
level.Error(logger).Log("msg", "failed to update object", "resource", gvk.Kind, "namespace", obj.GetNamespace(), "name", obj.GetName(), "err", err)
399
hadErrors = true
400
continue
401
}
402
}
403
}
404
405
if hadErrors {
406
return fmt.Errorf("encountered errors during execution")
407
}
408
return nil
409
},
410
}
411
412
return cmd
413
}
414
415
func filterAgentOwners(refs []meta_v1.OwnerReference) (filtered []meta_v1.OwnerReference, changed bool) {
416
filtered = make([]meta_v1.OwnerReference, 0, len(refs))
417
418
for _, ref := range refs {
419
if ref.Kind == "GrafanaAgent" && strings.HasPrefix(ref.APIVersion, "monitoring.grafana.com/") {
420
changed = true
421
continue
422
}
423
filtered = append(filtered, ref)
424
}
425
return
426
}
427
428
func testLogs() *cobra.Command {
429
cmd := &cobra.Command{
430
Use: "test-logs [config file]",
431
Short: "Collect logs but print entries instead of sending them to Loki.",
432
Long: `Starts Promtail using its '--dry-run' flag, which will only print logs instead of sending them to the remote server.
433
This can be useful for debugging and understanding how logs are being parsed.`,
434
Args: cobra.ExactArgs(1),
435
436
Run: func(_ *cobra.Command, args []string) {
437
file := args[0]
438
439
cfg := config.Config{}
440
err := config.LoadFile(file, false, &cfg)
441
if err != nil {
442
fmt.Fprintf(os.Stderr, "failed to validate config: %s\n", err)
443
os.Exit(1)
444
}
445
446
logger := log.NewLogfmtLogger(log.NewSyncWriter(os.Stderr))
447
l, err := logs.New(prometheus.NewRegistry(), cfg.Logs, logger, true)
448
if err != nil {
449
fmt.Fprintf(os.Stderr, "failed to start log collection: %s\n", err)
450
os.Exit(1)
451
}
452
defer l.Stop()
453
454
// Block until a shutdown signal is received.
455
sigs := make(chan os.Signal, 1)
456
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
457
sig := <-sigs
458
fmt.Fprintf(os.Stderr, "received shutdown %v signal, stopping...", sig)
459
},
460
}
461
462
return cmd
463
}
464
465
func must(err error) {
466
if err != nil {
467
panic(err)
468
}
469
}
470
471