feat: add http

main
git 2024-12-28 20:26:36 +08:00
parent b67467c0b4
commit 7840b82d75
Signed by: git
GPG Key ID: 3F65EFFA44207ADD
18 changed files with 781 additions and 238 deletions

85
.golangci.yml Normal file
View File

@ -0,0 +1,85 @@
# Code generated by gen-lint. DO NOT EDIT.
run:
timeout: 5m
issues:
# 显示所有 issue
max-issues-per-linter: 0
max-same-issues: 0
exclude-use-default: false
linters:
disable-all: true
enable:
# enable by default
- errcheck
- gosimple
- govet
- ineffassign
- staticcheck
- unused
# custom
- gci
- goconst
- gofmt
- goheader
- goimports
- gosec
- misspell
- nakedret
- revive
- unconvert
- unparam
linters-settings:
# 只开启特定的规则
errcheck:
exclude-functions:
- (*os.File).Close
- (io.Closer).Close
- (net/http.ResponseWriter).Write
- io.Copy
- os.RemoveAll
govet:
enable:
- shadow
goimports:
local-prefixes: git.ifooth.com/common/pkg
gci:
sections:
- standard
- default
- prefix(git.ifooth.com/common/pkg)
gosec:
includes:
- G201 # SQL query construction using format string
- G202 # SQL query construction using string concatenation
- G101 # Look for hard coded credentials
- G401 # Detect the usage of DES, RC4, MD5 or SHA1
- G402 # Look for bad TLS connection settings
- G403 # Ensure minimum RSA key length of 2048 bits
- G404 # Insecure random number source (rand)
- G504 # Import blocklist: net/http/cgi
misspell:
locale: US
revive:
rules:
- name: line-length-limit
arguments:
- 160
- name: function-length
arguments:
- 80 # statements
- 120 # lines
- name: cyclomatic
arguments:
- 30
- name: use-any
- name: early-return
- name: exported
arguments:
- checkPrivateReceivers
- sayRepetitiveInsteadOfStutters
- name: package-comments

View File

@ -1,62 +0,0 @@
package apis
import (
"log/slog"
"net"
"net/http"
"strings"
"time"
"github.com/go-chi/chi/v5/middleware"
)
// ClientIP getIP returns the ip address from the http request
func ClientIP(r *http.Request) string {
xForwardedFor := r.Header.Get("X-Forwarded-For")
ip := strings.TrimSpace(strings.Split(xForwardedFor, ",")[0])
if ip != "" {
return ip
}
ip = strings.TrimSpace(r.Header.Get("X-Real-Ip"))
if ip != "" {
return ip
}
if ip, _, err := net.SplitHostPort(strings.TrimSpace(r.RemoteAddr)); err == nil {
return ip
}
return ""
}
// Logger returns a `func(http.Handler) http.Handler` (middleware) that logs requests using slog.
func Logger(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
st := time.Now()
ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor)
defer func() {
msg := r.Method + " " + r.RequestURI + " " + r.Proto
ip := ClientIP(r)
status := ww.Status()
if status == 0 {
status = http.StatusOK
}
attrs := []slog.Attr{
slog.String("ip", ip),
slog.String("id", middleware.GetReqID(r.Context())),
slog.Int("status", status),
slog.Duration("latency", time.Since(st)),
slog.Int("length", ww.BytesWritten()),
}
slog.LogAttrs(r.Context(), slog.LevelInfo, msg, attrs...)
}()
next.ServeHTTP(ww, r)
}
return http.HandlerFunc(fn)
}

View File

@ -1,80 +0,0 @@
// Package apis for http
package apis
import (
"context"
"net/http"
"path/filepath"
"strings"
"github.com/go-chi/chi/v5/middleware"
"github.com/google/uuid"
"git.ifooth.com/common/pkg/components"
)
type contextKey struct {
name string
}
var (
reqCtxKey = &contextKey{"HTTPRequest"}
)
// RequestIdGenerator request_id
func RequestIdGenerator() string {
uid := uuid.New().String()
requestId := strings.Replace(uid, "-", "", -1)
return requestId
}
// RequestID reuqest_id
func RequestID(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
requestID := r.Header.Get(components.RequestIDHeaderKey)
if requestID == "" {
requestID = RequestIdGenerator()
}
ctx = components.WithRequestIDValue(ctx, requestID)
ctx = context.WithValue(ctx, middleware.RequestIDKey, requestID)
w.Header().Set(components.RequestIDHeaderKey, requestID)
next.ServeHTTP(w, r.WithContext(ctx))
}
return http.HandlerFunc(fn)
}
// AuthRequired API类型, 兼容多种鉴权模式
func AuthRequired(next http.Handler) http.Handler {
ignoreExtMap := map[string]struct{}{
".js": {},
".css": {},
".map": {},
".png": {},
}
fn := func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodOptions {
next.ServeHTTP(w, r)
return
}
// 静态资源过滤, 注意不会带鉴权信息
fileExt := filepath.Ext(r.URL.Path)
if _, ok := ignoreExtMap[fileExt]; ok {
next.ServeHTTP(w, r)
return
}
// switch {
// default:
// render.Render(w, r, rest.AbortWithUnauthorizedError(rest.UnauthorizedError))
// return
// }
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}

88
http/httpserver/io.go Normal file
View File

@ -0,0 +1,88 @@
package httpserver
import (
"bytes"
"fmt"
"io"
)
// LimitBuffer buf with limit
type LimitBuffer interface {
io.Writer
String() string
Remain() int
}
// limitBuffer buf with limit
type limitBuffer struct {
buf *bytes.Buffer
limit int
remain int
length int
}
// NewLimitBuffer ..
func NewLimitBuffer(limit int) LimitBuffer {
buf := bytes.NewBuffer(make([]byte, 0, limit))
return &limitBuffer{buf: buf, limit: limit, remain: limit}
}
func (b *limitBuffer) Write(p []byte) (n int, err error) {
defer func() {
b.length += n
}()
// discord
if b.remain <= 0 {
return len(p), nil
}
// write remain
if len(p) > b.remain {
n, err = b.buf.Write(p[:b.remain])
b.remain -= n
// alway return all writed length
n = len(p)
return
}
// write all
n, err = b.buf.Write(p)
b.remain -= n
return
}
// String ..
func (b *limitBuffer) String() string {
if b.length > b.limit {
return b.buf.String() + fmt.Sprintf("...(total %dB)", b.length)
}
return b.buf.String()
}
// Remain 剩余多少个字节没有写满
func (b *limitBuffer) Remain() int {
return b.remain
}
type teeReadCloser struct {
tee io.Reader
r io.Closer
}
// Read implement io.Read interface
func (t *teeReadCloser) Read(p []byte) (n int, err error) {
return t.tee.Read(p)
}
// Close implement io.Close interface
func (t *teeReadCloser) Close() error {
return t.r.Close()
}
// TeeReadCloser TeeReader with limit
func TeeReadCloser(r io.ReadCloser, w io.Writer) io.ReadCloser {
return &teeReadCloser{r: r, tee: io.TeeReader(r, w)}
}

View File

@ -0,0 +1,49 @@
package httpserver
import (
"bytes"
"io"
"testing"
"github.com/stretchr/testify/assert"
)
func TestLimitBufer(t *testing.T) {
buf := NewLimitBuffer(10)
n, err := buf.Write([]byte("1"))
assert.NoError(t, err)
assert.Equal(t, 1, n)
assert.Equal(t, "1", buf.String())
assert.Equal(t, 9, buf.Remain())
n, err = buf.Write([]byte("TestLimitBufer"))
// 设置limit但是不会返回 EOF
assert.NoError(t, err)
assert.Equal(t, 14, n)
assert.Equal(t, "1TestLimit...(total 15B)", buf.String())
assert.Equal(t, 0, buf.Remain())
n, err = buf.Write([]byte("TestLimitBufer"))
// 设置limit但是不会返回 EOF
assert.NoError(t, err)
assert.Equal(t, 14, n)
assert.Equal(t, "1TestLimit...(total 29B)", buf.String())
assert.Equal(t, 0, buf.Remain())
}
func TestTeeReader(t *testing.T) {
a := bytes.NewBuffer([]byte("TestLimitBufer"))
buf := NewLimitBuffer(10)
tee := TeeReadCloser(io.NopCloser(a), buf)
_, _ = io.CopyN(io.Discard, tee, 1)
assert.Equal(t, "T", buf.String())
assert.Equal(t, 9, buf.Remain())
io.Copy(io.Discard, io.LimitReader(tee, int64(buf.Remain())))
assert.Equal(t, "TestLimitB", buf.String())
assert.Equal(t, 0, buf.Remain())
io.Copy(io.Discard, tee)
assert.Equal(t, "TestLimitB...(total 14B)", buf.String())
}

View File

@ -1,4 +1,4 @@
package apis package rest
import ( import (
"context" "context"

View File

@ -1,4 +1,4 @@
package apis package rest
import ( import (
"bytes" "bytes"

View File

@ -1,4 +1,4 @@
package apis package rest
import ( import (
"net/http" "net/http"

View File

@ -1,4 +1,4 @@
package apis package rest
import ( import (
"reflect" "reflect"

84
http/rest/middleware.go Normal file
View File

@ -0,0 +1,84 @@
// Package apis for http
package rest
import (
"fmt"
"log/slog"
"net/http"
"path/filepath"
"time"
"github.com/go-chi/chi/v5/middleware"
"git.ifooth.com/common/pkg/http/httpserver"
"git.ifooth.com/common/pkg/http/restyclient"
)
// HandleLogger 记录请求日志
func HandleLogger(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
st := time.Now()
// 优先使用蓝鲸网关的request_id
reqId := r.Header.Get("X-Bkapi-Request-ID")
if reqId == "" {
reqId = r.Header.Get("X-Request-Id")
}
ctx := restyclient.WithRequestID(r.Context(), reqId)
r = r.WithContext(ctx)
limit := 2048
reqBuf := httpserver.NewLimitBuffer(limit)
r.Body = httpserver.TeeReadCloser(r.Body, reqBuf)
ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor)
respBuf := httpserver.NewLimitBuffer(limit)
ww.Tee(respBuf)
next.ServeHTTP(ww, r)
// 保证能读取前1K字符
// if reqBuf.Remain() > 0 {
// io.Copy(io.Discard, io.LimitReader(r.Body, int64(reqBuf.Remain())))
// }
msg := fmt.Sprintf("Handle %s %s From %s", r.Method, r.RequestURI, r.RemoteAddr)
slog.Info(msg, "req_id", reqId, "status", ww.Status(), "duration", time.Since(st), "req", reqBuf.String(), "resp", respBuf.String())
}
return http.HandlerFunc(fn)
}
// AuthRequired API类型, 兼容多种鉴权模式
func AuthRequired(next http.Handler) http.Handler {
ignoreExtMap := map[string]struct{}{
".js": {},
".css": {},
".map": {},
".png": {},
}
fn := func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodOptions {
next.ServeHTTP(w, r)
return
}
// 静态资源过滤, 注意不会带鉴权信息
fileExt := filepath.Ext(r.URL.Path)
if _, ok := ignoreExtMap[fileExt]; ok {
next.ServeHTTP(w, r)
return
}
// switch {
// default:
// render.Render(w, r, rest.AbortWithUnauthorizedError(rest.UnauthorizedError))
// return
// }
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}

54
http/rest/request.go Normal file
View File

@ -0,0 +1,54 @@
package rest
import (
"context"
"net/http"
"github.com/google/uuid"
)
type contextKey struct {
name string
}
var (
reqCtxKey = &contextKey{"HTTPRequest"}
)
type ctxKey int
const (
requestIDCtxKey = ctxKey(1)
requestIDHeaderKey = "X-Request-Id"
)
// HTTPRequest return svr's request
func HTTPRequest(ctx context.Context) *http.Request { // nolint
val, ok := ctx.Value(reqCtxKey).(*http.Request)
if !ok {
panic("missing request in context")
}
return val
}
// GenRequestID 生产 request_id
func GenRequestID() string {
id, _ := uuid.NewRandom()
return id.String()
}
// RequestIDValue 获取 RequestId 值
func RequestIDValue(ctx context.Context) string {
v, ok := ctx.Value(requestIDCtxKey).(string)
if !ok {
return ""
}
return v
}
// WithRequestID 设置 request_id
func WithRequestID(ctx context.Context, id string) context.Context {
newCtx := context.WithValue(ctx, requestIDCtxKey, id)
return newCtx
}

View File

@ -1,12 +1,10 @@
package apis package rest
import ( import (
"net/http" "net/http"
"github.com/go-chi/render" "github.com/go-chi/render"
"github.com/pkg/errors" "github.com/pkg/errors"
"git.ifooth.com/common/pkg/components"
) )
var ( var (
@ -14,28 +12,6 @@ var (
UnauthorizedError = errors.New("用户未登入") UnauthorizedError = errors.New("用户未登入")
) )
// HandlerFunc
type RestHandlerFunc func(r *http.Request) (interface{}, error)
func (h RestHandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) {
data, err := h(r)
if err != nil {
// handle returned error here.
render.Render(w, r, AbortWithBadRequestError(err))
return
}
switch v := data.(type) {
case render.Renderer:
if err := v.Render(w, r); err != nil {
render.Render(w, r, AbortWithBadRequestError(err))
return
}
default:
render.JSON(w, r, data)
}
}
// APIResponse 返回的标准结构 // APIResponse 返回的标准结构
type APIResponse struct { type APIResponse struct {
Err error `json:"-"` // low-level runtime error Err error `json:"-"` // low-level runtime error
@ -53,7 +29,7 @@ func (res *APIResponse) Render(w http.ResponseWriter, r *http.Request) error {
statusCode = http.StatusOK statusCode = http.StatusOK
} }
res.RequestId = components.RequestIDValue(r.Context()) res.RequestId = RequestIDValue(r.Context())
render.Status(r, statusCode) render.Status(r, statusCode)
return nil return nil

101
http/restyclient/client.go Normal file
View File

@ -0,0 +1,101 @@
// Package restyclient for http client
package restyclient
import (
"context"
"crypto/tls"
"net"
"net/http"
"sync"
"time"
"github.com/go-resty/resty/v2"
"github.com/google/uuid"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)
const (
timeout = time.Second * 30
)
var (
clientOnce sync.Once
silentClientOnce sync.Once
globalClient *resty.Client
globalSilentClient *resty.Client
)
var dialer = &net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}
// defaultTransport default transport
var defaultTransport http.RoundTripper = &http.Transport{
DialContext: dialer.DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
// NOCC:gas/tls(设计如此)
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // nolint
}
// New : 新建 Client, 设置公共参数tracing 等; 每次新建cookies不复用
func New() *resty.Client {
if globalClient == nil {
clientOnce.Do(func() {
globalClient = resty.New().
SetTransport(otelhttp.NewTransport(defaultTransport)).
SetTimeout(timeout).
SetCookieJar(nil).
SetDebugBodyLimit(1024).
OnAfterResponse(restyAfterResponseHook).
SetPreRequestHook(restyBeforeRequestHook).
OnError(restyErrHook).
SetHeader("User-Agent", "envmgr-restyclient")
})
}
return globalClient
}
// silentNew 安静模式,只打印错误日志
func silentNew() *resty.Client {
if globalSilentClient == nil {
silentClientOnce.Do(func() {
globalSilentClient = resty.New().
SetTransport(otelhttp.NewTransport(defaultTransport)).
SetTimeout(timeout).
SetCookieJar(nil).
SetDebugBodyLimit(1024).
// OnAfterResponse(restyAfterResponseHook).
// SetPreRequestHook(restyBeforeRequestHook).
OnError(restyErrHook).
SetHeader("User-Agent", "envmgr-restyclient")
})
}
return globalSilentClient
}
// R : New().R() 快捷方式, 已设置公共参数tracing 等
func R() *resty.Request {
return New().R()
}
// SilentR : 安静模式,只打印错误日志 已设置公共参数tracing 等, 只打印错误日志
func SilentR() *resty.Request {
return silentNew().R()
}
// GenRequestID 生产 request_id
func GenRequestID() string {
id, _ := uuid.NewRandom()
return id.String()
}
// WithRequestID 设置 request_id
func WithRequestID(ctx context.Context, id string) context.Context {
newCtx := context.WithValue(ctx, requestIDCtxKey, id)
return newCtx
}

View File

@ -0,0 +1,74 @@
package restyclient
import (
"encoding/json"
"errors"
"fmt"
"strconv"
resty "github.com/go-resty/resty/v2"
)
// CodeNotZeroErr ...
var CodeNotZeroErr = errors.New("resp code != 0")
// BKResult 蓝鲸返回规范的结构体
type BKResult[T any] struct {
Result bool `json:"result"` // 部分蓝鲸接口有, 按需校验
Code any `json:"code"`
Message string `json:"message"`
Data *T `json:"data"`
}
// NewBKResult create NewBKResult by resp
func NewBKResult[T any](resp *resty.Response) (*BKResult[T], error) {
if !resp.IsSuccess() {
return nil, fmt.Errorf("request failed, status: %s, message: %s", resp.Status(), resp.Body())
}
bkResult := new(BKResult[T])
if err := json.Unmarshal(resp.Body(), bkResult); err != nil {
return nil, err
}
if err := bkResult.ValidateCode(); err != nil {
return nil, err
}
return bkResult, nil
}
// NewBKData only create data by resp
func NewBKData[T any](resp *resty.Response) (*T, error) {
bkResult, err := NewBKResult[T](resp)
if err != nil {
return nil, err
}
return bkResult.Data, nil
}
// ValidateCode 返回结果是否OK
func (r *BKResult[T]) ValidateCode() error {
var resultCode int
switch code := r.Code.(type) {
case int:
resultCode = code
case float64:
resultCode = int(code)
case string:
c, err := strconv.Atoi(code)
if err != nil {
return err
}
resultCode = c
default:
return fmt.Errorf("conversion to int from %T not supported", code)
}
if resultCode != 0 {
return fmt.Errorf("%w, code=%d, message=%s", CodeNotZeroErr, resultCode, r.Message)
}
return nil
}

42
http/restyclient/hook.go Normal file
View File

@ -0,0 +1,42 @@
package restyclient
import (
"fmt"
"log/slog"
"net/http"
"github.com/dustin/go-humanize"
"github.com/go-resty/resty/v2"
)
// restyBeforeRequestHook 请求hook
func restyBeforeRequestHook(c *resty.Client, r *http.Request) error {
rid := getRequestID(r)
r.Header.Set(requestIDHeaderKey, rid)
rbody, err := reqToCurl(r)
if err != nil {
return err
}
slog.With("req_id", rid).Info("restyclient REQ", "body", rbody)
return nil
}
// restyAfterResponseHook 正常返回hook
func restyAfterResponseHook(c *resty.Client, resp *resty.Response) error {
// 最大打印 1024 个字符
body := string(resp.Body())
if len(body) > 1024 {
body = fmt.Sprintf("%s...(Total %s)", body[:1024], humanize.Bytes(uint64(len(body))))
}
slog.With("req_id", getRequestID(resp.RawResponse.Request)).Info("restyclient RESP", "status", resp.Status(), "duration", resp.Time(), "body", body)
return nil
}
// restyErrHook 错误hook
func restyErrHook(r *resty.Request, err error) {
slog.With("req_id", getRequestID(r.RawRequest)).Error("restyclient RESP", "err", err)
}

View File

@ -0,0 +1,162 @@
package restyclient
import (
"bytes"
"fmt"
"io"
"log/slog"
"net/http"
"time"
"github.com/dustin/go-humanize"
)
type ctxKey int
const (
requestIDCtxKey = ctxKey(1)
requestIDHeaderKey = "X-Request-Id"
)
var (
// maskKeys 敏感参数和头部key
maskKeys = map[string]struct{}{
"bk_app_secret": {},
"bk_token": {},
"Authorization": {},
"X-Bkapi-Authorization": {},
}
)
func getRequestID(r *http.Request) string {
v, ok := r.Context().Value(requestIDCtxKey).(string)
if ok && v != "" {
return v
}
rid := r.Header.Get(requestIDHeaderKey)
if rid != "" {
return rid
}
return GenRequestID()
}
// reqToCurl curl 格式的请求日志
func reqToCurl(r *http.Request) (string, error) {
// 过滤掉敏感信息, header 和 query
headers := ""
for key, values := range r.Header {
for _, value := range values {
if _, ok := maskKeys[key]; ok {
value = "***"
}
headers += fmt.Sprintf(" -H %q", fmt.Sprintf("%s: %s", key, value))
}
}
rawURL := *r.URL
queryValue := rawURL.Query()
for key := range queryValue {
if _, ok := maskKeys[key]; ok {
queryValue.Set(key, "<masked>")
}
}
rawURL.RawQuery = queryValue.Encode()
reqMsg := fmt.Sprintf("curl -X %s '%s'%s", r.Method, rawURL.String(), headers)
if r.Body != nil {
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
return "", err
}
r.Body.Close() // nolint
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
if len(bodyBytes) > 1024 {
reqMsg += fmt.Sprintf(" -d '%s...(Total %s)'", bodyBytes[:1024], humanize.Bytes(uint64(len(bodyBytes))))
} else {
reqMsg += fmt.Sprintf(" -d '%s'", bodyBytes)
}
}
return reqMsg, nil
}
// respToCurl 返回日志
func respToCurl(resp *http.Response, st time.Time) (string, error) {
var (
bodyBytes []byte
err error
)
if resp.Body != nil {
bodyBytes, err = io.ReadAll(resp.Body)
if err != nil {
return "", err
}
resp.Body.Close() // nolint
resp.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
}
if len(bodyBytes) > 1024 {
respMsg := fmt.Sprintf("[%s] %s %s...(Total %s)",
resp.Status, time.Since(st), bodyBytes[:1024], humanize.Bytes(uint64(len(bodyBytes))))
return respMsg, nil
}
if len(bodyBytes) > 0 {
respMsg := fmt.Sprintf("[%s] %s %s", resp.Status, time.Since(st), bodyBytes)
return respMsg, nil
}
respMsg := fmt.Sprintf("[%s] %s", resp.Status, time.Since(st))
return respMsg, nil
}
// curlLogTransport print curl log transport
type curlLogTransport struct {
Transport http.RoundTripper
}
// RoundTrip curlLog Transport
func (t *curlLogTransport) RoundTrip(req *http.Request) (*http.Response, error) {
st := time.Now()
rid := getRequestID(req)
req.Header.Set(requestIDHeaderKey, rid)
// 记录请求
rbody, err := reqToCurl(req)
if err != nil {
return nil, err
}
slog.With("req_id", rid).Info("restyclient REQ", "body", rbody)
resp, err := t.transport(req).RoundTrip(req)
if err != nil {
slog.With("req_id", rid).Error("restyclient RESP", "err", err)
return nil, err
}
// 记录返回
respBody, err := respToCurl(resp, st)
if err != nil {
return nil, err
}
slog.With("req_id", rid).Info("restyclient REQ", "body", respBody)
return resp, nil
}
func (t *curlLogTransport) transport(req *http.Request) http.RoundTripper { //nolint:unparam
if t.Transport != nil {
return t.Transport
}
return http.DefaultTransport
}
// NewCurlLogTransport make a new curl log transport, default transport can be nil
func NewCurlLogTransport(transport http.RoundTripper) http.RoundTripper {
return &curlLogTransport{Transport: transport}
}

36
logger/logger.go Normal file
View File

@ -0,0 +1,36 @@
// Package logger provider std slog logger
package logger
import (
"log/slog"
"os"
"path/filepath"
"strconv"
)
// Init 初始化 slog
func Init() {
textHandler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
AddSource: true,
Level: slog.LevelInfo,
ReplaceAttr: ReplaceSourceAttr,
})
logger := slog.New(textHandler)
slog.SetDefault(logger)
}
// ReplaceSourceAttr source 格式化为 dir/file:line 格式
func ReplaceSourceAttr(groups []string, a slog.Attr) slog.Attr {
if a.Key != slog.SourceKey {
return a
}
src, ok := a.Value.Any().(*slog.Source)
if !ok {
return a
}
a.Value = slog.StringValue(filepath.Base(src.File) + ":" + strconv.Itoa(src.Line))
return a
}

View File

@ -1,66 +0,0 @@
package metrics
import (
"net/http"
"strconv"
"time"
"github.com/go-chi/chi/v5/middleware"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/collectors"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
var (
reg = prometheus.NewRegistry()
// http 请求总量
httpRequestsTotal = prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Counter of HTTP requests to prime",
}, []string{"handler", "method", "code"})
// http 请求耗时
httpRequestDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "Histogram of latencies for HTTP requests to prime.",
Buckets: []float64{0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1},
}, []string{"handler", "method", "code"})
)
func init() {
reg.MustRegister(
collectors.NewGoCollector(),
collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}),
)
reg.MustRegister(httpRequestsTotal)
reg.MustRegister(httpRequestDuration)
}
// Handler
func Handler() http.HandlerFunc {
return promhttp.HandlerFor(reg, promhttp.HandlerOpts{Registry: reg}).ServeHTTP
}
// collectHTTPRequestMetric http metrics 处理
func collectHTTPRequestMetric(handler, method, code string, duration time.Duration) {
httpRequestsTotal.WithLabelValues(handler, method, code).Inc()
httpRequestDuration.WithLabelValues(handler, method, code).Observe(duration.Seconds())
}
// RequestCollect
func RequestCollect(name string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor)
t1 := time.Now()
defer func() {
collectHTTPRequestMetric(name, r.Method, strconv.Itoa(ww.Status()), time.Since(t1))
}()
next.ServeHTTP(ww, r)
}
return http.HandlerFunc(fn)
}
}