package logs import ( "net/http" "sort" "strconv" "strings" "time" "github.com/aws/aws-lambda-go/events" "gitlab.com/uafrica/go-utils/errors" "gitlab.com/uafrica/go-utils/logger" "gitlab.com/uafrica/go-utils/queues" ) var producer queues.Producer func Init(p queues.Producer) { producer = p } //Call this at the end of an API request handler to capture the req/res as well as all actions taken during the processing //(note: action list is only reset when this is called - so must be called after each handler, else action list has to be reset at the start) func LogIncomingAPIRequest(startTime time.Time, requestID string, claim map[string]interface{}, req events.APIGatewayProxyRequest, res events.APIGatewayProxyResponse) error { if producer == nil { return errors.Errorf("logs queue producer not set") } //todo: filter out some noisy (method+path) logger.Debugf("claim: %+v", claim) endTime := time.Now() var authType string var authUsername string if req.RequestContext.Identity.CognitoAuthenticationType != "" { authType = "cognito" split := strings.Split(req.RequestContext.Identity.CognitoAuthenticationProvider, ":") if len(split) > 0 { authUsername = split[len(split)-1] //= part after last ':' } } else { authType = "iam" split := strings.Split(req.RequestContext.Identity.UserArn, ":user/") if len(split) > 0 { authUsername = split[len(split)-1] //= part after ':user/' } } username, _ := claim["Username"].(string) accountID, _ := claim["AccountID"].(int64) if accountID == 0 { if accountIDParam, ok := req.QueryStringParameters["account_id"]; ok { if i64, err := strconv.ParseInt(accountIDParam, 10, 64); err == nil && i64 > 0 { accountID = i64 } } } apiLog := ApiLog{ StartTime: startTime, EndTime: endTime, DurMs: endTime.Sub(startTime).Milliseconds(), Method: req.HTTPMethod, Address: req.RequestContext.DomainName, Path: req.Path, ResponseCode: res.StatusCode, RequestID: requestID, InitialAuthType: authType, InitialAuthUsername: authUsername, SourceIP: req.RequestContext.Identity.SourceIP, UserAgent: req.RequestContext.Identity.UserAgent, Username: username, AccountID: accountID, Request: ApiLogRequest{ Headers: req.Headers, QueryParameters: req.QueryStringParameters, BodySize: len(req.Body), Body: req.Body, }, Response: ApiLogResponse{ Headers: res.Headers, BodySize: len(res.Body), Body: res.Body, }, Actions: nil, } //compile action list apiLog.Actions = relativeActionList(apiLog.StartTime, apiLog.EndTime) //sort action list on startTime, cause actions are added when they end, i.e. ordered by end time //and all non-actions were appended at the end of the list sort.Slice(apiLog.Actions, func(i, j int) bool { return apiLog.Actions[i].StartMs < apiLog.Actions[j].StartMs }) //also copy multi-value query parameters to the log as CSV array values for n, as := range req.MultiValueQueryStringParameters { apiLog.Request.QueryParameters[n] = "[" + strings.Join(as, ",") + "]" } //todo: filter out excessive req/res body content per (method+path) //todo: also need to do for all actions... if apiLog.Method == http.MethodGet { apiLog.Response.Body = "<not logged>" } logger.Debugf("Send api-log to SQS: %+v", apiLog) //todo: filter out sensitive values (e.g. OTP) if _, err := producer.NewEvent("API_LOGS"). Type("api-log"). RequestID(apiLog.RequestID). Send(apiLog); err != nil { return errors.Wrapf(err, "failed to send api-log") } return nil } //LogIncomingAPIRequest() //ApiLog is the SQS event details struct encoded as JSON document, sent to SQS, to be logged for each API handler executed. type ApiLog struct { StartTime time.Time `json:"start_time"` EndTime time.Time `json:"end_time"` DurMs int64 `json:"duration_ms"` //duration in milliseconds Method string `json:"method"` Address string `json:"address"` //server address for incoming and outgoing Path string `json:"path"` ResponseCode int `json:"response_code"` RequestID string `json:"request_id"` InitialAuthUsername string `json:"initial_auth_username,omitempty"` InitialAuthType string `json:"initial_auth_type,omitempty"` AccountID int64 `json:"account_id,omitempty"` Username string `json:"username,omitempty"` SourceIP string `json:"source_ip,omitempty"` //only logged for incoming API UserAgent string `json:"user_agent,omitempty"` //only for incoming, indicate type of browser when UI RelevantID string `json:"relevant_id,omitempty"` Request ApiLogRequest `json:"request"` Response ApiLogResponse `json:"response"` Actions []RelativeActionLog `json:"actions,omitempty"` } type ApiLogRequest struct { Headers map[string]string `json:"headers,omitempty"` QueryParameters map[string]string `json:"query_parameters,omitempty"` BodySize int `json:"body_size" search:"long"` //set even when body is truncated/omitted Body string `json:"body,omitempty"` //json body as a string } type ApiLogResponse struct { Headers map[string]string `json:"headers,omitempty"` BodySize int `json:"body_size"` //set even when body is truncated/omitted Body string `json:"body,omitempty"` //json content as a string }