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 }