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  }