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,
|
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
|
through a Slack webhook or by email, cf sample config file for how to
|
||||||
configure them.
|
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"`
|
SMTPUsername string `yaml:"smtp_username"`
|
||||||
|
|
||||||
SMTPPassword string `yaml:"smtp_password"`
|
SMTPPassword string `yaml:"smtp_password"`
|
||||||
|
|
||||||
|
GenericWebhookURLs []string `yaml:"generic_webhook_urls"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func basicAuth(handler http.Handler, username, password, realm string) http.Handler {
|
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...")
|
log.Fatal("Email address(es) specified but no smtp_server configured. Wrong configuration, aborting...")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
genericWebhookClient := configureGenericWebhookClient(cfg)
|
||||||
|
|
||||||
apiPrefix := cfg.APIPrefix
|
apiPrefix := cfg.APIPrefix
|
||||||
if apiPrefix == "" {
|
if apiPrefix == "" {
|
||||||
_, port, err := net.SplitHostPort(*bindAddr)
|
_, port, err := net.SplitHostPort(*bindAddr)
|
||||||
|
@ -148,7 +152,7 @@ func main() {
|
||||||
log.Printf("Using %s/listing as public URI", apiPrefix)
|
log.Printf("Using %s/listing as public URI", apiPrefix)
|
||||||
|
|
||||||
rand.Seed(time.Now().UnixNano())
|
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
|
// Make sure bugs directory exists
|
||||||
_ = os.Mkdir("bugs", os.ModePerm)
|
_ = os.Mkdir("bugs", os.ModePerm)
|
||||||
|
@ -176,6 +180,17 @@ func main() {
|
||||||
log.Fatal(http.ListenAndServe(*bindAddr, nil))
|
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) {
|
func loadConfig(configPath string) (*config, error) {
|
||||||
contents, err := ioutil.ReadFile(configPath)
|
contents, err := ioutil.ReadFile(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -50,3 +50,9 @@ email_from: Rageshake <rageshake@matrix.org>
|
||||||
smtp_server: localhost:25
|
smtp_server: localhost:25
|
||||||
smtp_username: myemailuser
|
smtp_username: myemailuser
|
||||||
smtp_password: myemailpass
|
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
|
slack *slackClient
|
||||||
|
|
||||||
|
genericWebhookClient *http.Client
|
||||||
cfg *config
|
cfg *config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,16 +77,23 @@ type jsonLogEntry struct {
|
||||||
Lines string `json:"lines"`
|
Lines string `json:"lines"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type genericWebhookPayload struct {
|
||||||
|
parsedPayload
|
||||||
|
ReportURL string `json:"report_url"`
|
||||||
|
ListingURL string `json:"listing_url"`
|
||||||
|
}
|
||||||
|
|
||||||
// the payload after parsing
|
// the payload after parsing
|
||||||
type parsedPayload struct {
|
type parsedPayload struct {
|
||||||
UserText string
|
UserText string `json:"user_text"`
|
||||||
AppName string
|
AppName string `json:"app"`
|
||||||
Data map[string]string
|
Data map[string]string `json:"data"`
|
||||||
Labels []string
|
Labels []string `json:"labels"`
|
||||||
Logs []string
|
Logs []string `json:"logs"`
|
||||||
LogErrors []string
|
LogErrors []string `json:"logErrors"`
|
||||||
Files []string
|
Files []string `json:"files"`
|
||||||
FileErrors []string
|
FileErrors []string `json:"fileErrors"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p parsedPayload) WriteTo(out io.Writer) {
|
func (p parsedPayload) WriteTo(out io.Writer) {
|
||||||
|
@ -492,9 +500,61 @@ func (s *submitServer) saveReport(ctx context.Context, p parsedPayload, reportDi
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := s.submitGenericWebhook(p, listingURL, resp.ReportURL); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return &resp, nil
|
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 {
|
func (s *submitServer) submitGithubIssue(ctx context.Context, p parsedPayload, listingURL string, resp *submitResponse) error {
|
||||||
if s.ghClient == nil {
|
if s.ghClient == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|
Reference in a new issue