175 lines
4.5 KiB
Go
175 lines
4.5 KiB
Go
package api
|
|
|
|
import (
|
|
"fmt"
|
|
"net/url"
|
|
"reflect"
|
|
"strings"
|
|
"time"
|
|
|
|
"apigo.cc/go/cast"
|
|
"apigo.cc/go/log"
|
|
"apigo.cc/go/safe"
|
|
)
|
|
|
|
const logTypeAPICall = "api_call"
|
|
|
|
type CallLog struct {
|
|
log.BaseLog
|
|
Action string `log:"pos:6,color:cyan"`
|
|
Method string `log:"pos:7,color:gray"`
|
|
URL string `log:"pos:8,color:gray"`
|
|
StatusCode int `log:"pos:9,color:magenta,keyname:Status"`
|
|
Code string `log:"pos:10,color:magenta"`
|
|
Error string `log:"pos:11,color:red"`
|
|
UsedTime float32 `log:"pos:12,color:green,precision:6"`
|
|
Stream bool `log:"pos:13"`
|
|
ResponseSize int64 `log:"pos:14,color:magenta,keyname:Size"`
|
|
RequestData any `log:"pos:15,color:cyan,keyname:Request"`
|
|
ResponseData any `log:"pos:16,color:magenta,keyname:Response"`
|
|
}
|
|
|
|
func (entry *CallLog) Reset() {
|
|
entry.BaseLog.Reset()
|
|
entry.Action = ""
|
|
entry.Method = ""
|
|
entry.URL = ""
|
|
entry.StatusCode = 0
|
|
entry.Code = ""
|
|
entry.Error = ""
|
|
entry.UsedTime = 0
|
|
entry.Stream = false
|
|
entry.ResponseSize = 0
|
|
entry.RequestData = nil
|
|
entry.ResponseData = nil
|
|
}
|
|
|
|
func init() {
|
|
log.RegisterType(logTypeAPICall, &CallLog{})
|
|
}
|
|
|
|
func logCall(options *CallOptions, config map[string]any, action, method, url string, request any, result *Result, started time.Time) {
|
|
logger := options.Logger
|
|
if logger == nil {
|
|
logger = log.DefaultLogger
|
|
}
|
|
if logger == nil || !logger.CheckLevel(log.INFO) {
|
|
return
|
|
}
|
|
entry := log.GetEntry[CallLog]()
|
|
logger.FillBase(entry.GetBaseLog(), logTypeAPICall)
|
|
entry.Action = action
|
|
entry.Method = method
|
|
entry.URL = sanitizeLogURL(url, config)
|
|
entry.StatusCode = result.StatusCode
|
|
entry.Code = result.Code
|
|
entry.Error = result.Error
|
|
entry.UsedTime = float32(time.Since(started).Seconds())
|
|
entry.Stream = options.Stream != nil
|
|
entry.ResponseSize = result.receivedBytes
|
|
logging, _ := config["logging"].(map[string]any)
|
|
if cast.Bool(logging["request"]) {
|
|
entry.RequestData = logSnapshot(request, logging)
|
|
}
|
|
if cast.Bool(logging["response"]) {
|
|
entry.ResponseData = logSnapshot(result.Data, logging)
|
|
}
|
|
logger.Log(entry)
|
|
}
|
|
|
|
func sanitizeLogURL(raw string, config map[string]any) string {
|
|
parsed, err := url.Parse(raw)
|
|
if err != nil {
|
|
return raw
|
|
}
|
|
configured, _ := config["query"].(map[string]any)
|
|
query := parsed.Query()
|
|
changed := false
|
|
for key, value := range configured {
|
|
if _, ok := value.(*safe.SecretPlaintext); ok {
|
|
query.Set(key, "***")
|
|
changed = true
|
|
}
|
|
}
|
|
if changed {
|
|
parsed.RawQuery = query.Encode()
|
|
}
|
|
return parsed.String()
|
|
}
|
|
|
|
func logSnapshot(value any, config map[string]any) any {
|
|
maxText := cast.Int(config["maxTextLength"])
|
|
if maxText <= 0 {
|
|
maxText = 4096
|
|
}
|
|
maxItems := cast.Int(config["maxArrayItems"])
|
|
if maxItems <= 0 {
|
|
maxItems = 3
|
|
}
|
|
maxDepth := cast.Int(config["maxDepth"])
|
|
if maxDepth <= 0 {
|
|
maxDepth = 4
|
|
}
|
|
return trimLogValue(reflect.ValueOf(value), 0, maxDepth, maxItems, maxText)
|
|
}
|
|
|
|
func trimLogValue(value reflect.Value, depth, maxDepth, maxItems, maxText int) any {
|
|
if !value.IsValid() {
|
|
return nil
|
|
}
|
|
for value.Kind() == reflect.Interface || value.Kind() == reflect.Ptr {
|
|
if value.IsNil() {
|
|
return nil
|
|
}
|
|
value = value.Elem()
|
|
}
|
|
if depth >= maxDepth {
|
|
return "[max depth]"
|
|
}
|
|
switch value.Kind() {
|
|
case reflect.String:
|
|
text := value.String()
|
|
if len(text) > maxText {
|
|
return text[:maxText] + fmt.Sprintf("...[truncated %d bytes]", len(text)-maxText)
|
|
}
|
|
return text
|
|
case reflect.Map:
|
|
out := map[string]any{}
|
|
iterator := value.MapRange()
|
|
for iterator.Next() {
|
|
out[cast.String(iterator.Key().Interface())] = trimLogValue(iterator.Value(), depth+1, maxDepth, maxItems, maxText)
|
|
}
|
|
return out
|
|
case reflect.Slice, reflect.Array:
|
|
if value.Type().Elem().Kind() == reflect.Uint8 {
|
|
return fmt.Sprintf("[binary %d bytes]", value.Len())
|
|
}
|
|
limit := value.Len()
|
|
if limit > maxItems {
|
|
limit = maxItems
|
|
}
|
|
out := make([]any, 0, limit+1)
|
|
for i := 0; i < limit; i++ {
|
|
out = append(out, trimLogValue(value.Index(i), depth+1, maxDepth, maxItems, maxText))
|
|
}
|
|
if value.Len() > limit {
|
|
out = append(out, fmt.Sprintf("[%d more items]", value.Len()-limit))
|
|
}
|
|
return out
|
|
case reflect.Struct:
|
|
out := map[string]any{}
|
|
typeOf := value.Type()
|
|
for i := 0; i < value.NumField(); i++ {
|
|
if typeOf.Field(i).PkgPath == "" {
|
|
out[cast.GetLowerName(typeOf.Field(i).Name)] = trimLogValue(value.Field(i), depth+1, maxDepth, maxItems, maxText)
|
|
}
|
|
}
|
|
return out
|
|
default:
|
|
if value.CanInterface() {
|
|
return value.Interface()
|
|
}
|
|
return strings.TrimSpace(fmt.Sprint(value))
|
|
}
|
|
}
|