package alog
var (
ConfigAppVersion = ""
ConfigSentryUrl = ""
ConfigSentryPublicKey = ""
)
func InitAlog(
version string,
sentryUrl, sentryPublicKey string,
) {
ConfigAppVersion = version
ConfigSentryUrl = sentryUrl
ConfigSentryPublicKey = sentryPublicKey
}
package alog
import (
"context"
)
func CheckContext(ctx context.Context) {
CheckTracker(GetTracker(ctx))
}
func WithTracker(parent context.Context) (context.Context, context.CancelFunc) {
tracker := NewTracker()
ctx := context.WithValue(parent, KeyTracker, tracker)
return ctx, func() {
CheckTracker(tracker)
}
}
package alog
import (
"fmt"
"github.com/getsentry/sentry-go"
"github.com/gin-gonic/gin"
"net"
"net/http"
"os"
"strings"
"time"
)
// GinWithLogger returns gin.HandlerFunc, it should be used as a middleware.
// Compare to gin.Logger(), it prints "TrackerID" for each request in addition.
func GinWithLogger() gin.HandlerFunc {
formatter := func(param gin.LogFormatterParams) string {
var statusColor, methodColor, resetColor string
if param.IsOutputColor() {
statusColor = param.StatusCodeColor()
methodColor = param.MethodColor()
resetColor = param.ResetColor()
}
if param.Latency > time.Minute {
// Truncate in a golang < 1.8 safe way
param.Latency = param.Latency - param.Latency%time.Second
}
var trackID, ok = param.Keys[KeyTrackID].(string)
if ok {
return fmt.Sprintf("[GIN] %v |%s %3d %s| %13v | %15s |%s %-7s %s[%-8s] %#v\n%s",
param.TimeStamp.Format("2006/01/02 - 15:04:05"),
statusColor, param.StatusCode, resetColor,
param.Latency,
param.ClientIP,
methodColor, param.Method, resetColor,
trackID,
param.Path,
param.ErrorMessage,
)
}
return fmt.Sprintf("[GIN] %v |%s %3d %s| %13v | %15s |%s %-7s %s %#v\n%s",
param.TimeStamp.Format("2006/01/02 - 15:04:05"),
statusColor, param.StatusCode, resetColor,
param.Latency,
param.ClientIP,
methodColor, param.Method, resetColor,
param.Path,
param.ErrorMessage,
)
}
cfg := gin.LoggerConfig{
Formatter: formatter,
}
return gin.LoggerWithConfig(cfg)
}
// GinWithTracker returns gin.HandlerFunc, it should be used as a middleware.
// It injects a Tracker to *gin.Context for each request.
func GinWithTracker() gin.HandlerFunc {
return func(c *gin.Context) {
tracker := NewTracker()
c.Set(KeyTracker, tracker)
c.Set(KeyTrackID, tracker.ID)
c.Header(GinHttpResponseHeader, tracker.ID)
c.Next()
if tracker.Exceptions != nil {
tracker.Request = sentry.NewRequest(c.Request)
if ConfigSentryUrl == "" {
go tracker.Print()
} else {
go BuildAndSendSentryEvent(tracker)
}
}
}
}
// GinWithRecover returns gin.HandlerFunc, it should be used as a middleware.
// It recovers your server from panic, and record the error in Tracker (to handle it later).
func GinWithRecover() gin.HandlerFunc {
return func(ctx *gin.Context) {
defer func() {
if err := recover(); err != nil {
// Check for a broken connection, as it is not really a condition that warrants a panic stack trace.
// 逻辑来自gin.RecoveryWithWriter()
var brokenPipe bool
if ne, ok := err.(*net.OpError); ok {
if se, ok := ne.Err.(*os.SyscallError); ok {
if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
brokenPipe = true
}
}
}
TraceStack(ctx, err)
// If the connection is dead, we can't write a status to it.
// 逻辑来自gin.RecoveryWithWriter()
if brokenPipe {
ctx.Error(err.(error))
ctx.Abort()
} else {
ctx.AbortWithStatus(http.StatusInternalServerError)
}
}
}()
ctx.Next()
}
}
package alog
import (
"context"
"errors"
"fmt"
"runtime"
"strings"
)
func TraceStack(ctx context.Context, e interface{}, trackValues ...map[string]interface{}) error {
err, ok := e.(error)
if !ok {
err = errors.New(fmt.Sprint(e))
}
// 取出所有栈
var stacks []*ExceptionStack
for i := 2; ; i++ {
pc, file, line, ok := runtime.Caller(i)
if !ok {
break
}
var stack ExceptionStack
stack.Filename = file
stack.Lineno = line
f := runtime.FuncForPC(pc)
words := strings.Split(f.Name(), ".")
if len(words) == 2 {
stack.Package, stack.Function = words[0], words[1]
}
stacks = append(stacks, &stack)
}
if len(stacks) != 0 {
setValuesToStack(stacks[0], trackValues)
}
// 如果ctx里有Tracker就放进去统一处理,否则直接丢到日志里去。
tracker := GetTracker(ctx)
if tracker != nil {
tracker.lock.Lock()
defer tracker.lock.Unlock()
for _, ex := range tracker.Exceptions {
if ex.Error == err {
ex.Stacks = append(ex.Stacks, stacks...)
return err
}
}
tracker.Exceptions = append(tracker.Exceptions, &Exception{
Error: err,
Stacks: stacks,
})
} else {
b := strings.Builder{}
b.WriteString(err.Error())
b.WriteByte('\n')
for _, stack := range stacks {
b.WriteString(fmt.Sprintf(" %s:%d\n", stack.Filename, stack.Lineno))
}
RECOVER.Println(b.String())
}
return err
}
// Deprecated: 推荐使用 CERecover 或者 CERecoverError
// Recover
func Recover(ctx context.Context) {
if err := recover(); err != nil {
TraceStack(ctx, err)
}
}
func CERecover(ctx context.Context, trackValues ...map[string]interface{}) {
if err := recover(); err != nil {
TraceStack(ctx, err, trackValues...)
}
}
func CERecoverError(ctx context.Context, errPointer *error, trackValues ...map[string]interface{}) {
if err := recover(); err != nil {
*errPointer = TraceStack(ctx, err, trackValues...)
}
}
package alog
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"github.com/getsentry/sentry-go"
"io/ioutil"
"net/http"
"os"
"reflect"
"runtime"
"strings"
"sync"
"time"
)
const (
KeyTracker = "_alog_Tracker"
KeyTrackID = "_alog_TrackID"
GinHttpResponseHeader = "X-Track"
)
type V map[string]interface{}
type Tracker struct {
ID string
Exceptions []*Exception
Request *sentry.Request
lock sync.Mutex
}
type Exception struct {
Error error
Stacks []*ExceptionStack
}
type ExceptionStack struct {
Filename string
Package string
Function string
Lineno int
Vars V
}
func NewTracker() *Tracker {
return &Tracker{
ID: string(RandomBytes(8)),
}
}
func (t *Tracker) Print() {
var buf = time.Now().UTC().AppendFormat([]byte("[TRACK] "), "2006/01/02 15:04:05 [")
buf = append(buf, []byte(t.ID)...)
buf = append(buf, ']', '\n')
for i, ex := range t.Exceptions {
buf = append(buf, []byte(fmt.Sprintf(" [%d] %s\n", i, ex.Error.Error()))...)
for _, stack := range ex.Stacks {
js, _ := json.Marshal(stack.Vars)
buf = append(buf, fmt.Sprintf(" %s:%d: %s\n", stack.Filename, stack.Lineno, js)...)
}
}
os.Stdout.Write(buf)
}
func GetTrackID(ctx context.Context) string {
tracker := GetTracker(ctx)
if tracker != nil {
return tracker.ID
}
return ""
}
func GetTracker(ctx context.Context) *Tracker {
tracker, ok := ctx.Value(KeyTracker).(*Tracker)
if ok {
return tracker
}
return nil
}
func setValuesToStack(stack *ExceptionStack, trackValues []map[string]interface{}) {
// 合并传入的参数
for _, tv := range trackValues {
if tv == nil {
continue
}
if stack.Vars == nil {
stack.Vars = tv
} else {
for k, v := range tv {
stack.Vars[k] = v
}
}
}
}
func ce(ctx context.Context, err error, trackValues []map[string]interface{}) {
var stack ExceptionStack
setValuesToStack(&stack, trackValues)
// 追踪当前的栈信息
if pc, file, line, ok := runtime.Caller(2); ok {
stack.Filename = file
stack.Lineno = line
f := runtime.FuncForPC(pc)
words := strings.Split(f.Name(), ".")
if len(words) == 2 {
stack.Package, stack.Function = words[0], words[1]
}
} else {
return
}
// 如果ctx里有Tracker就放进去统一处理,否则直接丢到日志里去。
tracker := GetTracker(ctx)
if tracker != nil {
tracker.lock.Lock()
defer tracker.lock.Unlock()
for _, ex := range tracker.Exceptions {
if errors.Is(ex.Error, err) {
ex.Stacks = append(ex.Stacks, &stack)
return
}
}
tracker.Exceptions = append(tracker.Exceptions, &Exception{
Error: err,
Stacks: []*ExceptionStack{&stack},
})
} else {
js, _ := json.Marshal(stack.Vars)
ERROR.Printf("%s:%d: %s. %s\n", stack.Filename, stack.Lineno, err, js)
}
}
// CE 意思是 CheckError ,为了方便按键而起这个名字。
func CE(ctx context.Context, err error, trackValues ...map[string]interface{}) {
if err == nil {
return
}
ce(ctx, err, trackValues)
}
// CEI 意思是 Check Error Interface,可以灵活处理interface{}。
// 建议不要用在 recover() 的情况,会丢失 panic() 的位置,请使用 CERecover 替代。
// 建议不要使用可比较值,否则可能与其他错误栈混在一起,最好用指针或者接口变量。
func CEI(ctx context.Context, err interface{}, trackValues ...map[string]interface{}) {
if err == nil {
return
}
e, ok := err.(error)
if ok {
ce(ctx, e, trackValues)
} else {
ce(ctx, errors.New(fmt.Sprintf("Interface<%T>: %v", err, err)), trackValues)
}
}
func BuildSentryEvent(tracker *Tracker) *sentry.Event {
if tracker.Exceptions == nil {
return nil
}
var exceptions []sentry.Exception
for _, ex := range tracker.Exceptions {
var trace sentry.Stacktrace
for i := len(ex.Stacks) - 1; i >= 0; i-- {
stack := ex.Stacks[i]
trace.Frames = append(trace.Frames, sentry.Frame{
Filename: stack.Filename,
Function: stack.Function,
Package: stack.Package,
Lineno: stack.Lineno,
Vars: stack.Vars,
})
}
var exception = sentry.Exception{
Value: ex.Error.Error(),
Type: reflect.TypeOf(ex.Error).String(),
Stacktrace: &trace,
}
exceptions = append(exceptions, exception)
}
event := sentry.Event{
Extra: map[string]interface{}{"TrackID": tracker.ID},
Level: "error",
Timestamp: time.Now().UTC(),
Sdk: sentry.SdkInfo{
Name: "sentry.go",
Version: sentry.Version,
},
Release: ConfigAppVersion,
Exception: exceptions,
Request: tracker.Request,
}
return &event
}
func SendSentryEvent(event *sentry.Event) error {
if event == nil {
return nil
}
js, _ := json.Marshal(event)
req, _ := http.NewRequest("POST", ConfigSentryUrl, bytes.NewReader(js))
auth := fmt.Sprintf("Sentry sentry_key=%s", ConfigSentryPublicKey)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Sentry-Auth", auth)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
body, _ := ioutil.ReadAll(resp.Body)
return errors.New(fmt.Sprintf("Sentry上报异常: %s. %s", resp.Status, body))
}
return nil
}
func BuildAndSendSentryEvent(tracker *Tracker) {
event := BuildSentryEvent(tracker)
err := SendSentryEvent(event)
if err != nil {
ERROR.Println(err)
}
}
func CheckTracker(tracker *Tracker) {
if tracker == nil {
return
}
if tracker.Exceptions != nil {
if ConfigSentryUrl == "" {
tracker.Print()
} else {
go BuildAndSendSentryEvent(tracker)
}
}
}
package alog
import (
"github.com/gin-gonic/gin"
"math/rand"
"sync/atomic"
"time"
)
var _seedCounter = new(int64)
func init() {
src := rand.NewSource(time.Now().UnixNano())
*_seedCounter = src.Int63()
}
// RandomBytes Generates random alphanumeric bytes.
func RandomBytes(length int) []byte {
const randomCodeCharSet = `1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ`
// rand.Source is not concurrency-safe. So create one every time (on stack?)
src := rand.NewSource(atomic.AddInt64(_seedCounter, 1))
code := make([]byte, length)
for i := 0; i < length; i++ {
code[i] = randomCodeCharSet[src.Int63()%62]
}
return code
}
func main() {
InitAlog("v1.0.0", "https://...", "812793r713452d") // 记得初始化!
g := gin.New()
g.Use(GinWithLogger(), GinWithTracker(), GinWithRecover()) // 注意顺序!
}