You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

218 lines
4.4 KiB

// Copyright 2018 Unknwon
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
package clog
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"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{
"Tracing",
"Information",
"Warning",
"Error",
"Fatal",
}
discordColors = []int{
0, // Trace
3843043, // Info
16761600, // Warn
13041721, // Error
9440319, // Fatal
}
)
type DiscordConfig struct {
// Minimum level of messages to be processed.
Level LEVEL
// Buffer size defines how many messages can be queued before hangs.
BufferSize int64
// Discord webhook URL.
URL string
// Username to be shown for the message.
// Leave empty to use default as set in the Discord.
Username string
}
type discord struct {
Adapter
url string
username string
}
func newDiscord() Logger {
return &discord{
Adapter: Adapter{
quitChan: make(chan struct{}),
},
}
}
func (d *discord) Level() LEVEL { return d.level }
func (d *discord) Init(v interface{}) error {
cfg, ok := v.(DiscordConfig)
if !ok {
return ErrConfigObject{"DiscordConfig", v}
}
if !isValidLevel(cfg.Level) {
return ErrInvalidLevel{}
}
d.level = cfg.Level
if len(cfg.URL) == 0 {
return errors.New("URL cannot be empty")
}
d.url = cfg.URL
d.username = cfg.Username
d.msgChan = make(chan *Message, cfg.BufferSize)
return nil
}
func (d *discord) ExchangeChans(errorChan chan<- error) chan *Message {
d.errorChan = errorChan
return d.msgChan
}
func buildDiscordPayload(username string, msg *Message) (string, error) {
payload := discordPayload{
Username: username,
Embeds: []*discordEmbed{
{
Title: discordTitles[msg.Level],
Description: msg.Body[8:],
Timestamp: time.Now().Format(time.RFC3339),
Color: discordColors[msg.Level],
},
},
}
p, err := json.Marshal(&payload)
if err != nil {
return "", err
}
return string(p), nil
}
type rateLimitMsg struct {
RetryAfter int64 `json:"retry_after"`
}
func (d *discord) postMessage(r io.Reader) (int64, error) {
resp, err := http.Post(d.url, "application/json", r)
if err != nil {
return -1, fmt.Errorf("HTTP Post: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode == 429 {
rlMsg := &rateLimitMsg{}
if err = json.NewDecoder(resp.Body).Decode(&rlMsg); err != nil {
return -1, fmt.Errorf("decode rate limit message: %v", err)
}
return rlMsg.RetryAfter, nil
} else if resp.StatusCode/100 != 2 {
data, _ := ioutil.ReadAll(resp.Body)
return -1, fmt.Errorf("%s", data)
}
return -1, nil
}
func (d *discord) write(msg *Message) {
payload, err := buildDiscordPayload(d.username, msg)
if err != nil {
d.errorChan <- fmt.Errorf("discord: builddiscordPayload: %v", err)
return
}
const RETRY_TIMES = 3
// Due to discord limit, try at most x times with respect to "retry_after" parameter.
for i := 1; i <= 3; i++ {
retryAfter, err := d.postMessage(bytes.NewReader([]byte(payload)))
if err != nil {
d.errorChan <- fmt.Errorf("discord: postMessage: %v", err)
return
}
if retryAfter > 0 {
time.Sleep(time.Duration(retryAfter) * time.Millisecond)
continue
}
return
}
d.errorChan <- fmt.Errorf("discord: failed to send message after %d retries", RETRY_TIMES)
}
func (d *discord) Start() {
LOOP:
for {
select {
case msg := <-d.msgChan:
d.write(msg)
case <-d.quitChan:
break LOOP
}
}
for {
if len(d.msgChan) == 0 {
break
}
d.write(<-d.msgChan)
}
d.quitChan <- struct{}{} // Notify the cleanup is done.
}
func (d *discord) Destroy() {
d.quitChan <- struct{}{}
<-d.quitChan
close(d.msgChan)
close(d.quitChan)
}
func init() {
Register(DISCORD, newDiscord)
}