Merge pull request #50 from matrix-org/michaelk/proxy_rageshake_requests
A generic webhook for notifications about reports.
This commit is contained in:
commit
a8c57f2eb9
6 changed files with 141 additions and 9 deletions
10
README.md
10
README.md
|
@ -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).
|
||||
|
||||
|
|
1
changelog.d/50.feature
Normal file
1
changelog.d/50.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Allow forwarding of a request to a webhook endpoint.
|
40
docs/generic_webhook.md
Normal file
40
docs/generic_webhook.md
Normal file
|
@ -0,0 +1,40 @@
|
|||
## Generic webhook request
|
||||
|
||||
If the configuration option `generic_webhook_urls` is set, then an asynchronous request to
|
||||
each endpoint listed will be sent in parallel, 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.
|
||||
You may need to provide a HTTP basic auth user/pass if configured on your rageshake server.
|
17
main.go
17
main.go
|
@ -71,6 +71,8 @@ type config struct {
|
|||
SMTPUsername string `yaml:"smtp_username"`
|
||||
|
||||
SMTPPassword string `yaml:"smtp_password"`
|
||||
|
||||
GenericWebhookURLs []string `yaml:"generic_webhook_urls"`
|
||||
}
|
||||
|
||||
func basicAuth(handler http.Handler, username, password, realm string) http.Handler {
|
||||
|
@ -134,6 +136,8 @@ func main() {
|
|||
log.Fatal("Email address(es) specified but no smtp_server configured. Wrong configuration, aborting...")
|
||||
}
|
||||
|
||||
genericWebhookClient := configureGenericWebhookClient(cfg)
|
||||
|
||||
apiPrefix := cfg.APIPrefix
|
||||
if apiPrefix == "" {
|
||||
_, port, err := net.SplitHostPort(*bindAddr)
|
||||
|
@ -148,7 +152,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)
|
||||
|
@ -176,6 +180,17 @@ func main() {
|
|||
log.Fatal(http.ListenAndServe(*bindAddr, nil))
|
||||
}
|
||||
|
||||
func configureGenericWebhookClient(cfg *config) (*http.Client) {
|
||||
if len(cfg.GenericWebhookURLs) == 0 {
|
||||
fmt.Println("No generic_webhook_urls configured.")
|
||||
return nil
|
||||
}
|
||||
fmt.Println("Will forward metadata of all requests to ", cfg.GenericWebhookURLs)
|
||||
return &http.Client{
|
||||
Timeout: time.Second * 300,
|
||||
}
|
||||
}
|
||||
|
||||
func loadConfig(configPath string) (*config, error) {
|
||||
contents, err := ioutil.ReadFile(configPath)
|
||||
if err != nil {
|
||||
|
|
|
@ -50,3 +50,9 @@ email_from: Rageshake <rageshake@matrix.org>
|
|||
smtp_server: localhost:25
|
||||
smtp_username: myemailuser
|
||||
smtp_password: myemailpass
|
||||
|
||||
|
||||
# a list of webhook URLs, (see docs/generic_webhook.md)
|
||||
generic_webhook_urls:
|
||||
- https://server.example.com/your-server/api
|
||||
- http://another-server.com/api
|
||||
|
|
76
submit.go
76
submit.go
|
@ -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,61 @@ 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
|
||||
}
|
||||
genericHookPayload := genericWebhookPayload{
|
||||
parsedPayload: p,
|
||||
ReportURL: reportURL,
|
||||
ListingURL: listingURL,
|
||||
}
|
||||
for _, url := range s.cfg.GenericWebhookURLs {
|
||||
// Enrich the parsedPayload with a reportURL and listingURL, to convert a single struct
|
||||
// to JSON easily
|
||||
|
||||
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 {
|
||||
log.Println("Unable to submit to URL ", url, " ", err)
|
||||
return err
|
||||
}
|
||||
log.Println("Making generic webhook request to URL ", url)
|
||||
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
|
||||
|
|
Reference in a new issue