diff --git a/.golangci.yml b/.golangci.yml index 6328f000c..c3df950cc 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -24,3 +24,12 @@ linters: - revive fast: false max-same-issues: 50 + +linters-settings: + revive: + ignore-generated-header: false + severity: warning + rules: + - name: unused-parameter + severity: warning + disabled: true diff --git a/.goreleaser.yml b/.goreleaser.yml index 2955682f3..09632e787 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -25,6 +25,7 @@ builds: - -s - -w - -X "github.com/acorn-io/runtime/pkg/version.Tag=v{{ .Version }}" + - -X 'github.com/acorn-io/runtime/pkg/usage.BaseURL="{{ .Env.USAGE_METRICS_URL }}" universal_binaries: - id: mac diff --git a/docs/docs/100-reference/01-command-line/acorn_install.md b/docs/docs/100-reference/01-command-line/acorn_install.md index 3a9a4ced4..3d380a90b 100644 --- a/docs/docs/100-reference/01-command-line/acorn_install.md +++ b/docs/docs/100-reference/01-command-line/acorn_install.md @@ -43,6 +43,7 @@ acorn install --controller-memory string The memory to allocate to the runtime-controller in the format of : (example 256Mi:1Gi) --controller-replicas int acorn-controller deployment replica count --controller-service-account-annotation strings annotation to apply to the acorn-system service account + --disable-usage-metrics Disable usage metrics collection (default is false) --event-ttl string Amount of time an Acorn event will be stored before being deleted (default '168h' - 7 days) --features strings Enable or disable features. (example foo=true,bar=false) -h, --help help for install diff --git a/pkg/apis/api.acorn.io/v1/types.go b/pkg/apis/api.acorn.io/v1/types.go index a7708ce37..30146ff4e 100644 --- a/pkg/apis/api.acorn.io/v1/types.go +++ b/pkg/apis/api.acorn.io/v1/types.go @@ -541,6 +541,9 @@ type Config struct { RegistryCPU *string `json:"registryCPU" name:"registry-cpu" usage:"The CPU to allocate to the registry in the format of : (example 200m:1000m)"` IgnoreResourceRequirements *bool `json:"ignoreResourceRequirements" name:"ignore-resource-requirements" usage:"Ignore memory and CPU requests and limits, intended for local development (default is false)"` RequireComputeClass *bool `json:"requireComputeClass" name:"require-compute-class" usage:"Require applications to have a Compute Class set (default is false)"` + + // Usage Metrics + DisableUsageMetrics *bool `json:"disableUsageMetrics" name:"disable-usage-metrics" usage:"Disable usage metrics collection (default is false)"` } type EncryptionKey struct { diff --git a/pkg/apis/api.acorn.io/v1/zz_generated.deepcopy.go b/pkg/apis/api.acorn.io/v1/zz_generated.deepcopy.go index fed62576e..179d85526 100644 --- a/pkg/apis/api.acorn.io/v1/zz_generated.deepcopy.go +++ b/pkg/apis/api.acorn.io/v1/zz_generated.deepcopy.go @@ -629,6 +629,11 @@ func (in *Config) DeepCopyInto(out *Config) { *out = new(bool) **out = **in } + if in.DisableUsageMetrics != nil { + in, out := &in.DisableUsageMetrics, &out.DisableUsageMetrics + *out = new(bool) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Config. diff --git a/pkg/config/config.go b/pkg/config/config.go index 7e69bb489..c36eb268d 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -164,6 +164,9 @@ func complete(ctx context.Context, c *apiv1.Config, getter kclient.Reader, inclu if c.AutoConfigureKarpenterDontEvictAnnotations == nil { c.AutoConfigureKarpenterDontEvictAnnotations = profile.AutoConfigureKarpenterDontEvictAnnotations } + if c.DisableUsageMetrics == nil { + c.DisableUsageMetrics = profile.DisableUsageMetrics + } return nil } @@ -471,6 +474,9 @@ func merge(oldConfig, newConfig *apiv1.Config) *apiv1.Config { if newConfig.AutoConfigureKarpenterDontEvictAnnotations != nil { mergedConfig.AutoConfigureKarpenterDontEvictAnnotations = newConfig.AutoConfigureKarpenterDontEvictAnnotations } + if newConfig.DisableUsageMetrics != nil { + mergedConfig.DisableUsageMetrics = newConfig.DisableUsageMetrics + } return &mergedConfig } diff --git a/pkg/controller/controller.go b/pkg/controller/controller.go index 4877d167a..407a90377 100644 --- a/pkg/controller/controller.go +++ b/pkg/controller/controller.go @@ -21,6 +21,8 @@ import ( "github.com/acorn-io/runtime/pkg/logserver" "github.com/acorn-io/runtime/pkg/scheme" "github.com/acorn-io/runtime/pkg/system" + "github.com/acorn-io/runtime/pkg/usage" + "github.com/acorn-io/runtime/pkg/version" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/wait" "sigs.k8s.io/controller-runtime/pkg/client" @@ -115,6 +117,8 @@ func (c *Controller) Start(ctx context.Context) error { logserver.StartServerWithDefaults() + go usage.Heartbeat(ctx, c.client, usage.ComponentController, 24*time.Hour, version.Get().String()) + // Every 5 minutes, delete EventInstances until only the most recent 1000 remain. // Use c.Router.Backend() to ensure we hit the cache when possible. // Note: the cache will only be populated for EventInstances if a handler for EventInstances has been registered. diff --git a/pkg/install/install.go b/pkg/install/install.go index 00b6e409d..e56b5e97c 100644 --- a/pkg/install/install.go +++ b/pkg/install/install.go @@ -31,6 +31,8 @@ import ( "github.com/acorn-io/runtime/pkg/scheme" "github.com/acorn-io/runtime/pkg/system" "github.com/acorn-io/runtime/pkg/term" + "github.com/acorn-io/runtime/pkg/usage" + "github.com/acorn-io/runtime/pkg/version" "github.com/acorn-io/z" "github.com/pterm/pterm" "github.com/sirupsen/logrus" @@ -308,6 +310,9 @@ func Install(ctx context.Context, image string, opts *Options) error { if !opts.Quiet { pterm.Success.Println("Installation done") } + + _ = usage.Pulse(ctx, c, usage.ComponentCLI, usage.ActionInstall, version.Get().String()) + return nil } diff --git a/pkg/openapi/generated/openapi_generated.go b/pkg/openapi/generated/openapi_generated.go index af3bfa039..67ec6241e 100644 --- a/pkg/openapi/generated/openapi_generated.go +++ b/pkg/openapi/generated/openapi_generated.go @@ -2628,8 +2628,15 @@ func schema_pkg_apis_apiacornio_v1_Config(ref common.ReferenceCallback) common.O Format: "", }, }, + "disableUsageMetrics": { + SchemaProps: spec.SchemaProps{ + Description: "Usage Metrics", + Type: []string{"boolean"}, + Format: "", + }, + }, }, - Required: []string{"ingressClassName", "clusterDomains", "letsEncrypt", "letsEncryptEmail", "letsEncryptTOSAgree", "setPodSecurityEnforceProfile", "podSecurityEnforceProfile", "httpEndpointPattern", "internalClusterDomain", "acornDNS", "acornDNSEndpoint", "autoUpgradeInterval", "recordBuilds", "publishBuilders", "builderPerProject", "internalRegistryPrefix", "ignoreUserLabelsAndAnnotations", "allowUserLabels", "allowUserAnnotations", "allowUserMetadataNamespaces", "workloadMemoryDefault", "workloadMemoryMaximum", "useCustomCABundle", "propagateProjectAnnotations", "propagateProjectLabels", "manageVolumeClasses", "volumeSizeDefault", "networkPolicies", "ingressControllerNamespace", "allowTrafficFromNamespace", "serviceLBAnnotations", "awsIdentityProviderArn", "eventTTL", "features", "certManagerIssuer", "profile", "autoConfigureKarpenterDontEvictAnnotations", "controllerMemory", "controllerCPU", "apiServerMemory", "apiServerCPU", "buildkitdMemory", "buildkitdCPU", "buildkitdServiceMemory", "buildkitdServiceCPU", "registryMemory", "registryCPU", "ignoreResourceRequirements", "requireComputeClass"}, + Required: []string{"ingressClassName", "clusterDomains", "letsEncrypt", "letsEncryptEmail", "letsEncryptTOSAgree", "setPodSecurityEnforceProfile", "podSecurityEnforceProfile", "httpEndpointPattern", "internalClusterDomain", "acornDNS", "acornDNSEndpoint", "autoUpgradeInterval", "recordBuilds", "publishBuilders", "builderPerProject", "internalRegistryPrefix", "ignoreUserLabelsAndAnnotations", "allowUserLabels", "allowUserAnnotations", "allowUserMetadataNamespaces", "workloadMemoryDefault", "workloadMemoryMaximum", "useCustomCABundle", "propagateProjectAnnotations", "propagateProjectLabels", "manageVolumeClasses", "volumeSizeDefault", "networkPolicies", "ingressControllerNamespace", "allowTrafficFromNamespace", "serviceLBAnnotations", "awsIdentityProviderArn", "eventTTL", "features", "certManagerIssuer", "profile", "autoConfigureKarpenterDontEvictAnnotations", "controllerMemory", "controllerCPU", "apiServerMemory", "apiServerCPU", "buildkitdMemory", "buildkitdCPU", "buildkitdServiceMemory", "buildkitdServiceCPU", "registryMemory", "registryCPU", "ignoreResourceRequirements", "requireComputeClass", "disableUsageMetrics"}, }, }, } diff --git a/pkg/profiles/default.go b/pkg/profiles/default.go index b17c80d22..4600bbf0e 100644 --- a/pkg/profiles/default.go +++ b/pkg/profiles/default.go @@ -76,5 +76,6 @@ func defaultProfile() apiv1.Config { IgnoreResourceRequirements: new(bool), RequireComputeClass: new(bool), AutoConfigureKarpenterDontEvictAnnotations: z.Pointer(true), + DisableUsageMetrics: new(bool), } } diff --git a/pkg/profiles/production.go b/pkg/profiles/production.go index 501caa77f..23616fbf9 100644 --- a/pkg/profiles/production.go +++ b/pkg/profiles/production.go @@ -37,5 +37,7 @@ func productionProfile() apiv1.Config { conf.RequireComputeClass = z.Pointer(true) conf.AutoConfigureKarpenterDontEvictAnnotations = z.Pointer(true) + conf.DisableUsageMetrics = z.Pointer(false) + return conf } diff --git a/pkg/usage/usage.go b/pkg/usage/usage.go new file mode 100644 index 000000000..e67ea62fc --- /dev/null +++ b/pkg/usage/usage.go @@ -0,0 +1,83 @@ +package usage + +import ( + "context" + "fmt" + "net/http" + "os" + "strings" + "time" + + "github.com/acorn-io/runtime/pkg/config" + "github.com/acorn-io/runtime/pkg/version" + "github.com/acorn-io/z" + "github.com/sirupsen/logrus" + "k8s.io/apimachinery/pkg/util/wait" + kclient "sigs.k8s.io/controller-runtime/pkg/client" +) + +type RuntimeComponent string +type RuntimeAction string + +const ( + BaseURL = "" + + ComponentCLI RuntimeComponent = "cli" + ComponentAPIServer RuntimeComponent = "api-server" + ComponentController RuntimeComponent = "controller" + + ActionInstall RuntimeAction = "install" // subpaths: /install/{version} + + ActionHeartbeat RuntimeAction = "heartbeat" // subpaths: /heartbeat/{version} + + EnvUsageMetrics = "ACORN_USAGE_METRICS" +) + +func Pulse(ctx context.Context, c kclient.Client, component RuntimeComponent, action RuntimeAction, elements ...string) error { + if !UsageMetricsEnabled(ctx, c) { + return nil + } + + url := fmt.Sprintf("%s/%s/%s", BaseURL, component, action) + for _, e := range elements { + url += "/" + e + } + + resp, err := http.Get(url) + if err != nil { + return err + } + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + return nil +} + +func Heartbeat(ctx context.Context, c kclient.Client, component RuntimeComponent, interval time.Duration, elements ...string) { + wait.UntilWithContext(ctx, func(_ context.Context) { + err := Pulse(ctx, c, component, ActionHeartbeat, elements...) + if err != nil { + logrus.Warnf("failed to send heartbeat for %q: %v", component, err) + } + }, interval) +} + +// UsageMetricsEnabled returns true if usage metrics are enabled. +// Usage Metrics are enabled by default, but are disabled by +// a) setting the ACORN_USAGE_METRICS environment variable to "disabled" +// b) setting the DisableUsageMetrics field in the acorn config to true +// c) running a development build (dirty or tag ending in -dev) +// d) running in an unofficial build (BaseURL is empty) +func UsageMetricsEnabled(ctx context.Context, c kclient.Client) bool { + enabled := true + if c != nil { + cfg, err := config.Get(ctx, c) + if err != nil { + return false + } + enabled = !z.Dereference(cfg.DisableUsageMetrics) + } + return enabled && os.Getenv(EnvUsageMetrics) != "disabled" && !version.Get().Dirty && !strings.HasSuffix(version.Get().Tag, "-dev") && BaseURL != "" +}