Skip to content

Commit a23faed

Browse files
committed
add telegram integration
1 parent df63b0d commit a23faed

File tree

6 files changed

+211
-9
lines changed

6 files changed

+211
-9
lines changed

.goxc.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"default",
44
"publish-github"
55
],
6-
"PackageVersion": "0.1.0",
6+
"PackageVersion": "0.1.1",
77
"TaskSettings": {
88
"publish-github": {
99
"owner": "reddec",

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ It’s tool for controlling processes like a supervisord but with some important
1010
* Easy to use - no dependencies. Just a single binary file pre-compilled for most major platforms
1111
* Easy to hack - monexec can be used as a Golang library with clean and simple architecture
1212
* Integrated with Consul - optionally, monexec can register all running processes as services and deregister on fail
13+
* Optional notification to Telegram
1314
* Supports gracefull and fast shutdown by signals
1415
* Developed for used inside Docker containers
1516
* Different strategies for processes

config.go

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,15 @@ import (
2020

2121
type Config struct {
2222
Services []monexec.Executable `yaml:"services"`
23-
Critical []string `yaml:"critical,omitempty"`
23+
Critical []string `yaml:"critical,omitempty"`
2424
Consul struct {
2525
URL string `yaml:"url"`
2626
TTL time.Duration `yaml:"ttl"`
2727
AutoDeregistrationTimeout time.Duration `yaml:"timeout"`
2828
Dynamic []string `yaml:"register,omitempty"`
2929
Permanent []string `yaml:"permanent,omitempty"`
3030
} `yaml:"consul"`
31+
Telegram *Telegram `yaml:"telegram,omitempty"`
3132
}
3233

3334
func (c *Config) MergeFrom(other *Config) error {
@@ -38,24 +39,32 @@ func (c *Config) MergeFrom(other *Config) error {
3839

3940
if c.Consul.URL == def.Consul.URL {
4041
c.Consul.URL = other.Consul.URL
41-
} else if c.Consul.URL != def.Consul.URL && other.Consul.URL != def.Consul.URL {
42+
} else if c.Consul.URL != def.Consul.URL && other.Consul.URL != def.Consul.URL && other.Consul.URL != c.Consul.URL {
4243
return errors.New("Different CONSUL definition (different URL) - specify same or only once")
4344
}
4445

4546
if c.Consul.TTL == def.Consul.TTL {
4647
c.Consul.TTL = other.Consul.TTL
47-
} else if c.Consul.TTL != def.Consul.TTL && other.Consul.TTL != def.Consul.TTL {
48+
} else if c.Consul.TTL != def.Consul.TTL && other.Consul.TTL != def.Consul.TTL && other.Consul.TTL != c.Consul.TTL {
4849
return errors.New("Different CONSUL definition (different TTL) - specify same or only once")
4950
}
5051

5152
if c.Consul.AutoDeregistrationTimeout == def.Consul.AutoDeregistrationTimeout {
5253
c.Consul.AutoDeregistrationTimeout = other.Consul.AutoDeregistrationTimeout
53-
} else if c.Consul.AutoDeregistrationTimeout != def.Consul.AutoDeregistrationTimeout && other.Consul.AutoDeregistrationTimeout != def.Consul.AutoDeregistrationTimeout {
54+
} else if c.Consul.AutoDeregistrationTimeout != def.Consul.AutoDeregistrationTimeout &&
55+
other.Consul.AutoDeregistrationTimeout != def.Consul.AutoDeregistrationTimeout &&
56+
other.Consul.AutoDeregistrationTimeout != c.Consul.AutoDeregistrationTimeout {
5457
return errors.New("Different CONSUL definition (different AutoDeregistrationTimeout) - specify same or only once")
5558
}
5659

5760
c.Consul.Permanent = append(c.Consul.Permanent, other.Consul.Permanent...)
5861
c.Consul.Dynamic = append(c.Consul.Dynamic, other.Consul.Dynamic...)
62+
63+
merged, err := mergeTelegram(c.Telegram, other.Telegram)
64+
if err != nil {
65+
return err
66+
}
67+
c.Telegram = merged
5968
return nil
6069
}
6170

@@ -87,6 +96,8 @@ func (config *Config) Run(sv container.Supervisor, ctx context.Context) error {
8796
critical := plugin.NewCritical(sv, log.New(os.Stderr, "[critical-plugin] ", log.LstdFlags), config.Critical...)
8897
sv.Events().AddHandler(critical)
8998

99+
// Initialize plugins
100+
// -- consul
90101
consulConfig := api.DefaultConfig()
91102
consulConfig.Address = config.Consul.URL
92103

@@ -104,8 +115,19 @@ func (config *Config) Run(sv container.Supervisor, ctx context.Context) error {
104115
consulLogger := log.New(os.Stderr, "[consul-plugin] ", log.LstdFlags)
105116
consulService := plugin.NewConsul(consul, config.Consul.TTL, config.Consul.AutoDeregistrationTimeout, consulLogger, consulRegs)
106117
defer consulService.Close()
107-
108118
sv.Events().AddHandler(consulService)
119+
120+
// -- telegram
121+
if config.Telegram != nil {
122+
err := config.Telegram.Prepare()
123+
if err != nil {
124+
log.Println("telegram plugin ont initialized due to", err)
125+
} else {
126+
sv.Events().AddHandler(config.Telegram)
127+
}
128+
}
129+
130+
// Run
109131
wg := sync.WaitGroup{}
110132
for _, exec := range config.Services {
111133
FillDefaultExecutable(&exec)
@@ -114,7 +136,6 @@ func (config *Config) Run(sv container.Supervisor, ctx context.Context) error {
114136
defer wg.Done()
115137
container.Wait(sv.Watch(ctx, exec.Factory, exec.Restart, exec.RestartTimeout, false))
116138
}(exec)
117-
118139
}
119140

120141
wg.Wait()

docs/index.md

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ Consul is a service registry and service discover system. MONEXEC can automatica
3131

3232
Auto(de)registration available for `run` or `start` commands.
3333

34-
Use general flag `--consul` (or env var `MONEXEC_CONSUL=true`) for enable Consul integration. Monexec will try register and update status of service in Consul local agent.
34+
Use general flag `--consul` (or env var `MONEXEC_CONSUL=true`) for enable Consul integration. Monexec will try register and update status of service in Consul local agent.
3535

3636
Monexec will continue work even if Consul becomes unavailable.
3737

@@ -62,6 +62,53 @@ Suppose Consul agent is running in host `registry`
6262
monexec run --consul --consul-address "http://registry:8500" -l srv1 -- nc -l 9000
6363
```
6464

65+
# How to integrate with Telegram
66+
67+
Since `0.1.1` you can receive notifications over Telegram.
68+
69+
You have to know:
70+
71+
* BOT token : can be obtained here http://t.me/botfather
72+
* Receipients ChatID's : can be obtained here http://t.me/MyTelegramID_bot
73+
74+
Message template (based on Golang templates) also required. We recommend use this:
75+
76+
```
77+
*{{.label}}*
78+
Service {{.label}} {{.action}}
79+
{{if .error}}⚠️ *Error:* {{.error}}{{end}}
80+
_time: {{.time}}_
81+
_host: {{.hostname}}_
82+
```
83+
84+
Available params:
85+
86+
* `.label` - name of service
87+
* `.action` - servce action. Can be `spawned` or `stopped`
88+
* `.time` - current time in UTC format with timezone
89+
* `.error` - error message available only on `stopped` action
90+
* `.hostname` - current hostname
91+
92+
Configuration avaiable only from .yaml files:
93+
94+
```yaml
95+
telegram:
96+
# BOT token
97+
token: "123456789:AAAAAAAAAAAAAAAAAAAAAA_BBBBBBBBBBBB"
98+
services:
99+
# services that will be monitored
100+
- "listener2"
101+
recipients:
102+
# List of telegrams chat id
103+
- 123456789
104+
template: |
105+
*{{.label}}*
106+
Service {{.label}} {{.action}}
107+
{{if .error}}⚠️ *Error:* {{.error}}{{end}}
108+
_time: {{.time}}_
109+
_host: {{.hostname}}_
110+
```
111+
65112
# Usage
66113
67114
`monexec <command> [command-flags...] [args,...]`
@@ -210,4 +257,4 @@ consul:
210257
critical:
211258
- consul
212259
213-
```
260+
```

sample/sample2.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,16 @@ services:
1010
consul:
1111
permanent:
1212
- listener2
13+
14+
telegram:
15+
token: "123456789:AAAAAAAAAAAAAAAAAAAAAA_BBBBBBBBBBBB"
16+
services:
17+
- "listener2"
18+
recipients:
19+
- 123456789
20+
template: |
21+
*{{.label}}*
22+
Service {{.label}} {{.action}}
23+
{{if .error}}⚠️ *Error:* {{.error}}{{end}}
24+
_time: {{.time}}_
25+
_host: {{.hostname}}_

telegram.go

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package main
2+
3+
import (
4+
"github.com/reddec/container"
5+
"text/template"
6+
"log"
7+
"os"
8+
"bytes"
9+
"gopkg.in/telegram-bot-api.v4"
10+
"github.com/pkg/errors"
11+
"time"
12+
)
13+
14+
type Telegram struct {
15+
Token string `yaml:"token"`
16+
Recipients []int64 `yaml:"recipients"`
17+
Services []string `yaml:"services"`
18+
Template string `yaml:"template"`
19+
20+
servicesSet map[string]bool `yaml:"-"`
21+
templateBin *template.Template `yaml:"-"`
22+
logger *log.Logger `yaml:"-"`
23+
bot *tgbotapi.BotAPI `yaml:"-"`
24+
hostname string
25+
}
26+
27+
func (c *Telegram) Prepare() error {
28+
c.servicesSet = make(map[string]bool)
29+
for _, srv := range c.Services {
30+
c.servicesSet[srv] = true
31+
}
32+
t, err := template.New("").Parse(c.Template)
33+
if err != nil {
34+
return err
35+
}
36+
c.templateBin = t
37+
c.logger = log.New(os.Stderr, "[telegram] ", log.LstdFlags)
38+
bot, err := tgbotapi.NewBotAPI(c.Token)
39+
if err != nil {
40+
return err
41+
}
42+
c.bot = bot
43+
c.hostname, _ = os.Hostname()
44+
return nil
45+
}
46+
47+
func (c *Telegram) Stopped(runnable container.Runnable, id container.ID, err error) {
48+
if c.servicesSet[runnable.Label()] {
49+
params := map[string]interface{}{
50+
"action": "stopped",
51+
"id": id,
52+
"error": err,
53+
"label": runnable.Label(),
54+
"hostname": c.hostname,
55+
"time": time.Now().String(),
56+
}
57+
c.renderAndSend(params)
58+
}
59+
}
60+
61+
func (c *Telegram) renderAndSend(params map[string]interface{}) {
62+
message := &bytes.Buffer{}
63+
renderErr := c.templateBin.Execute(message, params)
64+
if renderErr != nil {
65+
c.logger.Println("failed render:", renderErr, "; params:", params)
66+
} else {
67+
msg := tgbotapi.NewMessage(0, message.String())
68+
msg.ParseMode = "markdown"
69+
for _, r := range c.Recipients {
70+
msg.ChatID = r
71+
_, err := c.bot.Send(msg)
72+
if err != nil {
73+
c.logger.Println("failed send message to", r, "due to", err)
74+
}
75+
}
76+
}
77+
}
78+
79+
func (c *Telegram) Spawned(runnable container.Runnable, id container.ID) {
80+
if c.servicesSet[runnable.Label()] {
81+
params := map[string]interface{}{
82+
"action": "spawned",
83+
"id": id,
84+
"label": runnable.Label(),
85+
"hostname": c.hostname,
86+
"time": time.Now().String(),
87+
}
88+
c.renderAndSend(params)
89+
}
90+
}
91+
92+
func mergeTelegram(a *Telegram, b *Telegram) (*Telegram, error) {
93+
if a == nil {
94+
return b, nil
95+
}
96+
if b == nil {
97+
return a, nil
98+
}
99+
if a.Token == "" {
100+
a.Token = b.Token
101+
}
102+
if b.Token == "" {
103+
b.Token = a.Token
104+
}
105+
if a.Token != b.Token {
106+
return nil, errors.New("token are different")
107+
}
108+
if a.Template == "" {
109+
a.Template = b.Template
110+
}
111+
if b.Template == "" {
112+
b.Template = a.Template
113+
}
114+
if a.Template != b.Template {
115+
return nil, errors.New("different templates")
116+
}
117+
a.Recipients = append(a.Recipients, b.Recipients...)
118+
a.Services = append(a.Services, b.Services...)
119+
return a, nil
120+
}

0 commit comments

Comments
 (0)