github.com/grafana/synthetic-monitoring-agent@v0.64.2/cmd/synthetic-monitoring-agent/main.go (about) 1 package main 2 3 import ( 4 "context" 5 "flag" 6 "fmt" 7 "io" 8 "math" 9 "net" 10 "os" 11 "os/signal" 12 "path/filepath" 13 "syscall" 14 "time" 15 16 "github.com/KimMachineGun/automemlimit/memlimit" 17 "github.com/google/uuid" 18 "github.com/jpillora/backoff" 19 "github.com/prometheus/client_golang/prometheus" 20 "github.com/rs/zerolog" 21 "golang.org/x/sync/errgroup" 22 "google.golang.org/grpc/grpclog" 23 24 "github.com/grafana/synthetic-monitoring-agent/internal/adhoc" 25 "github.com/grafana/synthetic-monitoring-agent/internal/cache" 26 "github.com/grafana/synthetic-monitoring-agent/internal/cals" 27 "github.com/grafana/synthetic-monitoring-agent/internal/checks" 28 "github.com/grafana/synthetic-monitoring-agent/internal/feature" 29 "github.com/grafana/synthetic-monitoring-agent/internal/http" 30 "github.com/grafana/synthetic-monitoring-agent/internal/k6runner" 31 "github.com/grafana/synthetic-monitoring-agent/internal/k6version" 32 "github.com/grafana/synthetic-monitoring-agent/internal/labelmode" 33 "github.com/grafana/synthetic-monitoring-agent/internal/limits" 34 "github.com/grafana/synthetic-monitoring-agent/internal/metamonitoring" 35 "github.com/grafana/synthetic-monitoring-agent/internal/pusher" 36 pusherV1 "github.com/grafana/synthetic-monitoring-agent/internal/pusher/v1" 37 pusherV2 "github.com/grafana/synthetic-monitoring-agent/internal/pusher/v2" 38 "github.com/grafana/synthetic-monitoring-agent/internal/scraper" 39 "github.com/grafana/synthetic-monitoring-agent/internal/secrets" 40 "github.com/grafana/synthetic-monitoring-agent/internal/telemetry" 41 "github.com/grafana/synthetic-monitoring-agent/internal/tenants" 42 "github.com/grafana/synthetic-monitoring-agent/internal/usage" 43 "github.com/grafana/synthetic-monitoring-agent/internal/version" 44 "github.com/grafana/synthetic-monitoring-agent/pkg/pb/synthetic_monitoring" 45 ) 46 47 const ( 48 exitFail = 1 49 defTelemetryTimeSpan = 5 // min 50 ) 51 52 // run is the main entry point for the program. 53 // 54 // TODO(mem): refactor this function to be more readable. 55 // 56 //nolint:gocyclo // this function is doing a lot of configuration, and it ends up being long and with lots of branches. 57 func run(args []string, stdout io.Writer) error { 58 flags := flag.NewFlagSet(filepath.Base(args[0]), flag.ExitOnError) 59 60 var ( 61 features = feature.NewCollection() 62 config = struct { 63 DevMode bool 64 Debug bool 65 Verbose bool 66 ReportVersion bool 67 GrpcApiServerAddr string 68 GrpcInsecure bool 69 ApiToken Secret 70 EnableChangeLogLevel bool 71 EnableDisconnect bool 72 EnablePProf bool 73 HttpListenAddr string 74 K6URI string 75 K6Repository string 76 K6BlacklistedIP string 77 SelectedPublisher string 78 TelemetryTimeSpan int 79 AutoMemLimit bool 80 MemLimitRatio float64 81 DisableK6 bool 82 DisableUsageReports bool 83 CacheType cache.Kind 84 CacheLocalCapacity int 85 CacheLocalTTL time.Duration 86 MemcachedServers StringList 87 EnableProtocolSecrets bool 88 PushTelemetry bool 89 MetricsInterval time.Duration 90 }{ 91 GrpcApiServerAddr: "localhost:4031", 92 HttpListenAddr: "localhost:4050", 93 K6URI: "", 94 K6Repository: "/usr/libexec/sm-k6", 95 K6BlacklistedIP: "10.0.0.0/8", 96 SelectedPublisher: pusherV2.Name, 97 TelemetryTimeSpan: defTelemetryTimeSpan, 98 AutoMemLimit: true, 99 MemLimitRatio: 0.9, 100 CacheType: cache.KindAuto, 101 CacheLocalCapacity: 10000, 102 CacheLocalTTL: 5 * time.Minute, 103 MetricsInterval: time.Minute, 104 } 105 ) 106 107 flags.BoolVar(&config.DevMode, "dev", config.DevMode, "turn on all development flags") 108 flags.BoolVar(&config.Debug, "debug", config.Debug, "debug output (enables verbose)") 109 flags.BoolVar(&config.Verbose, "verbose", config.Verbose, "verbose logging") 110 flags.BoolVar(&config.ReportVersion, "version", config.ReportVersion, "report version and exit") 111 flags.StringVar(&config.GrpcApiServerAddr, "api-server-address", config.GrpcApiServerAddr, "GRPC API server address") 112 flags.BoolVar(&config.GrpcInsecure, "api-insecure", config.GrpcInsecure, "Don't use TLS with connections to GRPC API") 113 flags.Var(&config.ApiToken, "api-token", `synthetic monitoring probe authentication token (default "")`) 114 flags.BoolVar(&config.EnableChangeLogLevel, "enable-change-log-level", config.EnableChangeLogLevel, "enable changing the log level at runtime") 115 flags.BoolVar(&config.EnableDisconnect, "enable-disconnect", config.EnableDisconnect, "enable HTTP /disconnect endpoint") 116 flags.BoolVar(&config.EnablePProf, "enable-pprof", config.EnablePProf, "exposes profiling data via HTTP /debug/pprof/ endpoint") 117 flags.StringVar(&config.HttpListenAddr, "listen-address", config.HttpListenAddr, "listen address") 118 flags.StringVar(&config.K6URI, "k6-uri", config.K6URI, "Path or URI to a specific k6 binary, overrides k6 version autodetection") 119 flags.StringVar(&config.K6Repository, "k6-repository", config.K6Repository, "path to folder containing k6 binaries") 120 flags.StringVar(&config.K6BlacklistedIP, "blocked-nets", config.K6BlacklistedIP, 121 "IP networks to block in CIDR notation. Setting this to an empty string, or '0.0.0.0/32', will disable the blocklist.") 122 flags.StringVar(&config.SelectedPublisher, "publisher", config.SelectedPublisher, "publisher type") 123 flags.IntVar(&config.TelemetryTimeSpan, "telemetry-time-span", config.TelemetryTimeSpan, "time span between telemetry push executions per tenant") 124 flags.BoolVar(&config.AutoMemLimit, "enable-auto-memlimit", config.AutoMemLimit, "automatically set GOMEMLIMIT") 125 flags.BoolVar(&config.DisableK6, "disable-k6", config.DisableK6, "disables running k6 checks on this probe") 126 flags.BoolVar(&config.DisableUsageReports, "disable-usage-reports", config.DisableUsageReports, "Disable anonymous usage reports") 127 flags.Float64Var(&config.MemLimitRatio, "memlimit-ratio", config.MemLimitRatio, "fraction of available memory to use") 128 flags.Var(&features, "features", "optional feature flags") 129 flags.Var(&config.CacheType, "cache-type", "cache type: auto (memcached if servers provided, else local), memcached, local, or noop") 130 flags.IntVar(&config.CacheLocalCapacity, "cache-local-capacity", config.CacheLocalCapacity, "maximum number of items in local cache") 131 flags.DurationVar(&config.CacheLocalTTL, "cache-local-ttl", config.CacheLocalTTL, "default TTL for local cache items") 132 flags.Var(&config.MemcachedServers, "memcached-servers", "memcached servers") 133 flags.DurationVar(&config.MetricsInterval, "metrics-push-interval", config.MetricsInterval, "interval between internal metrics push cycles") 134 flags.BoolVar(&config.PushTelemetry, "experimental-push-telemetry", config.PushTelemetry, "enable pushing telemetry to the probe's tenant databases") 135 136 if err := flags.Parse(args[1:]); err != nil { 137 return err 138 } 139 140 if config.ReportVersion { 141 fmt.Printf( 142 "%s version=\"%s\" buildstamp=\"%s\" commit=\"%s\"\n", 143 flags.Name(), 144 version.Short(), 145 version.Buildstamp(), 146 version.Commit(), 147 ) 148 149 return nil 150 } 151 152 if config.DevMode { 153 config.Debug = true 154 config.EnableChangeLogLevel = true 155 config.EnableDisconnect = true 156 config.EnablePProf = true 157 } 158 159 if config.AutoMemLimit { 160 err := setupGoMemLimit(config.MemLimitRatio) 161 if err != nil { 162 return err 163 } 164 } 165 166 if !config.DisableK6 { 167 if err := features.Set(feature.K6); err != nil { 168 return fmt.Errorf("cannot set k6 feature: %w", err) 169 } 170 } 171 172 if _, _, err := net.SplitHostPort(config.GrpcApiServerAddr); err != nil { 173 // SplitHostPort errors if the address has no port. This is intended, as omitting the port in the address is 174 // almost likely a user error that is hard to troubleshoot otherwise. 175 return fmt.Errorf("parsing GRPC api server address %q: %w", config.GrpcApiServerAddr, err) 176 } 177 178 // If the token is provided on the command line, prefer that. Otherwise 179 // pull it from the environment variable SM_AGENT_API_TOKEN. If that's 180 // not available, fallback to API_TOKEN, which was the environment 181 // variable name previously used in the systemd unit files. 182 // 183 // Using API_TOKEN should be deprecated after March 1st, 2023. 184 config.ApiToken = Secret(stringFromEnv("API_TOKEN", stringFromEnv("SM_AGENT_API_TOKEN", string(config.ApiToken)))) 185 186 // Enable protocol secrets support via the "protocol-secrets" feature flag. 187 // This allows testing before enabling by default. 188 config.EnableProtocolSecrets = features.IsSet(feature.ProtocolSecrets) 189 190 if config.ApiToken == "" { 191 return fmt.Errorf("invalid API token") 192 } 193 194 baseCtx, cancel := context.WithCancel(context.Background()) 195 defer cancel() 196 197 g, ctx := errgroup.WithContext(baseCtx) 198 199 zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMs 200 201 zl := zerolog.New(stdout).With().Timestamp().Str("program", filepath.Base(args[0])).Logger() 202 203 switch { 204 case config.Debug: 205 zerolog.SetGlobalLevel(zerolog.DebugLevel) 206 207 zlGrpc := zl.With().Str("component", "grpc-go").Logger() 208 zl = zl.With().Caller().Logger() 209 config.Verbose = true 210 211 grpclog.SetLoggerV2(grpclog.NewLoggerV2WithVerbosity(zlGrpc, zlGrpc, zlGrpc, 99)) 212 213 case config.Verbose: 214 zerolog.SetGlobalLevel(zerolog.InfoLevel) 215 216 default: 217 zerolog.SetGlobalLevel(zerolog.WarnLevel) 218 } 219 220 g.Go(func() error { 221 return signalHandler(ctx, zl.With().Str("subsystem", "signal handler").Logger()) 222 }) 223 224 zl.Info(). 225 Str("version", version.Short()). 226 Str("commit", version.Commit()). 227 Str("buildstamp", version.Buildstamp()). 228 Str("features", features.String()). 229 Interface("config", config). 230 Msg("starting") 231 232 notifyAboutDeprecatedFeatureFlags(features, zl) 233 234 var usageReporter usage.Reporter 235 236 switch { 237 case config.DisableUsageReports: 238 usageReporter = usage.NewNoOPReporter() 239 default: 240 usageReporter = usage.NewHTTPReporter(usage.ProdStatsEndpoint) 241 242 zl.Info().Msg("enabled collecting anonymous usage reports. You can disable collection by setting -disable-usage-reports.") 243 } 244 245 promRegisterer := prometheus.NewRegistry() 246 247 if err := registerMetrics(promRegisterer); err != nil { 248 return err 249 } 250 251 cacheClient, err := setupCache( 252 ctx, 253 config.CacheType, 254 config.MemcachedServers, 255 config.CacheLocalCapacity, 256 config.CacheLocalTTL, 257 &zl, 258 ) 259 if err != nil { 260 return err 261 } 262 263 // to know if probe is connected to API 264 readynessHandler := NewReadynessHandler() 265 266 router := NewMux(MuxOpts{ 267 Logger: zl.With().Str("subsystem", "mux").Logger(), 268 PromRegisterer: promRegisterer, 269 isReady: readynessHandler, 270 changeLogLevelEnabled: config.EnableChangeLogLevel, 271 disconnectEnabled: config.EnableDisconnect, 272 pprofEnabled: config.EnablePProf, 273 }) 274 275 httpConfig := http.Config{ 276 ListenAddr: config.HttpListenAddr, 277 Logger: zl.With().Str("subsystem", "http").Logger(), 278 ReadTimeout: 5 * time.Second, 279 WriteTimeout: 60 * time.Second, 280 IdleTimeout: 15 * time.Second, 281 } 282 283 httpServer := http.NewServer(ctx, router, httpConfig) 284 285 httpListener, err := (&net.ListenConfig{}).Listen(ctx, "tcp", httpServer.ListenAddr()) 286 if err != nil { 287 return err 288 } 289 290 g.Go(func() error { 291 <-ctx.Done() 292 293 timeoutCtx, timeoutCancel := context.WithTimeout(context.Background(), 5*time.Second) 294 defer timeoutCancel() 295 296 // we probably cannot do anything meaningful with this 297 // error but return it anyways. 298 return httpServer.Shutdown(timeoutCtx) 299 }) 300 301 g.Go(func() error { 302 return httpServer.Run(httpListener) 303 }) 304 305 tenantCh := make(chan synthetic_monitoring.Tenant) 306 307 conn, err := newAPIServerClient(config.GrpcApiServerAddr, config.GrpcInsecure, string(config.ApiToken)) 308 if err != nil { 309 return fmt.Errorf("dialing GRPC server %s: %w", config.GrpcApiServerAddr, err) 310 } 311 defer conn.Close() 312 313 var k6Runner k6runner.Runner 314 315 if features.IsSet(feature.K6) { 316 if err := validateCIDR(config.K6BlacklistedIP); err != nil { 317 return err 318 } 319 320 k6Runner, err = k6runner.New(k6runner.RunnerOpts{ 321 Uri: config.K6URI, 322 Repository: config.K6Repository, 323 BlacklistedIP: config.K6BlacklistedIP, 324 Registerer: promRegisterer, 325 }) 326 if err != nil { 327 return fmt.Errorf("building k6 runner: %w", err) 328 } 329 } 330 331 tm := tenants.NewManager( 332 ctx, 333 synthetic_monitoring.NewTenantsClient(conn), 334 tenantCh, 335 tenants.DefaultCacheTimeout, 336 cacheClient, 337 zl.With().Str("subsystem", "tenant_manager").Logger(), 338 ) 339 340 pusherRegistry := pusher.NewRegistry[pusher.Factory]() 341 pusherRegistry.MustRegister(pusherV1.Name, pusherV1.NewPublisher) 342 pusherRegistry.MustRegister(pusherV2.Name, pusherV2.NewPublisher) 343 344 publisherFactory, err := pusherRegistry.Lookup(config.SelectedPublisher) 345 if err != nil { 346 return fmt.Errorf("creating publisher: %w", err) 347 } 348 349 publisher := publisherFactory(ctx, tm, zl.With().Str("subsystem", "publisher").Str("version", config.SelectedPublisher).Logger(), promRegisterer) 350 limits := limits.NewTenantLimits(tm) 351 secretProvider := secrets.NewSecretProvider(tm, 60*time.Second, zl.With().Str("subsystem", "secretstore").Logger()) 352 cals := cals.NewCostAttributionLabels(tm) 353 354 telemetry := telemetry.NewTelemeter( 355 ctx, uuid.New().String(), time.Duration(config.TelemetryTimeSpan)*time.Minute, 356 synthetic_monitoring.NewTelemetryClient(conn), 357 zl.With().Str("subsystem", "telemetry").Logger(), 358 promRegisterer, 359 ) 360 361 probeCh := make(chan *synthetic_monitoring.Probe, 1) 362 363 checksUpdater, err := checks.NewUpdater(checks.UpdaterOptions{ 364 Conn: conn, 365 Logger: zl.With().Str("subsystem", "updater").Logger(), 366 Backoff: newConnectionBackoff(), 367 Publisher: publisher, 368 TenantCh: tenantCh, 369 ProbeCh: probeCh, 370 IsConnected: readynessHandler.Set, 371 PromRegisterer: promRegisterer, 372 Features: features, 373 K6Runner: k6Runner, 374 ScraperFactory: scraper.New, 375 TenantLimits: limits, 376 SecretProvider: secretProvider, 377 Telemeter: telemetry, 378 UsageReporter: usageReporter, 379 CostAttributionLabels: cals, 380 LabellingMode: labelmode.New(tm), 381 SupportsProtocolSecrets: config.EnableProtocolSecrets, 382 }) 383 if err != nil { 384 return fmt.Errorf("cannot create checks updater: %w", err) 385 } 386 387 g.Go(func() error { 388 return checksUpdater.Run(ctx) 389 }) 390 391 adhocHandler, err := adhoc.NewHandler(adhoc.HandlerOpts{ 392 Conn: conn, 393 Logger: zl.With().Str("subsystem", "adhoc").Logger(), 394 Backoff: newConnectionBackoff(), 395 Publisher: publisher, 396 TenantCh: tenantCh, 397 PromRegisterer: promRegisterer, 398 Features: features, 399 K6Runner: k6Runner, 400 SecretProvider: secretProvider, 401 SupportsProtocolSecrets: config.EnableProtocolSecrets, 402 }) 403 if err != nil { 404 return fmt.Errorf("cannot create ad-hoc checks handler: %w", err) 405 } 406 407 g.Go(func() error { 408 return adhocHandler.Run(ctx) 409 }) 410 411 if config.PushTelemetry { 412 g.Go(func() error { 413 metricsHandler := metamonitoring.NewHandler(metamonitoring.HandlerOpts{ 414 Logger: zl.With().Str("subsystem", "metamonitoring").Logger(), 415 Registry: promRegisterer, 416 Publisher: publisher, 417 Interval: config.MetricsInterval, 418 ProbeCh: probeCh, 419 }) 420 421 return metricsHandler.Run(ctx) 422 }) 423 } 424 425 if k6Runner != nil { 426 k6VersionsLogger := zl.With().Str("subsystem", "k6versions").Logger() 427 428 k6VersionsHandler, err := k6version.NewHandler( 429 k6version.HandlerOpts{ 430 Logger: &k6VersionsLogger, 431 K6Runner: k6Runner, 432 K6Client: synthetic_monitoring.NewK6Client(conn), 433 }) 434 if err != nil { 435 return fmt.Errorf("cannot create k6versions handler: %w", err) 436 } 437 438 g.Go(func() error { 439 return k6VersionsHandler.Handle(ctx) 440 }) 441 } 442 443 return g.Wait() 444 } 445 446 func main() { 447 if err := run(os.Args, os.Stdout); err != nil { 448 fmt.Fprintf(os.Stderr, "E: %s\n", err) 449 os.Exit(exitFail) 450 } 451 } 452 453 func signalHandler(ctx context.Context, logger zerolog.Logger) error { 454 sigCh := make(chan os.Signal, 1) 455 456 signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM) 457 458 select { 459 case sig := <-sigCh: 460 logger.Info().Str("signal", sig.String()).Msg("shutting down") 461 return fmt.Errorf("got signal %s", sig) 462 463 case <-ctx.Done(): 464 logger.Info().Msg("shutting down") 465 return nil 466 } 467 } 468 469 func newConnectionBackoff() *backoff.Backoff { 470 return &backoff.Backoff{ 471 Min: 2 * time.Second, 472 Max: 30 * time.Second, 473 Factor: math.Pow(30./2., 1./8.), // reach the target in ~ 8 steps 474 Jitter: true, 475 } 476 } 477 478 func validateCIDR(ip string) error { 479 if ip != "" { 480 if _, _, err := net.ParseCIDR(ip); err != nil { 481 return err 482 } 483 } 484 485 return nil 486 } 487 488 func stringFromEnv(name string, override string) string { 489 if override != "" { 490 return override 491 } 492 493 return os.Getenv(name) 494 } 495 496 func notifyAboutDeprecatedFeatureFlags(features feature.Collection, zl zerolog.Logger) { 497 for _, ff := range []string{feature.K6, feature.Traceroute} { 498 if features.IsSet(ff) { 499 zl.Info().Msgf("the `%s` feature is now permanently enabled in the agent, you can remove it from the -features flag without loss of functionality", ff) 500 } 501 } 502 } 503 504 func setupGoMemLimit(ratio float64) error { 505 _, err := memlimit.SetGoMemLimitWithOpts( 506 memlimit.WithRatio(ratio), 507 memlimit.WithProvider( 508 memlimit.ApplyFallback( 509 memlimit.FromCgroup, // prefer cgroup limit if available 510 memlimit.FromSystem, // fallback to the system's memory 511 ), 512 ), 513 ) 514 if err != nil { 515 return fmt.Errorf("failed to set GOMEMLIMIT: %w", err) 516 } 517 518 return nil 519 } 520 521 func setupCache(ctx context.Context, cacheType cache.Kind, memcachedServers []string, localCapacity int, localTTL time.Duration, logger *zerolog.Logger) (cache.Cache, error) { 522 // Determine effective cache type with auto mode logic: 523 // auto + servers provided -> memcached -> local -> noop 524 // auto + no servers -> local -> noop 525 effectiveType := cacheType 526 if cacheType == cache.KindAuto { 527 if len(memcachedServers) > 0 { 528 effectiveType = cache.KindMemcached 529 } else { 530 effectiveType = cache.KindLocal 531 } 532 } 533 534 switch effectiveType { 535 case cache.KindMemcached: 536 if len(memcachedServers) == 0 { 537 logger.Warn().Msg("memcached type selected but no servers configured, falling back to local cache") 538 return setupLocalCache(localCapacity, localTTL, logger), nil 539 } 540 541 cacheConfig := cache.MemcachedConfig{ 542 Servers: memcachedServers, 543 Logger: logger.With().Str("subsystem", "cache").Logger(), 544 Timeout: 100 * time.Millisecond, 545 } 546 547 cacheClient, err := cache.NewMemcachedClient(ctx, cacheConfig) 548 if err != nil { 549 logger.Warn(). 550 Err(err). 551 Strs("servers", memcachedServers). 552 Msg("failed to initialize memcached cache") 553 554 return nil, fmt.Errorf("failed to initialize memcached client: %w", err) 555 } 556 557 logger.Info(). 558 Strs("servers", memcachedServers). 559 Msg("memcached cache initialized") 560 561 return cacheClient, nil 562 563 case cache.KindLocal: 564 return setupLocalCache(localCapacity, localTTL, logger), nil 565 566 case cache.KindNoop: 567 logger.Debug().Msg("noop cache selected") 568 return cache.NewNoop(logger.With().Str("subsystem", "cache").Logger()), nil 569 570 default: 571 logger.Warn(). 572 Stringer("type", effectiveType). 573 Msg("unknown cache type, falling back to local cache") 574 575 return setupLocalCache(localCapacity, localTTL, logger), nil 576 } 577 } 578 579 func setupLocalCache(capacity int, ttl time.Duration, logger *zerolog.Logger) cache.Cache { 580 localConfig := cache.LocalConfig{ 581 MaxCapacity: capacity, 582 InitialCapacity: capacity / 10, // 10% of max as initial capacity 583 DefaultTTL: ttl, 584 Logger: logger.With().Str("subsystem", "cache").Logger(), 585 } 586 587 localCache, err := cache.NewLocal(localConfig) 588 if err != nil { 589 logger.Warn(). 590 Err(err). 591 Int("capacity", capacity). 592 Dur("ttl", ttl). 593 Msg("failed to initialize local cache, using noop cache") 594 595 return cache.NewNoop(logger.With().Str("subsystem", "cache").Logger()) 596 } 597 598 logger.Info(). 599 Int("capacity", capacity). 600 Dur("ttl", ttl). 601 Msg("local cache initialized") 602 603 return localCache 604 }