package logs import ( "fmt" "gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/errors" "gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/string_utils" "gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/utils" "net/http" "net/url" "os" "reflect" "regexp" "runtime" "strings" "github.com/MindscapeHQ/raygun4go" "github.com/aws/aws-lambda-go/events" log "github.com/sirupsen/logrus" ) var logger *log.Entry var apiRequest *events.APIGatewayProxyRequest var currentRequestID *string var isDebug = false var build string var raygunClient *raygun4go.Client // Password filtering var passwordRegex = regexp.MustCompile(`(?i:\\?"password\\?"\s*:\s*\\?"(.*)\\?",).*`) func SanitiseLogs(logString string) string { logString = MaskPasswordsInJsonString(logString) return logString } // MaskPasswordsInJsonString takes a string and, if it is a JSON string, sanitises all the password. In order for the // regex to work correctly we need to prettify the JSON, so the function always returns a formatted JSON string. func MaskPasswordsInJsonString(jsonString string) string { var isValidJsonString bool isValidJsonString, jsonString = string_utils.PrettyJSON(jsonString) if !isValidJsonString { return jsonString } if passwordRegex.MatchString(jsonString) { result := passwordRegex.FindAllStringSubmatch(jsonString, -1) for _, match := range result { if len(match) > 1 { jsonString = strings.ReplaceAll(jsonString, match[1], "***") } } } return jsonString } func SanitiseFields(fields map[string]interface{}) map[string]interface{} { sanitisedFields := make(map[string]interface{}) // Check if each field is a string or string pointer, and sanitize them if they are for key, field := range fields { value := reflect.ValueOf(field) if value.Kind() == reflect.Ptr && value.IsValid() { pointerValue := value.Elem() if pointerValue.Kind() == reflect.String { sanitisedString := SanitiseLogs(pointerValue.String()) sanitisedFields[key] = &sanitisedString } } else if value.Kind() == reflect.String { sanitisedFields[key] = SanitiseLogs(value.String()) } else { // Don't sanitise fields that sanitisedFields[key] = field } } return sanitisedFields } func InitLogs(requestID *string, isDebugBuild bool, buildVersion string, request *events.APIGatewayProxyRequest, client *raygun4go.Client) { currentRequestID = requestID apiRequest = request isDebug = isDebugBuild build = buildVersion raygunClient = client if isDebugBuild { log.SetReportCaller(true) log.SetFormatter(&log.TextFormatter{ ForceColors: true, PadLevelText: true, DisableTimestamp: true, CallerPrettyfier: func(f *runtime.Frame) (string, string) { // Exclude the caller, will rather be added as a field return "", "" }, }) } else { log.SetReportCaller(true) log.SetFormatter(&log.JSONFormatter{ CallerPrettyfier: func(f *runtime.Frame) (string, string) { // Exclude the caller, will rather be added as a field return "", "" }}) } log.SetLevel(LogLevel()) val, exists := os.LookupEnv("DEBUGGING") if exists && val == "true" { log.SetLevel(log.TraceLevel) log.SetReportCaller(true) } logger = log.WithFields(log.Fields{ "environment": getEnvironment(), }) if requestID != nil { logger = log.WithFields(log.Fields{ "request_id": *requestID, }) } } func LogLevel() log.Level { logLevelString := os.Getenv("LOG_LEVEL") logLevel := log.InfoLevel if logLevelString != "" { logLevelString = strings.ToLower(logLevelString) switch logLevelString { case "error": logLevel = log.ErrorLevel case "warn": logLevel = log.WarnLevel case "info": logLevel = log.InfoLevel case "debug": logLevel = log.DebugLevel } log.SetLevel(logLevel) } return logLevel } func getEnvironment() string { environment := os.Getenv("ENVIRONMENT") if environment == "" { environment = "dev" os.Setenv("ENVIRONMENT", "dev") } return environment } func getLogger() *log.Entry { if logger == nil { logger = log.WithFields(log.Fields{ "environment": getEnvironment(), }) } return logger } func InfoWithFields(fields map[string]interface{}, message interface{}) { if reflect.TypeOf(message).Kind() == reflect.String { message = SanitiseLogs(message.(string)) } sanitisedFields := SanitiseFields(fields) getLogger().WithFields(sanitisedFields).Info(message) } func Info(format string, a ...interface{}) { message := SanitiseLogs(fmt.Sprintf(format, a...)) getLogger().Info(message) } func ErrorWithFields(fields map[string]interface{}, err error) { sanitisedFields := SanitiseFields(fields) sendRaygunError(sanitisedFields, err) getLogger().WithFields(sanitisedFields).Error(err) } func ErrorWithMsg(message string, err error) { if err == nil { err = errors.Error(message) } ErrorWithFields(map[string]interface{}{ "message": message, }, err) } func ErrorMsg(message string) { ErrorWithMsg(message, nil) } func Warn(format string, a ...interface{}) { message := SanitiseLogs(fmt.Sprintf(format, a...)) getLogger().Warn(message) } func WarnWithFields(fields map[string]interface{}, err error) { sanitisedFields := SanitiseFields(fields) getLogger().WithFields(sanitisedFields).Warn(err) } func SQLDebugInfo(sql string) { getLogger().WithFields(map[string]interface{}{ "sql": sql, }).Debug("SQL query") } func LogShipmentID(id int64) { InfoWithFields(map[string]interface{}{ "shipment_id": id, }, "Current-shipment-ID") } func LogRequestInfo(req events.APIGatewayProxyRequest) { InfoWithFields(map[string]interface{}{ "http_method": req.HTTPMethod, "path": req.Path, "api_gateway_request_id": req.RequestContext.RequestID, "user_cognito_auth_provider": req.RequestContext.Identity.CognitoAuthenticationProvider, "user_arn": req.RequestContext.Identity.UserArn, }, "Request Info start") } func LogApiAudit(fields log.Fields) { getLogger().WithFields(fields).Info("api-audit-log") } func LogSQSEvent(event events.SQSEvent) { InfoWithFields(map[string]interface{}{ "records": event.Records, }, "SQS event start") } func SetOutputToFile(file *os.File) { log.SetOutput(file) } func ClearInfo() { logger = nil } func sendRaygunError(fields map[string]interface{}, errToSend error) { if isDebug || raygunClient == nil { // Don't log raygun errors on debug return } env := getEnvironment() tags := []string{env} raygunClient.Version(build) tags = append(tags, build) if apiRequest != nil { methodAndPath := apiRequest.HTTPMethod + ": " + apiRequest.Path tags = append(tags, methodAndPath) fields["body"] = apiRequest.Body fields["query"] = apiRequest.QueryStringParameters fields["identity"] = apiRequest.RequestContext.Identity } raygunClient.Tags(tags) if currentRequestID != nil { fields["request_id"] = currentRequestID } fields["env"] = env sanitisedFields := SanitiseFields(fields) raygunClient.CustomData(sanitisedFields) raygunClient.Request(fakeHttpRequest()) if errToSend == nil { errToSend = errors.Error("") } err := raygunClient.SendError(errToSend) if err != nil { log.Println("Failed to send raygun error:", err.Error()) } } func fakeHttpRequest() *http.Request { if apiRequest == nil { return nil } // Mask authorization header for raygun logs headers := utils.DeepCopy(apiRequest.MultiValueHeaders).(map[string][]string) if len(headers["authorization"]) != 0 { headers["authorization"] = []string{"***"} } if len(headers["Authorization"]) != 0 { headers["Authorization"] = []string{"***"} } requestURL := url.URL{ Path: apiRequest.Path, Host: apiRequest.Headers["Host"], } request := http.Request{ Method: apiRequest.HTTPMethod, URL: &requestURL, Header: headers, } return &request }