You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

450 lines
12 KiB

package logger
import (
"bufio"
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"runtime"
"runtime/debug"
"strings"
"time"
"git.lowcodeplatform.net/fabric/models"
"github.com/segmentio/ksuid"
_ "github.com/segmentio/ksuid"
"go.uber.org/zap"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
"git.lowcodeplatform.net/packages/logger/types"
)
const (
headerXRequestID = "X-Request-ID"
headerXUserID = "X-User-ID"
headerXRequestUnit = "X-Request-Unit"
headerXRequestService = "X-Request-Service"
requestIDKey = "request-id"
ServiceTypeKey = "service-type"
ServiceIDKey = "service-id"
ConfigIDKey = "config-id"
requestUserAgentKey = "user-agent"
requestUnitKey = "unit-key"
requestServiceKey = "service-key"
requestBodyKey = "request-body"
responseBodyKey = "response-body"
methodKey = "method"
remoteAddrKey = "remote-addr"
userAgentKey = "user-agent"
durationKey = "duration"
urlKey = "url"
statusCodeKey = "status-code"
headerExpiresAtKey = "header_expires_at"
requestLengthKey = "request-length"
)
type statusCodeWriter struct {
http.ResponseWriter
statusCode int
}
func (w *statusCodeWriter) WriteHeader(statusCode int) {
w.statusCode = statusCode
w.ResponseWriter.WriteHeader(statusCode)
}
func (w *statusCodeWriter) Write(b []byte) (int, error) {
if w.statusCode == 0 {
w.statusCode = 200
}
return w.ResponseWriter.Write(b)
}
func (w *statusCodeWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
hj, ok := w.ResponseWriter.(http.Hijacker)
if !ok || hj == nil {
return nil, nil, fmt.Errorf("http.Hijacker interface is not implemented in given response writer")
}
return hj.Hijack()
}
func HTTPMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestID := r.Header.Get(headerXRequestID)
if requestID == "" {
requestID = ksuid.New().String()
r.Header.Add(headerXRequestID, requestID) // keep compatibility with old logger
}
w.Header().Set(headerXRequestID, requestID)
ctx := SetRequestIDCtx(r.Context(), requestID)
userID := r.Header.Get(headerXUserID)
if userID != "" {
w.Header().Set(headerXUserID, userID)
ctx = SetUserIDCtx(ctx, userID)
}
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// HttpClientHeaders устанавливает заголовки реквеста из контекста и headers
func HttpClientHeaders(ctx context.Context, req *http.Request, headers map[string]string) {
if req == nil {
return
}
for ctxField, headerField := range models.ProxiedHeaders {
if value := GetFieldCtx(ctx, ctxField); value != "" {
req.Header.Add(headerField, value)
}
}
if len(headers) > 0 {
for k, v := range headers {
req.Header.Add(k, v)
}
}
}
// HTTPMiddleWareParams параметры логирования, при пустых параметрах HTTPMiddlewareWithParams
// будет отрабатывать, как HTTPMiddleware.
type HTTPMiddleWareParams struct {
NeedToLogIncomingPostRequest bool
NeedToLogResponse bool
NeedToLogPanic bool
}
type Middleware interface {
HTTPMiddlewareWithParams(next http.Handler) http.Handler
}
type middleware struct {
params *HTTPMiddleWareParams
}
func NewHTTPMiddleware(params *HTTPMiddleWareParams) Middleware {
return &middleware{params: params}
}
// HTTPMiddlewareWithParams http middleware для записи логов запросов,
// параметры логирования определяются HTTPMiddleWareParams.
func (m *middleware) HTTPMiddlewareWithParams(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if m.params.NeedToLogPanic {
defer func() {
if rr := recover(); rr != nil {
stack := debug.Stack()
pc, file, line, _ := runtime.Caller(4)
Logger(r.Context()).Warn("panic happened",
zap.String("recover-error", fmt.Sprint(rr)),
zap.String("file", file),
zap.Int("line", line),
zap.String("function", runtime.FuncForPC(pc).Name()),
zap.ByteString("stack", stack),
zap.String("stacktrace", string(debug.Stack())),
zap.Int(statusCodeKey, 500),
types.URL("url", r.RequestURI),
zap.String("method", r.Method),
zap.String("remote-addr", r.RemoteAddr),
zap.String("user-agent", r.Header.Get("User-Agent")),
zap.String("header_expires_at", r.Header.Get("X-Expires-At")))
panic(r)
}
}()
}
var timeStart time.Time
if m.params.NeedToLogResponse {
timeStart = time.Now()
}
requestID := r.Header.Get(headerXRequestID)
if requestID == "" {
requestID = ksuid.New().String()
r.Header.Add(headerXRequestID, requestID) // keep compatibility with old logger
}
w.Header().Set(headerXRequestID, requestID)
ctx := SetRequestIDCtx(r.Context(), requestID)
userID := r.Header.Get(headerXUserID)
if userID != "" {
w.Header().Set(headerXUserID, userID)
ctx = SetUserIDCtx(ctx, userID)
}
if m.params.NeedToLogIncomingPostRequest && isPostWithContent(r) {
Logger(r.Context()).Info("incoming POST request",
types.URL("url", r.RequestURI),
zap.String(requestUnitKey, r.Header.Get(headerXRequestUnit)),
zap.String(requestServiceKey, r.Header.Get(headerXRequestService)),
zap.String(requestBodyKey, getRequestBodyCopy(r)),
zap.Int64("request-length", r.ContentLength),
zap.String("method", r.Method),
zap.String("remote-addr", r.RemoteAddr),
zap.String(requestUserAgentKey, r.Header.Get("User-Agent")),
zap.String("header_expires_at", r.Header.Get("X-Expires-At")))
}
sw := &statusCodeWriter{ResponseWriter: w}
*r = *r.WithContext(ctx)
next.ServeHTTP(sw, r)
if m.params.NeedToLogResponse {
Logger(r.Context()).Info("request completed",
zap.Float64("timing", time.Since(timeStart).Seconds()),
zap.Int(statusCodeKey, sw.statusCode),
types.URL("url", r.RequestURI),
zap.String(requestUnitKey, r.Header.Get(headerXRequestUnit)),
zap.String(requestServiceKey, r.Header.Get(headerXRequestService)),
zap.String(requestBodyKey, getRequestBodyCopy(r)),
zap.String("method", r.Method),
zap.String("remote-addr", r.RemoteAddr),
zap.String("user-agent", r.Header.Get("User-Agent")),
zap.String("header_expires_at", r.Header.Get("X-Expires-At")))
}
})
}
type loggerFormat struct {
logRequestFunc func(req *http.Request)
logResponseFunc func(context.Context, *http.Response, time.Time)
}
func HTTPClientLogging(hc *http.Client, options ...Option) {
lf := &loggerFormat{
logRequestFunc: defaultLogRequest,
logResponseFunc: defaultLogResponse,
}
for _, option := range options {
lf = option(lf)
}
if hc.Transport == nil {
hc.Transport = http.DefaultTransport
}
hc.Transport = &loggerTransport{
transport: hc.Transport,
logRequestFunc: lf.logRequestFunc,
logResponseFunc: lf.logResponseFunc,
}
}
// loggerTransport implements http.RoundTripper.
// When set as Transport of http.Client, it executes HTTP requests with logging.
// No field is mandatory.
type loggerTransport struct {
transport http.RoundTripper
logRequestFunc func(req *http.Request)
logResponseFunc func(context.Context, *http.Response, time.Time)
}
// Used if loggerTransport.logRequestFunc is not set.
var defaultLogRequest = func(r *http.Request) {
Logger(r.Context()).Info("external request",
zap.String(methodKey, r.Method),
types.URL(urlKey, r.URL.String()),
types.JSON(requestBodyKey, getRequestBodyCopy(r)),
zap.Int64(requestLengthKey, r.ContentLength))
}
// Used if loggerTransport.logResponseFunc is not set.
var defaultLogResponse = func(ctx context.Context, r *http.Response, begin time.Time) {
Logger(ctx).Info("external response",
zap.String(methodKey, r.Request.Method),
zap.Int(statusCodeKey, r.StatusCode),
types.URL(urlKey, r.Request.URL.String()),
types.JSON(responseBodyKey, getResponseBodyCopy(r)),
zap.Float64(durationKey, time.Since(begin).Seconds()),
)
}
// ExternalFormRequestLogger hides sensitive data from request.
// hides from url.
// hides from form values.
func ExternalFormRequestLogger() func(r *http.Request) {
return func(r *http.Request) {
Logger(r.Context()).Info("external request",
zap.String(methodKey, r.Method),
types.URL(urlKey, r.URL.String()),
types.URL(requestBodyKey, getRequestBodyCopy(r)),
zap.Int64(requestLengthKey, r.ContentLength))
}
}
// RoundTrip is the core part of this module and implements http.RoundTripper.
// Executes HTTP request with request/response logging.
func (lt *loggerTransport) RoundTrip(req *http.Request) (*http.Response, error) {
lt.logRequest(req)
begin := time.Now()
resp, err := lt.transport.RoundTrip(req)
if err != nil {
return nil, err
}
lt.logResponse(req.Context(), resp, begin)
return resp, err
}
func (lt *loggerTransport) logRequest(req *http.Request) {
lt.logRequestFunc(req)
}
func (lt *loggerTransport) logResponse(ctx context.Context, resp *http.Response, begin time.Time) {
lt.logResponseFunc(ctx, resp, begin)
}
func GRPCUnaryServerInterceptor(
ctx context.Context,
req interface{},
_ *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (interface{}, error) {
var requestID, userID string
if headers, ok := metadata.FromIncomingContext(ctx); ok {
if header, ok := headers["x-request-id"]; ok && len(header) > 0 {
requestID = header[0]
}
if header, ok := headers["x-user-id"]; ok && len(header) > 0 {
userID = header[0]
}
}
if requestID == "" {
requestID = ksuid.New().String()
}
ctx = AddToGRPCHeader(ctx, "x-request-id", requestID)
ctx = SetRequestIDCtx(ctx, requestID)
if userID != "" {
ctx = AddToGRPCHeader(ctx, "x-user-id", userID)
ctx = SetRequestIDCtx(ctx, userID)
}
return handler(ctx, req)
}
func GRPCUnaryClientInterceptor(
ctx context.Context,
method string,
req interface{},
reply interface{},
cc *grpc.ClientConn,
invoker grpc.UnaryInvoker,
opts ...grpc.CallOption,
) error {
var requestID, userID string
requestID = GetRequestIDCtx(ctx)
userID = GetUserIDCtx(ctx)
if requestID == "" {
requestID = ksuid.New().String()
}
ctx = AddToGRPCHeader(ctx, "x-request-id", requestID)
if userID != "" {
ctx = AddToGRPCHeader(ctx, "x-user-id", userID)
}
err := invoker(ctx, method, req, reply, cc, opts...)
return err
}
func getRequestBodyCopy(r *http.Request) string {
if r.Body == nil {
return ""
}
var reqBody []byte
reqBody, _ = io.ReadAll(r.Body)
r.Body = io.NopCloser(bytes.NewBuffer(reqBody))
return string(reqBody)
}
func isPostWithContent(r *http.Request) bool {
return r.Method == http.MethodPost && r.ContentLength != 0
}
func getResponseBodyCopy(r *http.Response) string {
if r.Body == nil {
return ""
}
var reqBody []byte
reqBody, _ = io.ReadAll(r.Body)
r.Body = io.NopCloser(bytes.NewBuffer(reqBody))
return string(reqBody)
}
func GRPCLoggingInterceptor(ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo, handler grpc.UnaryHandler,
) (
interface{}, error,
) {
begin := time.Now()
logMethodBefore(ctx, info.FullMethod, req)
res, err := handler(ctx, req)
logMethodAfter(ctx, info.FullMethod, begin, res, err)
return res, err
}
func logMethodAfter(ctx context.Context, path string, begin time.Time, resp interface{}, err error) {
parts := strings.Split(path, "/")
methodName := parts[len(parts)-1]
respRaw, respErr := json.Marshal(resp)
if respErr != nil {
Logger(ctx).Warn("logMethodAfter err:", zap.Error(respErr))
}
fields := []zap.Field{
zap.String("method", methodName),
types.JSON("response", string(respRaw)),
zap.Float64("duration", time.Since(begin).Seconds()),
}
if err != nil {
fields = append(fields, zap.String("error", err.Error()))
}
Logger(ctx).Info("finished", fields...)
}
func logMethodBefore(ctx context.Context, path string, req interface{}) {
parts := strings.Split(path, "/")
methodName := parts[len(parts)-1]
reqRaw, err := json.Marshal(req)
if err != nil {
Logger(ctx).Warn("logMethodBefore err:", zap.Error(err))
}
Logger(ctx).Info("enter", zap.String("method", methodName), types.JSON("request", string(reqRaw)))
}