Skip to content

Commit 1f0d3eb

Browse files
committed
initial commit
1 parent e5b8b5e commit 1f0d3eb

7 files changed

Lines changed: 324 additions & 0 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
bin/
2+
deploy.sh

Makefile

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
.DEFAULT_GOAL := help
2+
3+
PROJECT := notifymem
4+
SOURCES := $(wildcard *.go cmd/*/*.go)
5+
VERSION := $(shell git describe --tags 2>/dev/null || echo "Unknown")
6+
7+
build: $(SOURCES) ## Build the project
8+
@echo "Building $(PROJECT) ($(VERSION))"
9+
CGO_ENABLED=0 go build -ldflags "-X 'main.version=$(VERSION)'" -o bin/$(PROJECT) cmd/notifymem/*.go
10+
11+
.PHONY: help
12+
help: ## Display help
13+
@COL_W=20
14+
@grep -h '##' $(MAKEFILE_LIST) | \
15+
grep -v grep | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-'$$COL_W's\033[0m %s\n", $$1, $$2}'
16+
17+
.PHONY: run
18+
run: build ## Run the project
19+
./bin/$(PROJECT)
20+
21+
.PHONY: tidy
22+
tidy: ## Run go mod tidy
23+
go mod tidy -v

README.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Notifier for High Memory Usage
2+
3+
A simple tool to notify you when your system memory usage is too high.
4+
5+
Features:
6+
7+
- uses `notify-send` to send notifications when memory usage reaches a certain threshold
8+
- major Linux distributions are supported
9+
- configurable threshold, check memory usage interval and resend notification delay
10+
11+
## Usage
12+
13+
Example usage that monitors memory usage by checking every 2 seconds, sends a notification when memory usage is above 80% and will not send another notification until after 60 seconds.
14+
15+
```bash
16+
notifymem -threshold 80 -delay 60 -interval 2
17+
```
18+
19+
## Installation
20+
21+
First, download the latest release or clone the repository and build locally using `make build`, which will create a binary in the `bin` directory. Then, copy the binary to a directory, like `/opt/notifymem/bin`, and make the file executable, like `chmod +x /opt/notifymem/bin/notifymem`.
22+
23+
Next, create a systemd service file in `/etc/systemd/system/notifymem.service` with the following content:
24+
25+
```ini
26+
[Unit]
27+
Description=notifymem service: notify when memory usage reaches a threshold
28+
After=network.target
29+
30+
[Service]
31+
Type=simple
32+
Restart=always
33+
RestartSec=3
34+
User=your-username
35+
ExecStart=/opt/notifymem/bin/notifymem -threshold 80 -delay 60 -interval 2
36+
37+
[Install]
38+
WantedBy=multi-user.target
39+
```
40+
41+
Make sure to:
42+
43+
- replace `your-username` with your actual username
44+
- set `ExecStart` to the path where you copied the binary and use the desired options
45+
46+
Finally, enable and start the service:
47+
48+
```bash
49+
systemctl enable notifymem
50+
systemctl start notifymem
51+
```
52+
53+
Tips:
54+
55+
- check the status of the service using `systemctl status notifymem`
56+
- check the logs of the service using `journalctl -u notifymem` (follow the logs using `-f`)
57+
- reload the systemd daemon after making changes to the service file using `systemctl daemon-reload`

cmd/notifymem/main.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"flag"
6+
"fmt"
7+
"os"
8+
"time"
9+
10+
notifymem "github.com/shayanderson/notify-mem"
11+
)
12+
13+
var version string // do not remove
14+
15+
func errorExit(err error) {
16+
fmt.Fprintf(os.Stderr, "[error] %s\n", err)
17+
os.Exit(1)
18+
}
19+
20+
func main() {
21+
fmt.Printf("running notifymem (version %s)\n", version)
22+
23+
type flags struct {
24+
debug bool
25+
interval int
26+
resendDelay int
27+
threshold int
28+
}
29+
30+
f := flags{}
31+
flag.BoolVar(&f.debug, "debug", false, "enable debug mode")
32+
flag.IntVar(&f.interval, "interval", 2, "interval between memory checks in seconds")
33+
flag.IntVar(&f.resendDelay, "delay", 30, "delay between notifications being sent in seconds")
34+
flag.IntVar(&f.threshold, "threshold", 80, "memory threshold as a percentage")
35+
flag.Parse()
36+
37+
n := notifymem.NewNotifier()
38+
mOpts := notifymem.MonitorOptions{
39+
Interval: time.Duration(f.interval) * time.Second,
40+
ResendDelay: time.Duration(f.resendDelay) * time.Second,
41+
Threshold: f.threshold,
42+
}
43+
44+
if f.debug {
45+
mOpts.DebugFunc = func(s string) { fmt.Println(s) }
46+
}
47+
48+
m, err := notifymem.NewMonitor(n, mOpts)
49+
if err != nil {
50+
errorExit(err)
51+
}
52+
53+
ctx := context.Background()
54+
if err := m.Run(ctx); err != nil && err != context.Canceled {
55+
errorExit(err)
56+
}
57+
}

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module github.com/shayanderson/notify-mem
2+
3+
go 1.22.4

monitor.go

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
package notifymem
2+
3+
import (
4+
"bufio"
5+
"context"
6+
"errors"
7+
"fmt"
8+
"os"
9+
"runtime"
10+
"time"
11+
)
12+
13+
type memory struct {
14+
total int
15+
avail int
16+
}
17+
18+
type MonitorOptions struct {
19+
DebugFunc func(string)
20+
Interval time.Duration
21+
ResendDelay time.Duration
22+
Threshold int
23+
}
24+
25+
type Monitor struct {
26+
debugFunc func(string)
27+
interval time.Duration
28+
lastSentAt time.Time
29+
notifier Notifier
30+
resendDelay time.Duration
31+
threshold int
32+
}
33+
34+
func NewMonitor(notifier Notifier, opts MonitorOptions) (*Monitor, error) {
35+
if runtime.GOOS != "linux" {
36+
return nil, errors.New("notifymem only supports Linux")
37+
}
38+
39+
m := &Monitor{
40+
debugFunc: opts.DebugFunc,
41+
interval: opts.Interval,
42+
notifier: notifier,
43+
resendDelay: opts.ResendDelay,
44+
threshold: opts.Threshold,
45+
}
46+
47+
if m.interval < 100*time.Millisecond {
48+
return nil, errors.New("interval must be at least 100ms")
49+
}
50+
51+
if m.notifier == nil {
52+
return nil, errors.New("notifier is required")
53+
}
54+
55+
if m.resendDelay < 5*time.Second {
56+
return nil, errors.New("resend delay must be at least 5s")
57+
}
58+
59+
if m.threshold < 0 || m.threshold > 100 {
60+
return nil, errors.New("threshold must be between 0 and 100")
61+
}
62+
63+
return m, nil
64+
}
65+
66+
func (m *Monitor) debug(msg string) {
67+
if m.debugFunc != nil {
68+
m.debugFunc(msg)
69+
}
70+
}
71+
72+
func (m *Monitor) isThresholdReached() (bool, int, error) {
73+
mem, err := readMemory()
74+
if err != nil {
75+
return false, 0, err
76+
}
77+
78+
usage, err := memoryUsage(mem)
79+
if err != nil {
80+
return false, 0, err
81+
}
82+
83+
if usage >= m.threshold {
84+
return true, usage, nil
85+
}
86+
87+
return false, usage, nil
88+
}
89+
90+
func (m *Monitor) notify(usage int) error {
91+
if time.Since(m.lastSentAt) < m.resendDelay {
92+
m.debug("resend delay not reached")
93+
return nil
94+
}
95+
96+
m.debug("sending notification")
97+
m.lastSentAt = time.Now()
98+
return m.notifier.Notify(
99+
"Memory Usage Threshold Reached [notifymem]",
100+
fmt.Sprintf("Memory usage at %d%%", usage),
101+
)
102+
}
103+
104+
func (m *Monitor) Run(ctx context.Context) error {
105+
m.debug("staring monitor")
106+
for {
107+
select {
108+
case <-ctx.Done():
109+
m.debug("stopping monitor")
110+
return ctx.Err()
111+
112+
case <-time.After(m.interval):
113+
reached, usage, err := m.isThresholdReached()
114+
if err != nil {
115+
return err
116+
}
117+
m.debug(fmt.Sprintf("memory usage: %d%%", usage))
118+
119+
if reached {
120+
m.debug(fmt.Sprintf("threshold reached: %d%%", usage))
121+
if err := m.notify(usage); err != nil {
122+
return err
123+
}
124+
}
125+
126+
}
127+
}
128+
}
129+
130+
func memoryUsage(m memory) (int, error) {
131+
if m.total == 0 {
132+
return 0, errors.New("total memory is 0")
133+
}
134+
135+
return int((float64(m.total-m.avail) / float64(m.total)) * 100), nil
136+
}
137+
138+
func readMemory() (memory, error) {
139+
m := memory{}
140+
f, err := os.Open("/proc/meminfo")
141+
if err != nil {
142+
return m, err
143+
}
144+
defer f.Close()
145+
146+
s := bufio.NewScanner(f)
147+
for s.Scan() {
148+
var k string
149+
var v int
150+
_, err := fmt.Sscanf(s.Text(), "%s %d", &k, &v)
151+
if err != nil {
152+
continue
153+
}
154+
switch k {
155+
case "MemTotal:":
156+
m.total = v
157+
case "MemAvailable:":
158+
m.avail = v
159+
}
160+
}
161+
162+
return m, nil
163+
}

notifier.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package notifymem
2+
3+
import "fmt"
4+
5+
type Notifier interface {
6+
Notify(title, message string) error
7+
}
8+
9+
type notifier struct {
10+
}
11+
12+
func NewNotifier() *notifier {
13+
return &notifier{}
14+
}
15+
16+
func (n *notifier) Notify(title, message string) error {
17+
fmt.Println("notify:", title, message)
18+
return nil
19+
}

0 commit comments

Comments
 (0)