1
1
Fork 0

debugreport functional

This commit is contained in:
Colin 2024-06-18 18:48:15 -04:00
parent fd15ba3bb4
commit 08a2121ffe
21 changed files with 173 additions and 171 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
build_logs

View File

@ -1,73 +1,60 @@
You can stream and read files to a sentry DSN glitchtip or sentry itself I was fed up with how brittle sentry-cli was so I made this.
# DebugReport
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 Glitch
Go Glitch is a command-line utility that reads log messages from a file or stdin and sends them to Glitchtip, a self-hosted Sentry-compatible error tracking system.
DebugReport is a command-line utility designed to record terminal sessions using `asciinema`, upload the recordings to Hastebin, and send notifications to Pushover. This utility is ideal for capturing debug sessions and sharing them easily.
## Installation
To install the binary locally, you can use the provided installation script. Run the following command in your terminal:
To use DebugReport, ensure you have the `asciinema` tool installed on your system.
```sh
curl -sSL https://git.nixc.us/Nixius/go-glitch/raw/branch/master/install.sh | bash
```
### Installing Asciinema
This will download and install the Go Glitch binary to your local machine.
You can install `asciinema` by following the instructions on their [official website](https://asciinema.org/docs/installation).
## Usage
You can use Go Glitch by specifying a log file as an argument or by piping input to it.
You can use DebugReport to record your terminal sessions, upload the recordings to Hastebin, and send notifications to Pushover.
### Using a Log File
### Start a Recording
Run the `debugreport` command:
```sh
go-glitch /path/to/logfile
./debugreport
```
### Using Piped Input
You will see a message indicating that the recording has started. To stop the recording, press `Ctrl+D` or type `exit`.
### Sending Notifications
After the recording is finished, you will be prompted to specify if the notification is a high-priority issue:
```
Is this a priority notification? (yes/no):
```
- If you answer `yes`, the notification will be sent as a high-priority (emergency) notification with a retry interval of 60 seconds and an expire time of 1800 seconds (30 minutes).
- If you answer `no`, the notification will be sent as a low-priority notification.
## External Services
DebugReport interacts with the following external services:
- **Pushover**: Used to send notifications.
- **Hastebin**: Used to upload the recorded sessions.
Ensure you have accounts and necessary credentials for these services.
## Example
```sh
cat /path/to/logfile | go-glitch
./debugreport
```
## Configuration
1. The program will start recording the terminal session.
2. To stop the recording, press `Ctrl+D` or type `exit`.
3. After the recording is finished, you will be prompted to specify if the notification is a high-priority issue.
4. The recording will be uploaded to Hastebin and a notification will be sent to Pushover with the recording URL.
Go Glitch requires the `GLITCHTIP_DSN` environment variable to be set with your Glitchtip DSN. You can set this environment variable in your shell environment, Dockerfile, or `docker-compose.yml` file.
## Dependencies
### Shell Environment
Add the following line to your `.zshrc` or `.bashrc` file:
```sh
export GLITCHTIP_DSN="your-glitchtip-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 GLITCHTIP_DSN=your-glitchtip-dsn
```
### docker-compose.yml
If you are using Docker Compose, add the environment variable in your `docker-compose.yml` file:
```yaml
version: '3.8'
services:
go-glitch:
image: your-docker-image
environment:
- GLITCHTIP_DSN=your-glitchtip-dsn
```
Ensure `asciinema` is installed and properly configured in your environment.

View File

@ -0,0 +1,46 @@
{"version": 2, "width": 207, "height": 25, "timestamp": 1718750280, "env": {"SHELL": "/bin/zsh", "TERM": "xterm-256color"}}
[0.256711, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r\u001b]2;aedev@computers-iMac:~/dev/debugreport\u0007\u001b]1;..v/debugreport\u0007"]
[0.261332, "o", "\u001b]7;file://computers-iMac.localdomain/Users/aedev/dev/debugreport\u001b\\"]
[0.276765, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[35maedev\u001b[00m\u001b[36m@\u001b[00m\u001b[33mcomputers-iMac\u001b[00m\u001b[31m:\u001b[00m\u001b[36m~/dev/debugreport\u001b[00m\u001b[31m|\u001b[00m\u001b[36m⇒\u001b[00m \u001b[K"]
[0.276846, "o", "\u001b[?1h\u001b=\u001b[?2004h"]
[1.569833, "o", "l"]
[1.585655, "o", "\b\u001b[32ml\u001b[39m"]
[1.586509, "o", "\b\u001b[32ml\u001b[39m\u001b[90ms\u001b[39m\b"]
[1.704188, "o", "\b\u001b[32ml\u001b[32ms\u001b[39m"]
[1.841041, "o", "\u001b[?1l\u001b>"]
[1.841212, "o", "\u001b[?2004l"]
[1.844016, "o", "\r\r\n"]
[1.84507, "o", "\u001b]2;ls -G\u0007\u001b]1;ls\u0007"]
[1.862516, "o", "README.md \u001b[31mbuild.sh\u001b[39;49m\u001b[0m \u001b[1m\u001b[36mdist\u001b[39;49m\u001b[0m install.sh\r\nasciinema-20240618183800.cast \u001b[1m\u001b[36mbuild_logs\u001b[39;49m\u001b[0m go.mod main.go\r\n"]
[1.862743, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r"]
[1.862912, "o", "\u001b]2;aedev@computers-iMac:~/dev/debugreport\u0007\u001b]1;..v/debugreport\u0007"]
[1.86532, "o", "\u001b]7;file://computers-iMac.localdomain/Users/aedev/dev/debugreport\u001b\\"]
[1.885521, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[35maedev\u001b[00m\u001b[36m@\u001b[00m\u001b[33mcomputers-iMac\u001b[00m\u001b[31m:\u001b[00m\u001b[36m~/dev/debugreport\u001b[00m\u001b[31m|\u001b[00m\u001b[36m⇒\u001b[00m \u001b[K"]
[1.885742, "o", "\u001b[?1h\u001b=\u001b[?2004h"]
[3.353476, "o", "o"]
[3.359874, "o", "\b\u001b[1m\u001b[31mo\u001b[0m\u001b[39m"]
[3.360155, "o", "\b\u001b[1m\u001b[31mo\u001b[0m\u001b[39m\u001b[90mpen Sandsara-tracks\u001b[39m\u001b[19D"]
[4.076455, "o", "\b\u001b[1m\u001b[31mo\u001b[1m\u001b[31mk\u001b[0m\u001b[39m\u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[18D"]
[4.195769, "o", "\b\b\u001b[1m\u001b[31mo\u001b[1m\u001b[31mk\u001b[1m\u001b[31ma\u001b[0m\u001b[39m"]
[4.451507, "o", "\b\u001b[1m\u001b[31ma\u001b[1m\u001b[31my\u001b[0m\u001b[39m"]
[4.616607, "o", "\b\u001b[1m\u001b[31my\u001b[1m\u001b[31m \u001b[0m\u001b[39m"]
[4.619412, "o", "\b\b\u001b[1m\u001b[31my\u001b[0m\u001b[39m\u001b[0m\u001b[39m "]
[5.413241, "o", "\b\b\b\b\b"]
[5.998918, "o", "\u001b[0m\u001b[39me\u001b[1m\u001b[31mo\u001b[1m\u001b[31mk\u001b[1m\u001b[31ma\u001b[1m\u001b[31my\u001b[0m\u001b[39m \b\b\b\b\b"]
[6.005631, "o", "\b\u001b[1m\u001b[31me"]
[6.235222, "o", "\b\u001b[1m\u001b[31me\u001b[1m\u001b[31mc\u001b[1m\u001b[31mo\u001b[1m\u001b[31mk\u001b[1m\u001b[31ma\u001b[1m\u001b[31my\u001b[0m\u001b[39m \b\b\b\b\b"]
[6.331374, "o", "\u001b[1m\u001b[31mh\u001b[1m\u001b[31mo\u001b[1m\u001b[31mk\u001b[1m\u001b[31ma\u001b[1m\u001b[31my\u001b[0m\u001b[39m \b\b\b\b\b"]
[6.417782, "o", "\u001b[1C\u001b[1m\u001b[31mo\u001b[1m\u001b[31mk\u001b[1m\u001b[31ma\u001b[1m\u001b[31my\u001b[0m\u001b[39m \b\b\b\b\b"]
[6.52071, "o", "\u001b[1m\u001b[31m \u001b[1m\u001b[31mo\u001b[1m\u001b[31mk\u001b[1m\u001b[31ma\u001b[1m\u001b[31my\u001b[0m\u001b[39m \b\b\b\b\b"]
[6.535294, "o", "\b\b\b\b\b\u001b[0m\u001b[32me\u001b[0m\u001b[32mc\u001b[0m\u001b[32mh\u001b[0m\u001b[32mo\u001b[39m\u001b[0m\u001b[39m \u001b[0m\u001b[39mo\u001b[0m\u001b[39mk\u001b[0m\u001b[39ma\u001b[0m\u001b[39my\b\b\b\b"]
[6.986127, "o", "\u001b[?1l\u001b>"]
[6.986177, "o", "\u001b[?2004l"]
[6.989743, "o", "\r\r\n"]
[6.991177, "o", "\u001b]2;echo okay\u0007\u001b]1;echo\u0007"]
[7.024897, "o", "okay\r\n"]
[7.024932, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r"]
[7.02502, "o", "\u001b]2;aedev@computers-iMac:~/dev/debugreport\u0007\u001b]1;..v/debugreport\u0007"]
[7.027346, "o", "\u001b]7;file://computers-iMac.localdomain/Users/aedev/dev/debugreport\u001b\\"]
[7.040054, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[35maedev\u001b[00m\u001b[36m@\u001b[00m\u001b[33mcomputers-iMac\u001b[00m\u001b[31m:\u001b[00m\u001b[36m~/dev/debugreport\u001b[00m\u001b[31m|\u001b[00m\u001b[36m⇒\u001b[00m \u001b[K"]
[7.040286, "o", "\u001b[?1h\u001b=\u001b[?2004h"]
[8.494716, "o", "\u001b[?2004l\r\r\n"]

BIN
dist/debugreport_darwin_amd64 vendored Executable file

Binary file not shown.

BIN
dist/debugreport_darwin_arm64 vendored Executable file

Binary file not shown.

BIN
dist/debugreport_linux_amd64 vendored Executable file

Binary file not shown.

BIN
dist/debugreport_linux_amd64_static vendored Executable file

Binary file not shown.

BIN
dist/debugreport_linux_arm64 vendored Executable file

Binary file not shown.

BIN
dist/debugreport_linux_arm64_static vendored Executable file

Binary file not shown.

BIN
dist/debugreport_windows_amd64 vendored Executable file

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.

Binary file not shown.

9
go.mod
View File

@ -1,10 +1,3 @@
module go-glitch
module debugreport
go 1.21.1
require github.com/getsentry/sentry-go v0.27.0
require (
golang.org/x/sys v0.6.0 // indirect
golang.org/x/text v0.8.0 // indirect
)

22
go.sum
View File

@ -1,22 +0,0 @@
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=

View File

@ -1,8 +1,8 @@
#!/bin/bash
INSTALL_DIR="/usr/local/bin"
BINARY_NAME="go-glitch"
BASE_URL="https://git.nixc.us/Nixius/go-glitch/raw/branch/master/dist"
BINARY_NAME="debugreport"
BASE_URL="https://git.nixc.us/colin/debugreport/raw/branch/master/dist"
# Supported architectures

169
main.go
View File

@ -3,16 +3,15 @@ package main
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"mime/multipart"
"net"
"net/http"
"os"
"os/exec"
"os/user"
"path/filepath"
"strings"
"time"
)
@ -22,56 +21,58 @@ const pushoverURL = "https://api.pushover.net/1/messages.json"
const pushoverToken = "aunhi15sq2ervgjzyxm1msnnmucv41"
const pushoverUserKey = "ujFeJyjYFJd2Lwbygfw9cSCoeYiLBi"
// Asciinema server URL
const asciinemaServerURL = "https://asciinema.nixc.us/"
// Path to the asciinema installation ID file
const asciinemaInstallIDFile = "$HOME/.config/asciinema/install-id"
// Hastebin server URL
const hastebinServerURL = "https://haste.nixc.us/documents"
func main() {
// Check if asciinema CLI is authenticated
if !isAuthenticated() {
log.Println("Asciinema CLI is not authenticated. Please run 'asciinema auth' to authenticate.")
err := runAsciinemaAuth()
if err != nil {
log.Fatalf("Failed to authenticate asciinema CLI: %v", err)
}
}
// Generate a unique filename for the asciinema recording
timestamp := time.Now().Format("20060102150405")
filename := fmt.Sprintf("asciinema-%s.cast", timestamp)
// Inform the user how to stop the recording
fmt.Println("Starting asciinema recording. Press Ctrl+D or type 'exit' to stop the recording.")
// Start asciinema recording
cmd := exec.Command("asciinema", "rec", "--title", "Debug Report", "--command", "bash", filename)
cmd.Env = append(os.Environ(), "ASCIINEMA_API_URL="+asciinemaServerURL)
cmd := exec.Command("asciinema", "rec", filename)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
log.Println("Starting asciinema recording...")
err := cmd.Run()
err := cmd.Start()
if err != nil {
log.Fatalf("Failed to start asciinema recording: %v", err)
}
err = cmd.Wait()
if err != nil {
log.Fatalf("Asciinema recording process exited with error: %v", err)
}
log.Println("Asciinema recording finished:", filename)
// Collect system details
hostname, _ := os.Hostname()
currentUser, _ := user.Current()
ipAddress, err := getExternalIP()
if err != nil {
log.Fatalf("Failed to get external IP address: %v", err)
}
// Create the message with system details
message := fmt.Sprintf("New asciinema recording from host: %s, user: %s, IP: %s", hostname, currentUser.Username, ipAddress)
message := fmt.Sprintf("New asciinema recording from host: %s, user: %s", hostname, currentUser.Username)
log.Println("Message to be sent:", message)
// Upload the recording to Hastebin and get the URL
hastebinURL, err := uploadToHastebin(filename)
if err != nil {
log.Fatalf("Failed to upload recording to Hastebin: %v", err)
}
// Update the message with the Hastebin URL
message = fmt.Sprintf("New asciinema recording from host: %s, user: %s, URL: %s", hostname, currentUser.Username, hastebinURL)
// Ask the user if this is a priority notification
isPriority := askPriority()
log.Println("Priority status:", isPriority)
// Send the recording to Pushover
err = sendToPushover(filename, message, isPriority)
// Send the updated message to Pushover
err = sendToPushover(message, isPriority)
if err != nil {
log.Fatalf("Failed to send recording to Pushover: %v", err)
}
@ -79,51 +80,75 @@ func main() {
log.Println("Recording sent to Pushover successfully.")
}
// isAuthenticated checks if the asciinema CLI is authenticated
func isAuthenticated() bool {
installIDPath := os.ExpandEnv(asciinemaInstallIDFile)
_, err := os.Stat(installIDPath)
return !os.IsNotExist(err)
}
// runAsciinemaAuth runs the asciinema auth command to authenticate the CLI
func runAsciinemaAuth() error {
cmd := exec.Command("asciinema", "auth")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
// sendToPushover sends the recording file and message to the Pushover URL
func sendToPushover(filename, message string, priority bool) error {
// uploadToHastebin uploads the asciinema recording to Hastebin and returns the resulting URL
func uploadToHastebin(filename string) (string, error) {
file, err := os.Open(filename)
if err != nil {
return fmt.Errorf("failed to open recording file: %w", err)
return "", fmt.Errorf("failed to open recording file: %w", err)
}
defer file.Close()
body := &bytes.Buffer{}
_, err = io.Copy(body, file)
if err != nil {
return "", fmt.Errorf("failed to copy file content: %w", err)
}
req, err := http.NewRequest("POST", hastebinServerURL, body)
if err != nil {
return "", fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "text/plain")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("received non-200 response: %d", resp.StatusCode)
}
var result struct {
Key string `json:"key"`
}
err = json.NewDecoder(resp.Body).Decode(&result)
if err != nil {
return "", fmt.Errorf("failed to decode response: %w", err)
}
return fmt.Sprintf("https://haste.nixc.us/%s", result.Key), nil
}
// askPriority prompts the user to specify if the notification is a priority
func askPriority() bool {
reader := bufio.NewReader(os.Stdin)
fmt.Print("Is this a priority notification? (yes/no): ")
response, err := reader.ReadString('\n')
if err != nil {
log.Fatalf("Failed to read user input: %v", err)
}
response = strings.TrimSpace(strings.ToLower(response))
return response == "yes"
}
// sendToPushover sends the message to the Pushover URL
func sendToPushover(message string, priority bool) error {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("file", filepath.Base(file.Name()))
if err != nil {
return fmt.Errorf("failed to create form file: %w", err)
}
_, err = io.Copy(part, file)
if err != nil {
return fmt.Errorf("failed to copy file content: %w", err)
}
// Add Pushover parameters
writer.WriteField("token", pushoverToken)
writer.WriteField("user", pushoverUserKey)
writer.WriteField("message", message)
if priority {
writer.WriteField("priority", "1")
writer.WriteField("priority", "2")
writer.WriteField("retry", "60")
writer.WriteField("expire", "1800")
}
err = writer.Close()
err := writer.Close()
if err != nil {
return fmt.Errorf("failed to close writer: %w", err)
}
@ -147,31 +172,3 @@ func sendToPushover(filename, message string, priority bool) error {
return nil
}
// askPriority prompts the user to specify if the notification is a priority
func askPriority() bool {
reader := bufio.NewReader(os.Stdin)
fmt.Print("Is this a priority notification? (yes/no): ")
response, err := reader.ReadString('\n')
if err != nil {
log.Fatalf("Failed to read user input: %v", err)
}
response = strings.TrimSpace(strings.ToLower(response))
return response == "yes"
}
// getExternalIP fetches the current external IP address
func getExternalIP() (string, error) {
resp, err := http.Get("https://api.ipify.org")
if err != nil {
return "", fmt.Errorf("failed to get external IP: %w", err)
}
defer resp.Body.Close()
ip, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("failed to read IP response: %w", err)
}
return strings.TrimSpace(string(ip)), nil
}