github.com/hashicorp/packer@v1.14.3/packer/telemetry.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: BUSL-1.1 3 4 package packer 5 6 import ( 7 "context" 8 "fmt" 9 "log" 10 "os" 11 "path/filepath" 12 "sort" 13 "time" 14 15 checkpoint "github.com/hashicorp/go-checkpoint" 16 "github.com/hashicorp/packer-plugin-sdk/pathing" 17 packerVersion "github.com/hashicorp/packer/version" 18 "github.com/zclconf/go-cty/cty" 19 ) 20 21 type PackerTemplateType string 22 23 const ( 24 UnknownTemplate PackerTemplateType = "Unknown" 25 HCL2Template PackerTemplateType = "HCL2" 26 JSONTemplate PackerTemplateType = "JSON" 27 ) 28 29 const TelemetryVersion string = "beta/packer/7" 30 const TelemetryPanicVersion string = "beta/packer_panic/4" 31 32 var CheckpointReporter *CheckpointTelemetry 33 34 type PackerReport struct { 35 Spans []*TelemetrySpan `json:"spans"` 36 ExitCode int `json:"exit_code"` 37 Error string `json:"error"` 38 Command string `json:"command"` 39 TemplateType PackerTemplateType `json:"template_type"` 40 UseBundled bool `json:"use_bundled"` 41 } 42 43 type CheckpointTelemetry struct { 44 spans []*TelemetrySpan 45 signatureFile string 46 startTime time.Time 47 templateType PackerTemplateType 48 useBundled bool 49 } 50 51 func NewCheckpointReporter(disableSignature bool) *CheckpointTelemetry { 52 if disabled := os.Getenv("CHECKPOINT_DISABLE"); disabled != "" { 53 return nil 54 } 55 56 configDir, err := pathing.ConfigDir() 57 if err != nil { 58 log.Printf("[WARN] (telemetry) setup error: %s", err) 59 return nil 60 } 61 62 signatureFile := "" 63 if disableSignature { 64 log.Printf("[INFO] (telemetry) Checkpoint signature disabled") 65 } else { 66 signatureFile = filepath.Join(configDir, "checkpoint_signature") 67 } 68 69 return &CheckpointTelemetry{ 70 signatureFile: signatureFile, 71 startTime: time.Now().UTC(), 72 templateType: UnknownTemplate, 73 } 74 } 75 76 func (c *CheckpointTelemetry) baseParams(prefix string) *checkpoint.ReportParams { 77 version := packerVersion.Version 78 if packerVersion.VersionPrerelease != "" { 79 version += "-" + packerVersion.VersionPrerelease 80 } 81 82 return &checkpoint.ReportParams{ 83 Product: "packer", 84 SchemaVersion: prefix, 85 StartTime: c.startTime, 86 Version: version, 87 RunID: os.Getenv("PACKER_RUN_UUID"), 88 SignatureFile: c.signatureFile, 89 } 90 } 91 92 func (c *CheckpointTelemetry) ReportPanic(m string) error { 93 if c == nil { 94 return nil 95 } 96 panicParams := c.baseParams(TelemetryPanicVersion) 97 panicParams.Payload = m 98 panicParams.EndTime = time.Now().UTC() 99 100 // This timeout can be longer because it runs in the real main. 101 // We're also okay waiting a bit longer to collect panic information 102 ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) 103 defer cancel() 104 105 return checkpoint.Report(ctx, panicParams) 106 } 107 108 func (c *CheckpointTelemetry) AddSpan(name, pluginType string, options interface{}) *TelemetrySpan { 109 if c == nil { 110 return nil 111 } 112 log.Printf("[INFO] (telemetry) Starting %s %s", pluginType, name) 113 114 ts := &TelemetrySpan{ 115 Name: name, 116 Options: flattenConfigKeys(options), 117 StartTime: time.Now().UTC(), 118 Type: pluginType, 119 } 120 c.spans = append(c.spans, ts) 121 return ts 122 } 123 124 // SetTemplateType registers the template type being processed for a Packer command 125 func (c *CheckpointTelemetry) SetTemplateType(t PackerTemplateType) { 126 if c == nil { 127 return 128 } 129 130 c.templateType = t 131 } 132 133 // SetBundledUsage marks the template as using bundled plugins 134 func (c *CheckpointTelemetry) SetBundledUsage() { 135 if c == nil { 136 return 137 } 138 c.useBundled = true 139 } 140 141 func (c *CheckpointTelemetry) Finalize(command string, errCode int, err error) error { 142 if c == nil { 143 return nil 144 } 145 146 params := c.baseParams(TelemetryVersion) 147 params.EndTime = time.Now().UTC() 148 149 extra := &PackerReport{ 150 Spans: c.spans, 151 ExitCode: errCode, 152 Command: command, 153 } 154 if err != nil { 155 extra.Error = err.Error() 156 } 157 158 extra.UseBundled = c.useBundled 159 extra.TemplateType = c.templateType 160 params.Payload = extra 161 // b, _ := json.MarshalIndent(params, "", " ") 162 // log.Println(string(b)) 163 164 ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) 165 defer cancel() 166 167 log.Printf("[INFO] (telemetry) Finalizing.") 168 return checkpoint.Report(ctx, params) 169 } 170 171 type TelemetrySpan struct { 172 EndTime time.Time `json:"end_time"` 173 Error string `json:"error"` 174 Name string `json:"name"` 175 Options []string `json:"options"` 176 StartTime time.Time `json:"start_time"` 177 Type string `json:"type"` 178 } 179 180 func (s *TelemetrySpan) End(err error) { 181 if s == nil { 182 return 183 } 184 s.EndTime = time.Now().UTC() 185 log.Printf("[INFO] (telemetry) ending %s", s.Name) 186 if err != nil { 187 s.Error = err.Error() 188 } 189 } 190 191 func flattenConfigKeys(options interface{}) []string { 192 var flatten func(string, interface{}) []string 193 194 flatten = func(prefix string, options interface{}) (strOpts []string) { 195 switch opt := options.(type) { 196 case map[string]interface{}: 197 return flattenJSON(prefix, options) 198 case cty.Value: 199 return flattenHCL(prefix, opt) 200 default: 201 return nil 202 } 203 } 204 205 flattened := flatten("", options) 206 sort.Strings(flattened) 207 return flattened 208 } 209 210 func flattenJSON(prefix string, options interface{}) (strOpts []string) { 211 if m, ok := options.(map[string]interface{}); ok { 212 for k, v := range m { 213 if prefix != "" { 214 k = prefix + "/" + k 215 } 216 if n, ok := v.(map[string]interface{}); ok { 217 strOpts = append(strOpts, flattenJSON(k, n)...) 218 } else { 219 strOpts = append(strOpts, k) 220 } 221 } 222 } 223 return 224 } 225 226 func flattenHCL(prefix string, v cty.Value) (args []string) { 227 if v.IsNull() { 228 return []string{} 229 } 230 t := v.Type() 231 switch { 232 case t.IsObjectType(), t.IsMapType(): 233 if !v.IsKnown() { 234 return []string{} 235 } 236 it := v.ElementIterator() 237 for it.Next() { 238 key, val := it.Element() 239 keyStr := key.AsString() 240 241 if val.IsNull() { 242 continue 243 } 244 245 if prefix != "" { 246 keyStr = fmt.Sprintf("%s/%s", prefix, keyStr) 247 } 248 249 if val.Type().IsObjectType() || val.Type().IsMapType() { 250 args = append(args, flattenHCL(keyStr, val)...) 251 } else { 252 args = append(args, keyStr) 253 } 254 } 255 } 256 return args 257 }