313 lines
7.9 KiB
Go
313 lines
7.9 KiB
Go
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 <path to log file> 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)
|
|
}
|
|
}
|