Initial release v0.0.1: go-sink - A tool for sending logs to error tracking systems
This commit is contained in:
parent
4526d05aef
commit
5c49cadb5e
|
@ -0,0 +1,22 @@
|
|||
# Build Logs
|
||||
build_logs/
|
||||
|
||||
# OS generated files
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# IDE and editor files
|
||||
.cursor/
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Log files
|
||||
*.log
|
||||
log.json
|
|
@ -0,0 +1,144 @@
|
|||
You can stream and read files to a BugSink DSN, GlitchTip, or Sentry. I was fed up with how brittle sentry-cli was so I made this.
|
||||
|
||||
I might get around to making a whole CI/CD process for this which will upload the executables properly at some point and make this public.
|
||||
# Go Sink
|
||||
|
||||
Go Sink is a command-line utility that reads log messages from a file or stdin and sends them to BugSink, GlitchTip, or any compatible error tracking system.
|
||||
|
||||
## Installation
|
||||
|
||||
To install the binary locally, you can use the provided installation script. Run the following command in your terminal:
|
||||
|
||||
```sh
|
||||
# For the latest stable version
|
||||
curl -sSL https://git.nixc.us/Nixius/go-sink/raw/tag/v0.0.1/install.sh | bash
|
||||
|
||||
# Or to install a specific version (replace v0.0.1 with your desired version)
|
||||
curl -sSL https://git.nixc.us/Nixius/go-sink/raw/tag/v0.0.1/install.sh | bash
|
||||
```
|
||||
|
||||
This will download and install the Go Sink binary to your local machine.
|
||||
|
||||
## Usage
|
||||
|
||||
You can use Go Sink by specifying a log file as an argument or by piping input to it.
|
||||
|
||||
### Using a Log File
|
||||
|
||||
```sh
|
||||
go-sink /path/to/logfile
|
||||
```
|
||||
|
||||
### Using Piped Input
|
||||
|
||||
```sh
|
||||
cat /path/to/logfile | go-sink
|
||||
```
|
||||
|
||||
### Server Mode
|
||||
|
||||
You can also run Go Sink in server mode, which starts a webhook server that accepts log messages via HTTP requests:
|
||||
|
||||
```sh
|
||||
go-sink server
|
||||
```
|
||||
|
||||
The server listens on port 5050 by default. You can send log messages to the webhook endpoint:
|
||||
|
||||
```sh
|
||||
curl -X POST -H "Content-Type: application/json" \
|
||||
-d '{"log_level": "error", "message": "Error message", "service": "my-service"}' \
|
||||
http://localhost:5050/webhook
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Go Sink supports multiple environment variables for the DSN. You can use any of the following:
|
||||
|
||||
- `BUGSINK_DSN`: For BugSink instances
|
||||
- `GLITCHTIP_DSN`: For GlitchTip instances
|
||||
- `SENTRY_DSN`: For Sentry instances
|
||||
|
||||
If multiple environment variables are set, they are checked in the order listed above, and the first one found is used.
|
||||
|
||||
### Setting the DSN
|
||||
|
||||
#### Shell Environment
|
||||
|
||||
Add one of the following lines to your `.zshrc` or `.bashrc` file:
|
||||
|
||||
```sh
|
||||
export BUGSINK_DSN="your-dsn"
|
||||
# OR
|
||||
export GLITCHTIP_DSN="your-dsn"
|
||||
# OR
|
||||
export SENTRY_DSN="your-dsn"
|
||||
```
|
||||
|
||||
After adding the line, reload your shell configuration:
|
||||
|
||||
```sh
|
||||
source ~/.zshrc # for zsh users
|
||||
source ~/.bashrc # for bash users
|
||||
```
|
||||
|
||||
#### Dockerfile
|
||||
|
||||
If you are using a Docker container, add the environment variable in your `Dockerfile`:
|
||||
|
||||
```Dockerfile
|
||||
ENV BUGSINK_DSN=your-dsn
|
||||
# OR
|
||||
ENV GLITCHTIP_DSN=your-dsn
|
||||
# OR
|
||||
ENV SENTRY_DSN=your-dsn
|
||||
```
|
||||
|
||||
#### docker-compose.yml
|
||||
|
||||
If you are using Docker Compose, add the environment variable in your `docker-compose.yml` file:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
go-sink:
|
||||
image: your-docker-image
|
||||
environment:
|
||||
- BUGSINK_DSN=your-dsn
|
||||
# OR
|
||||
# - GLITCHTIP_DSN=your-dsn
|
||||
# OR
|
||||
# - SENTRY_DSN=your-dsn
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Testing Protocol
|
||||
|
||||
Go Sink includes a comprehensive test suite in `build-run-test.sh` that ensures all functionality works correctly:
|
||||
|
||||
1. **Build and Verification**: The script builds the application for all supported platforms and verifies that the binaries are created.
|
||||
|
||||
2. **Environment Variable Tests**: Each supported DSN environment variable is tested with both plain text and JSON input:
|
||||
- `BUGSINK_DSN`
|
||||
- `GLITCHTIP_DSN`
|
||||
- `SENTRY_DSN`
|
||||
|
||||
3. **Priority Tests**: The script tests that when multiple environment variables are set, the correct order of precedence is followed.
|
||||
|
||||
4. **Webhook Tests**: The server mode is tested with both single and batch webhook requests.
|
||||
|
||||
To run the tests, simply execute:
|
||||
|
||||
```sh
|
||||
./build-run-test.sh
|
||||
```
|
||||
|
||||
The script provides clear, color-coded output with pass/fail indicators for each test.
|
||||
|
||||
### Contributing
|
||||
|
||||
Before submitting changes, please ensure that:
|
||||
|
||||
1. You have run `./build-run-test.sh` to verify that all tests pass
|
||||
2. Your changes maintain compatibility with all supported DSN environment variables
|
||||
3. Any new features include appropriate tests in the test suite
|
|
@ -0,0 +1,119 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Exit on any error
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[0;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Set the DSN
|
||||
DSN="https://725ff4678a4b447c8aa22450b64d66ae@bugsink.aenow.com/6"
|
||||
echo -e "${BLUE}Testing with multiple environment variable options for DSN${NC}"
|
||||
echo "DSN: $DSN"
|
||||
|
||||
# Function to verify results
|
||||
verify_result() {
|
||||
if [ $? -eq 0 ]; then
|
||||
echo -e "${GREEN}✅ Test passed${NC}"
|
||||
else
|
||||
echo -e "${RED}❌ Test failed${NC}"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Build the application using build.sh
|
||||
echo -e "\n${BLUE}Building go-sink using build.sh...${NC}"
|
||||
./build.sh
|
||||
verify_result
|
||||
|
||||
# Check if the darwin_arm64 binary exists
|
||||
if [ ! -f "./dist/go-sink_darwin_arm64" ]; then
|
||||
echo -e "${RED}❌ Binary for darwin/arm64 not found. Make sure build.sh completed successfully.${NC}"
|
||||
exit 1
|
||||
else
|
||||
echo -e "${GREEN}✅ Binary found${NC}"
|
||||
fi
|
||||
|
||||
# Check for running server instances and kill them
|
||||
echo -e "\n${BLUE}Checking for existing go-sink server instances...${NC}"
|
||||
pkill -f "go-sink.*server" || true
|
||||
sleep 1
|
||||
|
||||
# Test 1a: Using GLITCHTIP_DSN with plain text
|
||||
echo -e "\n${YELLOW}Test 1a: Using GLITCHTIP_DSN environment variable with plain text...${NC}"
|
||||
export GLITCHTIP_DSN="$DSN"
|
||||
export SENTRY_DSN=""
|
||||
export BUGSINK_DSN=""
|
||||
echo "Plain text test with GLITCHTIP_DSN" | ./dist/go-sink_darwin_arm64
|
||||
verify_result
|
||||
|
||||
# Test 1b: Using GLITCHTIP_DSN with JSON
|
||||
echo -e "\n${YELLOW}Test 1b: Using GLITCHTIP_DSN environment variable with JSON...${NC}"
|
||||
echo '{"log_level": "error", "message": "JSON test with GLITCHTIP_DSN", "service": "test-suite", "test_id": "1b"}' | ./dist/go-sink_darwin_arm64
|
||||
verify_result
|
||||
|
||||
# Test 2a: Using SENTRY_DSN with plain text
|
||||
echo -e "\n${YELLOW}Test 2a: Using SENTRY_DSN environment variable with plain text...${NC}"
|
||||
export GLITCHTIP_DSN=""
|
||||
export SENTRY_DSN="$DSN"
|
||||
export BUGSINK_DSN=""
|
||||
echo "Plain text test with SENTRY_DSN" | ./dist/go-sink_darwin_arm64
|
||||
verify_result
|
||||
|
||||
# Test 2b: Using SENTRY_DSN with JSON
|
||||
echo -e "\n${YELLOW}Test 2b: Using SENTRY_DSN environment variable with JSON...${NC}"
|
||||
echo '{"log_level": "error", "message": "JSON test with SENTRY_DSN", "service": "test-suite", "test_id": "2b"}' | ./dist/go-sink_darwin_arm64
|
||||
verify_result
|
||||
|
||||
# Test 3a: Using BUGSINK_DSN with plain text
|
||||
echo -e "\n${YELLOW}Test 3a: Using BUGSINK_DSN environment variable with plain text...${NC}"
|
||||
export GLITCHTIP_DSN=""
|
||||
export SENTRY_DSN=""
|
||||
export BUGSINK_DSN="$DSN"
|
||||
echo "Plain text test with BUGSINK_DSN" | ./dist/go-sink_darwin_arm64
|
||||
verify_result
|
||||
|
||||
# Test 3b: Using BUGSINK_DSN with JSON
|
||||
echo -e "\n${YELLOW}Test 3b: Using BUGSINK_DSN environment variable with JSON...${NC}"
|
||||
echo '{"log_level": "error", "message": "JSON test with BUGSINK_DSN", "service": "test-suite", "test_id": "3b"}' | ./dist/go-sink_darwin_arm64
|
||||
verify_result
|
||||
|
||||
# Test environment variable priority (GLITCHTIP_DSN should take precedence)
|
||||
echo -e "\n${YELLOW}Test 4: Testing environment variable priority...${NC}"
|
||||
export GLITCHTIP_DSN="$DSN"
|
||||
export SENTRY_DSN="$DSN"
|
||||
export BUGSINK_DSN="$DSN"
|
||||
echo "Testing environment variable priority" | ./dist/go-sink_darwin_arm64
|
||||
verify_result
|
||||
|
||||
# Start the server with GLITCHTIP_DSN for webhook tests
|
||||
echo -e "\n${BLUE}Starting go-sink server with GLITCHTIP_DSN...${NC}"
|
||||
export GLITCHTIP_DSN="$DSN"
|
||||
export SENTRY_DSN=""
|
||||
export BUGSINK_DSN=""
|
||||
./dist/go-sink_darwin_arm64 server &
|
||||
SERVER_PID=$!
|
||||
|
||||
# Give the server time to start
|
||||
echo "Waiting for server to start..."
|
||||
sleep 2
|
||||
|
||||
# Test 5: Send webhook request
|
||||
echo -e "\n${YELLOW}Test 5: Sending a webhook request...${NC}"
|
||||
curl -X POST -H "Content-Type: application/json" -d '{"log_level": "error", "message": "Error via webhook", "service": "test-suite", "test_id": "5"}' http://localhost:5050/webhook
|
||||
verify_result
|
||||
|
||||
# Test 6: Send webhook batch request
|
||||
echo -e "\n${YELLOW}Test 6: Sending a webhook batch request...${NC}"
|
||||
curl -X POST -H "Content-Type: application/json" -d '[{"log_level": "error", "message": "Error in batch", "service": "test-suite", "test_id": "6.1"},{"log_level": "warning", "message": "Warning in batch", "service": "test-suite", "test_id": "6.2"}]' http://localhost:5050/webhook
|
||||
verify_result
|
||||
|
||||
# Clean up the server
|
||||
echo -e "\n${BLUE}Tests completed. Shutting down server...${NC}"
|
||||
kill $SERVER_PID || true
|
||||
|
||||
echo -e "\n${GREEN}All tests passed successfully!${NC}"
|
|
@ -0,0 +1,43 @@
|
|||
#!/bin/bash
|
||||
|
||||
ARCHITECTURES=("linux/amd64" "linux/arm64" "darwin/amd64" "darwin/arm64" "windows/amd64")
|
||||
PROJECT_NAME=$(basename "$(pwd)")
|
||||
|
||||
prepare_build() {
|
||||
mkdir -p dist build_logs
|
||||
if [ ! -f go.mod ]; then
|
||||
go mod init "$PROJECT_NAME"
|
||||
fi
|
||||
go mod tidy
|
||||
}
|
||||
|
||||
build_binary() {
|
||||
local os=$1
|
||||
local arch=$2
|
||||
local output="dist/${PROJECT_NAME}_${os}_${arch}"
|
||||
env GOOS=$os GOARCH=$arch go build -o $output . &> "build_logs/${os}_${arch}.log"
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Build failed for $os/$arch" >> "build_logs/error.log"
|
||||
else
|
||||
echo "Build succeeded for $os/$arch"
|
||||
fi
|
||||
if [ "$os" == "linux" ]; then
|
||||
env GOOS=$os GOARCH=$arch CGO_ENABLED=0 go build -a -ldflags '-extldflags "-static"' -o "${output}_static" . &> "build_logs/${os}_${arch}_static.log"
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Static build failed for $os/$arch" >> "build_logs/error.log"
|
||||
else
|
||||
echo "Static build succeeded for $os/$arch"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
prepare_build
|
||||
for arch in "${ARCHITECTURES[@]}"; do
|
||||
IFS="/" read -r os arch <<< "$arch"
|
||||
build_binary $os $arch
|
||||
done
|
||||
echo "Build process completed."
|
||||
}
|
||||
|
||||
main
|
|
@ -0,0 +1,7 @@
|
|||
Build failed for linux/amd64
|
||||
Static build failed for linux/amd64
|
||||
Build failed for linux/arm64
|
||||
Static build failed for linux/arm64
|
||||
Build failed for darwin/amd64
|
||||
Build failed for darwin/arm64
|
||||
Build failed for windows/amd64
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
53
go-glitch.go
53
go-glitch.go
|
@ -1,53 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/getsentry/sentry-go"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Check for the right number of command line arguments
|
||||
if len(os.Args) != 2 {
|
||||
fmt.Println("Usage: ./go-glitch \"Your log message here\"")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
logMessage := os.Args[1]
|
||||
|
||||
dsn := os.Getenv("SENTRY_DSN")
|
||||
if dsn == "" {
|
||||
fmt.Println("Error: SENTRY_DSN environment variable is not set.")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Initialize Sentry with the DSN
|
||||
err := sentry.Init(sentry.ClientOptions{
|
||||
Dsn: dsn,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("Error initializing Sentry: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Defer a function to flush Sentry's buffer
|
||||
defer func() {
|
||||
success := sentry.Flush(5 * time.Second)
|
||||
if !success {
|
||||
fmt.Println("Failed to flush Sentry buffer within the expected time.")
|
||||
} else {
|
||||
fmt.Println("Sentry buffer flushed successfully.")
|
||||
}
|
||||
}()
|
||||
|
||||
// Capture the message and output the event ID
|
||||
eventID := sentry.CaptureMessage(logMessage)
|
||||
if eventID != nil {
|
||||
fmt.Printf("Sent message to Sentry with event ID: %s\n", *eventID)
|
||||
} else {
|
||||
fmt.Println("Failed to send message to Sentry.")
|
||||
}
|
||||
}
|
||||
|
5
go.mod
5
go.mod
|
@ -1,9 +1,10 @@
|
|||
module go-glitch
|
||||
module go-sink
|
||||
|
||||
go 1.21.1
|
||||
|
||||
require github.com/getsentry/sentry-go v0.27.0
|
||||
|
||||
require (
|
||||
github.com/getsentry/sentry-go v0.27.0 // indirect
|
||||
golang.org/x/sys v0.6.0 // indirect
|
||||
golang.org/x/text v0.8.0 // indirect
|
||||
)
|
||||
|
|
16
go.sum
16
go.sum
|
@ -1,6 +1,22 @@
|
|||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps=
|
||||
github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
|
||||
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
||||
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
|
||||
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
#!/bin/bash
|
||||
|
||||
INSTALL_DIR="/usr/local/bin"
|
||||
BINARY_NAME="go-sink"
|
||||
|
||||
# Default to v0.0.1 if no version is specified
|
||||
VERSION=${1:-"v0.0.1"}
|
||||
BASE_URL="https://git.nixc.us/Nixius/go-sink/raw/tag/${VERSION}/dist"
|
||||
|
||||
# Supported architectures
|
||||
ARCHITECTURES=("linux/amd64" "linux/arm64" "linux/arm/v7" "darwin/amd64" "darwin/arm64")
|
||||
|
||||
OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
||||
ARCH="$(uname -m)"
|
||||
|
||||
case $ARCH in
|
||||
x86_64) ARCH="amd64" ;;
|
||||
arm64 | aarch64) ARCH="arm64" ;;
|
||||
arm*) ARCH="arm/v7" ;;
|
||||
*) echo "Unsupported architecture: $ARCH"; exit 1 ;;
|
||||
esac
|
||||
|
||||
BINARY_URL="${BASE_URL}/${BINARY_NAME}_${OS}_${ARCH}"
|
||||
|
||||
echo "Downloading and installing $BINARY_NAME version ${VERSION} from $BINARY_URL..."
|
||||
|
||||
# Check if we have write permission to the install directory
|
||||
if [ -w "${INSTALL_DIR}" ]; then
|
||||
curl -sSL "$BINARY_URL" -o "${INSTALL_DIR}/${BINARY_NAME}"
|
||||
chmod +x "${INSTALL_DIR}/${BINARY_NAME}"
|
||||
else
|
||||
curl -sSL "$BINARY_URL" -o "${INSTALL_DIR}/${BINARY_NAME}"
|
||||
chmod +x "${INSTALL_DIR}/${BINARY_NAME}"
|
||||
fi
|
||||
|
||||
echo "Installed $BINARY_NAME version ${VERSION} to $INSTALL_DIR"
|
|
@ -0,0 +1,312 @@
|
|||
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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
#!/bin/bash
|
||||
curl -X POST -H "Content-Type: application/json" -d '{"log_level": "error", "message": "This is an error log message via TEST.SH"}' http://localhost:5050/webhook
|
||||
echo test | ./dist/go-sink_darwin_arm64
|
||||
./dist/go-sink_darwin_arm64 log.log
|
|
@ -0,0 +1,19 @@
|
|||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
const (
|
||||
VersionMajor = 0
|
||||
VersionMinor = 0
|
||||
VersionPatch = 1
|
||||
)
|
||||
|
||||
// Version returns the current version as a string
|
||||
func Version() string {
|
||||
return fmt.Sprintf("%d.%d.%d", VersionMajor, VersionMinor, VersionPatch)
|
||||
}
|
||||
|
||||
// VersionInfo returns detailed version information
|
||||
func VersionInfo() string {
|
||||
return fmt.Sprintf("go-sink version %s", Version())
|
||||
}
|
Loading…
Reference in New Issue