common/logger.go (176 lines of code) (raw):
// Copyright © 2017 Microsoft <wastore@microsoft.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
package common
import (
"fmt"
"io"
"log"
"path"
"runtime"
"strings"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/blob"
datalakefile "github.com/Azure/azure-sdk-for-go/sdk/storage/azdatalake/file"
sharefile "github.com/Azure/azure-sdk-for-go/sdk/storage/azfile/file"
)
type ILogger interface {
ShouldLog(level LogLevel) bool
Log(level LogLevel, msg string)
Panic(err error)
}
type ILoggerCloser interface {
ILogger
CloseLog()
}
type ILoggerResetable interface {
OpenLog()
MinimumLogLevel() LogLevel
ILoggerCloser
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
const maxLogSize = 500 * 1024 * 1024
type jobLogger struct {
// maximum loglevel represents the maximum severity of log messages which can be logged to Job Log file.
// any message with severity higher than this will be ignored.
jobID JobID
minimumLevelToLog LogLevel // The maximum customer-desired log level for this job
file io.WriteCloser // The job's log file
logFileFolder string // The log file's parent folder, needed for opening the file at the right place
logger *log.Logger // The Job's logger
sanitizer LogSanitizer
logFileNameSuffix string // Used to allow more than 1 log per job, ex: front-end and back-end logs should be separate
}
func NewJobLogger(jobID JobID, minimumLevelToLog LogLevel, logFileFolder string, logFileNameSuffix string) ILoggerResetable {
return &jobLogger{
jobID: jobID,
minimumLevelToLog: minimumLevelToLog,
logFileFolder: logFileFolder,
sanitizer: NewAzCopyLogSanitizer(),
logFileNameSuffix: logFileNameSuffix,
}
}
func (jl *jobLogger) OpenLog() {
if jl.minimumLevelToLog == LogNone {
return
}
file, err := NewRotatingWriter(path.Join(jl.logFileFolder, jl.jobID.String()+jl.logFileNameSuffix+".log"), maxLogSize)
PanicIfErr(err)
jl.file = file
flags := log.LstdFlags | log.LUTC
utcMessage := fmt.Sprintf("Log times are in UTC. Local time is %s", time.Now().Format("2 Jan 2006 15:04:05"))
jl.logger = log.New(jl.file, "", flags)
// Log the Azcopy Version
jl.logger.Println("AzcopyVersion ", AzcopyVersion)
// Log the OS Environment and OS Architecture
jl.logger.Println("OS-Environment ", runtime.GOOS)
jl.logger.Println("OS-Architecture ", runtime.GOARCH)
jl.logger.Println(utcMessage)
}
func (jl *jobLogger) MinimumLogLevel() LogLevel {
return jl.minimumLevelToLog
}
func (jl *jobLogger) ShouldLog(level LogLevel) bool {
if level == LogNone {
return false
}
return level <= jl.minimumLevelToLog
}
func (jl *jobLogger) CloseLog() {
if jl.minimumLevelToLog == LogNone {
return
}
jl.logger.Println("Closing Log")
_ = jl.file.Close() // If it was already closed, that's alright. We wanted to close it, anyway.
}
func (jl jobLogger) Log(loglevel LogLevel, msg string) {
// If the logger for Job is not initialized i.e file is not open
// or logger instance is not initialized, then initialize it
// ensure all secrets are redacted
msg = jl.sanitizer.SanitizeLogMessage(msg)
// Go, and therefore the sdk, defaults to \n for line endings, so if the platform has a different line ending,
// we should replace them to ensure readability on the given platform.
if lineEnding != "\n" {
msg = strings.Replace(msg, "\n", lineEnding, -1)
}
if jl.ShouldLog(loglevel) {
jl.logger.Println(msg)
}
}
func (jl jobLogger) Panic(err error) {
jl.logger.Println(err) // We do NOT panic here as the app would terminate; we just log it
panic(err)
// We should never reach this line of code!
}
const TryEquals string = "Try=" // TODO: refactor so that this can be used by the retry policies too? So that when you search the logs for Try= you are guaranteed to find both types of retry (i.e. request send retries, and body read retries)
func NewBlobReadLogFunc(logger ILogger, fullUrl string) func(int32, error, blob.HTTPRange, bool) {
redactedUrl := URLStringExtension(fullUrl).RedactSecretQueryParamForLogging()
return func(failureCount int32, err error, r blob.HTTPRange, willRetry bool) {
retryMessage := "Will retry"
if !willRetry {
retryMessage = "Will NOT retry"
}
logger.Log(LogInfo, fmt.Sprintf(
"Error reading body of reply. Next try (if any) will be %s%d. %s. Error: %s. Offset: %d Count: %d URL: %s",
TryEquals, // so that retry wording for body-read retries is similar to that for URL-hitting retries
// We log the number of the NEXT try, not the failure just done, so that users searching the log for "Try=2"
// will find ALL retries, both the request send retries (which are logged as try 2 when they are made) and
// body read retries (for which only the failure is logged - so if we did the actual failure number, there would be
// not Try=2 in the logs if the retries work).
failureCount+1,
retryMessage,
err,
r.Offset,
r.Count,
redactedUrl))
}
}
func NewFileReadLogFunc(logger ILogger, fullUrl string) func(int32, error, sharefile.HTTPRange, bool) {
redactedUrl := URLStringExtension(fullUrl).RedactSecretQueryParamForLogging()
return func(failureCount int32, err error, r sharefile.HTTPRange, willRetry bool) {
retryMessage := "Will retry"
if !willRetry {
retryMessage = "Will NOT retry"
}
logger.Log(LogInfo, fmt.Sprintf(
"Error reading body of reply. Next try (if any) will be %s%d. %s. Error: %s. Offset: %d Count: %d URL: %s",
TryEquals, // so that retry wording for body-read retries is similar to that for URL-hitting retries
// We log the number of the NEXT try, not the failure just done, so that users searching the log for "Try=2"
// will find ALL retries, both the request send retries (which are logged as try 2 when they are made) and
// body read retries (for which only the failure is logged - so if we did the actual failure number, there would be
// not Try=2 in the logs if the retries work).
failureCount+1,
retryMessage,
err,
r.Offset,
r.Count,
redactedUrl))
}
}
func NewDatalakeReadLogFunc(logger ILogger, fullUrl string) func(int32, error, datalakefile.HTTPRange, bool) {
redactedUrl := URLStringExtension(fullUrl).RedactSecretQueryParamForLogging()
return func(failureCount int32, err error, r datalakefile.HTTPRange, willRetry bool) {
retryMessage := "Will retry"
if !willRetry {
retryMessage = "Will NOT retry"
}
logger.Log(LogInfo, fmt.Sprintf(
"Error reading body of reply. Next try (if any) will be %s%d. %s. Error: %s. Offset: %d Count: %d URL: %s",
TryEquals, // so that retry wording for body-read retries is similar to that for URL-hitting retries
// We log the number of the NEXT try, not the failure just done, so that users searching the log for "Try=2"
// will find ALL retries, both the request send retries (which are logged as try 2 when they are made) and
// body read retries (for which only the failure is logged - so if we did the actual failure number, there would be
// not Try=2 in the logs if the retries work).
failureCount+1,
retryMessage,
err,
r.Offset,
r.Count,
redactedUrl))
}
}
func IsForceLoggingDisabled() bool {
return GetLifecycleMgr().IsForceLoggingDisabled()
}
type S3HTTPTraceLogger struct {
logger ILogger
logLevel LogLevel
}
func NewS3HTTPTraceLogger(logger ILogger, level LogLevel) S3HTTPTraceLogger {
return S3HTTPTraceLogger{
logger: logger,
logLevel: level,
}
}
func (e S3HTTPTraceLogger) Write(msg []byte) (n int, err error) {
toPrint := string(msg)
e.logger.Log(e.logLevel, toPrint)
return len(toPrint), nil
}
type causer interface {
Cause() error
}
// Cause walks all the preceding errors and return the originating error.
func Cause(err error) error {
for err != nil {
cause, ok := err.(causer)
if !ok {
break
}
err = cause.Cause()
}
return err
}