package packer import ( "context" "log" "os" "path/filepath" "sort" "time" checkpoint "github.com/hashicorp/go-checkpoint" "github.com/hashicorp/packer/packer-plugin-sdk/pathing" packerVersion "github.com/hashicorp/packer/version" ) const TelemetryVersion string = "beta/packer/5" const TelemetryPanicVersion string = "beta/packer_panic/4" var CheckpointReporter *CheckpointTelemetry type PackerReport struct { Spans []*TelemetrySpan `json:"spans"` ExitCode int `json:"exit_code"` Error string `json:"error"` Command string `json:"command"` } type CheckpointTelemetry struct { spans []*TelemetrySpan signatureFile string startTime time.Time } func NewCheckpointReporter(disableSignature bool) *CheckpointTelemetry { if disabled := os.Getenv("CHECKPOINT_DISABLE"); disabled != "" { return nil } configDir, err := pathing.ConfigDir() if err != nil { log.Printf("[WARN] (telemetry) setup error: %s", err) return nil } signatureFile := "" if disableSignature { log.Printf("[INFO] (telemetry) Checkpoint signature disabled") } else { signatureFile = filepath.Join(configDir, "checkpoint_signature") } return &CheckpointTelemetry{ signatureFile: signatureFile, startTime: time.Now().UTC(), } } func (c *CheckpointTelemetry) baseParams(prefix string) *checkpoint.ReportParams { version := packerVersion.Version if packerVersion.VersionPrerelease != "" { version += "-" + packerVersion.VersionPrerelease } return &checkpoint.ReportParams{ Product: "packer", SchemaVersion: prefix, StartTime: c.startTime, Version: version, RunID: os.Getenv("PACKER_RUN_UUID"), SignatureFile: c.signatureFile, } } func (c *CheckpointTelemetry) ReportPanic(m string) error { if c == nil { return nil } panicParams := c.baseParams(TelemetryPanicVersion) panicParams.Payload = m panicParams.EndTime = time.Now().UTC() // This timeout can be longer because it runs in the real main. // We're also okay waiting a bit longer to collect panic information ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() return checkpoint.Report(ctx, panicParams) } func (c *CheckpointTelemetry) AddSpan(name, pluginType string, options interface{}) *TelemetrySpan { if c == nil { return nil } log.Printf("[INFO] (telemetry) Starting %s %s", pluginType, name) ts := &TelemetrySpan{ Name: name, Options: flattenConfigKeys(options), StartTime: time.Now().UTC(), Type: pluginType, } c.spans = append(c.spans, ts) return ts } func (c *CheckpointTelemetry) Finalize(command string, errCode int, err error) error { if c == nil { return nil } params := c.baseParams(TelemetryVersion) params.EndTime = time.Now().UTC() extra := &PackerReport{ Spans: c.spans, ExitCode: errCode, Command: command, } if err != nil { extra.Error = err.Error() } params.Payload = extra // b, _ := json.MarshalIndent(params, "", " ") // log.Println(string(b)) ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() log.Printf("[INFO] (telemetry) Finalizing.") return checkpoint.Report(ctx, params) } type TelemetrySpan struct { EndTime time.Time `json:"end_time"` Error string `json:"error"` Name string `json:"name"` Options []string `json:"options"` StartTime time.Time `json:"start_time"` Type string `json:"type"` } func (s *TelemetrySpan) End(err error) { if s == nil { return } s.EndTime = time.Now().UTC() log.Printf("[INFO] (telemetry) ending %s", s.Name) if err != nil { s.Error = err.Error() } } func flattenConfigKeys(options interface{}) []string { var flatten func(string, interface{}) []string flatten = func(prefix string, options interface{}) (strOpts []string) { if m, ok := options.(map[string]interface{}); ok { for k, v := range m { if prefix != "" { k = prefix + "/" + k } if n, ok := v.(map[string]interface{}); ok { strOpts = append(strOpts, flatten(k, n)...) } else { strOpts = append(strOpts, k) } } } return } flattened := flatten("", options) sort.Strings(flattened) return flattened }