some refactoring
This commit is contained in:
parent
627d4b821b
commit
e04079cb0b
@ -1,10 +1,7 @@
|
|||||||
package plugin
|
package plugin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
@ -12,190 +9,6 @@ import (
|
|||||||
"github.com/mattermost/mattermost/server/public/plugin"
|
"github.com/mattermost/mattermost/server/public/plugin"
|
||||||
)
|
)
|
||||||
|
|
||||||
type httpResponse struct {
|
|
||||||
Status string `json:"status"`
|
|
||||||
Error string `json:"error,omitempty"`
|
|
||||||
Data interface{} `json:"data,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SentryTag struct {
|
|
||||||
Key string `json:"key"`
|
|
||||||
Value string `json:"value"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SentryExceptionValue struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Value string `json:"value"`
|
|
||||||
Stacktrace struct {
|
|
||||||
Frames []struct {
|
|
||||||
Filename string `json:"filename"`
|
|
||||||
Function string `json:"function"`
|
|
||||||
Module string `json:"module"`
|
|
||||||
Lineno int `json:"lineno"`
|
|
||||||
Colno int `json:"colno"`
|
|
||||||
AbsPath string `json:"abs_path"`
|
|
||||||
ContextLine string `json:"context_line"`
|
|
||||||
} `json:"frames"`
|
|
||||||
} `json:"stacktrace"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SentryException struct {
|
|
||||||
Values []SentryExceptionValue `json:"values"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SentryEvent struct {
|
|
||||||
Project int `json:"project"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Message string `json:"message,omitempty"`
|
|
||||||
Level string `json:"level"`
|
|
||||||
Culprit string `json:"culprit,omitempty"`
|
|
||||||
Logger string `json:"logger,omitempty"`
|
|
||||||
Platform string `json:"platform,omitempty"`
|
|
||||||
WebURL string `json:"web_url"`
|
|
||||||
IssueID string `json:"issue_id"`
|
|
||||||
Tags [][]string `json:"tags"` // ← вот здесь
|
|
||||||
Exception *SentryException `json:"exception,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SentryPayload struct {
|
|
||||||
Action string `json:"action"`
|
|
||||||
Data struct {
|
|
||||||
Event SentryEvent `json:"event"`
|
|
||||||
TriggeredRule string `json:"triggered_rule"`
|
|
||||||
} `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func levelToColor(level string) string {
|
|
||||||
switch strings.ToLower(level) {
|
|
||||||
case "fatal":
|
|
||||||
return "#B10DC9" // фиолетовый, критический
|
|
||||||
case "error":
|
|
||||||
return "#FF4136" // красный
|
|
||||||
case "warning":
|
|
||||||
return "#FF851B" // оранжевый
|
|
||||||
case "log":
|
|
||||||
return "#AAAAAA" // серый
|
|
||||||
case "info":
|
|
||||||
return "#0074D9" // синий
|
|
||||||
case "debug":
|
|
||||||
return "#2ECC40" // зелёный
|
|
||||||
default:
|
|
||||||
return "#AAAAAA" // серый для неизвестных
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getTagFromArray(tags [][]string, key string) string {
|
|
||||||
for _, t := range tags {
|
|
||||||
if len(t) == 2 && t[0] == key {
|
|
||||||
return t[1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatStacktrace(ex *SentryExceptionValue) string {
|
|
||||||
if ex == nil || len(ex.Stacktrace.Frames) == 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
lines := make([]string, 0, len(ex.Stacktrace.Frames))
|
|
||||||
for _, f := range ex.Stacktrace.Frames {
|
|
||||||
lines = append(lines, fmt.Sprintf("%s:%d %s - %s", f.Filename, f.Lineno, f.Function, f.ContextLine))
|
|
||||||
}
|
|
||||||
return strings.Join(lines, "\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Plugin) handleWebhook(w http.ResponseWriter, r *http.Request) {
|
|
||||||
defer r.Body.Close()
|
|
||||||
|
|
||||||
var payload SentryPayload
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
||||||
p.API.LogError("Failed to decode Sentry payload", "error", err.Error())
|
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
event := payload.Data.Event
|
|
||||||
|
|
||||||
// Найти канал по projectID
|
|
||||||
channelID, err := p.getChannelForProject(strconv.Itoa(event.Project))
|
|
||||||
if err != nil || channelID == "" {
|
|
||||||
p.API.LogWarn("No channel linked for project", "project", event.Project)
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Основные теги
|
|
||||||
environment := getTagFromArray(event.Tags, "environment")
|
|
||||||
release := getTagFromArray(event.Tags, "release")
|
|
||||||
user := getTagFromArray(event.Tags, "user")
|
|
||||||
|
|
||||||
// Составляем attachment
|
|
||||||
attachment := &model.SlackAttachment{
|
|
||||||
Color: levelToColor(event.Level),
|
|
||||||
Title: event.Title,
|
|
||||||
TitleLink: event.WebURL,
|
|
||||||
Text: event.Message,
|
|
||||||
Fields: []*model.SlackAttachmentField{
|
|
||||||
{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: 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Plugin) InitApi() {
|
func (p *Plugin) InitApi() {
|
||||||
p.router = mux.NewRouter()
|
p.router = mux.NewRouter()
|
||||||
p.router.HandleFunc("/webhook", p.handleWebhook).Methods("POST")
|
p.router.HandleFunc("/webhook", p.handleWebhook).Methods("POST")
|
||||||
@ -254,42 +67,3 @@ func (p *Plugin) ExecuteCommand(
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Plugin) ensureBot() error {
|
|
||||||
const botUsername = "sentry"
|
|
||||||
|
|
||||||
user, appErr := p.API.GetUserByUsername(botUsername)
|
|
||||||
if appErr == nil && user != nil {
|
|
||||||
if !user.IsBot {
|
|
||||||
return fmt.Errorf("user @%s exists but is not a bot", botUsername)
|
|
||||||
}
|
|
||||||
p.botUserID = user.Id
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
bot := &model.Bot{
|
|
||||||
Username: botUsername,
|
|
||||||
DisplayName: "Sentry",
|
|
||||||
Description: "Sentry notifications bot",
|
|
||||||
}
|
|
||||||
|
|
||||||
createdBot, appErr := p.API.CreateBot(bot)
|
|
||||||
if appErr != nil {
|
|
||||||
p.API.LogError("Failed to create Sentry bot", "error", appErr.Error())
|
|
||||||
return appErr
|
|
||||||
}
|
|
||||||
|
|
||||||
p.botUserID = createdBot.UserId
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Plugin) getChannelForProject(project string) (string, error) {
|
|
||||||
key := "sentry:project:" + project
|
|
||||||
|
|
||||||
data, appErr := p.API.KVGet(key)
|
|
||||||
if appErr != nil || data == nil {
|
|
||||||
return "", appErr
|
|
||||||
}
|
|
||||||
|
|
||||||
return string(data), nil
|
|
||||||
}
|
|
||||||
|
|||||||
46
server/plugin/bot.go
Normal file
46
server/plugin/bot.go
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/mattermost/mattermost/server/public/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *Plugin) ensureBot() error {
|
||||||
|
const botUsername = "sentry"
|
||||||
|
|
||||||
|
user, appErr := p.API.GetUserByUsername(botUsername)
|
||||||
|
if appErr == nil && user != nil {
|
||||||
|
if !user.IsBot {
|
||||||
|
return fmt.Errorf("user @%s exists but is not a bot", botUsername)
|
||||||
|
}
|
||||||
|
p.botUserID = user.Id
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
bot := &model.Bot{
|
||||||
|
Username: botUsername,
|
||||||
|
DisplayName: "Sentry",
|
||||||
|
Description: "Sentry notifications bot",
|
||||||
|
}
|
||||||
|
|
||||||
|
createdBot, appErr := p.API.CreateBot(bot)
|
||||||
|
if appErr != nil {
|
||||||
|
p.API.LogError("Failed to create Sentry bot", "error", appErr.Error())
|
||||||
|
return appErr
|
||||||
|
}
|
||||||
|
|
||||||
|
p.botUserID = createdBot.UserId
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Plugin) getChannelForProject(project string) (string, error) {
|
||||||
|
key := "sentry:project:" + project
|
||||||
|
|
||||||
|
data, appErr := p.API.KVGet(key)
|
||||||
|
if appErr != nil || data == nil {
|
||||||
|
return "", appErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(data), nil
|
||||||
|
}
|
||||||
56
server/plugin/sentry_types.go
Normal file
56
server/plugin/sentry_types.go
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
package plugin
|
||||||
|
|
||||||
|
// Типы данных для работы с Sentry API
|
||||||
|
|
||||||
|
type httpResponse struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
Data interface{} `json:"data,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SentryTag struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SentryExceptionValue struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
Stacktrace struct {
|
||||||
|
Frames []struct {
|
||||||
|
Filename string `json:"filename"`
|
||||||
|
Function string `json:"function"`
|
||||||
|
Module string `json:"module"`
|
||||||
|
Lineno int `json:"lineno"`
|
||||||
|
Colno int `json:"colno"`
|
||||||
|
AbsPath string `json:"abs_path"`
|
||||||
|
ContextLine string `json:"context_line"`
|
||||||
|
} `json:"frames"`
|
||||||
|
} `json:"stacktrace"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SentryException struct {
|
||||||
|
Values []SentryExceptionValue `json:"values"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SentryEvent struct {
|
||||||
|
Project int `json:"project"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
Level string `json:"level"`
|
||||||
|
Culprit string `json:"culprit,omitempty"`
|
||||||
|
Logger string `json:"logger,omitempty"`
|
||||||
|
Platform string `json:"platform,omitempty"`
|
||||||
|
WebURL string `json:"web_url"`
|
||||||
|
IssueID string `json:"issue_id"`
|
||||||
|
Tags [][]string `json:"tags"`
|
||||||
|
Exception *SentryException `json:"exception,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SentryPayload struct {
|
||||||
|
Action string `json:"action"`
|
||||||
|
Data struct {
|
||||||
|
Event SentryEvent `json:"event"`
|
||||||
|
TriggeredRule string `json:"triggered_rule"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
138
server/plugin/webhook.go
Normal file
138
server/plugin/webhook.go
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/mattermost/mattermost/server/public/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func levelToColor(level string) string {
|
||||||
|
switch strings.ToLower(level) {
|
||||||
|
case "fatal":
|
||||||
|
return "#B10DC9" // фиолетовый, критический
|
||||||
|
case "error":
|
||||||
|
return "#FF4136" // красный
|
||||||
|
case "warning":
|
||||||
|
return "#FF851B" // оранжевый
|
||||||
|
case "log":
|
||||||
|
return "#AAAAAA" // серый
|
||||||
|
case "info":
|
||||||
|
return "#0074D9" // синий
|
||||||
|
case "debug":
|
||||||
|
return "#2ECC40" // зелёный
|
||||||
|
default:
|
||||||
|
return "#AAAAAA" // серый для неизвестных
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTagFromArray(tags [][]string, key string) string {
|
||||||
|
for _, t := range tags {
|
||||||
|
if len(t) == 2 && t[0] == key {
|
||||||
|
return t[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatStacktrace(ex *SentryExceptionValue) string {
|
||||||
|
if ex == nil || len(ex.Stacktrace.Frames) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := make([]string, 0, len(ex.Stacktrace.Frames))
|
||||||
|
for _, f := range ex.Stacktrace.Frames {
|
||||||
|
lines = append(lines, fmt.Sprintf("%s:%d %s - %s", f.Filename, f.Lineno, f.Function, f.ContextLine))
|
||||||
|
}
|
||||||
|
return strings.Join(lines, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Plugin) handleWebhook(w http.ResponseWriter, r *http.Request) {
|
||||||
|
defer r.Body.Close()
|
||||||
|
|
||||||
|
var payload SentryPayload
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||||
|
p.API.LogError("Failed to decode Sentry payload", "error", err.Error())
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
event := payload.Data.Event
|
||||||
|
|
||||||
|
channelID, err := p.getChannelForProject(strconv.Itoa(event.Project))
|
||||||
|
if err != nil || channelID == "" {
|
||||||
|
p.API.LogWarn("No channel linked for project", "project", event.Project)
|
||||||
|
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 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: 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)
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user