go-sink/main.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)
}
}