Select Git revision
-
James Page authoredJames Page authored
api-logs.go 5.71 KiB
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
}