package main import ( "bufio" "encoding/json" "flag" "fmt" "io" "log" "net/http" "os" "strings" "time" "github.com/getsentry/sentry-go" ) func getDSN() string { // Try multiple environment variables for DSN dsn := getEnvDSN() if dsn == "" { fmt.Fprintf(os.Stderr, "Error: No DSN environment variable found.\n") fmt.Fprintf(os.Stderr, "Please set BUGSINK_DSN (or GLITCHTIP_DSN/SENTRY_DSN for compatibility)\n") os.Exit(1) } return dsn } func main() { // Set up flag usage flag.Usage = func() { fmt.Fprintf(os.Stderr, "go-sink - A tool for sending logs to error tracking systems\n\n") fmt.Fprintf(os.Stderr, "Usage: %s [options] [file]\n\n", os.Args[0]) fmt.Fprintf(os.Stderr, "Options:\n") flag.PrintDefaults() fmt.Fprintf(os.Stderr, "\nEnvironment Variables:\n") fmt.Fprintf(os.Stderr, " BUGSINK_DSN DSN for BugSink (primary)\n") fmt.Fprintf(os.Stderr, " Also accepts GLITCHTIP_DSN and SENTRY_DSN for compatibility\n") fmt.Fprintf(os.Stderr, "\nExamples:\n") fmt.Fprintf(os.Stderr, " %s logfile.txt\n", os.Args[0]) fmt.Fprintf(os.Stderr, " cat logfile.txt | %s\n", os.Args[0]) fmt.Fprintf(os.Stderr, " %s server\n", os.Args[0]) fmt.Fprintf(os.Stderr, " %s --dsn \"https://example.com/123\" logfile.txt\n", os.Args[0]) fmt.Fprintf(os.Stderr, "\nVersion: %s\n", VersionInfo()) } // Add version and help flags showVersion := flag.Bool("version", false, "Show version information") showHelp := flag.Bool("help", false, "Show help information") flag.BoolVar(showHelp, "h", false, "Show help information (shorthand)") // Add DSN flag dsnFlag := flag.String("dsn", "", "Override DSN for this execution (takes precedence over environment variables)") flag.Parse() if *showHelp { flag.Usage() os.Exit(0) } if *showVersion { fmt.Println(VersionInfo()) os.Exit(0) } // Initialize Sentry var dsn string if *dsnFlag != "" { dsn = *dsnFlag } else { dsn = getDSN() } err := sentry.Init(sentry.ClientOptions{ Dsn: dsn, }) if err != nil { log.Fatalf("sentry.Init: %s", err) } if len(os.Args) > 1 && os.Args[1] == "server" { startServer() return } processLogs() defer func() { sentry.Flush(2 * time.Second) }() } // getEnvDSN checks multiple environment variables for a DSN func getEnvDSN() string { // Try different environment variables in order of preference envVars := []string{ "GLITCHTIP_DSN", "SENTRY_DSN", "BUGSINK_DSN", } for _, envVar := range envVars { if dsn := os.Getenv(envVar); dsn != "" { fmt.Printf("Using DSN from %s environment variable\n", envVar) return dsn } } return "" } func processLogs() { var reader io.Reader var filePath string if len(os.Args) == 2 { filePath = os.Args[1] file, err := os.Open(filePath) if err != nil { fmt.Fprintf(os.Stderr, "Error opening file: %s\n", err) os.Exit(1) } defer file.Close() reader = file fmt.Println("Reading from file:", filePath) } else if isInputFromPipe() { reader = os.Stdin fmt.Println("Reading from stdin") } else { fmt.Fprintf(os.Stderr, "Usage: %s OR pipe input\n", os.Args[0]) os.Exit(1) } scanner := bufio.NewScanner(reader) var content strings.Builder for scanner.Scan() { line := scanner.Text() content.WriteString(line) content.WriteString("\n") fmt.Println("Read line:", line) } if err := scanner.Err(); err != nil { fmt.Fprintf(os.Stderr, "Error reading input: %s\n", err) os.Exit(1) } logMessage := content.String() fmt.Println("Final log message:", logMessage) sendToSentry(logMessage) } func isInputFromPipe() bool { fileInfo, err := os.Stdin.Stat() if err != nil { return false } return fileInfo.Mode()&os.ModeCharDevice == 0 } func sendToSentry(logMessage string) { fmt.Println("Sending log message to Sentry:", logMessage) // Try to parse as JSON first var parsedLog map[string]interface{} err := json.Unmarshal([]byte(logMessage), &parsedLog) if err == nil { // Successfully parsed JSON // Check for common log fields level := "info" message := logMessage tags := make(map[string]string) if lvl, ok := parsedLog["level"].(string); ok { level = lvl } else if lvl, ok := parsedLog["log_level"].(string); ok { level = lvl } if msg, ok := parsedLog["message"].(string); ok { message = msg } else if msg, ok := parsedLog["msg"].(string); ok { message = msg } // Add any additional fields as tags for k, v := range parsedLog { if k != "level" && k != "log_level" && k != "message" && k != "msg" { if strVal, ok := v.(string); ok { tags[k] = strVal } else if jsonVal, err := json.Marshal(v); err == nil { tags[k] = string(jsonVal) } } } // Create a more structured event based on the level var eventID *sentry.EventID sentry.WithScope(func(scope *sentry.Scope) { scope.SetLevel(getSentryLevel(level)) // Add all the extracted fields as tags for k, v := range tags { scope.SetTag(k, v) } // Capture based on level if getSentryLevel(level) >= sentry.LevelError { eventID = sentry.CaptureException(fmt.Errorf(message)) } else { eventID = sentry.CaptureMessage(message) } }) if eventID != nil { fmt.Printf("Sent message to GlitchTip with event ID: %s\n", *eventID) } else { fmt.Fprintf(os.Stderr, "Failed to send message to GlitchTip.\n") } } else { // Not a valid JSON, send as a simple message eventID := sentry.CaptureMessage(logMessage) if eventID != nil { fmt.Printf("Sent message to GlitchTip with event ID: %s\n", *eventID) } else { fmt.Fprintf(os.Stderr, "Failed to send message to GlitchTip.\n") } } } // Helper function to convert string log levels to Sentry levels func getSentryLevel(level string) sentry.Level { switch strings.ToLower(level) { case "fatal", "panic": return sentry.LevelFatal case "error", "err": return sentry.LevelError case "warning", "warn": return sentry.LevelWarning case "info": return sentry.LevelInfo case "debug": return sentry.LevelDebug default: return sentry.LevelInfo } } func startServer() { http.HandleFunc("/webhook", func(w http.ResponseWriter, r *http.Request) { bodyBytes, err := io.ReadAll(r.Body) if err != nil { http.Error(w, "Error reading request body", http.StatusInternalServerError) fmt.Fprintf(os.Stderr, "Error reading request body: %s\n", err) return } fmt.Printf("Received request body: %s\n", bodyBytes) // Log the full request body var logMessage interface{} err = json.Unmarshal(bodyBytes, &logMessage) if err != nil { http.Error(w, "Invalid JSON", http.StatusBadRequest) fmt.Fprintf(os.Stderr, "Invalid JSON: %s\nReceived: %s\n", err, string(bodyBytes)) return } switch v := logMessage.(type) { case map[string]interface{}: logBytes, err := json.Marshal(v) if err != nil { http.Error(w, "Error processing log message", http.StatusInternalServerError) fmt.Fprintf(os.Stderr, "Error processing log message: %s\n", err) return } fmt.Printf("Processed log: %s\n", logBytes) sendToSentry(string(logBytes)) case []interface{}: for _, item := range v { if logItem, ok := item.(map[string]interface{}); ok { logBytes, err := json.Marshal(logItem) if err != nil { http.Error(w, "Error processing log message", http.StatusInternalServerError) fmt.Fprintf(os.Stderr, "Error processing log message: %s\n", err) return } fmt.Printf("Processed log item: %s\n", logBytes) sendToSentry(string(logBytes)) } else { http.Error(w, "Invalid JSON item in array", http.StatusBadRequest) fmt.Fprintf(os.Stderr, "Invalid JSON item in array: %s\n", item) return } } default: http.Error(w, "Invalid JSON format", http.StatusBadRequest) fmt.Fprintf(os.Stderr, "Invalid JSON format: %s\n", v) return } w.WriteHeader(http.StatusNoContent) }) fmt.Println("Starting server on 0.0.0.0:5050") err := http.ListenAndServe("0.0.0.0:5050", nil) if err != nil { fmt.Fprintf(os.Stderr, "Error starting server: %s\n", err) os.Exit(1) } }