Initial attempt at a generic webhook for reports.

Background submission to a notification server that doesn't block rageshake submission.
This commit is contained in:
Michael Kaye 2022-01-25 09:58:50 +00:00
parent 29c2f9a48f
commit 81865b0193
5 changed files with 130 additions and 9 deletions

4
Makefile Normal file
View File

@ -0,0 +1,4 @@
docker:
DOCKER_BUILDKIT=1 docker build -t rageshake:latest .
docker run --rm --name rageshake --network rageshake-search --mount type=bind,source=$(shell pwd)/bugs,target=/bugs --mount type=bind,source=$(shell pwd)/rageshake.yaml,target=/rageshake.yaml -p 127.0.0.1:9110:9110 rageshake:latest -listen :9110

View File

@ -105,3 +105,13 @@ You can get notifications when a new rageshake arrives on the server.
Currently this tool supports pushing notifications as GitHub issues in a repo,
through a Slack webhook or by email, cf sample config file for how to
configure them.
### Generic Webhook Notifications
You can receive a webhook notifications when a new rageshake arrives on the server.
These requests contain all the parsed metadata, and links to the uploaded files, and any github/gitlab
issues created.
Details on the request and expected response are [available](docs/generic\_webhook.md).

38
docs/generic_webhook.md Normal file
View File

@ -0,0 +1,38 @@
## Generic webhook request
If the configuration option `generic_webhook_url` is set, then a synchronous request to
the endpoint will be sent after the incoming request is parsed and the files are uploaded.
The webhook is designed for notification or other tracking services, and does not contain
the original log files uploaded.
(If you want the original log files, we suggest to implement the rageshake interface itself).
A sample JSON body is as follows:
```
{
'user_text': 'test\r\n\r\nIssue: No issue link given',
'app': 'element-web',
'data': {
'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:96.0) Gecko/20100101 Firefox/96.0',
'Version': '0f15ba34cdf5-react-0f15ba34cdf5-js-0f15ba34cdf5',
...
'user_id': '@michaelgoesforawalk:matrix.org'},
'labels': None,
'logs': [
'logs-0000.log.gz',
'logs-0001.log.gz',
'logs-0002.log.gz',
],
'logErrors': None,
'files': [
'screenshot.png'
],
'fileErrors': None,
'report_url': 'https://github.com/your-org/your-repo/issues/1251',
'listing_url': 'http://your-rageshake-server/api/listing/2022-01-25/154742-OOXBVGIX'
}
```
The log and other files can be individually downloaded by concatenating the `listing_url` and the `logs` or `files` name.

13
main.go
View File

@ -71,6 +71,8 @@ type config struct {
SMTPUsername string `yaml:"smtp_username"`
SMTPPassword string `yaml:"smtp_password"`
GenericWebhookURL string `yaml:"generic_webhook_url"`
}
func basicAuth(handler http.Handler, username, password, realm string) http.Handler {
@ -133,6 +135,15 @@ func main() {
if len(cfg.EmailAddresses) > 0 && cfg.SMTPServer == "" {
log.Fatal("Email address(es) specified but no smtp_server configured. Wrong configuration, aborting...")
}
var genericWebhookClient *http.Client
if cfg.GenericWebhookURL == "" {
fmt.Println("No generic_webhook_url configured.")
} else {
fmt.Println("Will forward metadata of all requests to ", cfg.GenericWebhookURL)
genericWebhookClient = &http.Client{
Timeout: time.Second * 300,
}
}
apiPrefix := cfg.APIPrefix
if apiPrefix == "" {
@ -148,7 +159,7 @@ func main() {
log.Printf("Using %s/listing as public URI", apiPrefix)
rand.Seed(time.Now().UnixNano())
http.Handle("/api/submit", &submitServer{ghClient, glClient, apiPrefix, slack, cfg})
http.Handle("/api/submit", &submitServer{ghClient, glClient, apiPrefix, slack, genericWebhookClient, cfg})
// Make sure bugs directory exists
_ = os.Mkdir("bugs", os.ModePerm)

View File

@ -57,6 +57,7 @@ type submitServer struct {
slack *slackClient
genericWebhookClient *http.Client
cfg *config
}
@ -76,16 +77,23 @@ type jsonLogEntry struct {
Lines string `json:"lines"`
}
type genericWebhookPayload struct {
parsedPayload
ReportURL string `json:"report_url"`
ListingURL string `json:"listing_url"`
}
// the payload after parsing
type parsedPayload struct {
UserText string
AppName string
Data map[string]string
Labels []string
Logs []string
LogErrors []string
Files []string
FileErrors []string
UserText string `json:"user_text"`
AppName string `json:"app"`
Data map[string]string `json:"data"`
Labels []string `json:"labels"`
Logs []string `json:"logs"`
LogErrors []string `json:"logErrors"`
Files []string `json:"files"`
FileErrors []string `json:"fileErrors"`
}
func (p parsedPayload) WriteTo(out io.Writer) {
@ -492,9 +500,59 @@ func (s *submitServer) saveReport(ctx context.Context, p parsedPayload, reportDi
return nil, err
}
if err := s.submitGenericWebhook(p, listingURL, resp.ReportURL); err != nil {
return nil, err
}
return &resp, nil
}
// submitGenericWebhook submits a basic JSON body to an endpoint configured in the config
//
// The request does not include the log body, only the metadata in the parsedPayload,
// with the required listingURL to obtain the logs over http if required.
//
// If a github or gitlab issue was previously made, the reportURL will also be passed.
//
// Uses a goroutine to handle the http request asynchronously as by this point all critical
// information has been stored.
func (s *submitServer) submitGenericWebhook(p parsedPayload, listingURL string, reportURL string) error {
if s.genericWebhookClient == nil {
return nil
}
url := s.cfg.GenericWebhookURL
log.Println("Submitting json to URL", url)
// Enrich the parsedPayload with a reportURL and listingURL, to convert a single struct
// to JSON easily
genericHookPayload := genericWebhookPayload{
parsedPayload: p,
ReportURL: reportURL,
ListingURL: listingURL,
}
payloadBuffer := new(bytes.Buffer)
json.NewEncoder(payloadBuffer).Encode(genericHookPayload)
req, err := http.NewRequest("POST", url, payloadBuffer)
req.Header.Set("Content-Type", "application/json")
if err != nil {
return err
}
go s.sendGenericWebhook(req)
return nil
}
func (s *submitServer) sendGenericWebhook(req *http.Request) {
resp, err := s.genericWebhookClient.Do(req)
if err != nil {
log.Println("Unable to submit notification", err)
} else {
defer resp.Body.Close()
log.Println("Got response", resp.Status)
}
}
func (s *submitServer) submitGithubIssue(ctx context.Context, p parsedPayload, listingURL string, resp *submitResponse) error {
if s.ghClient == nil {
return nil