328 lines
8.7 KiB
Go
328 lines
8.7 KiB
Go
package plugin
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
)
|
|
|
|
// handleEventAlert обрабатывает вебхук события Sentry
|
|
func (p *Plugin) handleEventAlert(w http.ResponseWriter, r *http.Request) {
|
|
var payload SentryPayload
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
p.API.LogError("Failed to decode Sentry event alert payload", "error", err.Error())
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
event := payload.Data.Event
|
|
|
|
project, err := p.getProject(strconv.Itoa(event.Project))
|
|
if err != nil || project == nil || project.ChannelID == "" {
|
|
p.API.LogWarn("No channel linked for project", "project", event.Project)
|
|
w.WriteHeader(http.StatusOK)
|
|
return
|
|
}
|
|
|
|
if !project.Hooks.EventAlert {
|
|
p.API.LogDebug("Event alert hook disabled for project", "project", project.Slug)
|
|
w.WriteHeader(http.StatusOK)
|
|
return
|
|
}
|
|
|
|
environment := getTagFromArray(event.Tags, "environment")
|
|
release := getTagFromArray(event.Tags, "release")
|
|
user := getTagFromArray(event.Tags, "user")
|
|
|
|
attachment := &model.SlackAttachment{
|
|
Color: levelToColor(event.Level),
|
|
Title: event.Title,
|
|
TitleLink: event.WebURL,
|
|
Text: event.Message,
|
|
Fields: []*model.SlackAttachmentField{
|
|
{
|
|
Title: "Project",
|
|
Value: fmt.Sprintf("%s (`%s`)", project.Name, project.Slug),
|
|
Short: true,
|
|
},
|
|
{Title: "Project ID", Value: strconv.Itoa(event.Project), Short: true},
|
|
{Title: "Issue ID", Value: event.IssueID, Short: true},
|
|
{Title: "Environment", Value: environment, Short: true},
|
|
{Title: "Level", Value: event.Level, Short: true},
|
|
{Title: "Culprit", Value: event.Culprit, Short: false},
|
|
{Title: "Logger", Value: event.Logger, Short: true},
|
|
{Title: "Platform", Value: event.Platform, Short: true},
|
|
{Title: "Release", Value: release, Short: true},
|
|
{Title: "User", Value: user, Short: true},
|
|
},
|
|
}
|
|
|
|
if event.Exception != nil && len(event.Exception.Values) > 0 {
|
|
for _, ex := range event.Exception.Values {
|
|
attachment.Fields = append(attachment.Fields, &model.SlackAttachmentField{
|
|
Title: "Exception",
|
|
Value: fmt.Sprintf(
|
|
"Type: %s\nValue: %s\nStacktrace:\n%s",
|
|
ex.Type,
|
|
ex.Value,
|
|
formatStacktrace(&ex),
|
|
),
|
|
Short: true,
|
|
})
|
|
}
|
|
}
|
|
|
|
for _, tag := range event.Tags {
|
|
if len(tag) != 2 {
|
|
continue
|
|
}
|
|
|
|
key := tag[0]
|
|
value := tag[1]
|
|
|
|
if key == "environment" || key == "release" || key == "user" {
|
|
continue
|
|
}
|
|
|
|
attachment.Fields = append(attachment.Fields, &model.SlackAttachmentField{
|
|
Title: key,
|
|
Value: value,
|
|
Short: true,
|
|
})
|
|
}
|
|
|
|
post := &model.Post{
|
|
UserId: p.botUserID,
|
|
ChannelId: project.ChannelID,
|
|
Props: map[string]interface{}{
|
|
"attachments": []*model.SlackAttachment{attachment},
|
|
},
|
|
}
|
|
|
|
if _, err := p.API.CreatePost(post); err != nil {
|
|
p.API.LogError("Failed to create post", "error", err.Error())
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
|
|
// handleComment обрабатывает вебхук комментария Sentry
|
|
func (p *Plugin) handleComment(w http.ResponseWriter, r *http.Request) {
|
|
var payload SentryCommentPayload
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
p.API.LogError("Failed to decode Sentry comment payload", "error", err.Error())
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
project, err := p.getProjectBySlug(payload.Data.ProjectSlug)
|
|
if err != nil || project == nil || project.ChannelID == "" {
|
|
p.API.LogWarn("No channel linked for project", "slug", payload.Data.ProjectSlug)
|
|
w.WriteHeader(http.StatusOK)
|
|
return
|
|
}
|
|
|
|
if !project.Hooks.Comment {
|
|
p.API.LogDebug("Comment hook disabled for project", "project", project.Slug)
|
|
w.WriteHeader(http.StatusOK)
|
|
return
|
|
}
|
|
|
|
attachment := &model.SlackAttachment{
|
|
Color: "#439FE0",
|
|
Title: "💬 New Sentry comment",
|
|
Text: fmt.Sprintf(
|
|
"*%s* commented on issue `%d`\n\n>%s",
|
|
payload.Actor.Name,
|
|
payload.Data.IssueID,
|
|
payload.Data.Comment,
|
|
),
|
|
Fields: []*model.SlackAttachmentField{
|
|
{Title: "Project", Value: project.Name, Short: true},
|
|
{Title: "Action", Value: payload.Action, Short: true},
|
|
{Title: "Comment ID", Value: strconv.Itoa(payload.Data.CommentID), Short: true},
|
|
},
|
|
}
|
|
|
|
post := &model.Post{
|
|
UserId: p.botUserID,
|
|
ChannelId: project.ChannelID,
|
|
Props: map[string]interface{}{
|
|
"attachments": []*model.SlackAttachment{attachment},
|
|
},
|
|
}
|
|
|
|
if _, err := p.API.CreatePost(post); err != nil {
|
|
p.API.LogError("Failed to create comment post", "error", err.Error())
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
|
|
// handleMetricAlert обрабатывает вебхук метрического алерта Sentry
|
|
func (p *Plugin) handleMetricAlert(w http.ResponseWriter, r *http.Request) {
|
|
var payload SentryMetricAlertPayload
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
p.API.LogError("Failed to decode metric alert payload", "error", err.Error())
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
alert := payload.Data.MetricAlert
|
|
rule := alert.AlertRule
|
|
|
|
var project *LinkedProject
|
|
if len(alert.Projects) > 0 {
|
|
project, _ = p.getProjectBySlug(alert.Projects[0])
|
|
}
|
|
|
|
if project == nil || project.ChannelID == "" {
|
|
p.API.LogWarn("No channel linked for metric alert", "projects", alert.Projects)
|
|
w.WriteHeader(http.StatusOK)
|
|
return
|
|
}
|
|
|
|
if !project.Hooks.MetricAlert {
|
|
p.API.LogDebug("Metric alert hook disabled for project", "project", project.Slug)
|
|
w.WriteHeader(http.StatusOK)
|
|
return
|
|
}
|
|
|
|
color := "#E01E5A" // default: critical
|
|
switch payload.Action {
|
|
case "resolved":
|
|
color = "#2EB67D"
|
|
case "warning":
|
|
color = "#ECB22E"
|
|
}
|
|
|
|
attachment := &model.SlackAttachment{
|
|
Color: color,
|
|
Title: payload.Data.DescriptionTitle,
|
|
TitleLink: payload.Data.WebURL,
|
|
Text: payload.Data.DescriptionText,
|
|
Fields: []*model.SlackAttachmentField{
|
|
{
|
|
Title: "Rule",
|
|
Value: rule.Name,
|
|
Short: true,
|
|
},
|
|
{
|
|
Title: "Aggregate",
|
|
Value: rule.Aggregate,
|
|
Short: true,
|
|
},
|
|
{
|
|
Title: "Query",
|
|
Value: rule.Query,
|
|
Short: false,
|
|
},
|
|
{
|
|
Title: "Window (min)",
|
|
Value: strconv.Itoa(rule.TimeWindow),
|
|
Short: true,
|
|
},
|
|
{
|
|
Title: "Action",
|
|
Value: payload.Action,
|
|
Short: true,
|
|
},
|
|
},
|
|
}
|
|
|
|
post := &model.Post{
|
|
UserId: p.botUserID,
|
|
ChannelId: project.ChannelID,
|
|
Props: map[string]interface{}{
|
|
"attachments": []*model.SlackAttachment{attachment},
|
|
},
|
|
}
|
|
|
|
if _, err := p.API.CreatePost(post); err != nil {
|
|
p.API.LogError("Failed to create metric alert post", "error", err.Error())
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
|
|
// handleIssue обрабатывает вебхук issue Sentry
|
|
func (p *Plugin) handleIssue(w http.ResponseWriter, r *http.Request) {
|
|
var payload SentryIssuePayload
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
p.API.LogError("Failed to decode issue payload", "error", err.Error())
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
issue := payload.Data.Issue
|
|
|
|
project, err := p.getProjectBySlug(issue.Project.Slug)
|
|
if err != nil || project == nil || project.ChannelID == "" {
|
|
p.API.LogWarn("No channel linked for issue", "project", issue.Project.Slug)
|
|
w.WriteHeader(http.StatusOK)
|
|
return
|
|
}
|
|
|
|
if !project.Hooks.Issue {
|
|
p.API.LogDebug("Issue hook disabled for project", "project", project.Slug)
|
|
w.WriteHeader(http.StatusOK)
|
|
return
|
|
}
|
|
|
|
color := "#E01E5A" // default: red
|
|
switch payload.Action {
|
|
case "resolved":
|
|
color = "#2EB67D"
|
|
case "archived":
|
|
color = "#6B7280"
|
|
case "unresolved":
|
|
color = "#F97316"
|
|
}
|
|
|
|
attachment := &model.SlackAttachment{
|
|
Color: color,
|
|
Title: fmt.Sprintf("[%s] %s", issue.ShortID, issue.Title),
|
|
TitleLink: issue.WebURL,
|
|
Text: fmt.Sprintf(
|
|
"**Action:** `%s`\n**Status:** `%s`\n**Level:** `%s`\n**Priority:** `%s`",
|
|
payload.Action,
|
|
issue.Status,
|
|
issue.Level,
|
|
issue.Priority,
|
|
),
|
|
Fields: []*model.SlackAttachmentField{
|
|
{Title: "Project", Value: issue.Project.Name, Short: true},
|
|
{Title: "Platform", Value: issue.Platform, Short: true},
|
|
{Title: "Type", Value: issue.IssueType, Short: true},
|
|
{Title: "Category", Value: issue.IssueCategory, Short: true},
|
|
{Title: "Events", Value: issue.Count, Short: true},
|
|
{Title: "Users", Value: strconv.Itoa(issue.UserCount), Short: true},
|
|
},
|
|
}
|
|
|
|
post := &model.Post{
|
|
UserId: p.botUserID,
|
|
ChannelId: project.ChannelID,
|
|
Props: map[string]interface{}{
|
|
"attachments": []*model.SlackAttachment{attachment},
|
|
},
|
|
}
|
|
|
|
if _, err := p.API.CreatePost(post); err != nil {
|
|
p.API.LogError("Failed to create issue post", "error", err.Error())
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|