123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218 |
- // 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)
- }
|