diff --git a/builder.go b/builder.go index 3f8502304..25d401861 100644 --- a/builder.go +++ b/builder.go @@ -2,23 +2,22 @@ package lifecycle import ( "fmt" - "io" - "os" - "path/filepath" - "sort" - - "github.com/pkg/errors" - "github.com/buildpacks/lifecycle/api" "github.com/buildpacks/lifecycle/buildpack" "github.com/buildpacks/lifecycle/env" "github.com/buildpacks/lifecycle/internal/encoding" "github.com/buildpacks/lifecycle/internal/fsutil" + "github.com/buildpacks/lifecycle/internal/telemetry" "github.com/buildpacks/lifecycle/launch" "github.com/buildpacks/lifecycle/layers" "github.com/buildpacks/lifecycle/log" "github.com/buildpacks/lifecycle/platform" "github.com/buildpacks/lifecycle/platform/files" + "github.com/pkg/errors" + "io" + "os" + "path/filepath" + "sort" ) type Platform interface { @@ -34,18 +33,19 @@ type BuildEnv interface { } type Builder struct { - AppDir string - BuildConfigDir string - LayersDir string - PlatformDir string - BuildExecutor buildpack.BuildExecutor - DirStore DirStore - Group buildpack.Group - Logger log.Logger - Out, Err io.Writer - Plan files.Plan - PlatformAPI *api.Version - AnalyzeMD files.Analyzed + AppDir string + BuildConfigDir string + LayersDir string + PlatformDir string + BuildExecutor buildpack.BuildExecutor + DirStore DirStore + Group buildpack.Group + Logger log.Logger + Out, Err io.Writer + Plan files.Plan + PlatformAPI *api.Version + AnalyzeMD files.Analyzed + TelemetrySender telemetry.TelemetrySender } func (b *Builder) Build() (*files.BuildMetadata, error) { @@ -85,7 +85,9 @@ func (b *Builder) Build() (*files.BuildMetadata, error) { b.Logger.Debug("Finding plan") inputs.Plan = filteredPlan.Find(buildpack.KindBuildpack, bp.ID) + callback := b.TelemetrySender.Log(telemetry.EventBuildpackBuild, "id", bp.ID, "version", bp.Version) br, err := b.BuildExecutor.Build(*bpTOML, inputs, b.Logger) + callback(err) if err != nil { return nil, err } diff --git a/cmd/lifecycle/builder.go b/cmd/lifecycle/builder.go index 06d56fc58..939103d40 100644 --- a/cmd/lifecycle/builder.go +++ b/cmd/lifecycle/builder.go @@ -2,6 +2,7 @@ package main import ( "errors" + "github.com/buildpacks/lifecycle/internal/telemetry" "github.com/BurntSushi/toml" @@ -74,20 +75,24 @@ func (b *buildCmd) Exec() error { } func (b *buildCmd) build(group buildpack.Group, plan files.Plan, analyzedMD files.Analyzed) error { + telemetrySender := telemetry.NewAISender(telemetry.InstrumentationKey) + defer telemetrySender.Shutdown() + builder := &lifecycle.Builder{ - AppDir: b.AppDir, - BuildConfigDir: b.BuildConfigDir, - LayersDir: b.LayersDir, - PlatformDir: b.PlatformDir, - BuildExecutor: &buildpack.DefaultBuildExecutor{}, - DirStore: platform.NewDirStore(b.BuildpacksDir, ""), - Group: group, - Logger: cmd.DefaultLogger, - Out: cmd.Stdout, - Err: cmd.Stderr, - Plan: plan, - PlatformAPI: b.PlatformAPI, - AnalyzeMD: analyzedMD, + AppDir: b.AppDir, + BuildConfigDir: b.BuildConfigDir, + LayersDir: b.LayersDir, + PlatformDir: b.PlatformDir, + BuildExecutor: &buildpack.DefaultBuildExecutor{}, + DirStore: platform.NewDirStore(b.BuildpacksDir, ""), + Group: group, + Logger: cmd.DefaultLogger, + Out: cmd.Stdout, + Err: cmd.Stderr, + Plan: plan, + PlatformAPI: b.PlatformAPI, + AnalyzeMD: analyzedMD, + TelemetrySender: telemetrySender, } md, err := builder.Build() if err != nil { diff --git a/cmd/lifecycle/cli/command.go b/cmd/lifecycle/cli/command.go index e25812e07..0e087c6eb 100644 --- a/cmd/lifecycle/cli/command.go +++ b/cmd/lifecycle/cli/command.go @@ -1,12 +1,12 @@ package cli import ( + "github.com/buildpacks/lifecycle/cmd" + "github.com/buildpacks/lifecycle/internal/telemetry" + "github.com/buildpacks/lifecycle/platform" "io" "log" "os" - - "github.com/buildpacks/lifecycle/cmd" - "github.com/buildpacks/lifecycle/platform" ) // Command defines the interface for running the lifecycle phases @@ -78,5 +78,14 @@ func Run(c Command, withPhaseName string, asSubcommand bool) { cmd.Exit(err) } cmd.DefaultLogger.Debugf("Executing command...") - cmd.Exit(c.Exec()) + cmd.Exit(execWithTelemetry(c.Exec, withPhaseName)) +} + +func execWithTelemetry(f func() error, withPhaseName string) error { + telemetrySender := telemetry.NewAISender(telemetry.InstrumentationKey) + defer telemetrySender.Shutdown() + callback := telemetrySender.Log(telemetry.EventLifecyclePhase, "command", withPhaseName) + err := f() + callback(err) + return err } diff --git a/go.mod b/go.mod index a00cad9c6..ceb19a95e 100644 --- a/go.mod +++ b/go.mod @@ -4,9 +4,7 @@ require ( github.com/BurntSushi/toml v1.3.2 github.com/GoogleContainerTools/kaniko v1.12.1 github.com/apex/log v1.9.0 - github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20230522190001-adf1bafd791a github.com/buildpacks/imgutil v0.0.0-20230626185301-726f02e4225c - github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589 github.com/containerd/containerd v1.7.2 github.com/docker/docker v24.0.2+incompatible github.com/docker/go-connections v0.4.0 @@ -15,6 +13,7 @@ require ( github.com/google/go-containerregistry v0.15.2 github.com/google/uuid v1.3.0 github.com/heroku/color v0.0.6 + github.com/microsoft/ApplicationInsights-Go v0.4.4 github.com/moby/buildkit v0.11.6 github.com/pkg/errors v0.9.1 github.com/sclevine/spec v1.4.0 @@ -25,6 +24,7 @@ require ( require ( cloud.google.com/go/compute v1.20.0 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect + code.cloudfoundry.org/clock v1.0.0 // indirect github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/Azure/go-autorest v14.2.0+incompatible // indirect @@ -51,8 +51,10 @@ require ( github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.12 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.19.2 // indirect github.com/aws/smithy-go v1.13.5 // indirect + github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20230522190001-adf1bafd791a // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589 // indirect github.com/cilium/ebpf v0.10.0 // indirect github.com/containerd/cgroups v1.1.0 // indirect github.com/containerd/continuity v0.4.1 // indirect @@ -69,6 +71,7 @@ require ( github.com/docker/go-units v0.5.0 // indirect github.com/ePirat/docker-credential-gitlabci v1.0.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/gofrs/uuid v3.3.0+incompatible // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang/protobuf v1.5.3 // indirect diff --git a/go.sum b/go.sum index 739d653e8..a630cf988 100644 --- a/go.sum +++ b/go.sum @@ -39,6 +39,9 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +code.cloudfoundry.org/clock v0.0.0-20180518195852-02e53af36e6c/go.mod h1:QD9Lzhd/ux6eNQVUDVRJX/RKTigpewimNYBi7ivZKY8= +code.cloudfoundry.org/clock v1.0.0 h1:kFXWQM4bxYvdBw2X8BbBeXwQNgfoWv1vqAk2ZZyBN2o= +code.cloudfoundry.org/clock v1.0.0/go.mod h1:QD9Lzhd/ux6eNQVUDVRJX/RKTigpewimNYBi7ivZKY8= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= @@ -210,6 +213,8 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gofrs/uuid v3.3.0+incompatible h1:8K4tyRfvU1CYPgJsveYFQMhpFd/wXNM7iK6rR7UHz84= +github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= @@ -341,6 +346,8 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5 github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/microsoft/ApplicationInsights-Go v0.4.4 h1:G4+H9WNs6ygSCe6sUyxRc2U81TI5Es90b2t/MwX5KqY= +github.com/microsoft/ApplicationInsights-Go v0.4.4/go.mod h1:fKRUseBqkw6bDiXTs3ESTiU/4YTIHsQS4W3fP2ieF4U= github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g= github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= @@ -373,6 +380,7 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= @@ -447,6 +455,7 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/tedsuo/ifrit v0.0.0-20180802180643-bea94bb476cc/go.mod h1:eyZnKCc955uh98WQvzOm0dgAeLnf2O0Rz0LPoC5ze+0= github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0= github.com/tj/assert v0.0.3 h1:Df/BlaZ20mq6kuai7f5z2TvPFiwC3xaWJSDQNiIS3Rk= github.com/tj/assert v0.0.3/go.mod h1:Ne6X72Q+TB1AteidzQncjw9PabbMp4PBMZ1k+vd1Pvk= diff --git a/internal/telemetry/sender.go b/internal/telemetry/sender.go new file mode 100644 index 000000000..bf58e5737 --- /dev/null +++ b/internal/telemetry/sender.go @@ -0,0 +1,89 @@ +package telemetry + +import ( + "os" + "strconv" + "time" + + "github.com/microsoft/ApplicationInsights-Go/appinsights" +) + +var ( + EnvCorrelationIdKey = "CORRELATION_ID" + EnvSubscriptionIdKey = "BP_SUBSCRIPTION_ID" + EnvCallerIdKey = "CALLER_ID" + + EventLifecyclePhase = "lifecycle.phase" + EventBuildpackBuild = "buildpack.build" + + InstrumentationKey = "55a59292-dbe1-4610-9be1-5ad2440bbbd7" +) + +type TelemetrySender interface { + Shutdown() error + Log(metricName string, keyValuePair ...string) func(err error) +} + +type Metric struct { + Name string + Properties map[string]string + Measurements map[string]float64 +} + +type aisender struct { + c appinsights.TelemetryClient +} + +func NewAISender(ikey string) TelemetrySender { + return &aisender{c: appinsights.NewTelemetryClient(ikey)} +} + +func (a aisender) send(m Metric) { + event := appinsights.NewEventTelemetry(m.Name) + event.Properties = m.Properties + event.Measurements = m.Measurements + if event.Properties == nil { + event.Properties = make(map[string]string) + } + event.Properties["correlationId"] = os.Getenv(EnvCorrelationIdKey) + event.Properties["subscriptionId"] = os.Getenv(EnvSubscriptionIdKey) + event.Properties["callerId"] = os.Getenv(EnvCallerIdKey) + a.c.Track(event) +} + +func (a aisender) Shutdown() error { + select { + case <-a.c.Channel().Close(10 * time.Second): + // If we got here, then all telemetry was submitted + // successfully, and we can proceed to exiting. + case <-time.After(30 * time.Second): + } + return nil +} + +func (s aisender) Log(name string, dims ...string) func(err error) { + timer := Timer{} + timer.Start() + + return func(err error) { + metric := Metric{ + Name: name, + Properties: make(map[string]string), + Measurements: make(map[string]float64), + } + if err != nil { + metric.Properties["result"] = "failed" + metric.Properties["exitCode"] = strconv.Itoa(ExitCode(err)) + metric.Properties["errorMessage"] = err.Error() + } else { + metric.Properties["result"] = "success" + } + + for i := 0; i < len(dims); i = i + 2 { + metric.Properties[dims[i]] = dims[i+1] + } + + metric.Measurements["durationInMs"] = float64(timer.Stop()) + s.send(metric) + } +} diff --git a/internal/telemetry/util.go b/internal/telemetry/util.go new file mode 100644 index 000000000..1da123d01 --- /dev/null +++ b/internal/telemetry/util.go @@ -0,0 +1,34 @@ +package telemetry + +import ( + "github.com/buildpacks/lifecycle/buildpack" + "github.com/buildpacks/lifecycle/cmd" + "os/exec" + "time" +) + +func ExitCode(err error) int { + if bpe, ok := err.(*buildpack.Error); ok && bpe.Cause() != nil { + return ExitCode(bpe.Cause()) + } + + if ee, ok := err.(*exec.ExitError); ok { + return ee.ExitCode() + } + if ef, ok := err.(*cmd.ErrorFail); ok { + return ef.Code + } + return cmd.CodeForFailed +} + +type Timer struct { + start int64 +} + +func (t *Timer) Start() { + t.start = time.Now().UnixMilli() +} + +func (t *Timer) Stop() int64 { + return time.Now().UnixMilli() - t.start +}