-
Notifications
You must be signed in to change notification settings - Fork 18
/
Copy pathdiscord.go
216 lines (186 loc) · 4.89 KB
/
discord.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
package clog
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"strings"
"time"
)
type (
discordEmbed struct {
Title string `json:"title"`
Description string `json:"description"`
Timestamp string `json:"timestamp"`
Color int `json:"color"`
}
discordPayload struct {
Username string `json:"username,omitempty"`
Embeds []*discordEmbed `json:"embeds"`
}
)
var (
discordTitles = []string{
"Trace",
"Information",
"Warning",
"Error",
"Fatal",
}
discordColors = []int{
0, // Trace
3843043, // Info
16761600, // Warn
13041721, // Error
9440319, // Fatal
}
)
// DiscordConfig is the config object for the Discord logger.
type DiscordConfig struct {
// Minimum logging level of messages to be processed.
Level Level
// Discord webhook URL.
URL string
// Username to be shown in the message.
// Leave empty to use default as set in the Discord.
Username string
// Title for different levels, must have exact 5 elements in the order of
// Trace, Info, Warn, Error, and Fatal.
Titles []string
// Colors for different levels, must have exact 5 elements in the order of
// Trace, Info, Warn, Error, and Fatal.
Colors []int
}
var _ Logger = (*discordLogger)(nil)
type discordLogger struct {
*noopLogger
url string
username string
titles []string
colors []int
client *http.Client
}
func (l *discordLogger) buildPayload(m Messager) (string, error) {
descPrefixLen := strings.Index(m.String(), "] ")
if descPrefixLen == -1 {
descPrefixLen = 0
} else {
descPrefixLen += 2
}
payload := discordPayload{
Username: l.username,
Embeds: []*discordEmbed{
{
Title: l.titles[m.Level()],
Description: m.String()[descPrefixLen:],
Timestamp: time.Now().Format(time.RFC3339),
Color: l.colors[m.Level()],
},
},
}
p, err := json.Marshal(&payload)
if err != nil {
return "", err
}
return string(p), nil
}
func (l *discordLogger) postMessage(r io.Reader) (int64, error) {
resp, err := l.client.Post(l.url, "application/json", r)
if err != nil {
return -1, fmt.Errorf("HTTP request: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusTooManyRequests {
rateLimitMsg := struct {
RetryAfter int64 `json:"retry_after"`
}{}
if err = json.NewDecoder(resp.Body).Decode(&rateLimitMsg); err != nil {
return -1, fmt.Errorf("decode rate limit message: %v", err)
}
return rateLimitMsg.RetryAfter, nil
} else if resp.StatusCode/100 != 2 {
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return -1, fmt.Errorf("read HTTP response body: %v", err)
}
return -1, fmt.Errorf("non-success response status code %d with body: %s", resp.StatusCode, data)
}
return -1, nil
}
func (l *discordLogger) Write(m Messager) error {
payload, err := l.buildPayload(m)
if err != nil {
return fmt.Errorf("build payload: %v", err)
}
const retryTimes = 3
// Try at most X times with respect to "retry_after" parameter.
for i := 1; i <= retryTimes; i++ {
retryAfter, err := l.postMessage(bytes.NewReader([]byte(payload)))
if err != nil {
return fmt.Errorf("post message: %v", err)
}
if retryAfter > 0 {
time.Sleep(time.Duration(retryAfter) * time.Millisecond)
continue
}
return nil
}
return fmt.Errorf("gave up after %d retries", retryTimes)
}
// DefaultDiscordName is the default name for the Discord logger.
const DefaultDiscordName = "discord"
// NewDiscord initializes and appends a new Discord logger with default name
// to the managed list.
func NewDiscord(vs ...interface{}) error {
return NewDiscordWithName(DefaultDiscordName, vs...)
}
// NewDiscordWithName initializes and appends a new Discord logger with given
// name to the managed list.
func NewDiscordWithName(name string, vs ...interface{}) error {
return New(name, DiscordIniter(), vs...)
}
// DiscordIniter returns the initer for the Discord logger.
func DiscordIniter() Initer {
return func(name string, vs ...interface{}) (Logger, error) {
var cfg *DiscordConfig
for i := range vs {
switch v := vs[i].(type) {
case DiscordConfig:
cfg = &v
}
}
if cfg == nil {
return nil, fmt.Errorf("config object with the type '%T' not found", DiscordConfig{})
} else if cfg.URL == "" {
return nil, errors.New("empty URL")
}
titles := discordTitles
if cfg.Titles != nil {
if len(cfg.Titles) != 5 {
return nil, fmt.Errorf("titles must have exact 5 elements, but got %d", len(cfg.Titles))
}
titles = cfg.Titles
}
colors := discordColors
if cfg.Colors != nil {
if len(cfg.Colors) != 5 {
return nil, fmt.Errorf("colors must have exact 5 elements, but got %d", len(cfg.Colors))
}
colors = cfg.Colors
}
return &discordLogger{
noopLogger: &noopLogger{
name: name,
level: cfg.Level,
},
url: cfg.URL,
username: cfg.Username,
titles: titles,
colors: colors,
client: http.DefaultClient,
}, nil
}
}