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  }