github.com/GoogleCloudPlatform/compute-image-tools/cli_tools@v0.0.0-20240516224744-de2dabc4ed1b/common/utils/logging/service/logger.go (about)

     1  //  Copyright 2019 Google Inc. All Rights Reserved.
     2  //
     3  //  Licensed under the Apache License, Version 2.0 (the "License");
     4  //  you may not use this file except in compliance with the License.
     5  //  You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  //  Unless required by applicable law or agreed to in writing, software
    10  //  distributed under the License is distributed on an "AS IS" BASIS,
    11  //  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  //  See the License for the specific language governing permissions and
    13  //  limitations under the License.
    14  
    15  package service
    16  
    17  import (
    18  	"bytes"
    19  	"encoding/hex"
    20  	"encoding/json"
    21  	"errors"
    22  	"fmt"
    23  	"io/ioutil"
    24  	"log"
    25  	"net/http"
    26  	"os"
    27  	"runtime/debug"
    28  	"strings"
    29  	"sync"
    30  	"time"
    31  
    32  	daisy "github.com/GoogleCloudPlatform/compute-daisy"
    33  	"github.com/google/uuid"
    34  	"github.com/minio/highwayhash"
    35  
    36  	"github.com/GoogleCloudPlatform/compute-image-tools/cli_tools/common/utils/daisyutils"
    37  	"github.com/GoogleCloudPlatform/compute-image-tools/proto/go/pb"
    38  )
    39  
    40  var (
    41  	httpClient            httpClientInterface = &http.Client{Timeout: 5 * time.Second}
    42  	serverURL                                 = deinterleave(serverURLProdP1, serverURLProdP2)
    43  	key                                       = deinterleave(keyP1, keyP2)
    44  	serverLogEnabled                          = true
    45  	logMutex                                  = sync.Mutex{}
    46  	nextRequestWaitMillis int64
    47  )
    48  
    49  // constants used by logging
    50  const (
    51  	ImageImportAction        = "ImageImport"
    52  	ImageExportAction        = "ImageExport"
    53  	InstanceImportAction     = "InstanceImport"
    54  	MachineImageImportAction = "MachineImageImport"
    55  	OneStepImageImportAction = "OneStepImageImport"
    56  	WindowsUpgrade           = "WindowsUpgrade"
    57  	InstanceExportAction     = "InstanceExport"
    58  	MachineImageExportAction = "MachineImageExport"
    59  
    60  	// These strings should be interleaved to construct the real URL. This is just to (hopefully)
    61  	// fool github URL scanning bots.
    62  	serverURLProdP1 = "hts/frbslgigp.ogepscmv/ieo/eaylg"
    63  	serverURLProdP2 = "tp:/ieaeogn-agolai.o/1frlglgc/o"
    64  	keyP1           = "AzSCO1066k_gFH2sJg3I"
    65  	keyP2           = "IaymztUIWu9U8THBeTx"
    66  
    67  	targetSizeGb            = "target-size-gb"
    68  	sourceSizeGb            = "source-size-gb"
    69  	importFileFormat        = "import-file-format"
    70  	inflationType           = "inflation-type"
    71  	inflationTime           = "inflation-time"
    72  	inflationFallbackReason = "inflation-fallback-reason"
    73  	shadowInflationTime     = "shadow-inflation-time"
    74  	shadowDiskMatchResult   = "shadow-disk-match-result"
    75  	isUEFICompatibleImage   = "is-uefi-compatible-image"
    76  	isUEFIDetected          = "is-uefi-detected"
    77  	uefiBootable            = "uefi-bootable"
    78  	biosBootable            = "bios-bootable"
    79  	rootFS                  = "root-fs"
    80  
    81  	statusStart   = "Start"
    82  	statusSuccess = "Success"
    83  	statusFailure = "Failure"
    84  )
    85  
    86  type logResult string
    87  
    88  const (
    89  	failedOnCreateRequest          logResult = "FailedOnCreateRequest"
    90  	failedOnCreateRequestJSON      logResult = "FailedOnCreateRequestJSON"
    91  	failedToParseResponse          logResult = "FailedToParseResponse"
    92  	failedAfterRetry               logResult = "FailedAfterRetry"
    93  	failedOnUndefinedResponse      logResult = "FailedOnUndefinedResponse"
    94  	failedOnMissingResponseDetails logResult = "FailedOnMissingResponseDetails"
    95  	serverLogDisabled              logResult = "ServerLogDisabled"
    96  )
    97  
    98  type httpClientInterface interface {
    99  	Do(req *http.Request) (*http.Response, error)
   100  }
   101  
   102  func deinterleave(p1, p2 string) string {
   103  	l1 := len(p1)
   104  	l2 := len(p2)
   105  	if l1 != l2 && l1 != l2+1 {
   106  		panic("Failed to prepare required data for the tool.")
   107  	}
   108  	strBytes := make([]byte, len(p1)+len(p2))
   109  	for i := range p1 {
   110  		strBytes[i*2] = p1[i]
   111  	}
   112  	for i := range p2 {
   113  		strBytes[i*2+1] = p2[i]
   114  	}
   115  	return string(strBytes)
   116  }
   117  
   118  // Logger is responsible for logging to firelog server
   119  type Logger struct {
   120  	ServerURL string
   121  	ID        string
   122  	Action    string
   123  	TimeStart time.Time
   124  	Params    InputParams
   125  	mutex     sync.Mutex
   126  }
   127  
   128  // NewLoggingServiceLogger creates a new server logger
   129  func NewLoggingServiceLogger(action string, params InputParams) *Logger {
   130  	return &Logger{
   131  		ServerURL: serverURL,
   132  		ID:        uuid.New().String(),
   133  		Action:    action,
   134  		TimeStart: time.Now(),
   135  		Params:    params,
   136  		mutex:     sync.Mutex{},
   137  	}
   138  }
   139  
   140  // logStart logs a "start" info to server
   141  func (l *Logger) logStart() (*ComputeImageToolsLogExtension, logResult) {
   142  	logExtension := l.createComputeImageToolsLogExtension(statusStart, nil)
   143  	return logExtension, l.sendLogToServer(logExtension)
   144  }
   145  
   146  // logSuccess logs a "success" info to server
   147  func (l *Logger) logSuccess(loggable Loggable) (*ComputeImageToolsLogExtension, logResult) {
   148  	logExtension := l.createComputeImageToolsLogExtension(statusSuccess, l.getOutputInfo(loggable, nil))
   149  	return logExtension, l.sendLogToServer(logExtension)
   150  }
   151  
   152  // logFailure logs a "failure" info to server
   153  func (l *Logger) logFailure(err error, loggable Loggable) (*ComputeImageToolsLogExtension, logResult) {
   154  	logExtension := l.createComputeImageToolsLogExtension(statusFailure, l.getOutputInfo(loggable, err))
   155  	return logExtension, l.sendLogToServer(logExtension)
   156  }
   157  
   158  func (l *Logger) createComputeImageToolsLogExtension(status string, outputInfo *OutputInfo) *ComputeImageToolsLogExtension {
   159  	l.mutex.Lock()
   160  	defer l.mutex.Unlock()
   161  
   162  	return &ComputeImageToolsLogExtension{
   163  		ID:            l.ID,
   164  		CloudBuildID:  os.Getenv(daisyutils.BuildIDOSEnvVarName),
   165  		ToolAction:    l.Action,
   166  		Status:        status,
   167  		ElapsedTimeMs: time.Since(l.TimeStart).Nanoseconds() / 1e6,
   168  		EventTimeMs:   time.Now().UnixNano() / 1e6,
   169  		InputParams:   &l.Params,
   170  		OutputInfo:    outputInfo,
   171  	}
   172  }
   173  
   174  func getFailureReason(err error) string {
   175  	return daisyutils.RemovePrivacyLogTag(err.Error())
   176  }
   177  
   178  func getAnonymizedFailureReason(err error) string {
   179  	derr := daisy.ToDError(err)
   180  	if derr == nil {
   181  		return ""
   182  	}
   183  	anonymizedErrs := []string{}
   184  	for _, m := range derr.AnonymizedErrs() {
   185  		anonymizedErrs = append(anonymizedErrs, daisyutils.RemovePrivacyLogInfo(m))
   186  	}
   187  	return strings.Join(anonymizedErrs, "\n")
   188  }
   189  
   190  func (l *Logger) getOutputInfo(loggable Loggable, err error) *OutputInfo {
   191  	o := OutputInfo{}
   192  
   193  	if loggable != nil {
   194  		o.TargetsSizeGb = loggable.GetValueAsInt64Slice(targetSizeGb)
   195  		o.SourcesSizeGb = loggable.GetValueAsInt64Slice(sourceSizeGb)
   196  		o.ImportFileFormat = loggable.GetValue(importFileFormat)
   197  		o.InflationType = loggable.GetValue(inflationType)
   198  		o.InflationTime = loggable.GetValueAsInt64Slice(inflationTime)
   199  		o.ShadowInflationTime = loggable.GetValueAsInt64Slice(shadowInflationTime)
   200  		o.ShadowDiskMatchResult = loggable.GetValue(shadowDiskMatchResult)
   201  		o.IsUEFICompatibleImage = loggable.GetValueAsBool(isUEFICompatibleImage)
   202  		o.IsUEFIDetected = loggable.GetValueAsBool(isUEFIDetected)
   203  		o.InflationFallbackReason = loggable.GetValue(inflationFallbackReason)
   204  
   205  		// TODO: ideally we suppose to set only o.InspectionResults. Remove after
   206  		// the dependency is eliminated.
   207  		if l.Params.ImageImportParams != nil {
   208  			l.Params.ImageImportParams.InspectionResults = loggable.GetInspectionResults()
   209  		}
   210  		o.InspectionResults = loggable.GetInspectionResults()
   211  	}
   212  
   213  	if err != nil {
   214  		o.FailureMessage = getFailureReason(err)
   215  		o.FailureMessageWithoutPrivacyInfo = getAnonymizedFailureReason(err)
   216  		if loggable != nil {
   217  			o.SerialOutputs = loggable.ReadSerialPortLogs()
   218  		}
   219  	}
   220  
   221  	return &o
   222  }
   223  
   224  func (l *Logger) runWithServerLogging(function func() (Loggable, error),
   225  	projectPointer *string) (*ComputeImageToolsLogExtension, error) {
   226  
   227  	var logExtension *ComputeImageToolsLogExtension
   228  
   229  	// Send log asynchronously. No need to interrupt the main flow when failed to send log, just
   230  	// keep moving.
   231  	var wg sync.WaitGroup
   232  
   233  	wg.Add(1)
   234  	go func() {
   235  		defer wg.Done()
   236  		l.logStart()
   237  	}()
   238  
   239  	loggable, err := runWithRecovery(function)
   240  	l.updateParams(projectPointer)
   241  	if err != nil {
   242  		wg.Add(1)
   243  		go func() {
   244  			defer wg.Done()
   245  			logExtension, _ = l.logFailure(err, loggable)
   246  
   247  			// Remove new lines from multi-line failure messages as gcloud depends on
   248  			// log prefix to filter out relevant log lines. Making this change in
   249  			// daisy/error.go Error() func would potentially affect other clients of
   250  			// Daisy with unclear consequences, thus limiting the change to
   251  			// import/export wrappers
   252  			log.Println(removeNewLinesFromMultilineError(logExtension.OutputInfo.FailureMessage))
   253  		}()
   254  	} else {
   255  		wg.Add(1)
   256  		go func() {
   257  			defer wg.Done()
   258  			logExtension, _ = l.logSuccess(loggable)
   259  		}()
   260  	}
   261  
   262  	wg.Wait()
   263  	return logExtension, err
   264  }
   265  
   266  func removeNewLinesFromMultilineError(s string) string {
   267  	// first line in a multi error line is of "Multiple errors" type and doesn't need a separator
   268  	firstNewLineRemoved := strings.Replace(s, "\n", " ", 1)
   269  	return strings.ReplaceAll(firstNewLineRemoved, "\n", "; ")
   270  }
   271  
   272  // RunWithServerLogging runs the function with server logging
   273  func RunWithServerLogging(action string, params InputParams, projectPointer *string,
   274  	function func() (Loggable, error)) error {
   275  	l := NewLoggingServiceLogger(action, params)
   276  	_, err := l.runWithServerLogging(function, projectPointer)
   277  	return err
   278  }
   279  
   280  func (l *Logger) sendLogToServer(logExtension *ComputeImageToolsLogExtension) logResult {
   281  	r := l.sendLogToServerWithRetry(logExtension, 3)
   282  	return r
   283  }
   284  
   285  func (l *Logger) sendLogToServerWithRetry(logExtension *ComputeImageToolsLogExtension, maxRetry int) logResult {
   286  	logMutex.Lock()
   287  	defer logMutex.Unlock()
   288  
   289  	for i := 0; i < maxRetry; i++ {
   290  		// Before sending a new request, wait for a while if server asked to do so
   291  		if nextRequestWaitMillis > 0 {
   292  			nextRequestWaitMillis = 0
   293  			time.Sleep(time.Duration(nextRequestWaitMillis) * time.Millisecond)
   294  		}
   295  
   296  		logRequestJSON, err := l.constructLogRequest(logExtension)
   297  		if err != nil {
   298  			fmt.Println("Failed to log to server: failed to prepare json log data.")
   299  			return failedOnCreateRequestJSON
   300  		}
   301  
   302  		if !serverLogEnabled {
   303  			return serverLogDisabled
   304  		}
   305  
   306  		req, err := http.NewRequest("POST", l.ServerURL, bytes.NewBuffer(logRequestJSON))
   307  		if err != nil {
   308  			fmt.Println("Failed to create log request: ", err)
   309  			return failedOnCreateRequest
   310  		}
   311  
   312  		req.Header.Set("Content-Type", "application/json")
   313  		req.Header.Set("Content-Encoding", "gzip")
   314  		req.Header.Set("X-Goog-Api-Key", key)
   315  		resp, err := httpClient.Do(req)
   316  		if err != nil {
   317  			fmt.Println("Failed to log to server: ", err)
   318  			continue
   319  		}
   320  
   321  		defer resp.Body.Close()
   322  		body, _ := ioutil.ReadAll(resp.Body)
   323  		var lr logResponse
   324  		if err = json.Unmarshal(body, &lr); err != nil {
   325  			fmt.Println("Failed to parse log response: ", err, "\nResponse: ", string(body))
   326  			return failedToParseResponse
   327  		}
   328  
   329  		// Honor "NextRequestWaitMillis" from server for traffic control. However, wait no more than 5s to prevent a long
   330  		// stuck
   331  		if lr.NextRequestWaitMillis > 0 {
   332  			nextRequestWaitMillis = lr.NextRequestWaitMillis
   333  			if nextRequestWaitMillis > 5000 {
   334  				nextRequestWaitMillis = 5000
   335  			}
   336  		}
   337  
   338  		// Honor "ResponseAction" from server to control retrying
   339  		if len(lr.LogResponseDetails) > 0 {
   340  			action := lr.LogResponseDetails[0].ResponseAction
   341  			if action == deleteRequest || action == responseActionUnknown {
   342  				// Log success or unknown status, just return
   343  				return logResult(action)
   344  			} else if action == retryRequestLater {
   345  				// Retry as server asked
   346  				continue
   347  			}
   348  			// Return if client failed to receive a defined response action
   349  			fmt.Println("Failed to log to server: undefined response action: ", action)
   350  			return failedOnUndefinedResponse
   351  		}
   352  
   353  		// Return if client failed to receive response details from server
   354  		fmt.Println("Failed to log to server: missing response details")
   355  		return failedOnMissingResponseDetails
   356  	}
   357  
   358  	fmt.Println("Failed to log to server after retrying")
   359  	return failedAfterRetry
   360  }
   361  
   362  func (l *Logger) constructLogRequest(logExtension *ComputeImageToolsLogExtension) ([]byte, error) {
   363  	if logExtension == nil {
   364  		return nil, fmt.Errorf("won't log a nil event")
   365  	}
   366  	eventStr, err := json.Marshal(logExtension)
   367  	if err != nil {
   368  		return nil, err
   369  	}
   370  
   371  	now := time.Now().UnixNano() / 1000000
   372  	req := logRequest{
   373  		ClientInfo: clientInfo{
   374  			ClientType: "COMPUTE_IMAGE_TOOLS",
   375  		},
   376  		LogSource:     1201,
   377  		RequestTimeMs: now,
   378  		LogEvent: []logEvent{
   379  			{
   380  				EventTimeMs:         now,
   381  				EventUptimeMs:       time.Since(l.TimeStart).Nanoseconds() / 1e6,
   382  				SourceExtensionJSON: string(eventStr),
   383  			},
   384  		},
   385  	}
   386  
   387  	reqStr, err := json.Marshal(req)
   388  	return reqStr, err
   389  }
   390  
   391  // runWithRecovery executes the function `inner`. If a panic occurs,
   392  // it is trapped, and the panic's contents are used to create loggable and err.
   393  func runWithRecovery(inner func() (Loggable, error)) (loggable Loggable, err error) {
   394  	defer func() {
   395  		if recovered := recover(); recovered != nil {
   396  			log.Printf("Fatal error: %v", recovered)
   397  			loggable = literalLoggable{
   398  				traceLogs: []string{
   399  					fmt.Sprintf("Captured panic: %v", recovered),
   400  					"stacktrace from panic: \n" + string(debug.Stack()),
   401  				},
   402  			}
   403  			err = errors.New("A fatal error has occurred. " +
   404  				"Please submit an issue at https://github.com/GoogleCloudPlatform/compute-image-tools/issues")
   405  		}
   406  	}()
   407  	return inner()
   408  }
   409  
   410  // Hash a given string for obfuscation
   411  func Hash(s string) string {
   412  	hash, _ := highwayhash.New([]byte("compute-image-tools-obfuscate-01"))
   413  	hash.Write([]byte(s))
   414  	return hex.EncodeToString(hash.Sum(nil))
   415  }
   416  
   417  // Loggable contains fields relevant to import and export logging.
   418  type Loggable interface {
   419  	GetValue(key string) string
   420  	GetValueAsBool(key string) bool
   421  	GetValueAsInt64Slice(key string) []int64
   422  	GetInspectionResults() *pb.InspectionResults
   423  	ReadSerialPortLogs() []string
   424  }