Files
cybertron-log-enricher/service/ErrorProcessor.go
2025-01-03 19:32:21 +05:30

247 lines
7.4 KiB
Go

package service
import (
"encoding/json"
"go.uber.org/zap"
"log-enricher/configs"
"log-enricher/models/es"
dbPackage "log-enricher/pkg/db"
"log-enricher/pkg/encoder"
"log-enricher/pkg/log"
"log-enricher/pkg/symbolicator"
"path"
"strings"
"time"
)
type ErrorProcessor struct {
logger *log.Logger
elasticSearchClient *dbPackage.ElasticSearchClient
sourceMapFetcher *SourceMapService
awsConfig configs.AwsConfig
}
type Frame struct {
Filename string `json:"filename"`
Function string `json:"function"`
InApp bool `json:"in_app"`
Lineno int `json:"lineno"`
Colno int `json:"colno"`
SourceMapPath string `json:"source_map_path,omitempty"`
}
type Stacktrace struct {
Frames []Frame `json:"frames"`
}
type MechanismData struct {
Function string `json:"function"`
Handler string `json:"handler"`
Target string `json:"target"`
}
type Mechanism struct {
Type string `json:"type"`
Handled bool `json:"handled"`
Data MechanismData `json:"data"`
}
type Extra struct {
ReleaseId string `json:"release_id"`
}
type Exception struct {
Type string `json:"type"`
Value string `json:"value"`
Stacktrace Stacktrace `json:"stacktrace"`
Mechanism Mechanism `json:"mechanism"`
ProjectId string `json:"project_id,omitempty"`
ReleaseId string `json:"release_id,omitempty"`
Breadcrumbs interface{} `json:"breadcrumbs,omitempty"`
Extra interface{} `json:"extra,omitempty"`
Request interface{} `json:"request,omitempty"`
Contexts interface{} `json:"contexts,omitempty"`
AwsAccount string `json:"aws_account,omitempty"`
}
func NewErrorProcessor(logger *log.Logger, elasticSearchClient *dbPackage.ElasticSearchClient, sourceMapFetcherService *SourceMapService, awsConfig configs.AwsConfig) *ErrorProcessor {
return &ErrorProcessor{
logger: logger,
elasticSearchClient: elasticSearchClient,
sourceMapFetcher: sourceMapFetcherService,
awsConfig: awsConfig,
}
}
func extractFilename(url string) string {
// Remove any trailing characters you don't want (in case there are any)
url = strings.Trim(url, `"`)
// Check if the URL starts with "app:///" and remove the scheme if present this is to parse react native file names
if strings.HasPrefix(url, "app:///") {
var stringWithprefixRemoved = strings.TrimPrefix(url, "app:///")
var stringWithSuffixRemoved = strings.Split(stringWithprefixRemoved, "?")[0]
return stringWithSuffixRemoved + ".map"
}
// Extract the base (filename) from the URL path
filename := path.Base(url)
return filename + ".map"
}
func extractPlatform(payload Exception) string {
if len(payload.Stacktrace.Frames) == 0 {
return "WEB"
}
var url = payload.Stacktrace.Frames[0].Filename
if strings.HasPrefix(url, "app:///") {
return "REACT_NATIVE"
}
return "WEB"
}
func getSignificantStack(trace symbolicator.SymbolicatedStackTrace) string {
if len(trace.Frames) != 0 {
return trace.Frames[len(trace.Frames)-1].Token
}
return ""
}
func (ep *ErrorProcessor) getCacheStack(payload Exception, hash string) ([]symbolicator.SymbolicatedFrame, error) {
cachedStack, total, err := ep.elasticSearchClient.GetStackTraceByErrorHash(hash, payload.ProjectId)
var frames []symbolicator.SymbolicatedFrame
if total > 0 {
frameBytes, marshalErr := json.Marshal(cachedStack["stack_trace"])
if marshalErr != nil {
ep.logger.Error("unable to serilize cache frame", zap.Error(marshalErr))
} else {
err = json.Unmarshal(frameBytes, &frames)
}
}
return frames, err
}
func (ep *ErrorProcessor) ProcessError(error []byte) {
ep.logger.Info("processing error in consumer")
var payload Exception
err := json.Unmarshal(error, &payload)
var validFrames []Frame
var invalidFrames []Frame
var platform = extractPlatform(payload)
var awsAccount = payload.AwsAccount
var appendedAccount = "-" + awsAccount
//for PPL to maintain backward compatability we will leave the account name as blank which
//is hacky but could not find better workaround for now
if awsAccount == "ppl" || awsAccount == "" {
appendedAccount = ""
}
if err != nil {
ep.logger.Error("error in unmarshalling exception", zap.Error(err))
return
}
if payload.Extra == nil {
ep.logger.Error("payload extra is nil")
return
}
extraMap, conversionOk := payload.Extra.(map[string]interface{})
if !conversionOk {
ep.logger.Error("error in unmarshalling exception")
return
}
releaseId, ok := extraMap["release_id"].(string)
if !ok {
ep.logger.Error("Release id is not found")
return
}
//getting source map path
for i, frame := range payload.Stacktrace.Frames {
fileName := extractFilename(frame.Filename)
projectId := payload.ProjectId
frame := &payload.Stacktrace.Frames[i]
ep.logger.Info("processing frame", zap.String("filename", fileName))
ep.logger.Info("processing frame", zap.String("filename", frame.SourceMapPath))
//todo make release dynamic
sourceMapPath, err := ep.sourceMapFetcher.GetSourceMap(fileName, projectId, releaseId, ep.awsConfig.Bucket+appendedAccount)
if err != nil {
ep.logger.Error("error occured in fetching source map skipping this frame", zap.Error(err))
invalidFrames = append(invalidFrames, *frame)
continue
}
frame.SourceMapPath = sourceMapPath
validFrames = append(validFrames, *frame)
}
payload.Stacktrace.Frames = validFrames
var frames, marshalErr = json.Marshal(&payload.Stacktrace)
if marshalErr != nil {
ep.logger.Error("unable to serilize frames", zap.Error(marshalErr))
}
var symbolicatorCommand = ""
var args = []string{""}
if platform == "REACT_NATIVE" {
symbolicatorCommand = "node"
args = []string{"bins/cybertron-symbolicator-react-native/index.js", string(frames)}
} else {
symbolicatorCommand = "bins/source-map"
args = []string{string(frames)}
}
//make md5 hash of error
hash := encoder.Md5Encode(string(frames) + payload.Value)
var output symbolicator.SymbolicatedStackTrace
//cache stack processing
cachedStackTrace, err := ep.getCacheStack(payload, hash)
if len(cachedStackTrace) > 0 {
output.Frames = cachedStackTrace
} else {
command := &symbolicator.Command{
Cmd: symbolicatorCommand,
Args: args,
}
if len(frames) != 0 {
output, err = symbolicator.SymbolicatorClient(command)
}
for _, v := range invalidFrames {
output.Frames = append(output.Frames, symbolicator.SymbolicatedFrame{
Token: v.Filename,
OriginalLine: v.Lineno,
Lines: []string{"non symbolic code"},
Start: 0,
End: 0,
})
}
if err != nil {
ep.logger.Error("error occured in symobilicator client", zap.Error(err))
}
}
//creating es document
ep.logger.Info("processed document successfully saving it to elasticsearch", zap.String("hash", hash))
errorDocument := &es.ErrorDocument{
Error: payload.Value,
Title: payload.Type,
StackTrace: output.Frames,
SignificantStack: getSignificantStack(output),
ErrorHash: hash,
ProjectId: payload.ProjectId,
ReleaseVersion: releaseId,
CreatedAt: time.Now().Unix(),
Breadcrumbs: payload.Breadcrumbs,
Extra: payload.Extra,
Request: payload.Request,
Contexts: payload.Contexts,
}
elastic_err := ep.elasticSearchClient.IndexDocument(errorDocument)
if elastic_err != nil {
ep.logger.Info("error occured in saving document to elasticsearch", zap.Error(elastic_err))
return
}
}