package telegramhook import ( "bytes" "encoding/json" "errors" "fmt" "html" "net/http" "net/url" "os" "strings" "sync" "time" "git.company.lan/gopkg/logrus" ) // TelegramHook to send logs via the Telegram API. type TelegramHook struct { client *http.Client mu sync.RWMutex appName string authToken string chatId string threadId string level logrus.Level sync bool } // Option defines a method for additional configuration when instantiating TelegramHook type Option func(*TelegramHook) // Timeout sets http call timeout for telegram client func WithTimeout(timeout time.Duration) Option { return func(h *TelegramHook) { if timeout > 0 { h.client.Timeout = timeout } } } // WithLevel set level func WithLevel(level logrus.Level) Option { return func(h *TelegramHook) { h.SetLevel(level) } } // WithSync sets logging to telegram as synchronous func WithSync(sync bool) Option { return func(h *TelegramHook) { h.SetSync(sync) } } // NewTelegramHook creates a new instance of a hook targeting the Telegram API. func NewTelegramHook(appName, authToken, chatId, threadId string, options ...Option) (*TelegramHook, error) { client := &http.Client{} return NewTelegramHookWithClient(appName, authToken, chatId, threadId, client, options...) } // NewTelegramHookWithClient creates a new instance of a hook targeting the Telegram API with custom http.Client. func NewTelegramHookWithClient(appName, authToken, chatId, threadId string, client *http.Client, options ...Option) (*TelegramHook, error) { h := TelegramHook{ client: client, appName: appName, authToken: authToken, chatId: chatId, threadId: threadId, level: logrus.ErrorLevel, sync: false, } for _, opt := range options { opt(&h) } // Verify the API token is valid and correct before continuing if err := h.verifyToken(); err != nil { return nil, err } return &h, nil } // apiRequest encapsulates the request structure we are sending to the Telegram API. type apiRequest struct { ChatId string `json:"chat_id"` ThreadId string `json:"message_thread_id,omitempty"` Text string `json:"text"` ParseMode string `json:"parse_mode,omitempty"` } // apiResponse encapsulates the response structure received from the Telegram API. type apiResponse struct { Ok bool `json:"ok"` ErrorCode *int `json:"error_code,omitempty"` Desc *string `json:"description,omitempty"` Result *any `json:"result,omitempty"` } // verifyToken issues a test request to the Telegram API to ensure the provided token is correct and valid. func (h *TelegramHook) verifyToken() error { endpoint, _ := url.JoinPath(h.ApiEndpoint(), "getMe") res, err := h.client.Get(endpoint) if err != nil { return err } defer res.Body.Close() apiRes := apiResponse{} if err := json.NewDecoder(res.Body).Decode(&apiRes); err != nil { return err } if !apiRes.Ok { // Received an error from the Telegram API msg := "Received error response from Telegram API" if apiRes.ErrorCode != nil { msg = fmt.Sprintf("%s (error code %d)", msg, *apiRes.ErrorCode) } if apiRes.Desc != nil { msg = fmt.Sprintf("%s: %s", msg, *apiRes.Desc) } j, _ := json.MarshalIndent(apiRes, "", "\t") return fmt.Errorf("%s\n%s", msg, j) } return nil } // sendMessage issues the provided message to the Telegram API. func (h *TelegramHook) sendMessage(msg string) error { apiReq := apiRequest{ ChatId: h.ChatId(), ThreadId: h.ThreadId(), Text: msg, ParseMode: "HTML", } b, err := json.Marshal(apiReq) if err != nil { return err } endpoint, _ := url.JoinPath(h.ApiEndpoint(), "sendMessage") res, err := h.client.Post(endpoint, "application/json", bytes.NewReader(b)) if err != nil { fmt.Fprintf(os.Stderr, "Encountered error when issuing request to Telegram API, %v", err) return err } defer res.Body.Close() apiRes := apiResponse{} if err := json.NewDecoder(res.Body).Decode(&apiRes); err != nil { return err } if !apiRes.Ok { // Received an error from the Telegram API msg := "Received error response from Telegram API" if apiRes.ErrorCode != nil { msg = fmt.Sprintf("%s (error code %d)", msg, *apiRes.ErrorCode) } if apiRes.Desc != nil { msg = fmt.Sprintf("%s: %s", msg, *apiRes.Desc) } return errors.New(msg) } return nil } // createMessage crafts an HTML-formatted message to send to the Telegram API. func (h *TelegramHook) createMessage(entry *logrus.Entry) string { var msg string switch entry.Level { case logrus.PanicLevel: msg = "PANIC" case logrus.FatalLevel: msg = "FATAL" case logrus.ErrorLevel: msg = "ERROR" case logrus.WarnLevel: msg = "WARNING" case logrus.InfoLevel: msg = "INFO" case logrus.DebugLevel: msg = "DEBUG" } msg = strings.Join([]string{msg, h.AppName()}, "@") msg = strings.Join([]string{msg, entry.Message}, " - ") if len(entry.Data) > 0 { msg = strings.Join([]string{msg, "
"}, "\n")
for k, v := range entry.Data {
msg = strings.Join([]string{msg, html.EscapeString(fmt.Sprintf("\t%s: %+v", k, v))}, "\n")
}
msg = strings.Join([]string{msg, ""}, "\n")
}
return msg
}
// Levels returns the log levels that the hook should be enabled for.
func (h *TelegramHook) Levels() []logrus.Level {
h.mu.RLock()
defer h.mu.RUnlock()
return logrus.AllLevels[:h.level+1]
}
// Fire emits a log message to the Telegram API.
func (h *TelegramHook) Fire(entry *logrus.Entry) error {
msg := h.createMessage(entry)
if h.Sync() {
if err := h.sendMessage(msg); err != nil {
fmt.Fprintf(os.Stderr, "Unable to send message, %v", err)
return err
}
}
go h.sendMessage(msg)
return nil
}
// ApiEndpoint
func (h *TelegramHook) ApiEndpoint() string {
h.mu.RLock()
defer h.mu.RUnlock()
return fmt.Sprintf("https://api.telegram.org/bot%s", h.authToken)
}
// AppName
func (h *TelegramHook) AppName() string {
h.mu.RLock()
defer h.mu.RUnlock()
return h.appName
}
func (h *TelegramHook) SetAppName(appName string) {
h.mu.Lock()
defer h.mu.Unlock()
h.appName = appName
}
// AuthToken
func (h *TelegramHook) AuthToken() string {
h.mu.RLock()
defer h.mu.RUnlock()
return h.authToken
}
func (h *TelegramHook) SetAuthToken(authToken string) {
h.mu.Lock()
defer h.mu.Unlock()
h.authToken = authToken
}
// ChatId
func (h *TelegramHook) ChatId() string {
h.mu.RLock()
defer h.mu.RUnlock()
return h.chatId
}
func (h *TelegramHook) SetChatId(chatId string) {
h.mu.Lock()
defer h.mu.Unlock()
h.chatId = chatId
}
// ThreadId
func (h *TelegramHook) ThreadId() string {
h.mu.RLock()
defer h.mu.RUnlock()
return h.threadId
}
func (h *TelegramHook) SetThreadId(threadId string) {
h.mu.Lock()
defer h.mu.Unlock()
h.threadId = threadId
}
// Level
func (h *TelegramHook) Level() logrus.Level {
h.mu.RLock()
defer h.mu.RUnlock()
return h.level
}
func (h *TelegramHook) SetLevel(level logrus.Level) {
h.mu.Lock()
defer h.mu.Unlock()
h.level = level
}
// Sync
func (h *TelegramHook) Sync() bool {
h.mu.RLock()
defer h.mu.RUnlock()
return h.sync
}
func (h *TelegramHook) SetSync(sync bool) {
h.mu.Lock()
defer h.mu.Unlock()
h.sync = sync
}