first commit
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 andoma
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,89 @@
|
||||
# Telegram Logrus Hook
|
||||
|
||||
This hook emits log messages (and corresponding fields) to the Telegram API for [logrus](https://git.company.lan/gopkg/logrus).
|
||||
|
||||
## Installation
|
||||
|
||||
Install the package with:
|
||||
|
||||
```
|
||||
go get git.company.lan/gopkg/telegramhook
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
See the tests for working examples. Also:
|
||||
|
||||
```go
|
||||
import (
|
||||
"time"
|
||||
|
||||
log "git.company.lan/gopkg/logrus"
|
||||
"git.company.lan/gopkg/telegramhook"
|
||||
)
|
||||
|
||||
func main() {
|
||||
hook, err := telegramhook.NewTelegramHook(
|
||||
"MyCoolApp",
|
||||
"MYTELEGRAMTOKEN",
|
||||
"@mycoolusername",
|
||||
telegramhook.WithAsync(true),
|
||||
telegramhook.WithTimeout(30 * time.Second),
|
||||
telegramhook.WithLevel(logrus.ErrorLevel),
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatalf("Encountered error when creating Telegram hook: %s", err)
|
||||
}
|
||||
log.AddHook(hook)
|
||||
|
||||
// Receive messages on failures
|
||||
log.Errorf("Uh oh...")
|
||||
...
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
Also you can set custom http.Client to use SOCKS5 proxy for example
|
||||
|
||||
```go
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
log "git.company.lan/gopkg/logrus"
|
||||
"git.company.lan/gopkg/telegramhook"
|
||||
"golang.org/x/net/proxy"
|
||||
)
|
||||
|
||||
func main() {
|
||||
dialer, err := proxy.SOCKS5("tcp", "127.0.0.1:54321", nil, proxy.Direct)
|
||||
dialContext := func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
return dialer.Dial(network, address)
|
||||
}
|
||||
httpTransport := &http.Transport{
|
||||
DialContext: dialContext,
|
||||
DisableKeepAlives: true,
|
||||
}
|
||||
httpClient := &http.Client{Transport: httpTransport}
|
||||
|
||||
hook, err := telegramhook.NewTelegramHookWithClient(
|
||||
"MyCoolApp",
|
||||
"MYTELEGRAMTOKEN",
|
||||
"@mycoolusername",
|
||||
httpClient,
|
||||
telegramhook.WithAsync(true),
|
||||
telegramhook.WithTimeout(30 * time.Second),
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatalf("Encountered error when creating Telegram hook: %s", err)
|
||||
}
|
||||
log.AddHook(hook)
|
||||
|
||||
// Receive messages on failures
|
||||
log.Errorf("Uh oh...")
|
||||
...
|
||||
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,7 @@
|
||||
module git.company.lan/gopkg/telegramhook
|
||||
|
||||
go 1.21.5
|
||||
|
||||
require git.company.lan/gopkg/logrus v0.0.0-20240403185826-6e11474d0c90
|
||||
|
||||
require golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
|
||||
@@ -0,0 +1,15 @@
|
||||
git.company.lan/gopkg/logrus v0.0.0-20240403185826-6e11474d0c90 h1:7lQC66WPJfhfy4VAVzxcY+vS7atQaHYSxAJSdGpV/1k=
|
||||
git.company.lan/gopkg/logrus v0.0.0-20240403185826-6e11474d0c90/go.mod h1:ZkKoQvI1GMra/NL4xFGq/hfazVL5aWZAUUC5WvBXt+4=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
+321
@@ -0,0 +1,321 @@
|
||||
package telegramhook
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"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
|
||||
async bool
|
||||
}
|
||||
|
||||
// Option defines a method for additional configuration when instantiating TelegramHook
|
||||
type Option func(*TelegramHook)
|
||||
|
||||
// Async sets logging to telegram as asynchronous
|
||||
func WithAsync(async bool) Option {
|
||||
return func(h *TelegramHook) {
|
||||
h.SetAsync(async)
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
// New 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,
|
||||
async: 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 *interface{} `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 fmt.Errorf(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 = "<b>PANIC</b>"
|
||||
case logrus.FatalLevel:
|
||||
msg = "<b>FATAL</b>"
|
||||
case logrus.ErrorLevel:
|
||||
msg = "<b>ERROR</b>"
|
||||
case logrus.WarnLevel:
|
||||
msg = "<b>WARNING</b>"
|
||||
case logrus.InfoLevel:
|
||||
msg = "<b>INFO</b>"
|
||||
case logrus.DebugLevel:
|
||||
msg = "<b>DEBUG</b>"
|
||||
}
|
||||
|
||||
msg = strings.Join([]string{msg, h.AppName()}, "@")
|
||||
msg = strings.Join([]string{msg, entry.Message}, " - ")
|
||||
|
||||
if len(entry.Data) > 0 {
|
||||
msg = strings.Join([]string{msg, "<pre>"}, "\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, "</pre>"}, "\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.Async() {
|
||||
go h.sendMessage(msg)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := h.sendMessage(msg); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Unable to send message, %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Async
|
||||
func (h *TelegramHook) Async() bool {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
return h.async
|
||||
}
|
||||
|
||||
func (h *TelegramHook) SetAsync(async bool) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
h.async = async
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package telegramhook
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
log "git.company.lan/gopkg/logrus"
|
||||
)
|
||||
|
||||
func TestNewTelegramHook(t *testing.T) {
|
||||
_, err := NewTelegramHook("", "", "", "")
|
||||
if err == nil {
|
||||
t.Errorf("No error on invalid Telegram API token.")
|
||||
}
|
||||
|
||||
_, err = NewTelegramHook("", os.Getenv("TELEGRAM_TOKEN"), "", "")
|
||||
if err != nil {
|
||||
t.Fatalf("Error on valid Telegram API token: %s", err)
|
||||
}
|
||||
|
||||
h, _ := NewTelegramHook("testing", os.Getenv("TELEGRAM_TOKEN"), os.Getenv("TELEGRAM_TARGET"), "")
|
||||
if err != nil {
|
||||
t.Fatalf("Error on valid Telegram API token and target: %s", err)
|
||||
}
|
||||
log.AddHook(h)
|
||||
|
||||
log.WithError(errors.New("an error")).WithFields(log.Fields{
|
||||
"animal": "walrus",
|
||||
"number": 1,
|
||||
"size": 10,
|
||||
"html": "<b>bold</b>",
|
||||
}).Errorf("A walrus appears")
|
||||
}
|
||||
Reference in New Issue
Block a user