├── .gitignore ├── .gitlab-ci.yml ├── .gitmodules ├── Dockerfile-6 ├── Dockerfile-7 ├── Makefile ├── Makefile.git ├── README.md ├── config ├── init.lua ├── plugins │ ├── common │ │ ├── additional_prometheus_labels.example.lua │ │ └── prometheus_gauge.lua │ ├── cpu_plugin.lua │ ├── diskstat_plugin.lua │ ├── io_plugin.lua │ ├── loadavg_plugin.lua │ ├── memory_plugin.lua │ ├── messages_plugin.lua │ ├── mountpoints_plugin.lua │ ├── net_dev_plugin.lua │ ├── net_netstat_plugin.lua │ ├── net_tcp_plugin.lua │ ├── ntp_plugin.lua │ ├── numa_plugin.lua │ ├── other │ │ ├── loadavg.lua │ │ └── net.lua │ ├── pgbouncer_plugin.lua │ ├── postgres_plugin.lua │ ├── prometheus_plugin.lua │ ├── runit_plugin.lua │ ├── snmp_plugin.lua │ └── temperature_plugin.lua └── zabbix.conf ├── img ├── await.png ├── cpu-intr.png ├── cpu-proc.png ├── cpu-time.png ├── io-syscall.png ├── numa-access.png ├── numa-alloc.png ├── tcp-errors.png └── tcp-speed.png ├── rpm ├── SOURCES │ └── zalua-logrotate.in └── SPECS │ ├── .gitignore │ └── zalua.spec └── src └── zalua ├── cmd └── main.go ├── daemon └── daemon.go ├── dsl ├── cmd.go ├── cmd_test.go ├── crypto.go ├── db_parse_rows.go ├── dsl.go ├── filepath.go ├── http_util.go ├── humanize_time.go ├── ioutil.go ├── json.go ├── log.go ├── os.go ├── plugin.go ├── plugin_parser.go ├── plugin_test.go ├── plugin_test.lua ├── postgres.go ├── prometheus.go ├── prometheus_counter.go ├── prometheus_counter_vec.go ├── prometheus_gauge.go ├── prometheus_gauge_vec.go ├── prometheus_test.go ├── prometheus_test.lua ├── regexp.go ├── statfs_linux.go ├── storage.go ├── storage_helpers.go ├── strings.go ├── tac.go ├── tcp.go ├── time.go ├── tls_util.go ├── xml.go ├── xml_test.go ├── yaml.go └── yaml_test.go ├── logger ├── dup_linux.go ├── dup_windows.go └── log.go ├── protocol └── protocol.go ├── server ├── handler.go └── init.go ├── settings └── settings.go ├── socket ├── alive.go ├── client.go └── listen_loop.go └── storage ├── storage.go └── storage_item.go /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | rpm-tmp.* 3 | rpm/BUILD/* 4 | rpm/SOURCES/* 5 | pkg/ 6 | .idea 7 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | types: 2 | - build 3 | 4 | cache: 5 | untracked: false 6 | 7 | rpm_6: 8 | 9 | image: vadv/centos-6-go:1.9.5 10 | 11 | type: build 12 | 13 | script: 14 | - make docker 15 | - rsync -aP /build/RPMS/x86_64/*.rpm rsync://repo.itv.restr.im/infra/6/x86_64/ 16 | - curl http://repo.itv.restr.im/update?repo=infra/6/x86_64 17 | 18 | artifacts: 19 | paths: 20 | - /build/RPMS/x86_64/*.rpm 21 | 22 | tags: 23 | - lowcapacity 24 | 25 | only: 26 | - tags 27 | 28 | rpm_7: 29 | 30 | image: vadv/centos-7-go:1.9.5 31 | 32 | type: build 33 | 34 | script: 35 | - make docker 36 | - rsync -aP /build/RPMS/x86_64/*.rpm rsync://repo.itv.restr.im/infra/7/x86_64/ 37 | - curl http://repo.itv.restr.im/update?repo=infra/7/x86_64 38 | 39 | artifacts: 40 | paths: 41 | - /build/RPMS/x86_64/*.rpm 42 | 43 | tags: 44 | - lowcapacity 45 | 46 | only: 47 | - tags 48 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "src/github.com/yuin/gopher-lua"] 2 | path = src/vendor/github.com/yuin/gopher-lua 3 | url = https://github.com/yuin/gopher-lua.git 4 | [submodule "src/golang.org/x/net"] 5 | path = src/vendor/golang.org/x/net 6 | url = https://go.googlesource.com/net 7 | [submodule "src/github.com/lib/pq"] 8 | path = src/vendor/github.com/lib/pq 9 | url = https://github.com/lib/pq.git 10 | [submodule "src/gopkg.in/yaml.v2"] 11 | path = src/vendor/gopkg.in/yaml.v2 12 | url = https://gopkg.in/yaml.v2 13 | [submodule "src/gopkg.in/xmlpath.v2"] 14 | path = src/vendor/gopkg.in/xmlpath.v2 15 | url = https://gopkg.in/xmlpath.v2 16 | [submodule "src/vendor/github.com/prometheus/client_golang"] 17 | path = src/vendor/github.com/prometheus/client_golang 18 | url = https://github.com/prometheus/client_golang.git 19 | [submodule "src/vendor/github.com/prometheus/procfs"] 20 | path = src/vendor/github.com/prometheus/procfs 21 | url = https://github.com/prometheus/procfs.git 22 | [submodule "src/vendor/github.com/beorn7/perks"] 23 | path = src/vendor/github.com/beorn7/perks 24 | url = https://github.com/beorn7/perks.git 25 | [submodule "src/vendor/github.com/golang/protobuf"] 26 | path = src/vendor/github.com/golang/protobuf 27 | url = https://github.com/golang/protobuf.git 28 | [submodule "src/vendor/github.com/prometheus/common"] 29 | path = src/vendor/github.com/prometheus/common 30 | url = https://github.com/prometheus/common.git 31 | [submodule "src/vendor/github.com/matttproud/golang_protobuf_extensions"] 32 | path = src/vendor/github.com/matttproud/golang_protobuf_extensions 33 | url = https://github.com/matttproud/golang_protobuf_extensions.git 34 | [submodule "src/vendor/github.com/prometheus/client_model"] 35 | path = src/vendor/github.com/prometheus/client_model 36 | url = https://github.com/prometheus/client_model.git 37 | -------------------------------------------------------------------------------- /Dockerfile-6: -------------------------------------------------------------------------------- 1 | FROM centos:6 2 | 3 | RUN echo -e "[wandisco-Git]\nname=CentOS-6 - Wandisco Git\nbaseurl=http://opensource.wandisco.com/centos/6/git/\$basearch/\nenabled=1\ngpgcheck=0" > /etc/yum.repos.d/wandisco-git.repo 4 | RUN yum install -y epel-release 5 | RUN yum install -y wget make git gzip rpm-build nc 6 | RUN yum groupinstall -y "Development tools" 7 | 8 | RUN wget https://dl.google.com/go/go1.9.5.linux-amd64.tar.gz -O /tmp/go.linux-amd64.tar.gz && \ 9 | tar xvf /tmp/go.linux-amd64.tar.gz -C /usr/local && \ 10 | rm -f /tmp/go.linux-amd64.tar.gz && \ 11 | ln -s /usr/local/go/bin/go /usr/local/bin/go && \ 12 | ln -s /usr/local/go/bin/godoc /usr/local/bin/godoc && \ 13 | ln -s /usr/local/go/bin/gofmt /usr/local/bin/gofmt 14 | -------------------------------------------------------------------------------- /Dockerfile-7: -------------------------------------------------------------------------------- 1 | FROM centos:7 2 | 3 | RUN yum install -y epel-release 4 | RUN yum install -y wget make git gzip rpm-build nc 5 | RUN yum groupinstall -y "Development tools" 6 | 7 | RUN wget https://dl.google.com/go/go1.9.5.linux-amd64.tar.gz -O /tmp/go.linux-amd64.tar.gz && \ 8 | tar xvf /tmp/go.linux-amd64.tar.gz -C /usr/local && \ 9 | rm -f /tmp/go.linux-amd64.tar.gz && \ 10 | ln -s /usr/local/go/bin/go /usr/local/bin/go && \ 11 | ln -s /usr/local/go/bin/godoc /usr/local/bin/godoc && \ 12 | ln -s /usr/local/go/bin/gofmt /usr/local/bin/gofmt 13 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NAME=zalua 2 | BINARY=./bin/${NAME} 3 | SOURCEDIR=./src 4 | SOURCES := $(shell find $(SOURCEDIR) -name '*.go') 5 | 6 | VERSION := $(shell git describe --abbrev=0 --tags) 7 | SHA := $(shell git rev-parse --short HEAD) 8 | 9 | GOPATH ?= /usr/local/go 10 | GOPATH := ${CURDIR}:${GOPATH} 11 | export GOPATH 12 | 13 | $(BINARY): $(SOURCES) 14 | go build -o ${BINARY} -ldflags "-X main.BuildVersion=$(VERSION)-$(SHA)" $(SOURCEDIR)/$(NAME)/cmd/main.go 15 | 16 | run: clean $(BINARY) 17 | ${BINARY} 18 | 19 | test: 20 | go test -x -v zalua/dsl 21 | 22 | tar: clean 23 | mkdir -p rpm/SOURCES 24 | tar --transform='s,^\.,$(NAME)-$(VERSION),'\ 25 | -czf rpm/SOURCES/$(NAME)-$(VERSION).tar.gz .\ 26 | --exclude=rpm/SOURCES 27 | 28 | docker: submodule_check tar 29 | cp -a $(CURDIR)/rpm /build 30 | cp -a $(CURDIR)/rpm/SPECS/$(NAME).spec /build/SPECS/$(NAME)-$(VERSION).spec 31 | sed -i 's|%define version unknown|%define version $(VERSION)|g' /build/SPECS/$(NAME)-$(VERSION).spec 32 | chown -R root:root /build 33 | rpmbuild -ba --define '_topdir /build'\ 34 | /build/SPECS/$(NAME)-$(VERSION).spec 35 | 36 | clean: 37 | rm -f $(BINARY) 38 | rm -f rpm-tmp.* 39 | 40 | .DEFAULT_GOAL: $(BINARY) 41 | 42 | include Makefile.git 43 | -------------------------------------------------------------------------------- /Makefile.git: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make 2 | 3 | submodules: 4 | git submodule init 5 | git submodule update 6 | 7 | submodule_update: 8 | git submodule update 9 | 10 | submodule_pull: 11 | git submodule foreach "git pull" 12 | 13 | submodule_check: 14 | @-test -d .git -a .gitmodules && \ 15 | git submodule status \ 16 | | grep -q "^-" \ 17 | && $(MAKE) submodules || true 18 | @-test -d .git -a .gitmodules && \ 19 | git submodule status \ 20 | | grep -q "^+" \ 21 | && $(MAKE) submodule_update || true 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ZaLua 2 | 3 | ## Цели: 4 | 5 | * легко расширяемый мониторинг написаный на скриптовом языке 6 | * использовать транспорт zabbix-agent 7 | * публиковать метрики для prometheus 8 | 9 | ## Решение: 10 | 11 | толстый go-бинарь с встроенным lua-интерпретатором 12 | 13 | ## Запуск и получение метрик 14 | 15 | При запуске проверяется что по сокету `/tmp/zalua-mon.sock` отвечает сервер на ping-pong сообщение, 16 | иначе запускается демон-сервер который начинает слушать этот сокет. 17 | демон пытает запустить `/etc/zalua/init.lua` dsl которого описан ниже. 18 | 19 | ### Доступные команды клиента: 20 | 21 | ``` 22 | -v, -version, --version 23 | Get version 24 | -e, --execute-file, execute file (without server) 25 | Execute dsl from file (for testing case) 26 | -k, -kill, --kill, kill 27 | Kill server 28 | -m, -metrics, --list-metrics, metrics 29 | List of known metrics 30 | -p, -plugins, --plugins, plugins 31 | List of running plugins 32 | -g, -get, --get, --get-metric, get 33 | Get metric value 34 | -ping, --ping, ping 35 | Ping pong game 36 | ``` 37 | 38 | ### Пример вывода: 39 | 40 | ``` 41 | $ zalua -p 42 | /etc/zalua/plugins/bad_plugin.lua false /etc/zalua/plugins/buddyinfo_plugin.lua at 1: parse error 43 | /etc/zalua/plugins/numa_plugin.lua false 44 | /etc/zalua/plugins/snmp_plugin.lua true 45 | 46 | 47 | $ zalua -m 48 | system.disk.read_bytes[/video] 23454385.65 1495748781 49 | system.disk.read_ops[/video] 46.46 1495748781 50 | system.disk.utilization[/video] 51.02 1495748781 51 | system.disk.write_bytes[/video] 6872556.41 1495748781 52 | system.disk.write_ops[/video] 25.72 1495748781 53 | runit.problem Found problem with runit services: 'nginx' has linked, but isn't running 1495748781 54 | 55 | $ zalua -g system.tcp.active 56 | 0.00 57 | 58 | $ zalua -g 'system.disk.write_ops point=/video' 59 | 25.72 60 | ``` 61 | 62 | ### Пример UserParameter: 63 | 64 | ``` 65 | UserParameter=disk.utilization[*], /usr/bin/zalua -g system.disk.$1.utilization 66 | ``` 67 | 68 | ## lua-DSL 69 | 70 | * *plugin*: 71 | * `p = plugin.new(filename)` загрузить плагин 72 | * `p:run()` запустить плагин 73 | * `p:stop()` остановить плагин вызвав ошибку 74 | * `p:is_running()` запущен или нет плагин 75 | * `p:error()` текст последний ошибки или nil 76 | * `p:was_stopped()` остановлен ли был плагин при помощи stop() 77 | 78 | * *metrics*: 79 | * `metrics.set(key, val, [, ||, ])` установить значение метрики key, val может быть string, number. ttl по дефолту 300 секунд 80 | * `metrics.set_speed(key, val, ...)` тоже самое, но считает скорость измерения 81 | * `metrics.set_counter_speed(key, val, ...)` тоже самое, но считает только положительную скорость измерения 82 | * `metrics.get(key, [])` получить значение метрики key 83 | * `metrics.list()` список `{key:"xxx", value:"xxx", at:xxx, tags: {"k":"v"}}` 84 | * `metrics.delete(key, [])` удалить значение метрики key 85 | 86 | * *prometheus*: 87 | * `prometheus.listen(str)` разместить метрики для prometheus на указанном адресе 88 | 89 | * *prometheus_gauge*: 90 | * `gauge = prometheus_gauge.new({help = "help..", name = "name..", "namespace" = "...", "subsystem" = "..."})` зарегистрировать gauge 91 | * `gauge:add(number)` 92 | * `gauge:set(number)` 93 | 94 | * *prometheus_gauge_labels*: 95 | * `gauge, err = prometheus_gauge_labels.new({..., labels = {"vec1", "vec2"}})` зарегистрировать vector gauge, перерегистрация может вызвать ошибку если векторы не совпадают 96 | * `gauge:add({vec1 = "value_vec_1", vec2 = "value_vec_2"}, number)` 97 | * `gauge:set(...)` 98 | 99 | * *prometheus_counter*: 100 | * `counter = prometheus_counter.new({help = "help..", name = "name..", "namespace" = "...", "subsystem" = "..."})` зарегистрировать counter 101 | * `counter:inc()` 102 | * `counter:add(number)` 103 | 104 | * *prometheus_counter_lables*: 105 | * `counter, err = prometheus_counter_lables.new({..., labels = {"vec1", "vec2"}})` зарегистрировать vector counter, перерегистрация может вызвать ошибку если векторы не совпадают 106 | * `counter:inc({vec1 = "value_vec_1", vec2 = "value_vec_2"})` 107 | * `counter:add(..., number)` 108 | 109 | * *postgres*: 110 | * `db, err = postgres.open({database="xxx", host="127.0.0.1", user="xxx", password="xxx"})` открыть коннект 111 | * `rows, err, column_count, row_count = db:query()` выполнить запрос 112 | * `db:close()` закрыть коннект 113 | 114 | * *tcp*: 115 | * `telnet, err = tcp.open("xxx:xxx")` открыть коннект 116 | * `err = telnet:write("xxxx")` записать в коннект 117 | * `telnet:close()` закрыть коннект 118 | 119 | * *tac*: 120 | * `scanner = tac.open("filepath")` открыть файл 121 | * `scanner:line()` получить последнюю линию (closure), в случае отсутвия таковой вернеться nil 122 | * `scanner:close()` закрыть файл 123 | 124 | * *ioutil*: 125 | * `ioutil.readfile(filename)` вернуть содержимое файла 126 | 127 | * *crypto*: 128 | * `crypto.md5(string)` вернуть md5 от строки 129 | 130 | * *cmd*: 131 | * `state, err = cmd.exec(string)` выполнить exec через shell, возвращает `{"code"=0, "stderr"="", "stdout"=""}` 132 | 133 | * *goruntime*: 134 | * `goruntime.goos` порт runtime.GOOS 135 | * `goruntime.goarch` порт runtime.GOARCH 136 | 137 | * *parser*: 138 | позволяет загрузить при помощи https://golang.org/pkg/plugin/ библиотеку с переменная которая вернет интерфейс: `type Parser interface { ProcessData(string) (map[string]string, error) }` 139 | * `p, err = parser.load(filename.so, variable_name="NewParser")` загрузить плагин с экспортированным именем `variable_name` в filename.so 140 | * `table, err = p:parse(str)` обработать строчку 141 | 142 | * *strings*: 143 | * `strings.split(str, delim)` порт golang strings.split() 144 | * `strings.has_prefix(str1, str2)` порт golang strings.hasprefix() 145 | * `strings.has_suffix(str1, str2)` порт golang strings.hassuffix() 146 | * `strings.trim(str1, str2)` порт golang strings.trim() 147 | 148 | * *json*: 149 | * `json.encode(N)` lua-table в string 150 | * `json.decode(N)` string в lua-table 151 | 152 | * *yaml*: 153 | * `yaml.decode(N)` string в lua-table 154 | 155 | * *filepath*: 156 | * `filepath.base(filename)` порт golang filepath.Base() 157 | * `filepath.dir(filename)` порт golang filepath.Dir() 158 | * `filepath.ext(filename)` порт golang filepath.Ext() 159 | * `filepath.glob(mask)` порт golang filepath.Glob(), в случае ошибки возращает nil. 160 | 161 | * *xmlpath*: 162 | * `table, err = xmlpath.parse(data, path)` возвращает таблицу с обработаной `data` по xmlpath `path` 163 | 164 | * *http*: 165 | * `result, err = http.get(url)` возвращает `result = {body, code}` и ошибку, захардкожен 10секундный таймаут. 166 | * `result, err = http.unescape(url)` порт url.QueryUnescape(query) 167 | * `result = http.escape(url)` порт url.QueryEscape(query) 168 | 169 | * *goos*: 170 | * `stat = goos.stat(filename)` goos.stat возвращает таблицу с полями `stat = {size, is_dir, mod_time}`, в случае ошибки возращает nil. 171 | * `goos.pagesize()` возвращет pagesize 172 | 173 | * *syscall*: 174 | * `stat, err = syscall.statfs("/mount/point")` возвращает `stat = {size, free, avail, files, files_free}` 175 | 176 | * *time*: 177 | * `time.sleep(N)` проспать N секунд 178 | * `time.unix()` время в секундах 179 | * `time.unix_nano()` время в наносекундах 180 | * `time.parse("2006-Jan-02", "2018-Mar-02")` golang порт time.Parse(), возвращает unixts и ошибку 181 | 182 | * *log*: 183 | * `log.error(msg)` сообщение в лог с уровнем error 184 | * `log.info(msg)` с уровнем info 185 | 186 | * *tls_util*: 187 | * `tls_util.cert_not_after("server_name", ["server_name:443" || "address:port"])` возвращает unixts и ошибку 188 | 189 | * *human*: 190 | * `human.time(int)` возвращает время в текстовом формате 191 | 192 | ## Примеры плагинов 193 | 194 | ### Diskstat 195 | 196 | ![await](/img/await.png) 197 | 198 | * пытается сопоставить блочному девайсу /mount/pount 199 | * рассчитывает await и utilization по тем блочным девайсам, по которым ядро не ведет статистику (mdraid) 200 | 201 | ### IO 202 | 203 | ![io](/img/io-syscall.png) 204 | 205 | * суммирует /proc/*pid*/io 206 | * рассчитывает эффективность чтения из vfs cache как соотношение логического и физического чтения rchar/read_bytes 207 | 208 | ### SNMP 209 | 210 | данные из /proc/net/snmp 211 | 212 | ![tcp connections](/img/tcp-speed.png) 213 | ![tcp error](/img/tcp-errors.png) 214 | 215 | ### CPU 216 | 217 | cpu time (не нормированное по кол-ву CPU) 218 | ![process state](/img/cpu-time.png) 219 | 220 | состояние процессов 221 | ![process state](/img/cpu-proc.png) 222 | 223 | interrupts и context switching 224 | ![process state](/img/cpu-intr.png) 225 | 226 | ### NUMA 227 | 228 | статистика выделения памяти и доступа к ней 229 | 230 | ![numa access](/img/numa-access.png) 231 | ![numa allocate](/img/numa-alloc.png) 232 | -------------------------------------------------------------------------------- /config/init.lua: -------------------------------------------------------------------------------- 1 | -- этот init-файл обеспечивает запуск всех плагинов в выше лежащей директории "/plugin/*_plugin.lua": 2 | -- * рестарт плагина в случае его модификации (md5) 3 | -- * стоп плагина если он был удален (runit-like) 4 | -- * запуск плагина если он появился в директории (runit-like) 5 | 6 | local current_file = debug.getinfo(1).source 7 | local plugins_dir = filepath.dir(current_file).."/".."plugins" 8 | local plugins = {} -- {filename= {plugin = p, md5 = md5}, ...} 9 | local try_plugins = {} -- {filename = count_of_try} 10 | 11 | -- удаление плагина 12 | function stop_and_delete_plugin(file) 13 | local metadata = plugins[file] 14 | if not(metadata == nil) then metadata["plugin"]:stop() end 15 | -- пересоздаем плагины, проще способа удалить по ключу не нашел 16 | local new_plugins = {} 17 | for old_file, old_metadata in pairs(plugins) do 18 | if not(old_file == file) then new_plugins[old_file] = old_metadata end 19 | end 20 | plugins = new_plugins 21 | end 22 | 23 | -- (пере)загрузка конкретного плагина 24 | function re_run_plugin_from_file(file) 25 | 26 | local metadata = plugins[file] 27 | local current_md5 = crypto.md5(ioutil.readfile(file)) 28 | 29 | -- старт плагина 30 | if metadata == nil then 31 | metadata = {} 32 | local p = plugin.new(file) 33 | metadata["plugin"] = p 34 | metadata["md5"] = current_md5 35 | p:run() 36 | plugins[file] = metadata 37 | return 38 | end 39 | 40 | -- если файл изменился - останавливаем старый и запускаем новый 41 | if not(metadata["md5"] == current_md5) then 42 | metadata["plugin"]:stop() 43 | local p = plugin.new(file) 44 | metadata["plugin"] = p 45 | metadata["md5"] = current_md5 46 | p:run() 47 | plugins[file] = metadata 48 | end 49 | 50 | end 51 | 52 | -- запуск и остановка плагинов 53 | function re_run_if_needed() 54 | 55 | local found_files = {} 56 | for file, _ in pairs(plugins) do found_files[file] = false end 57 | 58 | for _, file in pairs(filepath.glob(plugins_dir.."/*_plugin.lua")) do 59 | found_files[file] = true 60 | re_run_plugin_from_file(file) 61 | end 62 | 63 | -- нужно остановить те, что не найдены 64 | for file, found in pairs(found_files) do 65 | if not found then 66 | local metadata = plugins[file] 67 | stop_and_delete_plugin(file) 68 | end 69 | end 70 | 71 | end 72 | 73 | -- первоначальный запуск 74 | re_run_if_needed() 75 | 76 | -- супервизор для плагинов 77 | while true do 78 | time.sleep(5) 79 | local error_count = 0 80 | 81 | re_run_if_needed() 82 | -- проверка статусов всех плагинов 83 | for file, metadata in pairs(plugins) do 84 | local p = metadata["plugin"] 85 | if not p:is_running() then 86 | local err = p:error() 87 | if err then 88 | -- плагин не запущен, и завершился с ошибкой 89 | log.error(err) 90 | p:run() 91 | error_count = error_count + 1 92 | metrics.set("zalua.error.last", err) 93 | else 94 | -- плагин остановлен и не завершился с ошибкой 95 | -- попробуем его запустить позднее через перезапуск (после минуты) 96 | local try_count = try_plugins[file] 97 | if try_count == nil then try_count = 0 end 98 | -- отправляем на рестарт 99 | if try_count > 10 then 100 | stop_and_delete_plugin(file) 101 | try_count = 0 102 | end 103 | try_count = try_count + 1 104 | try_plugins[file] = try_count 105 | 106 | end 107 | end 108 | end 109 | metrics.set("zalua.error.count", error_count) 110 | 111 | end 112 | -------------------------------------------------------------------------------- /config/plugins/common/additional_prometheus_labels.example.lua: -------------------------------------------------------------------------------- 1 | local labels = {} 2 | labels.fqdn = strings.trim(ioutil.readfile("/proc/sys/kernel/hostname"), "\n") 3 | 4 | return labels 5 | -------------------------------------------------------------------------------- /config/plugins/common/prometheus_gauge.lua: -------------------------------------------------------------------------------- 1 | -- загружаем дополнительные labels из соседнего файла additional_prometheus_labels.lua 2 | -- если он присутствует 3 | local current_dir = filepath.dir(debug.getinfo(1).source) 4 | local additional_labels = {} 5 | if goos.stat(current_dir.."/additional_prometheus_labels.lua") then 6 | additional_labels = require "additional_prometheus_labels" 7 | end 8 | 9 | -- http://lua-users.org/wiki/SimpleLuaClasses 10 | local prom_gauge = {} 11 | prom_gauge.__index = prom_gauge 12 | 13 | function prom_gauge:new(options) 14 | local gauge = {} 15 | setmetatable(gauge, prom_gauge) 16 | gauge.options = options 17 | options.labels = options.labels or {} 18 | -- не забываем добавлять дополнительные labels 19 | for k, _ in pairs(additional_labels) do table.insert(options.labels, k) end 20 | gauge.additional_labels = additional_labels 21 | gauge.prometheus = prometheus_gauge_labels.new(options) 22 | return gauge 23 | end 24 | 25 | function prom_gauge:set(value, labels) 26 | -- немного удобства 27 | if not(type(value) == "number") then labels, value = value, labels end 28 | labels = labels or {} 29 | -- не забываем добавлять дополнительные labels 30 | local real_labels = labels 31 | for k,v in pairs(self.additional_labels) do real_labels[k] = v end 32 | self.prometheus:set(real_labels, value) 33 | end 34 | 35 | function prom_gauge:set_from_metrics(key, labels) 36 | -- немного удобства 37 | if not(type(key) == "string") then labels, key = key, labels end 38 | local value = metrics.get(key) -- nil or string 39 | if not value then return end 40 | -- установка значения 41 | local real_labels = labels or {} 42 | for k,v in pairs(self.additional_labels) do real_labels[k] = v end 43 | self.prometheus:set(real_labels, tonumber(value)) 44 | end 45 | 46 | return prom_gauge 47 | -------------------------------------------------------------------------------- /config/plugins/cpu_plugin.lua: -------------------------------------------------------------------------------- 1 | package.path = filepath.dir(debug.getinfo(1).source)..'/common/?.lua;'.. package.path 2 | guage = require "prometheus_gauge" 3 | 4 | -- обработка строки из /proc/stat 5 | function read_cpu_values(str) 6 | -- https://www.kernel.org/doc/Documentation/filesystems/proc.txt 7 | local fields = { "user", "nice", "system", "idle", "iowait", "irq", "softirq", "steal", "guest", "guest_nice" } 8 | local row, offset = {}, 1 9 | for value in str:gmatch("(%d+)") do 10 | row[fields[offset]] = tonumber(value) 11 | offset = offset + 1 12 | end 13 | return row 14 | end 15 | 16 | -- регистрируем prometheus метрики 17 | gauge_cpu_usage = guage:new({ 18 | help = "system cpu usage", 19 | namespace = "system", 20 | subsystem = "cpu", 21 | name = "usage", 22 | labels = { "cpu", "type" } 23 | }) 24 | 25 | -- главный loop 26 | while true do 27 | local cpu_count = 0 28 | for line in io.lines("/proc/stat") do 29 | 30 | -- считаем cpu_count 31 | local number = line:match("^cpu(%d+)%s+.*") 32 | if number then 33 | number = tonumber(number) + 1; if number > cpu_count then cpu_count = number end 34 | end 35 | 36 | -- разбираем строчку которая начинается с ^(cpu%d+) 37 | local cpu_number, cpu_number_line = line:match("^cpu(%d+) (.*)") 38 | if cpu_number_line then 39 | local cpu_number_values = read_cpu_values(cpu_number_line) 40 | for key, value in pairs(cpu_number_values) do 41 | local storage_key = "system.cpu."..cpu_number.."."..key 42 | metrics.set_counter_speed(storage_key, tonumber(value)) 43 | -- берем подсчитанный из кэша 44 | local value = metrics.get(storage_key) 45 | if value then gauge_cpu_usage:set(tonumber(value), {cpu = cpu_number, type = key}) end 46 | end 47 | end 48 | 49 | -- разбираем строчку которая начинается с ^(cpu ) 50 | local cpu_all_line = line:match("^cpu%s+(.*)") 51 | if cpu_all_line then 52 | local cpu_all_values = read_cpu_values(cpu_all_line) 53 | for key, value in pairs(cpu_all_values) do 54 | metrics.set_counter_speed("system.cpu.total."..key, value) 55 | end 56 | end 57 | 58 | -- вычисляем running, blocked 59 | local processes = line:match("^procs_(.*)") 60 | if processes then 61 | local key, val = string.match(processes, "^(%S+)%s+(%d+)") 62 | metrics.set("system.processes."..key, tonumber(val)) 63 | end 64 | 65 | -- вычисляем context switching 66 | local ctxt = line:match("^ctxt (%d+)") 67 | if ctxt then metrics.set_counter_speed("system.stat.ctxt", tonumber(ctxt)) end 68 | 69 | -- вычисляем processes 70 | local processes = line:match("^processes (%d+)") 71 | if processes then metrics.set_counter_speed("system.processes.fork_rate", tonumber(processes)) end 72 | 73 | -- вычисляем interupts 74 | local intr = line:match("^intr (%d+)") 75 | if intr then metrics.set_counter_speed("system.stat.intr", tonumber(intr)) end 76 | 77 | end 78 | metrics.set("system.cpu.count", cpu_count) 79 | time.sleep(60) 80 | end 81 | -------------------------------------------------------------------------------- /config/plugins/diskstat_plugin.lua: -------------------------------------------------------------------------------- 1 | package.path = filepath.dir(debug.getinfo(1).source)..'/common/?.lua;'.. package.path 2 | guage = require "prometheus_gauge" 3 | 4 | -- парсит содержимое /proc/diskstats 5 | function diskstat() 6 | local result = {} 7 | -- https://www.kernel.org/doc/Documentation/ABI/testing/procfs-diskstats 8 | local pattern = "(%d+)%s+(%d+)%s+(%S+)%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+)" 9 | for line in io.lines("/proc/diskstats") do 10 | local major, minor, dev_name, 11 | rd_ios, rd_merges_or_rd_sec, rd_sec_or_wr_ios, rd_ticks_or_wr_sec, 12 | wr_ios, wr_merges, wr_sec, wr_ticks, ios_pgr, tot_ticks, rq_ticks = line:match(pattern) 13 | result[dev_name] = { 14 | major = tonumber(major), minor = tonumber(minor), 15 | rd_ios = tonumber(rd_ios), rd_merges_or_rd_sec = tonumber(rd_merges_or_rd_sec), 16 | rd_sec_or_wr_ios = tonumber(rd_sec_or_wr_ios), rd_ticks_or_wr_sec = tonumber(rd_ticks_or_wr_sec), 17 | wr_ios = tonumber(wr_ios), wr_merges = tonumber(wr_merges), 18 | wr_sec = tonumber(wr_sec), wr_ticks = tonumber(wr_ticks), 19 | ios_pgr = tonumber(ios_pgr), tot_ticks = tonumber(tot_ticks), 20 | rq_ticks = tonumber(rq_ticks) 21 | } 22 | end 23 | return result 24 | end 25 | 26 | -- /dev/sda => mountpoint, /dev/mapper/vg0-lv_slashroot => / 27 | -- мы ищем только прямое совпадение! 28 | function get_mountpoint_from_mounts(full_dev_name) 29 | for line in io.lines("/proc/mounts") do 30 | local reg_full_dev_name = full_dev_name:gsub("%-", "%S") 31 | local mountpoint = line:match("^"..reg_full_dev_name.."%s+(%S+)%s+") 32 | if mountpoint then return mountpoint end 33 | end 34 | end 35 | 36 | -- sdXD => mountpoint 37 | function sd_mountpoint(sdX) 38 | -- нет задачи получить lvm, или md - мы это определяем ниже, 39 | -- тут мы ловим только напрямую подмонтированные 40 | return get_mountpoint_from_mounts("/dev/"..sdX) 41 | end 42 | 43 | -- dm-X => mountpoint 44 | function dm_mountpoint(dmX) 45 | local name = ioutil.readfile("/sys/block/"..dmX.."/dm/name"):gsub("^%s+", ""):gsub("%s+$", "") 46 | if not name then return nil end 47 | return get_mountpoint_from_mounts("/dev/mapper/"..name) 48 | end 49 | 50 | -- mdX => mountpoint 51 | function md_mountpoint(mdX) 52 | return get_mountpoint_from_mounts("/dev/"..mdX) 53 | end 54 | 55 | -- sd, md, dm => mountpoint 56 | function get_mountpoint_by_dev(dev) 57 | if dev:match("^sd") then return sd_mountpoint(dev) end 58 | if dev:match("^dm") then return dm_mountpoint(dev) end 59 | if dev:match("^md") then return md_mountpoint(dev) end 60 | end 61 | 62 | -- mdX => raid0, raid1, ... 63 | function md_level(mdX) 64 | local data = ioutil.readfile("/sys/block/"..mdX.."/md/level") 65 | if data then 66 | return data:gsub("%s+$", "") 67 | else 68 | return nil 69 | end 70 | end 71 | 72 | -- mdX => {sda = X, dm-0 = Y} 73 | function md_device_sizes(mdX) 74 | local result = {} 75 | for _, path in pairs(filepath.glob("/sys/block/"..mdX.."/slaves/*")) do 76 | local dev = path:match("/sys/block/"..mdX.."/slaves/(%S+)$") 77 | result[dev] = tonumber(ioutil.readfile(path.."/size")) 78 | end 79 | return result 80 | end 81 | 82 | -- вычисляем значения которые зависят от предыдущих значений 83 | calc_values = {} 84 | function calc_value(dev, values) 85 | if calc_values[dev] == nil then calc_values[dev] = {} end 86 | if calc_values[dev]["data"] == nil then calc_values[dev]["data"] = {} end 87 | -- проставляем при первом проходе previous 88 | if calc_values[dev]["data"]["previous"] == nil then calc_values[dev]["data"]["previous"] = values; return; end 89 | 90 | local previous, current = calc_values[dev]["data"]["previous"], values 91 | 92 | -- вычисляем await https://github.com/sysstat/sysstat/blob/v11.5.6/common.c#L816 93 | local ticks = ((current.rd_ticks_or_wr_sec - previous.rd_ticks_or_wr_sec) + (current.wr_ticks - previous.wr_ticks)) 94 | local io_sec = (current.rd_ios + current.wr_ios) - (previous.rd_ios + previous.wr_ios) 95 | if (io_sec > 0) and (ticks > 0) then calc_values[dev]["await"] = ticks / io_sec end 96 | if (io_sec == 0) or (ticks == 0) then calc_values[dev]["await"] = 0 end 97 | 98 | 99 | -- перетираем предыдущее значение 100 | calc_values[dev]["data"]["previous"] = values 101 | end 102 | 103 | -- регистрируем prometheus метрики 104 | gauge_disk_bytes = guage:new({ 105 | help = "system disk usage in bytes", 106 | namespace = "system", 107 | subsystem = "disk", 108 | name = "bytes", 109 | labels = { "mountpoint", "type" } 110 | }) 111 | 112 | gauge_disk_ops = guage:new({ 113 | help = "system disk usage in ops", 114 | namespace = "system", 115 | subsystem = "disk", 116 | name = "ops", 117 | labels = { "mountpoint", "type" } 118 | }) 119 | 120 | gauge_disk_utilization = guage:new({ 121 | help = "system disk utilization in percents", 122 | namespace = "system", 123 | subsystem = "disk", 124 | name = "utilization", 125 | labels = { "mountpoint" } 126 | }) 127 | 128 | gauge_disk_await = guage:new({ 129 | help = "system disk await in ms", 130 | namespace = "system", 131 | subsystem = "disk", 132 | name = "await", 133 | labels = { "mountpoint" } 134 | }) 135 | 136 | -- главный loop 137 | while true do 138 | 139 | local devices_info, all_stats = {}, {} 140 | for dev, values in pairs(diskstat()) do 141 | if dev:match("^sd") or dev:match("^md") or dev:match("^dm") then 142 | local mountpoint = get_mountpoint_by_dev(dev) 143 | -- запоминаем только те, по которым мы нашли mountpoint 144 | if mountpoint then devices_info[dev] = {}; devices_info[dev]["mountpoint"] = mountpoint; end 145 | -- all stats мы заполняем для всех устройств, так как будет некое шульмование с mdX 146 | all_stats[dev] = { 147 | utilization = values.tot_ticks / 10, 148 | read_bytes = values.rd_sec_or_wr_ios * 512, read_ops = values.rd_ios, 149 | write_bytes = values.wr_sec * 512, write_ops = values.wr_ios 150 | } 151 | calc_value(dev, values) 152 | end 153 | end 154 | 155 | local discovery = {} 156 | -- теперь пришло время отослать собранные данные 157 | for dev, info in pairs(devices_info) do 158 | 159 | local mountpoint = info["mountpoint"] 160 | local discovery_item = {}; discovery_item["{#MOUNTPOINT}"] = mountpoint; table.insert(discovery, discovery_item) 161 | local utilization, await = 0, nil 162 | 163 | if dev:match("^sd") or dev:match("^dm") then 164 | utilization = all_stats[dev]["utilization"] 165 | await = calc_values[dev]["await"] 166 | end 167 | 168 | -- а вот с md пошло шульмование про utilization 169 | if dev:match("^md") then 170 | local slaves_info = md_device_sizes(dev) 171 | local total_slave_size = 0; for _, size in pairs(slaves_info) do total_slave_size = total_slave_size + size end 172 | local raid_level = md_level(dev) 173 | if raid_level then -- пропускаем непонятный raid 174 | -- для raid{0,1} просчитываем utilization с весом 175 | -- вес высчитывается = (размер slave) / (сумму размера slave-устройств) 176 | if (raid_level == "raid0") or (raid_level == "raid1") then 177 | for slave, size in pairs(slaves_info) do 178 | local weight = size / total_slave_size 179 | utilization = utilization + (all_stats[slave]["utilization"] * weight) 180 | local slave_await = calc_values[slave]["await"] 181 | if slave_await then 182 | if await == nil then await = 0 end 183 | await = await + (slave_await * weight) 184 | end 185 | end 186 | end 187 | end 188 | end 189 | 190 | metrics.set_counter_speed("system.disk.utilization["..mountpoint.."]", utilization) 191 | if await then metrics.set("system.disk.await["..mountpoint.."]", await) end 192 | 193 | -- остсылем все остальные метрики 194 | metrics.set_counter_speed("system.disk.read_bytes_in_sec["..mountpoint.."]", all_stats[dev]["read_bytes"]) 195 | metrics.set_counter_speed("system.disk.read_ops_in_sec["..mountpoint.."]", all_stats[dev]["read_ops"]) 196 | metrics.set_counter_speed("system.disk.write_bytes_in_sec["..mountpoint.."]", all_stats[dev]["write_bytes"]) 197 | metrics.set_counter_speed("system.disk.write_ops_in_sec["..mountpoint.."]", all_stats[dev]["write_ops"]) 198 | metrics.set_counter_speed("system.disk.all_ops_in_sec["..mountpoint.."]", all_stats[dev]["read_ops"] + all_stats[dev]["write_ops"]) 199 | 200 | -- prometheus 201 | gauge_disk_utilization:set_from_metrics("system.disk.utilization["..mountpoint.."]", {mountpoint = mountpoint}) 202 | gauge_disk_await:set_from_metrics("system.disk.await["..mountpoint.."]", {mountpoint = mountpoint}) 203 | gauge_disk_bytes:set_from_metrics("system.disk.read_bytes_in_sec["..mountpoint.."]", {mountpoint = mountpoint, type = "read"}) 204 | gauge_disk_bytes:set_from_metrics("system.disk.write_bytes_in_sec["..mountpoint.."]", {mountpoint = mountpoint, type = "write"}) 205 | gauge_disk_ops:set_from_metrics("system.disk.read_ops_in_sec["..mountpoint.."]", {mountpoint = mountpoint, type = "read"}) 206 | gauge_disk_ops:set_from_metrics("system.disk.write_ops_in_sec["..mountpoint.."]", {mountpoint = mountpoint, type = "write"}) 207 | 208 | end 209 | 210 | metrics.set("system.disk.discovery", json.encode({data = discovery})) 211 | time.sleep(60) 212 | end 213 | -------------------------------------------------------------------------------- /config/plugins/io_plugin.lua: -------------------------------------------------------------------------------- 1 | -- необходимы права 4777 на исполняемый go-файл 2 | 3 | function read_proc_io() 4 | local row = {} 5 | for _,file in ipairs(filepath.glob("/proc/*/io")) do 6 | local data = ioutil.readfile(file) 7 | if data then 8 | for _, line in pairs(strings.split(data, "\n")) do 9 | local key, val = line:match("^(%S+):%s+(%d+)$") 10 | if key and val then 11 | if not row[key] then row[key] = 0 end 12 | row[key] = row[key] + tonumber(val) 13 | end 14 | end 15 | end 16 | end 17 | return row 18 | end 19 | 20 | -- главный loop 21 | while true do 22 | 23 | -- сбираем статистику за snapshot_timeout 24 | local snapshot_timeout = 10 25 | local row1 = read_proc_io() 26 | time.sleep(snapshot_timeout) 27 | local row2 = read_proc_io() 28 | 29 | local logical_read = row2["rchar"] - row1["rchar"] 30 | local physical_read = row2["read_bytes"] - row1["read_bytes"] 31 | if (logical_read > 0) and (physical_read > 0) then 32 | metrics.set("system.io.read_hit", logical_read/physical_read) 33 | else 34 | metrics.set("system.io.read_hit", 0) 35 | end 36 | 37 | metrics.set_counter_speed("system.io.syscr", row1["syscr"]) 38 | metrics.set_counter_speed("system.io.syscw", row1["syscw"]) 39 | 40 | time.sleep(60-snapshot_timeout) 41 | end 42 | -------------------------------------------------------------------------------- /config/plugins/loadavg_plugin.lua: -------------------------------------------------------------------------------- 1 | package.path = filepath.dir(debug.getinfo(1).source)..'/common/?.lua;'.. package.path 2 | guage = require "prometheus_gauge" 3 | 4 | -- регистрируем prometheus метрики 5 | la1 = guage:new({ 6 | help = "system loadavg 1m", 7 | namespace = "system", 8 | subsystem = "cpu", 9 | name = "la_1m" 10 | }) 11 | 12 | la5 = guage:new({ 13 | help = "system loadavg 5m", 14 | namespace = "system", 15 | subsystem = "cpu", 16 | name = "la_5m" 17 | }) 18 | 19 | la15 = guage:new({ 20 | help = "system loadavg 15m", 21 | namespace = "system", 22 | subsystem = "cpu", 23 | name = "la_15m" 24 | }) 25 | 26 | while true do 27 | local line = strings.trim(ioutil.readfile("/proc/loadavg"), "\n") 28 | local data = strings.split(line, " ") 29 | la1:set(tonumber(data[1])) 30 | la5:set(tonumber(data[2])) 31 | la15:set(tonumber(data[3])) 32 | time.sleep(60) 33 | end 34 | -------------------------------------------------------------------------------- /config/plugins/memory_plugin.lua: -------------------------------------------------------------------------------- 1 | package.path = filepath.dir(debug.getinfo(1).source)..'/common/?.lua;'.. package.path 2 | guage = require "prometheus_gauge" 3 | 4 | function process(file) 5 | local result = {} 6 | for line in io.lines(file) do 7 | -- MemTotal: 8060536 kB 8 | local key, value = line:match("(%S+)%:%s+%d+%s+kB"), line:match("%S%:%s+(%d+)%s+kB") 9 | if (not(key == nil)) and (not(value == nil)) then result[key] = tonumber(value*1024) end 10 | end 11 | return result 12 | end 13 | 14 | -- регистрируем prometheus метрики 15 | gauge_memory = guage:new({ 16 | help = "system memory discovery", 17 | namespace = "system", 18 | subsystem = "memory", 19 | name = "bytes", 20 | labels = {"type"} 21 | }) 22 | 23 | 24 | -- основной loop 25 | while true do 26 | local row = process("/proc/meminfo") 27 | local total, free, cached, shared, buffers = 0, 0, 0, 0, 0 28 | for key, val in pairs(row) do 29 | if key == "MemFree" then 30 | free = val 31 | elseif key == "MemTotal" then 32 | total = val 33 | elseif key == "MemShared" then 34 | shared = val 35 | elseif key == "Buffers" then 36 | buffers = val 37 | elseif key == "Cached" then 38 | cached = val 39 | end 40 | gauge_memory:set({type = key}, val) 41 | end 42 | metrics.set("system.memory.free", tostring(free)) 43 | metrics.set("system.memory.cached", tostring(cached)) 44 | metrics.set("system.memory.shared", tostring(shared)) 45 | metrics.set("system.memory.buffers", tostring(buffers)) 46 | metrics.set("system.memory.other", tostring(total-free-cached)) 47 | time.sleep(60) 48 | end 49 | -------------------------------------------------------------------------------- /config/plugins/messages_plugin.lua: -------------------------------------------------------------------------------- 1 | if not goos.stat('/var/log/messages') then return end 2 | 3 | local syslog_layout = "Jan 2 15:04:05 2006" 4 | 5 | while true do 6 | 7 | local min_time = os.time()-(24*60*60) 8 | local count_oom, count_segfault, count_runit = 0, 0, 0 9 | 10 | local scanner = tac.open("/var/log/messages") 11 | while true do 12 | local line = scanner:line() 13 | if line == nil then break end 14 | -- отрезаем первые символы в которых находиться время и прибавляем год 15 | local time_value = line:sub(0, 15) 16 | time_value = time_value .. " ".. os.date("%Y") 17 | -- пытаемся распарсить 18 | local log_time, err = time.parse(syslog_layout, time_value) 19 | -- выходим, если распаршенное время меньше min_time 20 | if err == nil then if log_time < min_time then break end end 21 | if string.find(line, "Out of memory: Kill process %d+ (%S+)") then count_oom = count_oom + 1 end 22 | if string.find(line, "kernel: (%S+)%[%d+%]: segfault at ") then count_segfault = count_segfault + 1 end 23 | if string.find(line, "runit: service '%S+' exit code: '%d+'") then count_runit = count_runit + 1 end 24 | end 25 | scanner:close() 26 | 27 | local messages = "ok" 28 | if count_oom + count_segfault + count_runit > 0 then 29 | messages = "Найдено проблем с OOM: " .. count_oom .. ", проблем с segfault: ".. count_segfault ..", проблем с падением сервисов runit: ".. count_runit .." за последние 24 часов." 30 | end 31 | metrics.set("system.messages.problem", messages, 10*60) 32 | 33 | 34 | time.sleep(5 * 60) 35 | end 36 | -------------------------------------------------------------------------------- /config/plugins/mountpoints_plugin.lua: -------------------------------------------------------------------------------- 1 | package.path = filepath.dir(debug.getinfo(1).source)..'/common/?.lua;'.. package.path 2 | guage = require "prometheus_gauge" 3 | 4 | local ignore_fs = "^(autofs|binfmt_misc|bpf|cgroup2?|configfs|debugfs|devpts|devtmpfs|fusectl|hugetlbfs|mqueue|nsfs|overlay|proc|procfs|pstore|rpc_pipefs|securityfs|selinuxfs|squashfs|sysfs|tracefs)$" 5 | local ignore_mountpoints = "^/(dev|proc|sys|var/lib/docker/.+)($|/)" 6 | 7 | -- регистрируем prometheus метрики 8 | bytes_free = guage:new({ 9 | help = "system filesystem percent free bytes", 10 | namespace = "system", 11 | subsystem = "disk", 12 | name = "percent_bytes_free", 13 | labels = { "device", "mountpoint", "fs" } 14 | }) 15 | 16 | inodes_free = guage:new({ 17 | help = "system filesystem percent free inodes", 18 | namespace = "system", 19 | subsystem = "disk", 20 | name = "percent_inodes_free", 21 | labels = { "device", "mountpoint", "fs" } 22 | }) 23 | 24 | total_expose = guage:new({ 25 | help = "system filesystem expose", 26 | namespace = "system", 27 | subsystem = "disk", 28 | name = "expose", 29 | labels = { "type", "device", "mountpoint", "fs" } 30 | }) 31 | 32 | -- главный loop 33 | while true do 34 | for line in io.lines("/proc/mounts") do 35 | 36 | local data = strings.split(line, " ") 37 | local device, mountpoint, fs = data[1], data[2], data[3] 38 | 39 | if not regexp.match(fs, ignore_fs) then 40 | if not regexp.match(mountpoint, ignore_mountpoints) then 41 | local info, err = syscall.statfs(mountpoint) 42 | if err then 43 | log.error("get syscall.statfs for "..mountpoint.." error: "..err) 44 | else 45 | if not (info["size"] == 0) then 46 | local free = 100 - (info["free"]/info["size"])*100 47 | bytes_free:set(free, {device=device, mountpoint=mountpoint, fs=fs}) 48 | end 49 | if not (info["files"] == 0 ) then 50 | local free = 100 - (info["files_free"]/info["files"])*100 51 | inodes_free:set(free, {device=device, mountpoint=mountpoint, fs=fs}) 52 | end 53 | for k, v in pairs(info) do 54 | total_expose:set(v, {type=k, device=device, mountpoint=mountpoint, fs=fs}) 55 | end 56 | end 57 | end 58 | end 59 | 60 | end 61 | time.sleep(60) 62 | end 63 | -------------------------------------------------------------------------------- /config/plugins/net_dev_plugin.lua: -------------------------------------------------------------------------------- 1 | package.path = filepath.dir(debug.getinfo(1).source)..'/common/?.lua;'.. package.path 2 | guage = require "prometheus_gauge" 3 | 4 | directions = {"rx", "tx"} 5 | types = {"bytes", "packets", "errs", "drop", "fifo", "frame", "compressed", "multicast"} 6 | proc_net_fields = {} 7 | for _, direction in pairs(directions) do 8 | for _, t in pairs(types) do 9 | table.insert(proc_net_fields, direction.."."..t) 10 | end 11 | end 12 | 13 | -- регистрируем prometheus метрики 14 | gauge_net = guage:new({ 15 | help = "system net info", 16 | namespace = "system", 17 | subsystem = "net", 18 | name = "info", 19 | labels = { "direction", "interface", "type" } 20 | }) 21 | 22 | -- обработка строки из /proc/net/dev без ethX: 23 | function proc_net_field_value(str) 24 | local row, offset = {}, 1 25 | for value in str:gmatch("(%d+)") do 26 | row[proc_net_fields[offset]] = tonumber(value) 27 | offset = offset + 1 28 | end 29 | return row 30 | end 31 | 32 | -- основной loop 33 | while true do 34 | local discovery = {} 35 | for line in io.lines("/proc/net/dev") do 36 | local interface, row = line:match("(%S+):%s+(.+)$") 37 | if not (interface == nil) and (interface:match("^vlan") or interface:match("^bond") or interface:match("^eth")) then 38 | local discovery_item = {}; discovery_item["{#DEV}"] = interface; table.insert(discovery, discovery_item) 39 | for _, direction in pairs(directions) do 40 | for _, t in pairs(types) do 41 | local key = direction.."."..t 42 | local value = proc_net_field_value(row) 43 | metrics.set_speed("system.net."..key.."["..interface.."]", value[key]) 44 | gauge_net:set_from_metrics("system.net."..key.."["..interface.."]", {direction = direction, type = t, interface = interface}) 45 | end 46 | end 47 | end 48 | end 49 | metrics.set("system.net.discovery", json.encode({data = discovery})) 50 | time.sleep(60) 51 | end 52 | -------------------------------------------------------------------------------- /config/plugins/net_netstat_plugin.lua: -------------------------------------------------------------------------------- 1 | package.path = filepath.dir(debug.getinfo(1).source)..'/common/?.lua;'.. package.path 2 | guage = require "prometheus_gauge" 3 | 4 | -- регистрируем prometheus метрики 5 | netstat = guage:new({ 6 | help="system tcp netstat", namespace="system", subsystem="tcp", 7 | name="netstat", labels={"type"} 8 | }) 9 | 10 | -- главный loop 11 | while true do 12 | 13 | local result, tcp_keys, tcp_vals = {}, {}, {} 14 | for line in io.lines("/proc/net/netstat") do 15 | if string.match(line, "TcpExt:") then 16 | if string.match(line, "TcpExt%:%s+%d") then tcp_vals = strings.split(line, " ") else tcp_keys = strings.split(line, " ") end 17 | end 18 | end 19 | 20 | for i, k in pairs(tcp_keys) do result[k] = tonumber(tcp_vals[i]) end 21 | for k, v in pairs(result) do 22 | if not(k == "TcpExt:") then 23 | local zabbix_key = "system.tcp.netstat["..k.."]" 24 | metrics.set_counter_speed(zabbix_key, v) 25 | netstat:set_from_metrics(zabbix_key, {type=k}) 26 | end 27 | end 28 | 29 | time.sleep(60) 30 | end 31 | -------------------------------------------------------------------------------- /config/plugins/net_tcp_plugin.lua: -------------------------------------------------------------------------------- 1 | package.path = filepath.dir(debug.getinfo(1).source)..'/common/?.lua;'.. package.path 2 | guage = require "prometheus_gauge" 3 | 4 | function read_ss() 5 | local state, err = cmd.exec("/usr/sbin/ss --summary") 6 | if err == nil then 7 | -- команда не завершилась с ошибкой 8 | if not(state == nil) then 9 | -- есть stdout/stderr 10 | local result = {} 11 | for _, line in pairs(strings.split(state.stdout, "\n")) do 12 | if line:match("^TCP: ") then 13 | result["established"] = tonumber(line:match("estab (%d+),")) 14 | result["closed"] = tonumber(line:match("closed (%d+),")) 15 | result["orphaned"] = tonumber(line:match("orphaned (%d+),")) 16 | result["synrecv"] = tonumber(line:match("synrecv (%d+),")) 17 | local tw1, tw2 = line:match("timewait (%d+)/(%d+)") 18 | result["timewait"] = tonumber(tw1) + tonumber(tw2) 19 | return result 20 | end 21 | end 22 | end 23 | end 24 | end 25 | 26 | tcp_state = guage:new({ name = "system_tcp_state", labels = {"type"} }) 27 | 28 | -- главный loop 29 | while true do 30 | local result = read_ss() 31 | if result then for k, v in pairs(result) do tcp_state:set({type=k}, v) end end 32 | time.sleep(60) 33 | end 34 | -------------------------------------------------------------------------------- /config/plugins/ntp_plugin.lua: -------------------------------------------------------------------------------- 1 | if not(goruntime.goos == "linux") then return end 2 | 3 | while true do 4 | 5 | local state, err = cmd.exec("ntpstat") 6 | if not(err == nil) then 7 | -- команда завершилась с ошибкой 8 | if not(state == nil) then 9 | -- есть stdout/stderr 10 | local msg = strings.split(state.stdout, "\n")[1] 11 | metrics.set("ntp.stat", "состояние синхронизации: "..tostring(msg)) 12 | else 13 | -- state == nil означает что команда не найдена 14 | metrics.set("ntp.stat", "команда ntpstat не найдена") 15 | return 16 | end 17 | else 18 | -- команда выполнилась без ошибки 19 | metrics.set("ntp.stat", "ok") 20 | end 21 | 22 | time.sleep(60) 23 | end 24 | -------------------------------------------------------------------------------- /config/plugins/numa_plugin.lua: -------------------------------------------------------------------------------- 1 | if not goos.stat('/sys/devices/system/node/node1/numastat') then return end 2 | 3 | -- основной loop 4 | while true do 5 | 6 | local numastats = {total={}} 7 | 8 | local paths = filepath.glob('/sys/devices/system/node/node*/numastat') 9 | for _, path in pairs(paths) do 10 | local node_num = path:match('/node(%d+)/') 11 | numastats[node_num] = {} 12 | for line in io.lines(path) do 13 | local key, val = line:match('(%S+)%s+(%d+)') 14 | numastats[node_num][key] = tonumber(val) 15 | if numastats.total[key] == nil then numastats.total[key] = 0 end 16 | numastats.total[key] = numastats.total[key] + val 17 | end 18 | end 19 | 20 | local discovery = {} 21 | for num, stats in pairs(numastats) do 22 | local discovery_item = {}; discovery_item["{#NUMA_NODE}"] = num; table.insert(discovery, discovery_item) 23 | metrics.set_counter_speed('system.numa.numa_hit['..num..']', stats['numa_hit']) -- A process wanted to allocate memory from this node and succeeded. 24 | metrics.set_counter_speed('system.numa.numa_miss['..num..']', stats['numa_miss']) -- A process wanted to allocate memory from another node but ended up with memory from this node 25 | metrics.set_counter_speed('system.numa.numa_foreign['..num..']', stats['numa_foreign']) -- A process wanted to allocate memory from this node but ended up with memory from another node 26 | metrics.set_counter_speed('system.numa.interleave_hit['..num..']', stats['interleave_hit']) -- Interleaving wanted to allocate memory from this node and succeeded 27 | metrics.set_counter_speed('system.numa.local_node['..num..']', stats['local_node']) -- A process ran on this node and got memory from it 28 | metrics.set_counter_speed('system.numa.other_node['..num..']', stats['other_node']) -- A process ran on this node and got memory from another node 29 | end 30 | metrics.set("system.numa.discovery", json.encode({data = discovery})) 31 | 32 | time.sleep(60) 33 | end 34 | -------------------------------------------------------------------------------- /config/plugins/other/loadavg.lua: -------------------------------------------------------------------------------- 1 | function loadavg() 2 | local pattern = "(%d*.%d+)%s+(%d*.%d+)%s+(%d*.%d+)%s+(%d+)/(%d+)%s+(%d+)" 3 | 4 | local file = io.open("/proc/loadavg") 5 | local content = file:read("*a") 6 | file:close() 7 | 8 | local minute1avg, minute5avg, minute15avg, runnable, exist, lastpid = string.match(content, pattern) 9 | return { minute1avg = tonumber(minute1avg), minute5avg = tonumber(minute5avg), 10 | minute15avg = tonumber(minute15avg), runnable = tonumber(runnable), 11 | exist = tonumber(exist), lastpid = tonumber(lastpid) } 12 | end 13 | 14 | while true do 15 | local avg = loadavg() 16 | metrics.set("system.processes.total", avg.exist) 17 | -- metrics.set("system.la.1", avg.minute1avg) 18 | -- metrics.set("system.la.5", avg.minute5avg) 19 | -- metrics.set("system.la.15", avg.minute15avg) 20 | metrics.set_counter_speed("system.fork_rate", avg.lastpid) 21 | time.sleep(60) 22 | end 23 | -------------------------------------------------------------------------------- /config/plugins/other/net.lua: -------------------------------------------------------------------------------- 1 | proc_net_fields = { 2 | "rx.bytes", "rx.packets", "rx.errs", "rx.drop", "rx.fifo", "rx.frame", "rx.compressed", "rx.multicast", 3 | "tx.bytes", "tx.packets", "tx.errs", "tx.drop", "tx.fifo", "tx.frame", "tx.compressed", "tx.multicast", 4 | } 5 | 6 | -- обработка строки из /proc/net/dev без ethX: 7 | function proc_net_field_values(str) 8 | local row, offset = {}, 1 9 | for value in str:gmatch("(%d+)") do 10 | row[proc_net_fields[offset]] = tonumber(value) 11 | offset = offset + 1 12 | end 13 | return row 14 | end 15 | 16 | -- основной loop 17 | while true do 18 | local discovery = {} 19 | for line in io.lines("/proc/net/dev") do 20 | local interface, row = line:match("(%S+):%s+(.+)$") 21 | if not (interface == nil) and (interface:match("^vlan") or interface:match("^bond") or interface:match("^eth")) then 22 | local discovery_item = {}; discovery_item["{#DEV}"] = interface; table.insert(discovery, discovery_item) 23 | for _, key in pairs(proc_net_fields) do 24 | local values = proc_net_field_values(row) 25 | metrics.set_speed("system.net."..key.."["..interface.."]", values[key]) 26 | end 27 | end 28 | end 29 | metrics.set("system.net.discovery", json.encode({data = discovery})) 30 | time.sleep(60) 31 | end 32 | -------------------------------------------------------------------------------- /config/plugins/pgbouncer_plugin.lua: -------------------------------------------------------------------------------- 1 | local enabled = false 2 | if goos.stat('/etc/service/pgbouncer-6432') then enabled = true end 3 | if not enabled then return end 4 | 5 | local connection = { 6 | host = '127.0.0.1', 7 | port = 6432, 8 | user = 'postgres', 9 | database = 'pgbouncer', 10 | } 11 | local pgbouncer_db, err = postgres.open(connection) 12 | if err then error(err) end 13 | 14 | while true do 15 | 16 | local rows, err = pgbouncer_db:query("show stats") 17 | if err then pgbouncer_db:close(); error(err) end 18 | local total_query_count, total_received, total_sent, total_query_time, total_wait_time = 0, 0, 0, 0, 0 19 | for _, row in pairs(rows) do 20 | total_query_count = total_query_count + tonumber(row[3]) 21 | total_received = total_received + tonumber(row[4]) 22 | total_sent = total_sent + tonumber(row[5]) 23 | total_query_time = total_query_time + tonumber(row[7]) 24 | total_wait_time = total_wait_time + tonumber(row[8]) 25 | end 26 | metrics.set_counter_speed('pgbouncer.query.count', total_query_count) 27 | metrics.set_counter_speed('pgbouncer.query.total_time', total_query_time/1000000) 28 | metrics.set_counter_speed('pgbouncer.query.wait_time', total_wait_time/1000000) 29 | metrics.set_counter_speed('pgbouncer.sent', total_sent) 30 | metrics.set_counter_speed('pgbouncer.received', total_received) 31 | 32 | local rows, err = pgbouncer_db:query("show clients") 33 | if err then pgbouncer_db:close(); error(err) end 34 | local client_counts = 0 35 | for _, row in pairs(rows) do client_counts = client_counts + 1 end 36 | metrics.set('pgbouncer.clients.count', client_counts) 37 | 38 | time.sleep(60) 39 | end 40 | -------------------------------------------------------------------------------- /config/plugins/postgres_plugin.lua: -------------------------------------------------------------------------------- 1 | -- немного тупого "auto-discovery" для включения плагина 2 | local enabled = false 3 | if goos.stat('/var/lib/postgresql') then enabled = true end 4 | if goos.stat('/var/lib/pgsql') then enabled = true end 5 | if not enabled then return end 6 | 7 | -- для работы данного плагина необходимо 8 | -- 1. создать пользователя root `create user root with superuser;` 9 | -- 2. дать разрешение в pg_hba: `local all root peer` не надо притворяться что unix-root не имеет полный доступ в базу 10 | local connection = { 11 | host = '/tmp', 12 | user = 'root', 13 | database = 'postgres' 14 | } 15 | if goos.stat('/var/run/postgresql/.s.PGSQL.5432') then connection.host = '/var/run/postgresql' end 16 | 17 | local previous_values = {} 18 | 19 | -- открываем "главный" коннект 20 | local main_db, err = postgres.open(connection) 21 | if err then error(err) end 22 | 23 | -- устанавливаем лимит на выполнение любого запроса 10s 24 | local _, err = main_db:query("set statement_timeout to '10s'") 25 | if err then main_db:close(); error(err) end 26 | 27 | -- пробуем активировать расширение pg_stat_statements 28 | local use_pg_stat_statements = false 29 | local rows, err = main_db:query("select count(*) from pg_catalog.pg_extension where extname = 'pg_stat_statements'") 30 | if not err then 31 | if rows[1][1] == 1 then 32 | use_pg_stat_statements = true 33 | else 34 | _, err = main_db:query("create extension pg_stat_statements;") 35 | if err then 36 | log.error("enable `pg_stat_statements`: "..tostring(err)) 37 | else 38 | use_pg_stat_statements = true 39 | end 40 | end 41 | end 42 | 43 | while true do 44 | local discovery = {} 45 | 46 | -- in recovery (is slave)? 47 | local rows, err = main_db:query("select pg_catalog.pg_is_in_recovery() as pg_is_in_recovery") 48 | if err then main_db:close(); error(err) end 49 | local pg_is_in_recovery = rows[1][1] 50 | 51 | if pg_is_in_recovery then 52 | -- is slave 53 | local rows, err = main_db:query("select extract(epoch from pg_last_xact_replay_timestamp())") 54 | if err then main_db:close(); error(err) end 55 | -- брать во внимание что pg_last_xact_replay_timestamp это значение из времени мастера 56 | local replication_lag = time.unix() - rows[1][1] -- возможно clock_timestamp() 57 | metrics.set('postgres.wal.last_apply', replication_lag) 58 | else 59 | -- is master 60 | local _, err = main_db:query("select txid_current()") 61 | if err then main_db:close(); error(err) end 62 | metrics.set('postgres.wal.last_apply', 0) 63 | local rows, err = main_db:query("select pg_catalog.pg_xlog_location_diff (pg_catalog.pg_current_xlog_location(),'0/00000000')") 64 | if err then main_db:close(); error(err) end 65 | metrics.set_counter_speed('postgres.wal.write_bytes_in_sec', rows[1][1]) 66 | end 67 | 68 | -- если активно pg_stat_statements 69 | if use_pg_stat_statements then 70 | local rows, err = main_db:query("select sum(total_time) as times, sum(calls) as calls, \ 71 | sum(blk_read_time) as disk_read_time, sum(blk_write_time) as disk_write_time, sum(total_time - blk_read_time - blk_write_time) as other_time \ 72 | from public.pg_stat_statements;") 73 | if not err then 74 | local current_times, current_calls, current_time = rows[1][1], rows[1][2], time.unix() 75 | metrics.set_counter_speed('postgres.time.disk_read_ms', rows[1][3]) 76 | metrics.set_counter_speed('postgres.time.disk_write_ms', rows[1][4]) 77 | metrics.set_counter_speed('postgres.time.other_ms', rows[1][5]) 78 | local prev_times, prev_calls, prev_time = previous_values['total_time'], previous_values['calls'], previous_values['time'] 79 | if prev_times then 80 | local diff_times, diff_calls, diff_time = (current_times - prev_times), (current_calls - prev_calls), (current_time - prev_time) 81 | if (diff_times > 0) and (diff_calls > 0) and (diff_time > 0) then 82 | metrics.set('postgres.queries.count', diff_calls/diff_time) 83 | metrics.set('postgres.queries.avg_time_ms', diff_times/diff_calls) 84 | end 85 | end 86 | previous_values['total_time'], previous_values['calls'], previous_values['time'] = current_times, current_calls, current_time 87 | end 88 | end 89 | 90 | -- < 9.6 только! (количество файлов pg_xlog) 91 | local rows, err = main_db:query("select count(*) from pg_catalog.pg_ls_dir('pg_xlog')") 92 | if not err then metrics.set('postgres.wal.count', rows[1][1]) end 93 | 94 | -- для >= 9.6 только 95 | local rows, err = main_db:query("select count(*) from pg_catalog.pg_stat_activity where wait_event is not null") 96 | if not err then metrics.set('postgres.connections.waiting', rows[1][1]) end 97 | 98 | -- кол-во autovacuum воркеров 99 | local rows, err = main_db:query("select count(*) from pg_catalog.pg_stat_activity where \ 100 | query like '%autovacuum%' and state <> 'idle'") 101 | if not err then metrics.set('postgres.connections.autovacuum', rows[1][1]) end 102 | 103 | -- кол-во коннектов 104 | local rows, err = main_db:query("select state, count(*) from pg_catalog.pg_stat_activity group by state") 105 | if not err then 106 | for _, state in pairs({'active', 'idle', 'idle in transaction'}) do 107 | local state_value = 0 108 | -- если находим такой state в результатах, то присваеваем его 109 | for _, row in pairs(rows) do 110 | if (row[1] == state) then 111 | state_value = row[2] 112 | end 113 | end 114 | if state == 'idle in transaction' then state = 'idle_in_transaction' end 115 | metrics.set('postgres.connections.'..state, state_value) 116 | end 117 | end 118 | 119 | -- долго играющие запросы 120 | local long_queries_msg = "ok" 121 | local rows, err = main_db:query("select \ 122 | s.query, extract(epoch from now() - s.query_start)::int as age, s.state \ 123 | from pg_catalog.pg_stat_get_activity(null::integer) s \ 124 | where not(s.state = 'idle') \ 125 | and extract(epoch from now() - s.query_start)::int > 4500") 126 | if not err then 127 | local msg, count = "", 0 128 | for _, row in pairs(rows) do 129 | msg = msg .. " `" .. row[1] .. "` в состоянии `" .. tostring(row[3]) .. "` и запущен " .. tostring(row[2]) .. " секунд назад. " 130 | count = count + 1 131 | end 132 | if count > 0 then long_queries_msg = "Найдено ".. tostring(count) .. " запрос(ов) которые запущены давно: ".. msg end 133 | end 134 | metrics.set('postgres.queries.long', long_queries_msg) 135 | 136 | -- кол-во локов 137 | local rows, err = main_db:query("select lower(mode), count(mode) FROM pg_catalog.pg_locks group by 1") 138 | if not err then 139 | for _, lock in pairs({'accessshare', 'rowshare', 'rowexclusive', 'shareupdateexclusive', 'share', 'sharerowexclusive', 'exclusive', 'accessexclusive'}) do 140 | local lock_full_name, lock_value = lock..'lock', 0 141 | for _, row in pairs(rows) do 142 | if (row[1] == lock_full_name) then 143 | lock_value = row[2] 144 | end 145 | end 146 | metrics.set('postgres.locks.'..lock, lock_value) 147 | end 148 | end 149 | 150 | -- хит по блокам и статистика по строкам 151 | local rows, err = main_db:query("select sum(blks_hit) as blks_hit, sum(blks_read) as blks_read, \ 152 | sum(tup_returned) as tup_returned, sum(tup_fetched) as tup_fetched, sum(tup_inserted) as tup_inserted, \ 153 | sum(tup_updated) as tup_updated, sum(tup_deleted) as tup_deleted \ 154 | from pg_catalog.pg_stat_database") 155 | if not err then 156 | local current_hit, current_read = rows[1][1], rows[1][2] 157 | local prev_hit, prev_read = previous_values['blks_hit'], previous_values['blks_read'] 158 | if prev_hit then 159 | local hit_rate, diff_hit, diff_read = 100, (current_hit - prev_hit), (current_read - prev_read) 160 | if (diff_hit > 0) and (diff_read > 0) then 161 | hit_rate = 100*diff_hit/(diff_read+diff_hit) 162 | end 163 | metrics.set('postgres.blks.hit_rate', hit_rate) 164 | end 165 | metrics.set_counter_speed('postgres.blks.hit', current_hit) 166 | metrics.set_counter_speed('postgres.blks.read', current_read) 167 | previous_values['blks_hit'], previous_values['blks_read'] = current_hit, current_read 168 | metrics.set_counter_speed('postgres.rows.returned', rows[1][3]) 169 | metrics.set_counter_speed('postgres.rows.fetched', rows[1][4]) 170 | metrics.set_counter_speed('postgres.rows.inserted', rows[1][5]) 171 | metrics.set_counter_speed('postgres.rows.updated', rows[1][6]) 172 | metrics.set_counter_speed('postgres.rows.deleted', rows[1][7]) 173 | end 174 | 175 | -- выполняем из главной базы общий запрос на размеры и статусы 176 | local rows, err = main_db:query("select \ 177 | datname, pg_catalog.pg_database_size(datname::text), pg_catalog.age(datfrozenxid) \ 178 | from pg_catalog.pg_database where datistemplate = false") 179 | if err then main_db:close(); error(err) end 180 | for _, row in pairs(rows) do 181 | local dbname, size, age = row[1], row[2], row[3] 182 | metrics.set('postgres.database.size['..dbname..']', size) 183 | metrics.set('postgres.database.age['..dbname..']', age) 184 | local discovery_item = {}; discovery_item["{#DATABASE}"] = dbname; table.insert(discovery, discovery_item) 185 | end 186 | metrics.set("postgres.database.discovery", json.encode({data = discovery})) 187 | time.sleep(60) 188 | end 189 | -------------------------------------------------------------------------------- /config/plugins/prometheus_plugin.lua: -------------------------------------------------------------------------------- 1 | -- бесконечно слушаем указанный порт 2 | prometheus.listen(":2345") 3 | 4 | -- в случае падения нас перезапустят 5 | while true do 6 | time.sleep(1) 7 | end 8 | -------------------------------------------------------------------------------- /config/plugins/runit_plugin.lua: -------------------------------------------------------------------------------- 1 | -- плагин который сообщает только одну метрику в zabbix 2 | -- все процессы запущены и работают или какие-то не запущены или "флапают" 3 | local services = {} 4 | 5 | -- главный loop 6 | while true do 7 | 8 | local problem = '' -- пытаемся сократить кол-во сообщений в zabbix 9 | 10 | -- если есть /etc/service 11 | if goos.stat('/etc/service') then 12 | for _, file in pairs(filepath.glob('/etc/service/*')) do 13 | 14 | local name = file:match('^/etc/service/(%S+)$') 15 | local run = (ioutil.readfile(file..'/supervise/stat') == "run\n") 16 | local uptime, stat = 0, goos.stat(file..'/supervise/pid') 17 | if stat then uptime = (time.unix() - stat.mod_time) end 18 | 19 | if run then 20 | if uptime < 60 then 21 | -- подозрительный сервис 22 | if services[name] and (services[name] < 60) then 23 | -- был до этого уже замечен, отмечаем как флапающий 24 | local desc = "'"..name.."' перезапускается" 25 | if problem == '' then problem = desc else problem = problem..', '..desc end 26 | end 27 | end 28 | else 29 | -- процесс слинкован, но не запущен и это уже плохо 30 | local desc = "'"..name.."' был слинкован в /etc/services/, но не запущен" 31 | if problem == '' then problem = desc else problem = problem..', '..desc end 32 | end 33 | services[name] = uptime 34 | 35 | end 36 | 37 | if problem == '' then problem = 'ok' else problem = 'Найдены проблемы с runit сервисами: '..problem end 38 | metrics.set('runit.problem', problem) 39 | else 40 | metrics.set('runit.problem', 'ok') 41 | end 42 | time.sleep(60) 43 | end 44 | -------------------------------------------------------------------------------- /config/plugins/snmp_plugin.lua: -------------------------------------------------------------------------------- 1 | if not goos.stat('/proc/net/snmp') then return end 2 | 3 | -- парсит tcp строку из /proc/net/snmp 4 | function parse_tcp(str) 5 | 6 | local tcp_fields = { 7 | 'RtoAlgorithm', 'RtoMin', 'RtoMax', 8 | 'MaxConn', 'ActiveOpens', 'PassiveOpens', 9 | 'AttemptFails', 'EstabResets', 'CurrEstab', 10 | 'InSegs', 'OutSegs', 'RetransSegs', 'InErrs', 11 | 'OutRsts', 'InCsumErrors'} 12 | 13 | local row, offset = {}, 1 14 | for value in str:gmatch("(%d+)") do 15 | row[tcp_fields[offset]] = tonumber(value) 16 | offset = offset + 1 17 | end 18 | 19 | return row 20 | 21 | end 22 | 23 | 24 | -- основной loop 25 | while true do 26 | 27 | for line in io.lines('/proc/net/snmp') do 28 | -- парсим tcp статистику 29 | local tcp_data_line = line:match('Tcp:%s+(%d+.+)$') 30 | if tcp_data_line then 31 | local row = parse_tcp(tcp_data_line) 32 | metrics.set_counter_speed('system.tcp.active', row['ActiveOpens']) -- The number of active TCP openings per second 33 | metrics.set_counter_speed('system.tcp.passive', row['PassiveOpens']) -- The number of passive TCP openings per second 34 | metrics.set_counter_speed('system.tcp.failed', row['AttemptFails']) -- The number of failed TCP connection attempts per second 35 | metrics.set_counter_speed('system.tcp.resets', row['EstabResets']) -- The number of TCP connection resets 36 | metrics.set_counter_speed('system.tcp.retransmit', row['RetransSegs']) 37 | metrics.set('system.tcp.established', row['CurrEstab']) -- The number of currently open connections 38 | end 39 | end 40 | 41 | time.sleep(60) 42 | end 43 | -------------------------------------------------------------------------------- /config/plugins/temperature_plugin.lua: -------------------------------------------------------------------------------- 1 | local enabled = false 2 | for _, file in ipairs(filepath.glob("/sys/bus/platform/devices/coretemp*/temp*_input")) do 3 | log.info("found coretemp file: "..file..", start plugin") 4 | enabled = true 5 | break 6 | end 7 | if not enabled then return end 8 | 9 | function get_max_temp() 10 | local data = {} 11 | for _, file in ipairs(filepath.glob("/sys/bus/platform/devices/coretemp*/temp*_input")) do 12 | local temp = tonumber(ioutil.readfile(file))/1000 13 | table.insert(data, temp) 14 | end 15 | table.sort(data) 16 | return data[#data] 17 | end 18 | 19 | function get_crit_temp() 20 | local data = {} 21 | for _, file in ipairs(filepath.glob("/sys/bus/platform/devices/coretemp*/temp*_crit")) do 22 | local temp = tonumber(ioutil.readfile(file))/1000 23 | table.insert(data, temp) 24 | end 25 | return data[#data] 26 | end 27 | 28 | -- главный loop 29 | while true do 30 | local maxt = get_max_temp() 31 | metrics.set("system.cpu.temp", maxt) 32 | local critt = get_crit_temp() 33 | metrics.set("system.cpu.temp_crit", critt) 34 | 35 | time.sleep(10) 36 | end 37 | -------------------------------------------------------------------------------- /config/zabbix.conf: -------------------------------------------------------------------------------- 1 | UserParameter=zalua.error.last,/opt/restream/zabbix/bin/zalua get zalua.error.last 2 | UserParameter=zalua.error.count,/opt/restream/zabbix/bin/zalua get zalua.error.count 3 | 4 | # snmp_plugin 5 | UserParameter=system.tcp.active,/opt/restream/zabbix/bin/zalua get system.tcp.active 6 | UserParameter=system.tcp.passive,/opt/restream/zabbix/bin/zalua get system.tcp.passive 7 | UserParameter=system.tcp.failed,/opt/restream/zabbix/bin/zalua get system.tcp.failed 8 | UserParameter=system.tcp.resets,/opt/restream/zabbix/bin/zalua get system.tcp.resets 9 | UserParameter=system.tcp.established,/opt/restream/zabbix/bin/zalua get system.tcp.established 10 | UserParameter=system.tcp.retransmit,/opt/restream/zabbix/bin/zalua get system.tcp.retransmit 11 | 12 | # numa_plugin 13 | UserParameter=system.numa.discovery,/opt/restream/zabbix/bin/zalua get system.numa.discovery 14 | UserParameter=system.numa.numa_hit[*],/opt/restream/zabbix/bin/zalua get system.numa.numa_hit[$1] 15 | UserParameter=system.numa.numa_miss[*],/opt/restream/zabbix/bin/zalua get system.numa.numa_miss[$1] 16 | UserParameter=system.numa.numa_foreign[*],/opt/restream/zabbix/bin/zalua get system.numa.numa_foreign[$1] 17 | UserParameter=system.numa.interleave_hit[*],/opt/restream/zabbix/bin/zalua get system.numa.interleave_hit[$1] 18 | UserParameter=system.numa.local_node[*],/opt/restream/zabbix/bin/zalua get system.numa.local_node[$1] 19 | UserParameter=system.numa.other_node[*],/opt/restream/zabbix/bin/zalua get system.numa.other_node[$1] 20 | 21 | # diskstats_plugin 22 | UserParameter=system.disk.discovery,/opt/restream/zabbix/bin/zalua get system.disk.discovery 23 | UserParameter=system.disk.utilization[*],/opt/restream/zabbix/bin/zalua get system.disk.utilization[$1] 24 | UserParameter=system.disk.await[*],/opt/restream/zabbix/bin/zalua get system.disk.await[$1] 25 | UserParameter=system.disk.read_bytes_in_sec[*],/opt/restream/zabbix/bin/zalua get system.disk.read_bytes_in_sec[$1] 26 | UserParameter=system.disk.read_ops_in_sec[*],/opt/restream/zabbix/bin/zalua get system.disk.read_ops_in_sec[$1] 27 | UserParameter=system.disk.write_bytes_in_sec[*],/opt/restream/zabbix/bin/zalua get system.disk.write_bytes_in_sec[$1] 28 | UserParameter=system.disk.write_ops_in_sec[*],/opt/restream/zabbix/bin/zalua get system.disk.write_ops_in_sec[$1] 29 | UserParameter=system.disk.all_ops_in_sec[*],/opt/restream/zabbix/bin/zalua get system.disk.all_ops_in_sec[$1] 30 | 31 | # cpu_plugin 32 | UserParameter=system.processes.running,/opt/restream/zabbix/bin/zalua get system.processes.running 33 | UserParameter=system.processes.blocked,/opt/restream/zabbix/bin/zalua get system.processes.blocked 34 | UserParameter=system.processes.fork_rate,/opt/restream/zabbix/bin/zalua get system.processes.fork_rate 35 | UserParameter=system.cpu.total.user,/opt/restream/zabbix/bin/zalua get system.cpu.total.user 36 | UserParameter=system.cpu.total.nice,/opt/restream/zabbix/bin/zalua get system.cpu.total.nice 37 | UserParameter=system.cpu.total.system,/opt/restream/zabbix/bin/zalua get system.cpu.total.system 38 | UserParameter=system.cpu.total.idle,/opt/restream/zabbix/bin/zalua get system.cpu.total.idle 39 | UserParameter=system.cpu.total.iowait,/opt/restream/zabbix/bin/zalua get system.cpu.total.iowait 40 | UserParameter=system.cpu.total.irq,/opt/restream/zabbix/bin/zalua get system.cpu.total.irq 41 | UserParameter=system.cpu.total.softirq,/opt/restream/zabbix/bin/zalua get system.cpu.total.softirq 42 | UserParameter=system.cpu.total.steal,/opt/restream/zabbix/bin/zalua get system.cpu.total.steal 43 | UserParameter=system.cpu.total.guest,/opt/restream/zabbix/bin/zalua get system.cpu.total.guest 44 | UserParameter=system.cpu.total.guest_nice,/opt/restream/zabbix/bin/zalua get system.cpu.total.guest_nice 45 | UserParameter=system.cpu.count,/opt/restream/zabbix/bin/zalua get system.cpu.count 46 | UserParameter=system.stat.ctxt,/opt/restream/zabbix/bin/zalua get system.stat.ctxt 47 | UserParameter=system.stat.intr,/opt/restream/zabbix/bin/zalua get system.stat.intr 48 | UserParameter=system.cpu.temp,/opt/restream/zabbix/bin/zalua get system.cpu.temp 49 | UserParameter=system.cpu.temp_crit,/opt/restream/zabbix/bin/zalua get system.cpu.temp_crit 50 | 51 | # io_plugin 52 | UserParameter=system.io.read_hit,/opt/restream/zabbix/bin/zalua get system.io.read_hit 53 | UserParameter=system.io.syscr,/opt/restream/zabbix/bin/zalua get system.io.syscr 54 | UserParameter=system.io.syscw,/opt/restream/zabbix/bin/zalua get system.io.syscw 55 | 56 | # runit_plugin 57 | UserParameter=runit.problem,/opt/restream/zabbix/bin/zalua get runit.problem 58 | 59 | # postgresql_plugin 60 | UserParameter=postgres.database.discovery,/opt/restream/zabbix/bin/zalua get postgres.database.discovery 61 | UserParameter=postgres.time.disk_read_ms,/opt/restream/zabbix/bin/zalua get postgres.time.disk_read_ms 62 | UserParameter=postgres.time.disk_write_ms,/opt/restream/zabbix/bin/zalua get postgres.time.disk_write_ms 63 | UserParameter=postgres.time.other_ms,/opt/restream/zabbix/bin/zalua get postgres.time.other_ms 64 | UserParameter=postgres.wal.last_apply,/opt/restream/zabbix/bin/zalua get postgres.wal.last_apply 65 | UserParameter=postgres.wal.write_bytes_in_sec,/opt/restream/zabbix/bin/zalua get postgres.wal.write_bytes_in_sec 66 | UserParameter=postgres.wal.count,/opt/restream/zabbix/bin/zalua get postgres.wal.count 67 | UserParameter=postgres.connections.autovacuum,/opt/restream/zabbix/bin/zalua get postgres.connections.autovacuum 68 | UserParameter=postgres.connections.active,/opt/restream/zabbix/bin/zalua get postgres.connections.active 69 | UserParameter=postgres.connections.idle,/opt/restream/zabbix/bin/zalua get postgres.connections.idle 70 | UserParameter=postgres.connections.waiting,/opt/restream/zabbix/bin/zalua get postgres.connections.waiting 71 | UserParameter=postgres.connections.idle_in_transaction,/opt/restream/zabbix/bin/zalua get postgres.connections.idle_in_transaction 72 | UserParameter=postgres.queries.count,/opt/restream/zabbix/bin/zalua get postgres.queries.count 73 | UserParameter=postgres.queries.avg_time_ms,/opt/restream/zabbix/bin/zalua get postgres.queries.avg_time_ms 74 | UserParameter=postgres.queries.long,/opt/restream/zabbix/bin/zalua get postgres.queries.long 75 | UserParameter=postgres.database.size[*],/opt/restream/zabbix/bin/zalua get postgres.database.size[$1] 76 | UserParameter=postgres.database.age[*],/opt/restream/zabbix/bin/zalua get postgres.database.age[$1] 77 | UserParameter=postgres.locks.accessshare,/opt/restream/zabbix/bin/zalua get postgres.locks.accessshare 78 | UserParameter=postgres.locks.rowshare,/opt/restream/zabbix/bin/zalua get postgres.locks.rowshare 79 | UserParameter=postgres.locks.rowexclusive,/opt/restream/zabbix/bin/zalua get postgres.locks.rowexclusive 80 | UserParameter=postgres.locks.shareupdateexclusive,/opt/restream/zabbix/bin/zalua get postgres.locks.shareupdateexclusive 81 | UserParameter=postgres.locks.share,/opt/restream/zabbix/bin/zalua get postgres.locks.share 82 | UserParameter=postgres.locks.sharerowexclusive,/opt/restream/zabbix/bin/zalua get postgres.locks.sharerowexclusive 83 | UserParameter=postgres.locks.exclusive,/opt/restream/zabbix/bin/zalua get postgres.locks.exclusive 84 | UserParameter=postgres.locks.accessexclusive,/opt/restream/zabbix/bin/zalua get postgres.locks.accessexclusive 85 | UserParameter=postgres.blks.hit_rate,/opt/restream/zabbix/bin/zalua get postgres.blks.hit_rate 86 | UserParameter=postgres.blks.hit,/opt/restream/zabbix/bin/zalua get postgres.blks.hit 87 | UserParameter=postgres.blks.read,/opt/restream/zabbix/bin/zalua get postgres.blks.read 88 | UserParameter=postgres.rows.returned,/opt/restream/zabbix/bin/zalua get postgres.rows.returned 89 | UserParameter=postgres.rows.fetched,/opt/restream/zabbix/bin/zalua get postgres.rows.fetched 90 | UserParameter=postgres.rows.inserted,/opt/restream/zabbix/bin/zalua get postgres.rows.inserted 91 | UserParameter=postgres.rows.updated,/opt/restream/zabbix/bin/zalua get postgres.rows.updated 92 | UserParameter=postgres.rows.deleted,/opt/restream/zabbix/bin/zalua get postgres.rows.deleted 93 | 94 | # pgbouncer_plugin 95 | UserParameter=pgbouncer.query.count,/opt/restream/zabbix/bin/zalua get pgbouncer.query.count 96 | UserParameter=pgbouncer.query.total_time,/opt/restream/zabbix/bin/zalua get pgbouncer.query.total_time 97 | UserParameter=pgbouncer.query.wait_time,/opt/restream/zabbix/bin/zalua get pgbouncer.query.wait_time 98 | UserParameter=pgbouncer.sent,/opt/restream/zabbix/bin/zalua get pgbouncer.sent 99 | UserParameter=pgbouncer.received,/opt/restream/zabbix/bin/zalua get pgbouncer.received 100 | UserParameter=pgbouncer.clients.count,/opt/restream/zabbix/bin/zalua get pgbouncer.clients.count 101 | 102 | # messages 103 | UserParameter=system.messages.problem,/opt/restream/zabbix/bin/zalua get system.messages.problem 104 | 105 | # ntp 106 | UserParameter=ntp.stat,/opt/restream/zabbix/bin/zalua get ntp.stat 107 | -------------------------------------------------------------------------------- /img/await.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vadv/zalua/fd14ccbbd5d3dfaa89a5f622659ddce5862977ea/img/await.png -------------------------------------------------------------------------------- /img/cpu-intr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vadv/zalua/fd14ccbbd5d3dfaa89a5f622659ddce5862977ea/img/cpu-intr.png -------------------------------------------------------------------------------- /img/cpu-proc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vadv/zalua/fd14ccbbd5d3dfaa89a5f622659ddce5862977ea/img/cpu-proc.png -------------------------------------------------------------------------------- /img/cpu-time.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vadv/zalua/fd14ccbbd5d3dfaa89a5f622659ddce5862977ea/img/cpu-time.png -------------------------------------------------------------------------------- /img/io-syscall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vadv/zalua/fd14ccbbd5d3dfaa89a5f622659ddce5862977ea/img/io-syscall.png -------------------------------------------------------------------------------- /img/numa-access.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vadv/zalua/fd14ccbbd5d3dfaa89a5f622659ddce5862977ea/img/numa-access.png -------------------------------------------------------------------------------- /img/numa-alloc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vadv/zalua/fd14ccbbd5d3dfaa89a5f622659ddce5862977ea/img/numa-alloc.png -------------------------------------------------------------------------------- /img/tcp-errors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vadv/zalua/fd14ccbbd5d3dfaa89a5f622659ddce5862977ea/img/tcp-errors.png -------------------------------------------------------------------------------- /img/tcp-speed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vadv/zalua/fd14ccbbd5d3dfaa89a5f622659ddce5862977ea/img/tcp-speed.png -------------------------------------------------------------------------------- /rpm/SOURCES/zalua-logrotate.in: -------------------------------------------------------------------------------- 1 | /var/log/zabbix/zalua.log { 2 | daily 3 | rotate 7 4 | size 10M 5 | compress 6 | create 0664 zabbix zabbix 7 | missingok 8 | postrotate 9 | rm -f /tmp/zalua-mon.sock 10 | endscript 11 | } 12 | -------------------------------------------------------------------------------- /rpm/SPECS/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /rpm/SPECS/zalua.spec: -------------------------------------------------------------------------------- 1 | %define version unknown 2 | %define bin_name zalua 3 | %define debug_package %{nil} 4 | 5 | Name: %{bin_name} 6 | Version: %{version} 7 | Release: 1%{?dist} 8 | Summary: ZaLua: zabbix metric aggregator with plugin in lua 9 | License: BSD 10 | URL: http://git.itv.restr.im/infra/%{bin_name} 11 | Source: %{bin_name}-%{version}.tar.gz 12 | Source1: zalua-logrotate.in 13 | Requires: zabbix-agent 14 | 15 | %define restream_dir /opt/restream/ 16 | %define restream_zabbix_bin_dir %{restream_dir}/zabbix/bin 17 | 18 | %description 19 | This package provides zabbix monitoring with lua. 20 | 21 | %prep 22 | %setup 23 | 24 | %build 25 | make 26 | 27 | %post 28 | rm -f /tmp/%{bin_name}-mon.sock 29 | 30 | %install 31 | # bin 32 | %{__mkdir} -p %{buildroot}%{restream_zabbix_bin_dir} 33 | install -m 4777 bin/%{bin_name} %{buildroot}%{restream_zabbix_bin_dir} 34 | # logrotate 35 | %{__mkdir} -p %{buildroot}/%{_sysconfdir}/logrotate.d 36 | %{__install} -m 0644 %{SOURCE1} %{buildroot}/%{_sysconfdir}/logrotate.d/%{bin_name} 37 | # plugins 38 | %{__mkdir} -p %{buildroot}%{_sysconfdir}/%{bin_name}/plugins 39 | cp -v config/plugins/*.lua %{buildroot}%{_sysconfdir}/%{bin_name}/plugins/ 40 | %{__mkdir} -p %{buildroot}%{_sysconfdir}/%{bin_name}/plugins/common 41 | cp -v config/plugins/common/*.lua %{buildroot}%{_sysconfdir}/%{bin_name}/plugins/common 42 | %{__install} -m 0644 config/init.lua %{buildroot}%{_sysconfdir}/%{bin_name}/init.lua 43 | # zabbix 44 | %{__mkdir} -p %{buildroot}/%{_sysconfdir}/zabbix/zabbix.d 45 | %{__install} -m 0644 config/zabbix.conf %{buildroot}/%{_sysconfdir}/zabbix/zabbix.d/%{bin_name}.conf 46 | 47 | %clean 48 | rm -rf %{buildroot} 49 | 50 | %files 51 | %defattr(-,root,root,-) 52 | %{restream_zabbix_bin_dir}/%{bin_name} 53 | %{_sysconfdir}/%{bin_name}/init.lua 54 | %{_sysconfdir}/%{bin_name}/plugins 55 | %{_sysconfdir}/logrotate.d/%{bin_name} 56 | %{_sysconfdir}/zabbix/zabbix.d/%{bin_name}.conf 57 | %doc README.md 58 | -------------------------------------------------------------------------------- /src/zalua/cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | 8 | lua "github.com/yuin/gopher-lua" 9 | 10 | "zalua/daemon" 11 | "zalua/dsl" 12 | "zalua/logger" 13 | "zalua/protocol" 14 | "zalua/server" 15 | "zalua/settings" 16 | "zalua/socket" 17 | ) 18 | 19 | var BuildVersion = "unknown" 20 | 21 | func main() { 22 | 23 | help := func() { 24 | fmt.Fprintf(os.Stderr, "%s commands:\n", os.Args[0]) 25 | fmt.Fprintf(os.Stderr, "-v, -version, --version\n\tGet version\n") 26 | fmt.Fprintf(os.Stderr, "-e, --execute-file, execute file (without server)\n\tExecute dsl from file (for testing case)\n") 27 | fmt.Fprintf(os.Stderr, "-k, -kill, --kill, kill \n\tKill server\n") 28 | fmt.Fprintf(os.Stderr, "-m, -metrics, --list-metrics, metrics\n\tList of known metrics\n") 29 | fmt.Fprintf(os.Stderr, "-p, -plugins, --plugins, plugins\n\tList of running plugins\n") 30 | fmt.Fprintf(os.Stderr, "-g, -get, --get, --get-metric, get \n\tGet metric value\n") 31 | fmt.Fprintf(os.Stderr, "-ping, --ping, ping\n\tPing pong game\n") 32 | os.Exit(1) 33 | } 34 | 35 | if len(os.Args) == 2 { 36 | switch os.Args[1] { 37 | case "-v", "--version", "-version": 38 | fmt.Printf("%s version: %s\n", os.Args[0], BuildVersion) 39 | os.Exit(1) 40 | case "-h", "-help", "--help": 41 | help() 42 | } 43 | } 44 | 45 | // тестовый запуск плагина 46 | if len(os.Args) > 2 { 47 | if os.Args[1] == "-e" || os.Args[1] == "--execute-file" { 48 | fd := os.Stdout 49 | logger.DupFdToStd(fd) 50 | log.SetOutput(fd) 51 | state := lua.NewState() 52 | dsl.Register(dsl.NewConfig(), state) 53 | file := os.Args[2] 54 | if err := state.DoFile(file); err != nil { 55 | log.Printf("[FATAL] execute %s: %s\n", file, err.Error()) 56 | } 57 | return 58 | } 59 | } 60 | 61 | // демонизация 62 | if !socket.Alive() { 63 | if !daemon.IsDaemon() { 64 | fmt.Fprintf(os.Stderr, "Socket %s is not alive, start daemon\n", settings.SocketPath()) 65 | } 66 | if err := daemon.Daemonize("/"); err != nil { 67 | fmt.Fprintf(os.Stderr, "Daemonize error: %s\n", err.Error()) 68 | os.Exit(2) 69 | } 70 | if !daemon.IsDaemon() { 71 | fmt.Fprintf(os.Stderr, "Daemon starting, continue as client\n") 72 | } 73 | } 74 | 75 | if daemon.IsDaemon() { 76 | 77 | // настройка лога 78 | fd, err := logger.GetLogFD() 79 | if err != nil { 80 | fmt.Fprintf(os.Stderr, "Can't open log file: %s\n", err.Error()) 81 | os.Exit(3) 82 | } 83 | logger.DupFdToStd(fd) 84 | log.SetOutput(fd) 85 | log.Printf("[INFO] Start server\n") 86 | 87 | // мы должны запустить сервис 88 | server.DoInit() 89 | if err := socket.ListenLoop(server.ClientHandler); err != nil { 90 | log.Printf("[FATAL] Listen server: %s\n", err.Error()) 91 | } 92 | // здесь мы по идее не должны не когда оказаться 93 | os.Exit(5) 94 | } 95 | 96 | client, err := socket.GetClient() 97 | if err != nil { 98 | fmt.Fprintf(os.Stderr, "%s\n", err.Error()) 99 | os.Exit(7) 100 | } 101 | 102 | // отправляем сообщение серверу 103 | msg := "" 104 | arg := "" 105 | if len(os.Args) > 1 { 106 | arg = os.Args[1] 107 | } 108 | switch arg { 109 | 110 | case "", "-ping", "--ping", "ping": 111 | msg = protocol.PING 112 | 113 | case "-k", "-kill", "--kill", "kill": 114 | msg = protocol.COMMAND_KILL 115 | 116 | case "-p", "-plugins", "--plugins", "--list-plugins", "plugins": 117 | if len(os.Args) != 2 { 118 | help() 119 | } 120 | msg = protocol.LIST_OF_PLUGINS 121 | 122 | case "-m", "-metrics", "--metrics", "--list-metrics", "metrics": 123 | if len(os.Args) != 2 { 124 | help() 125 | } 126 | msg = protocol.LIST_OF_METRICS 127 | 128 | case "-g", "-get", "--get", "--get-metric", "get": 129 | if len(os.Args) < 3 { 130 | help() 131 | } 132 | msg = fmt.Sprintf("%s %s", protocol.GET_METRIC_VALUE, os.Args[2]) 133 | 134 | default: 135 | fmt.Fprintf(os.Stderr, "unknown command\n") 136 | os.Exit(8) 137 | } 138 | 139 | result, err := client.SendMessage(msg) 140 | if err != nil { 141 | fmt.Fprintf(os.Stderr, "send message error: %s\n", err.Error()) 142 | os.Exit(10) 143 | } 144 | switch result { 145 | case protocol.UNKNOWN_METRIC: 146 | fmt.Fprintf(os.Stderr, "zalua: unknown metric '%s'\n", os.Args[2]) 147 | os.Exit(11) 148 | case protocol.UNKNOWN_COMMAND: 149 | fmt.Fprintf(os.Stderr, "zalua: unknown command\n") 150 | os.Exit(12) 151 | case protocol.COMMAND_ERROR: 152 | fmt.Fprintf(os.Stderr, "zalua: command error\n") 153 | os.Exit(13) 154 | case protocol.EMPTY: 155 | fmt.Fprintf(os.Stdout, "") 156 | default: 157 | fmt.Fprintf(os.Stdout, "%s\n", result) 158 | } 159 | 160 | } 161 | -------------------------------------------------------------------------------- /src/zalua/daemon/daemon.go: -------------------------------------------------------------------------------- 1 | package daemon 2 | 3 | // https://github.com/golang/go/issues/227 4 | // https://habrahabr.ru/post/187668/ 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "os/exec" 10 | "path/filepath" 11 | "time" 12 | ) 13 | 14 | const ( 15 | envDaemonName = "_runned_is_daemon" 16 | envDaemonValue = "1" 17 | ) 18 | 19 | func Daemonize(pwd string) error { 20 | return reborn(pwd) 21 | } 22 | 23 | func reborn(workDir string) (err error) { 24 | if !IsDaemon() { 25 | var path string 26 | if path, err = filepath.Abs(os.Args[0]); err != nil { 27 | return 28 | } 29 | cmd := exec.Command(path) // daemonize without parametrs 30 | envVar := fmt.Sprintf("%s=%s", envDaemonName, envDaemonValue) 31 | cmd.Env = append(os.Environ(), envVar) 32 | if err = cmd.Start(); err != nil { 33 | return 34 | } 35 | // пытаемся продолжить как клиент 36 | time.Sleep(time.Second) 37 | return 38 | } 39 | if len(workDir) != 0 { 40 | if err = os.Chdir(workDir); err != nil { 41 | return 42 | } 43 | } 44 | return 45 | } 46 | 47 | func IsDaemon() bool { 48 | return os.Getenv(envDaemonName) == envDaemonValue 49 | } 50 | -------------------------------------------------------------------------------- /src/zalua/dsl/cmd.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | import ( 4 | "bytes" 5 | "os/exec" 6 | "runtime" 7 | "syscall" 8 | "time" 9 | 10 | lua "github.com/yuin/gopher-lua" 11 | ) 12 | 13 | func (d *dslConfig) dslCmdExec(L *lua.LState) int { 14 | command := L.CheckString(1) 15 | 16 | var cmd *exec.Cmd 17 | 18 | switch runtime.GOOS { 19 | case "linux", "darwin": 20 | cmd = exec.Command("sh", "-c", command) 21 | case "windows": 22 | cmd = exec.Command("cmd", "/c", command) 23 | default: 24 | L.Push(lua.LNil) 25 | L.Push(lua.LString(`unsupported os`)) 26 | return 2 27 | } 28 | 29 | stdout, stderr := bytes.Buffer{}, bytes.Buffer{} 30 | cmd.Stderr = &stderr 31 | cmd.Stdout = &stdout 32 | 33 | if err := cmd.Start(); err != nil { 34 | L.Push(lua.LNil) 35 | L.Push(lua.LString(err.Error())) 36 | return 2 37 | } 38 | 39 | done := make(chan error) 40 | go func() { 41 | done <- cmd.Wait() 42 | }() 43 | 44 | select { 45 | case <-time.After(time.Duration(10) * time.Second): 46 | L.Push(lua.LNil) 47 | L.Push(lua.LString(`execute timeout after 10 seconds`)) 48 | return 2 49 | case err := <-done: 50 | result := L.CreateTable(0, 0) 51 | L.SetField(result, "stdout", lua.LString(stdout.String())) 52 | L.SetField(result, "stderr", lua.LString(stderr.String())) 53 | L.SetField(result, "code", lua.LNumber(-1)) 54 | 55 | if err != nil { 56 | if exiterr, ok := err.(*exec.ExitError); ok { 57 | if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { 58 | L.SetField(result, "code", lua.LNumber(int64(status.ExitStatus()))) 59 | } 60 | } 61 | L.Push(result) 62 | L.Push(lua.LString(err.Error())) 63 | return 2 64 | } 65 | L.SetField(result, "code", lua.LNumber(0)) 66 | L.Push(result) 67 | return 1 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/zalua/dsl/cmd_test.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | import ( 4 | "testing" 5 | 6 | lua "github.com/yuin/gopher-lua" 7 | ) 8 | 9 | func TestExecCmd(t *testing.T) { 10 | 11 | testStr := ` 12 | local result, err = cmd.exec("unknown command") 13 | if (err == nil) then error("run unknown "..tostring(result.code)) end 14 | 15 | local result, err = cmd.exec("echo ok") 16 | if not(err == nil) then error(err) end 17 | 18 | local out = "ok\n"; if goruntime.goos == "windows" then out = "ok\r\n" end 19 | if not(result.stdout == out) then error(result.stdout) end 20 | ` 21 | 22 | state := lua.NewState() 23 | Register(NewConfig(), state) 24 | if err := state.DoString(testStr); err != nil { 25 | t.Fatalf("execute lua error: %s\n", err.Error()) 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/zalua/dsl/crypto.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | import ( 4 | "crypto/md5" 5 | "fmt" 6 | 7 | lua "github.com/yuin/gopher-lua" 8 | ) 9 | 10 | func (d *dslConfig) dslCryptoMD5(L *lua.LState) int { 11 | h := md5.New() 12 | h.Write([]byte(L.CheckString(1))) 13 | L.Push(lua.LString(fmt.Sprintf("%x", h.Sum(nil)))) 14 | return 1 15 | } 16 | -------------------------------------------------------------------------------- /src/zalua/dsl/db_parse_rows.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | import ( 4 | "database/sql" 5 | "log" 6 | 7 | lua "github.com/yuin/gopher-lua" 8 | ) 9 | 10 | // парсит sql.rows и `rows, err, column_count, row_count = db:query()` выполнить запрос 11 | func parseRows(sqlRows *sql.Rows, L *lua.LState) (luaRows *lua.LTable, resultErr error, luaColumnCount lua.LNumber, luaRowCount lua.LNumber) { 12 | cols, err := sqlRows.Columns() 13 | if err != nil { 14 | resultErr = err 15 | return 16 | } 17 | // пробегаем по строкам 18 | luaRows = L.CreateTable(0, len(cols)) 19 | rowCount := 1 20 | for sqlRows.Next() { 21 | columns := make([]interface{}, len(cols)) 22 | pointers := make([]interface{}, len(cols)) 23 | for i, _ := range columns { 24 | pointers[i] = &columns[i] 25 | } 26 | err := sqlRows.Scan(pointers...) 27 | if err != nil { 28 | resultErr = err 29 | return 30 | } 31 | luaRow := L.CreateTable(0, len(cols)) 32 | for i, _ := range cols { 33 | valueP := pointers[i].(*interface{}) 34 | value := *valueP 35 | switch converted := value.(type) { 36 | case bool: 37 | luaRow.RawSetInt(i+1, lua.LBool(converted)) 38 | case float64: 39 | luaRow.RawSetInt(i+1, lua.LNumber(converted)) 40 | case int64: 41 | luaRow.RawSetInt(i+1, lua.LNumber(converted)) 42 | case string: 43 | luaRow.RawSetInt(i+1, lua.LString(converted)) 44 | case []byte: 45 | luaRow.RawSetInt(i+1, lua.LString(string(converted))) 46 | case nil: 47 | luaRow.RawSetInt(i+1, lua.LNil) 48 | default: 49 | log.Printf("[ERROR] postgresql unknown type (value: `%#v`, converted: `%#v`)\n", value, converted) 50 | luaRow.RawSetInt(i+1, lua.LNil) // на самом деле ничего не значит 51 | } 52 | } 53 | luaRows.RawSet(lua.LNumber(rowCount), luaRow) 54 | rowCount++ 55 | } 56 | luaColumnCount = lua.LNumber(len(cols) + 1) 57 | luaRowCount = lua.LNumber(rowCount) 58 | return 59 | } 60 | -------------------------------------------------------------------------------- /src/zalua/dsl/dsl.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | import ( 4 | "runtime" 5 | 6 | lua "github.com/yuin/gopher-lua" 7 | ) 8 | 9 | type dslConfig struct{} 10 | 11 | func NewConfig() *dslConfig { 12 | return &dslConfig{} 13 | } 14 | 15 | func Register(config *dslConfig, L *lua.LState) { 16 | 17 | plugin := L.NewTypeMetatable("plugin") 18 | L.SetGlobal("plugin", plugin) 19 | L.SetField(plugin, "new", L.NewFunction(config.dslNewPlugin)) 20 | L.SetField(plugin, "__index", L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{ 21 | "filename": config.dslPluginFilename, 22 | "run": config.dslPluginRun, 23 | "stop": config.dslPluginStop, 24 | "error": config.dslPluginError, 25 | "was_stopped": config.dslPluginWasStopped, 26 | "is_running": config.dslPluginIsRunning, 27 | })) 28 | 29 | tacScanner := L.NewTypeMetatable("tac") 30 | L.SetGlobal("tac", tacScanner) 31 | L.SetField(tacScanner, "open", L.NewFunction(config.dslTacOpen)) 32 | L.SetField(tacScanner, "__index", L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{ 33 | "line": config.dslTacLine, 34 | "close": config.dslTacClose, 35 | })) 36 | 37 | postgres := L.NewTypeMetatable("postgres") 38 | L.SetGlobal("postgres", postgres) 39 | L.SetField(postgres, "open", L.NewFunction(config.dslNewPgsqlConn)) 40 | L.SetField(postgres, "__index", L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{ 41 | "close": config.dslPgsqlClose, 42 | "query": config.dslPgsqlQuery, 43 | })) 44 | 45 | tcp := L.NewTypeMetatable("tcp") 46 | L.SetGlobal("tcp", tcp) 47 | L.SetField(tcp, "open", L.NewFunction(config.dslNewTCPConn)) 48 | L.SetField(tcp, "__index", L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{ 49 | "close": config.dslTCPClose, 50 | "write": config.dslTCPWrite, 51 | })) 52 | 53 | dslPluginParser := L.NewTypeMetatable("parser") 54 | L.SetGlobal("parser", dslPluginParser) 55 | L.SetField(dslPluginParser, "load", L.NewFunction(config.dslNewPluginParser)) 56 | L.SetField(dslPluginParser, "__index", L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{ 57 | "parse": config.dslPluginParserParse, 58 | })) 59 | 60 | storage := L.NewTypeMetatable("metrics") 61 | L.SetGlobal("metrics", storage) 62 | L.SetField(storage, "get", L.NewFunction(config.dslStorageGet)) 63 | L.SetField(storage, "set", L.NewFunction(config.dslStorageSet)) 64 | L.SetField(storage, "set_speed", L.NewFunction(config.dslStorageSetSpeed)) 65 | L.SetField(storage, "set_counter_speed", L.NewFunction(config.dslStorageSetCounterSpeed)) 66 | L.SetField(storage, "list", L.NewFunction(config.dslStorageList)) 67 | L.SetField(storage, "delete", L.NewFunction(config.dslStorageDelete)) 68 | 69 | ioutil := L.NewTypeMetatable("ioutil") 70 | L.SetGlobal("ioutil", ioutil) 71 | L.SetField(ioutil, "readfile", L.NewFunction(config.dslIoutilReadFile)) 72 | L.SetField(ioutil, "read_file", L.NewFunction(config.dslIoutilReadFile)) 73 | 74 | filepath := L.NewTypeMetatable("filepath") 75 | L.SetGlobal("filepath", filepath) 76 | L.SetField(filepath, "base", L.NewFunction(config.dslFilepathBasename)) 77 | L.SetField(filepath, "dir", L.NewFunction(config.dslFilepathDir)) 78 | L.SetField(filepath, "ext", L.NewFunction(config.dslFilepathExt)) 79 | L.SetField(filepath, "glob", L.NewFunction(config.dslFilepathGlob)) 80 | 81 | os := L.NewTypeMetatable("goos") 82 | L.SetGlobal("goos", os) 83 | L.SetField(os, "stat", L.NewFunction(config.dslOsStat)) 84 | L.SetField(os, "pagesize", L.NewFunction(config.dslOsPagesize)) 85 | 86 | syscall := L.NewTypeMetatable("syscall") 87 | L.SetGlobal("syscall", syscall) 88 | L.SetField(syscall, "statfs", L.NewFunction(config.dslStatFs)) 89 | 90 | time := L.NewTypeMetatable("time") 91 | L.SetGlobal("time", time) 92 | L.SetField(time, "unix", L.NewFunction(config.dslTimeUnix)) 93 | L.SetField(time, "unix_nano", L.NewFunction(config.dslTimeUnixNano)) 94 | L.SetField(time, "sleep", L.NewFunction(config.dslTimeSleep)) 95 | L.SetField(time, "parse", L.NewFunction(config.dslTimeParse)) 96 | 97 | http := L.NewTypeMetatable("http") 98 | L.SetGlobal("http", http) 99 | L.SetField(http, "get", L.NewFunction(config.dslHttpGet)) 100 | L.SetField(http, "post", L.NewFunction(config.dslHttpPost)) 101 | L.SetField(http, "escape", L.NewFunction(config.dslHttpEscape)) 102 | L.SetField(http, "unescape", L.NewFunction(config.dslHttpUnEscape)) 103 | 104 | strings := L.NewTypeMetatable("strings") 105 | L.SetGlobal("strings", strings) 106 | L.SetField(strings, "split", L.NewFunction(config.dslStringsSplit)) 107 | L.SetField(strings, "has_prefix", L.NewFunction(config.dslStringsHasPrefix)) 108 | L.SetField(strings, "has_suffix", L.NewFunction(config.dslStringsHasSuffix)) 109 | L.SetField(strings, "trim", L.NewFunction(config.dslStringsTrim)) 110 | 111 | log := L.NewTypeMetatable("log") 112 | L.SetGlobal("log", log) 113 | L.SetField(log, "error", L.NewFunction(config.dslLogError)) 114 | L.SetField(log, "info", L.NewFunction(config.dslLogInfo)) 115 | 116 | crypto := L.NewTypeMetatable("crypto") 117 | L.SetGlobal("crypto", crypto) 118 | L.SetField(crypto, "md5", L.NewFunction(config.dslCryptoMD5)) 119 | 120 | json := L.NewTypeMetatable("json") 121 | L.SetGlobal("json", json) 122 | L.SetField(json, "decode", L.NewFunction(config.dslJsonDecode)) 123 | L.SetField(json, "encode", L.NewFunction(config.dslJsonEncode)) 124 | 125 | yaml := L.NewTypeMetatable("yaml") 126 | L.SetGlobal("yaml", yaml) 127 | L.SetField(yaml, "decode", L.NewFunction(config.dslYamlDecode)) 128 | 129 | xmlPath := L.NewTypeMetatable("xmlpath") 130 | L.SetGlobal("xmlpath", xmlPath) 131 | L.SetField(xmlPath, "parse", L.NewFunction(config.dslXmlParse)) 132 | 133 | cmd := L.NewTypeMetatable("cmd") 134 | L.SetGlobal("cmd", cmd) 135 | L.SetField(cmd, "exec", L.NewFunction(config.dslCmdExec)) 136 | 137 | tlsUtil := L.NewTypeMetatable("tls_util") 138 | L.SetGlobal("tls_util", tlsUtil) 139 | L.SetField(tlsUtil, "cert_not_after", L.NewFunction(config.dslTLSUtilCertGetNotAfter)) 140 | 141 | human := L.NewTypeMetatable("human") 142 | L.SetGlobal("human", human) 143 | L.SetField(human, "time", L.NewFunction(config.dslHumanizeTime)) 144 | 145 | goruntime := L.NewTypeMetatable("goruntime") 146 | L.SetGlobal("goruntime", goruntime) 147 | L.SetField(goruntime, "goarch", lua.LString(runtime.GOARCH)) 148 | L.SetField(goruntime, "goos", lua.LString(runtime.GOOS)) 149 | 150 | regexp := L.NewTypeMetatable("regexp") 151 | L.SetGlobal("regexp", regexp) 152 | L.SetField(regexp, "compile", L.NewFunction(config.dslRegexpCompile)) 153 | L.SetField(regexp, "match", L.NewFunction(config.dslRegexpIsMatch)) 154 | L.SetField(regexp, "__index", L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{ 155 | "match": config.dslRegexpMatch, 156 | "find_all_string": config.dslRegexpFindAllString, 157 | "find_all": config.dslRegexpFindAllString, 158 | "find_string": config.dslRegexpFindString, 159 | "find": config.dslRegexpFindString, 160 | })) 161 | 162 | prometheus := L.NewTypeMetatable("prometheus") 163 | L.SetGlobal("prometheus", prometheus) 164 | L.SetField(prometheus, "listen", L.NewFunction(config.dslPrometheusListen)) 165 | 166 | prometheus_counter := L.NewTypeMetatable("prometheus_counter") 167 | L.SetGlobal("prometheus_counter", prometheus_counter) 168 | L.SetField(prometheus_counter, "new", L.NewFunction(config.dslNewPrometheusCounter)) 169 | L.SetField(prometheus_counter, "__index", L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{ 170 | "inc": config.dslPrometheusCounterInc, 171 | "add": config.dslPrometheusCounterAdd, 172 | })) 173 | 174 | prometheus_counter_lables := L.NewTypeMetatable("prometheus_counter_lables") 175 | L.SetGlobal("prometheus_counter_lables", prometheus_counter_lables) 176 | L.SetField(prometheus_counter_lables, "new", L.NewFunction(config.dslNewPrometheusCounterVec)) 177 | L.SetField(prometheus_counter_lables, "__index", L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{ 178 | "inc": config.dslPrometheusCounterVecInc, 179 | "add": config.dslPrometheusCounterVecAdd, 180 | })) 181 | 182 | prometheus_gauge := L.NewTypeMetatable("prometheus_gauge") 183 | L.SetGlobal("prometheus_gauge", prometheus_gauge) 184 | L.SetField(prometheus_gauge, "new", L.NewFunction(config.dslNewPrometheusGauge)) 185 | L.SetField(prometheus_gauge, "__index", L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{ 186 | "add": config.dslPrometheusGaugeAdd, 187 | "set": config.dslPrometheusGaugeSet, 188 | })) 189 | 190 | prometheus_gauge_labels := L.NewTypeMetatable("prometheus_gauge_labels") 191 | L.SetGlobal("prometheus_gauge_labels", prometheus_gauge_labels) 192 | L.SetField(prometheus_gauge_labels, "new", L.NewFunction(config.dslNewPrometheusGaugeVec)) 193 | L.SetField(prometheus_gauge_labels, "__index", L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{ 194 | "add": config.dslPrometheusGaugeVecAdd, 195 | "set": config.dslPrometheusGaugeVecSet, 196 | })) 197 | } 198 | -------------------------------------------------------------------------------- /src/zalua/dsl/filepath.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | import ( 4 | "path/filepath" 5 | 6 | "github.com/yuin/gopher-lua" 7 | ) 8 | 9 | func (d *dslConfig) dslFilepathBasename(L *lua.LState) int { 10 | path := L.CheckString(1) 11 | L.Push(lua.LString(filepath.Base(path))) 12 | return 1 13 | } 14 | 15 | func (d *dslConfig) dslFilepathDir(L *lua.LState) int { 16 | path := L.CheckString(1) 17 | L.Push(lua.LString(filepath.Dir(path))) 18 | return 1 19 | } 20 | 21 | func (d *dslConfig) dslFilepathExt(L *lua.LState) int { 22 | path := L.CheckString(1) 23 | L.Push(lua.LString(filepath.Ext(path))) 24 | return 1 25 | } 26 | 27 | func (d *dslConfig) dslFilepathGlob(L *lua.LState) int { 28 | pattern := L.CheckString(1) 29 | files, err := filepath.Glob(pattern) 30 | if err != nil { 31 | L.Push(lua.LNil) 32 | return 1 33 | } 34 | result := L.CreateTable(len(files), 0) 35 | for _, file := range files { 36 | result.Append(lua.LString(file)) 37 | } 38 | L.Push(result) 39 | return 1 40 | } 41 | -------------------------------------------------------------------------------- /src/zalua/dsl/http_util.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "net/url" 9 | "time" 10 | 11 | lua "github.com/yuin/gopher-lua" 12 | ) 13 | 14 | const USER_AGENT = `zalua` 15 | 16 | func (d *dslConfig) dslHttpGet(L *lua.LState) int { 17 | url := L.CheckString(1) 18 | timeout := time.Duration(10 * time.Second) 19 | client := http.Client{ 20 | Timeout: timeout, 21 | } 22 | req, err := http.NewRequest("GET", url, nil) 23 | if err != nil { 24 | L.Push(lua.LNil) 25 | L.Push(lua.LString(fmt.Sprintf("http create request: %s\n", err.Error()))) 26 | return 2 27 | } 28 | req.Header.Set("User-Agent", USER_AGENT) 29 | response, err := client.Do(req) 30 | if err != nil { 31 | L.Push(lua.LNil) 32 | L.Push(lua.LString(fmt.Sprintf("http error: %s\n", err.Error()))) 33 | return 2 34 | } 35 | defer response.Body.Close() 36 | data, err := ioutil.ReadAll(response.Body) 37 | if err != nil { 38 | L.Push(lua.LNil) 39 | L.Push(lua.LString(fmt.Sprintf("http read response error: %s\n", err.Error()))) 40 | return 2 41 | } 42 | // write response 43 | result := L.NewTable() 44 | L.SetField(result, "code", lua.LNumber(response.StatusCode)) 45 | L.SetField(result, "body", lua.LString(string(data))) 46 | L.SetField(result, "url", lua.LString(url)) 47 | L.Push(result) 48 | return 1 49 | } 50 | 51 | func (d *dslConfig) dslHttpPost(L *lua.LState) int { 52 | 53 | url := L.CheckString(1) 54 | body := L.CheckString(2) 55 | 56 | buf := bytes.NewBuffer([]byte(body)) 57 | 58 | timeout := time.Duration(10 * time.Second) 59 | client := http.Client{ 60 | Timeout: timeout, 61 | } 62 | req, err := http.NewRequest("POST", url, buf) 63 | if err != nil { 64 | L.Push(lua.LNil) 65 | L.Push(lua.LString(fmt.Sprintf("http create request: %s\n", err.Error()))) 66 | return 2 67 | } 68 | 69 | req.Header.Set("User-Agent", USER_AGENT) 70 | req.Header.Set("Content-Type", "application/json") 71 | 72 | response, err := client.Do(req) 73 | if err != nil { 74 | L.Push(lua.LNil) 75 | L.Push(lua.LString(fmt.Sprintf("http error: %s\n", err.Error()))) 76 | return 2 77 | } 78 | defer response.Body.Close() 79 | data, err := ioutil.ReadAll(response.Body) 80 | if err != nil { 81 | L.Push(lua.LNil) 82 | L.Push(lua.LString(fmt.Sprintf("http read response error: %s\n", err.Error()))) 83 | return 2 84 | } 85 | // write response 86 | result := L.NewTable() 87 | L.SetField(result, "code", lua.LNumber(response.StatusCode)) 88 | L.SetField(result, "body", lua.LString(string(data))) 89 | L.SetField(result, "url", lua.LString(url)) 90 | L.Push(result) 91 | return 1 92 | } 93 | 94 | func (d *dslConfig) dslHttpEscape(L *lua.LState) int { 95 | query := L.CheckString(1) 96 | escapedUrl := url.QueryEscape(query) 97 | L.Push(lua.LString(escapedUrl)) 98 | return 1 99 | } 100 | 101 | func (d *dslConfig) dslHttpUnEscape(L *lua.LState) int { 102 | query := L.CheckString(1) 103 | url, err := url.QueryUnescape(query) 104 | if err != nil { 105 | L.Push(lua.LNil) 106 | L.Push(lua.LString(fmt.Sprintf("unescape error: %s\n", err.Error()))) 107 | return 2 108 | } 109 | L.Push(lua.LString(url)) 110 | return 1 111 | } 112 | -------------------------------------------------------------------------------- /src/zalua/dsl/humanize_time.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "sort" 7 | "time" 8 | 9 | lua "github.com/yuin/gopher-lua" 10 | ) 11 | 12 | func (d *dslConfig) dslHumanizeTime(L *lua.LState) int { 13 | tt := L.CheckInt64(1) 14 | then := time.Unix(tt, 0) 15 | L.Push(lua.LString(humanTime(then))) 16 | return 1 17 | } 18 | 19 | // Seconds-based time units 20 | const ( 21 | humanDay = 24 * time.Hour 22 | humanWeek = 7 * humanDay 23 | humanMonth = 30 * humanDay 24 | humanYear = 12 * humanMonth 25 | humanLongTime = 37 * humanYear 26 | ) 27 | 28 | // Time formats a time into a relative string. 29 | // 30 | // Time(someT) -> "3 weeks ago" 31 | func humanTime(then time.Time) string { 32 | return humanRelTime(then, time.Now(), "ago", "from now") 33 | } 34 | 35 | // A RelTimeMagnitude struct contains a relative time point at which 36 | // the relative format of time will switch to a new format string. A 37 | // slice of these in ascending order by their "D" field is passed to 38 | // CustomRelTime to format durations. 39 | // 40 | // The Format field is a string that may contain a "%s" which will be 41 | // replaced with the appropriate signed label (e.g. "ago" or "from 42 | // now") and a "%d" that will be replaced by the quantity. 43 | // 44 | // The DivBy field is the amount of time the time difference must be 45 | // divided by in order to display correctly. 46 | // 47 | // e.g. if D is 2*time.Minute and you want to display "%d minutes %s" 48 | // DivBy should be time.Minute so whatever the duration is will be 49 | // expressed in minutes. 50 | type humanRelTimeMagnitude struct { 51 | D time.Duration 52 | Format string 53 | DivBy time.Duration 54 | } 55 | 56 | var defaultMagnitudes = []humanRelTimeMagnitude{ 57 | {time.Second, "now", time.Second}, 58 | {2 * time.Second, "1 second %s", 1}, 59 | {time.Minute, "%d seconds %s", time.Second}, 60 | {2 * time.Minute, "1 minute %s", 1}, 61 | {time.Hour, "%d minutes %s", time.Minute}, 62 | {2 * time.Hour, "1 hour %s", 1}, 63 | {humanDay, "%d hours %s", time.Hour}, 64 | {2 * humanDay, "1 day %s", 1}, 65 | {humanWeek, "%d days %s", humanDay}, 66 | {2 * humanWeek, "1 week %s", 1}, 67 | {humanMonth, "%d weeks %s", humanWeek}, 68 | {2 * humanMonth, "1 month %s", 1}, 69 | {humanYear, "%d months %s", humanMonth}, 70 | {18 * humanMonth, "1 year %s", 1}, 71 | {2 * humanYear, "2 years %s", 1}, 72 | {humanLongTime, "%d years %s", humanYear}, 73 | {math.MaxInt64, "a long while %s", 1}, 74 | } 75 | 76 | // RelTime formats a time into a relative string. 77 | // 78 | // It takes two times and two labels. In addition to the generic time 79 | // delta string (e.g. 5 minutes), the labels are used applied so that 80 | // the label corresponding to the smaller time is applied. 81 | // 82 | // RelTime(timeInPast, timeInFuture, "earlier", "later") -> "3 weeks earlier" 83 | func humanRelTime(a, b time.Time, albl, blbl string) string { 84 | return humanCustomRelTime(a, b, albl, blbl, defaultMagnitudes) 85 | } 86 | 87 | // CustomRelTime formats a time into a relative string. 88 | // 89 | // It takes two times two labels and a table of relative time formats. 90 | // In addition to the generic time delta string (e.g. 5 minutes), the 91 | // labels are used applied so that the label corresponding to the 92 | // smaller time is applied. 93 | func humanCustomRelTime(a, b time.Time, albl, blbl string, magnitudes []humanRelTimeMagnitude) string { 94 | lbl := albl 95 | diff := b.Sub(a) 96 | 97 | if a.After(b) { 98 | lbl = blbl 99 | diff = a.Sub(b) 100 | } 101 | 102 | n := sort.Search(len(magnitudes), func(i int) bool { 103 | return magnitudes[i].D > diff 104 | }) 105 | 106 | if n >= len(magnitudes) { 107 | n = len(magnitudes) - 1 108 | } 109 | mag := magnitudes[n] 110 | args := []interface{}{} 111 | escaped := false 112 | for _, ch := range mag.Format { 113 | if escaped { 114 | switch ch { 115 | case 's': 116 | args = append(args, lbl) 117 | case 'd': 118 | args = append(args, diff/mag.DivBy) 119 | } 120 | escaped = false 121 | } else { 122 | escaped = ch == '%' 123 | } 124 | } 125 | return fmt.Sprintf(mag.Format, args...) 126 | } 127 | -------------------------------------------------------------------------------- /src/zalua/dsl/ioutil.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | import ( 4 | "io/ioutil" 5 | 6 | lua "github.com/yuin/gopher-lua" 7 | ) 8 | 9 | func (d *dslConfig) dslIoutilReadFile(L *lua.LState) int { 10 | filename := L.CheckString(1) 11 | data, err := ioutil.ReadFile(filename) 12 | if err == nil { 13 | L.Push(lua.LString(string(data))) 14 | } else { 15 | L.Push(lua.LNil) 16 | } 17 | return 1 18 | } 19 | -------------------------------------------------------------------------------- /src/zalua/dsl/json.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "strconv" 7 | 8 | lua "github.com/yuin/gopher-lua" 9 | ) 10 | 11 | var ( 12 | errJsonFunction = errors.New("cannot encode function to JSON") 13 | errJsonChannel = errors.New("cannot encode channel to JSON") 14 | errJsonState = errors.New("cannot encode state to JSON") 15 | errJsonUserData = errors.New("cannot encode userdata to JSON") 16 | errJsonNested = errors.New("cannot encode recursively nested tables to JSON") 17 | ) 18 | 19 | type jsonValue struct { 20 | lua.LValue 21 | visited map[*lua.LTable]bool 22 | } 23 | 24 | func (c *dslConfig) dslJsonDecode(L *lua.LState) int { 25 | str := L.CheckString(1) 26 | 27 | var value interface{} 28 | err := json.Unmarshal([]byte(str), &value) 29 | if err != nil { 30 | L.Push(lua.LNil) 31 | L.Push(lua.LString(err.Error())) 32 | return 2 33 | } 34 | L.Push(fromJSON(L, value)) 35 | return 1 36 | } 37 | 38 | func (c *dslConfig) dslJsonEncode(L *lua.LState) int { 39 | value := L.CheckAny(1) 40 | 41 | visited := make(map[*lua.LTable]bool) 42 | data, err := toJSON(value, visited) 43 | if err != nil { 44 | L.Push(lua.LNil) 45 | L.Push(lua.LString(err.Error())) 46 | return 2 47 | } 48 | L.Push(lua.LString(string(data))) 49 | return 1 50 | } 51 | 52 | func (j jsonValue) MarshalJSON() ([]byte, error) { 53 | return toJSON(j.LValue, j.visited) 54 | } 55 | 56 | func toJSON(value lua.LValue, visited map[*lua.LTable]bool) (data []byte, err error) { 57 | switch converted := value.(type) { 58 | case lua.LBool: 59 | data, err = json.Marshal(converted) 60 | case lua.LChannel: 61 | err = errJsonChannel 62 | case lua.LNumber: 63 | data, err = json.Marshal(converted) 64 | case *lua.LFunction: 65 | err = errJsonFunction 66 | case *lua.LNilType: 67 | data, err = json.Marshal(converted) 68 | case *lua.LState: 69 | err = errJsonState 70 | case lua.LString: 71 | data, err = json.Marshal(converted) 72 | case *lua.LTable: 73 | var arr []jsonValue 74 | var obj map[string]jsonValue 75 | 76 | if visited[converted] { 77 | panic(errJsonNested) 78 | } 79 | visited[converted] = true 80 | 81 | converted.ForEach(func(k lua.LValue, v lua.LValue) { 82 | i, numberKey := k.(lua.LNumber) 83 | if numberKey && obj == nil { 84 | index := int(i) - 1 85 | if index != len(arr) { 86 | // map out of order; convert to map 87 | obj = make(map[string]jsonValue) 88 | for i, value := range arr { 89 | obj[strconv.Itoa(i+1)] = value 90 | } 91 | obj[strconv.Itoa(index+1)] = jsonValue{v, visited} 92 | return 93 | } 94 | arr = append(arr, jsonValue{v, visited}) 95 | return 96 | } 97 | if obj == nil { 98 | obj = make(map[string]jsonValue) 99 | for i, value := range arr { 100 | obj[strconv.Itoa(i+1)] = value 101 | } 102 | } 103 | obj[k.String()] = jsonValue{v, visited} 104 | }) 105 | if obj != nil { 106 | data, err = json.Marshal(obj) 107 | } else { 108 | data, err = json.Marshal(arr) 109 | } 110 | case *lua.LUserData: 111 | // TODO: call metatable __tostring? 112 | err = errJsonUserData 113 | } 114 | return 115 | } 116 | 117 | func fromJSON(L *lua.LState, value interface{}) lua.LValue { 118 | switch converted := value.(type) { 119 | case bool: 120 | return lua.LBool(converted) 121 | case float64: 122 | return lua.LNumber(converted) 123 | case string: 124 | return lua.LString(converted) 125 | case []interface{}: 126 | arr := L.CreateTable(len(converted), 0) 127 | for _, item := range converted { 128 | arr.Append(fromJSON(L, item)) 129 | } 130 | return arr 131 | case map[string]interface{}: 132 | tbl := L.CreateTable(0, len(converted)) 133 | for key, item := range converted { 134 | tbl.RawSetH(lua.LString(key), fromJSON(L, item)) 135 | } 136 | return tbl 137 | } 138 | return lua.LNil 139 | } 140 | -------------------------------------------------------------------------------- /src/zalua/dsl/log.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | lua "github.com/yuin/gopher-lua" 8 | 9 | "zalua/logger" 10 | ) 11 | 12 | func dslLog(level string, L *lua.LState) { 13 | logTime := time.Now().Format("02/01/2006 15:04:05") 14 | fd, err := logger.GetLogFD() 15 | if err != nil { 16 | L.RaiseError("internal error: %s", err.Error()) 17 | } 18 | fmt.Fprintf(fd, "%s [%s] %s\n", logTime, level, L.CheckString(1)) 19 | } 20 | 21 | func (d *dslConfig) dslLogError(L *lua.LState) int { 22 | dslLog("ERROR", L) 23 | return 0 24 | } 25 | 26 | func (d *dslConfig) dslLogInfo(L *lua.LState) int { 27 | dslLog("INFO", L) 28 | return 0 29 | } 30 | -------------------------------------------------------------------------------- /src/zalua/dsl/os.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | import ( 4 | "os" 5 | 6 | lua "github.com/yuin/gopher-lua" 7 | ) 8 | 9 | func (d *dslConfig) dslOsStat(L *lua.LState) int { 10 | path := L.CheckString(1) 11 | stat, err := os.Stat(path) 12 | if err != nil { 13 | L.Push(lua.LNil) 14 | return 1 15 | } 16 | result := L.NewTable() 17 | L.SetField(result, "size", lua.LNumber(stat.Size())) 18 | L.SetField(result, "is_dir", lua.LBool(stat.IsDir())) 19 | L.SetField(result, "mod_time", lua.LNumber(stat.ModTime().Unix())) 20 | L.Push(result) 21 | return 1 22 | } 23 | 24 | func (d *dslConfig) dslOsPagesize(L *lua.LState) int { 25 | L.Push(lua.LNumber(os.Getpagesize())) 26 | return 1 27 | } 28 | -------------------------------------------------------------------------------- /src/zalua/dsl/plugin.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "strings" 8 | "sync" 9 | "time" 10 | 11 | lua "github.com/yuin/gopher-lua" 12 | ) 13 | 14 | const stopPluginMessage = `plugin was stopped` 15 | 16 | type plugin struct { 17 | sync.Mutex 18 | state *lua.LState 19 | filename string 20 | startedAt int64 21 | completedAt int64 22 | running bool 23 | lastErr error 24 | cancelFunc context.CancelFunc 25 | } 26 | 27 | type plugins struct { 28 | sync.Mutex 29 | list map[string]*plugin 30 | } 31 | 32 | func (l *plugins) insertPlugin(p *plugin) { 33 | l.Lock() 34 | defer l.Unlock() 35 | l.list[p.getFilename()] = p 36 | } 37 | 38 | func (l *plugins) allPlugins() []*plugin { 39 | l.Lock() 40 | defer l.Unlock() 41 | result := make([]*plugin, 0) 42 | for _, p := range l.list { 43 | result = append(result, p) 44 | } 45 | return result 46 | } 47 | 48 | var allPlugins = &plugins{list: make(map[string]*plugin)} 49 | 50 | // получение плагина из lua-state 51 | func checkPlugin(L *lua.LState) *plugin { 52 | ud := L.CheckUserData(1) 53 | if v, ok := ud.Value.(*plugin); ok { 54 | return v 55 | } 56 | L.ArgError(1, "plugin expected") 57 | return nil 58 | } 59 | 60 | // получение последней ошибки 61 | func (p *plugin) getLastError() error { 62 | p.Lock() 63 | defer p.Unlock() 64 | if err := p.lastErr; err != nil { 65 | if strings.Contains(err.Error(), `context canceled`) { 66 | return fmt.Errorf(stopPluginMessage) 67 | } 68 | } 69 | return p.lastErr 70 | } 71 | 72 | // получение статуса об остановке 73 | func (p *plugin) wasStopped() bool { 74 | if err := p.getLastError(); err != nil && err.Error() == stopPluginMessage { 75 | return true 76 | } 77 | return false 78 | } 79 | 80 | // получение статуса - запущено или нет 81 | func (p *plugin) getIsRunning() bool { 82 | p.Lock() 83 | defer p.Unlock() 84 | return p.running 85 | } 86 | 87 | // получение файла - запущено или нет 88 | func (p *plugin) getFilename() string { 89 | p.Lock() 90 | defer p.Unlock() 91 | return p.filename 92 | } 93 | 94 | // запуск плагина 95 | func (p *plugin) start() { 96 | p.Lock() 97 | state := lua.NewState() 98 | Register(NewConfig(), state) 99 | p.state = state 100 | p.lastErr = nil 101 | p.startedAt = time.Now().Unix() 102 | p.running = true 103 | ctx, cancelFunc := context.WithCancel(context.Background()) 104 | p.state.SetContext(ctx) 105 | p.cancelFunc = cancelFunc 106 | p.Unlock() 107 | 108 | p.lastErr = p.state.DoFile(p.getFilename()) 109 | p.running = false 110 | p.completedAt = time.Now().Unix() 111 | } 112 | 113 | // создание плагина 114 | func (c *dslConfig) dslNewPlugin(L *lua.LState) int { 115 | p := &plugin{filename: L.CheckString(1)} 116 | ud := L.NewUserData() 117 | ud.Value = p 118 | L.SetMetatable(ud, L.GetTypeMetatable("plugin")) 119 | L.Push(ud) 120 | log.Printf("[INFO] Start plugin: `%s`\n", p.filename) 121 | allPlugins.insertPlugin(p) 122 | return 1 123 | } 124 | 125 | // получение file name плагина 126 | func (c *dslConfig) dslPluginFilename(L *lua.LState) int { 127 | p := checkPlugin(L) 128 | L.Push(lua.LString(p.getFilename())) 129 | return 1 130 | } 131 | 132 | // запуск плагина в отдельном стейте 133 | func (c *dslConfig) dslPluginRun(L *lua.LState) int { 134 | p := checkPlugin(L) 135 | go p.start() 136 | 137 | return 0 138 | } 139 | 140 | // получение ошибки 141 | func (c *dslConfig) dslPluginError(L *lua.LState) int { 142 | p := checkPlugin(L) 143 | err := p.getLastError() 144 | if err == nil { 145 | L.Push(lua.LNil) 146 | } else { 147 | L.Push(lua.LString(err.Error())) 148 | } 149 | return 1 150 | } 151 | 152 | // получение статуса запущен или нет 153 | func (c *dslConfig) dslPluginIsRunning(L *lua.LState) int { 154 | p := checkPlugin(L) 155 | L.Push(lua.LBool(p.getIsRunning())) 156 | return 1 157 | } 158 | 159 | // остановка плагина 160 | func (c *dslConfig) dslPluginStop(L *lua.LState) int { 161 | p := checkPlugin(L) 162 | log.Printf("[INFO] Stop plugin: `%s`\n", p.filename) 163 | p.cancelFunc() 164 | return 0 165 | } 166 | 167 | // плагин был остановлен 168 | func (c *dslConfig) dslPluginWasStopped(L *lua.LState) int { 169 | p := checkPlugin(L) 170 | L.Push(lua.LBool(p.wasStopped())) 171 | return 1 172 | } 173 | 174 | // список все плагинов 175 | func ListOfPlugins() []string { 176 | result := []string{} 177 | for _, p := range allPlugins.allPlugins() { 178 | err := p.getLastError() 179 | errStr := "" 180 | if err != nil { 181 | errStr = fmt.Sprintf("%s", err.Error()) 182 | errStr = strings.TrimSpace(errStr) 183 | } 184 | result = append(result, fmt.Sprintf("%s\t\t%t\t\t%v", p.getFilename(), p.getIsRunning(), errStr)) 185 | } 186 | return result 187 | } 188 | -------------------------------------------------------------------------------- /src/zalua/dsl/plugin_parser.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | import ( 4 | "log" 5 | goplugin "plugin" 6 | 7 | lua "github.com/yuin/gopher-lua" 8 | ) 9 | 10 | type PluginParserInterface interface { 11 | ProcessData(string) (map[string]string, error) 12 | } 13 | 14 | type pluginParser struct { 15 | filename string 16 | parser PluginParserInterface 17 | } 18 | 19 | // получение plugins из lua-state 20 | func checkPluginParser(L *lua.LState) *pluginParser { 21 | ud := L.CheckUserData(1) 22 | if v, ok := ud.Value.(*pluginParser); ok { 23 | return v 24 | } 25 | L.ArgError(1, "plugin parser expected") 26 | return nil 27 | } 28 | 29 | // загрузка парсера 30 | func (c *dslConfig) dslNewPluginParser(L *lua.LState) int { 31 | filename := L.CheckString(1) 32 | symbolName := `NewParser` 33 | if L.GetTop() > 1 { 34 | symbolName = L.CheckString(2) 35 | } 36 | p, err := goplugin.Open(filename) 37 | if err != nil { 38 | L.Push(lua.LNil) 39 | L.Push(lua.LString(err.Error())) 40 | return 2 41 | } 42 | sym, err := p.Lookup(symbolName) 43 | if err != nil { 44 | L.Push(lua.LNil) 45 | L.Push(lua.LString(err.Error())) 46 | return 2 47 | } 48 | newPluginParser := sym.(PluginParserInterface) 49 | ud := L.NewUserData() 50 | ud.Value = &pluginParser{parser: newPluginParser, filename: filename} 51 | L.SetMetatable(ud, L.GetTypeMetatable("parser")) 52 | L.Push(ud) 53 | log.Printf("[INFO] Loaded parser plugin `%s` from `%s`\n", symbolName, filename) 54 | return 1 55 | } 56 | 57 | // выполнение парсинга 58 | func (c *dslConfig) dslPluginParserParse(L *lua.LState) int { 59 | p := checkPluginParser(L) 60 | data := L.CheckString(2) 61 | t, err := p.parser.ProcessData(data) 62 | if err != nil { 63 | L.Push(lua.LNil) 64 | L.Push(lua.LString(err.Error())) 65 | return 2 66 | } 67 | if t == nil { 68 | L.Push(lua.LNil) 69 | return 1 70 | } 71 | luaRow := L.CreateTable(0, len(t)) 72 | for key, value := range t { 73 | luaRow.RawSetString(key, lua.LString(value)) 74 | } 75 | L.Push(luaRow) 76 | return 1 77 | } 78 | -------------------------------------------------------------------------------- /src/zalua/dsl/plugin_test.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | "testing" 7 | 8 | lua "github.com/yuin/gopher-lua" 9 | ) 10 | 11 | func TestPluginStop(t *testing.T) { 12 | 13 | testLua1 := ` 14 | p = plugin.new("plugin_test.lua") 15 | p:run() 16 | time.sleep(1) 17 | err = p:error(); if err then error(err) end 18 | 19 | p:stop() -- нужно проверить что плагин реально остановился 20 | time.sleep(2) 21 | err = p:error(); if err then error(err) end 22 | ` 23 | 24 | testLuas2 := ` 25 | state = p:was_stopped() 26 | if not state then error("plugin must be stopped") end 27 | ` 28 | 29 | if err := os.Setenv(`LOG_PATH`, os.DevNull); err != nil { 30 | t.Fatalf("set log: %s\n", err) 31 | } 32 | 33 | state := lua.NewState() 34 | Register(NewConfig(), state) 35 | if err := state.DoString(testLua1); err != nil { 36 | if !strings.Contains(err.Error(), stopPluginMessage) { 37 | t.Fatalf("execute lua error: %s\n", err.Error()) 38 | } 39 | } else { 40 | t.Fatalf("must be stop error message\n") 41 | } 42 | 43 | if err := state.DoString(testLuas2); err != nil { 44 | t.Fatalf("execute lua error: %s\n", err.Error()) 45 | } 46 | } 47 | 48 | func TestPluginMetricSet(t *testing.T) { 49 | testLua := ` 50 | local tags = {} 51 | tags["a"] = "a" 52 | 53 | metrics.set("x1", 10, 2, tags) 54 | metrics.set("x2", 10, tags, 2) 55 | ` 56 | state := lua.NewState() 57 | Register(NewConfig(), state) 58 | if err := state.DoString(testLua); err != nil { 59 | t.Fatalf("execute lua error: %s\n", err.Error()) 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/zalua/dsl/plugin_test.lua: -------------------------------------------------------------------------------- 1 | x = 0 2 | while true do 3 | metrics.set("x", x) 4 | time.sleep(1) 5 | x = x + 1 6 | if x > 3 then error("timeout in plugin") end 7 | end 8 | -------------------------------------------------------------------------------- /src/zalua/dsl/postgres.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "log" 7 | 8 | _ "github.com/lib/pq" 9 | lua "github.com/yuin/gopher-lua" 10 | ) 11 | 12 | type pgsqlConn struct { 13 | host string 14 | database string 15 | user string 16 | passwd string 17 | sslmode string 18 | port int 19 | db *sql.DB 20 | } 21 | 22 | func (p *pgsqlConn) connectionString() string { 23 | return fmt.Sprintf("host='%s' port='%d' user='%s' dbname='%s' password='****' sslmode='%s' fallback_application_name='zalua' connect_timeout='5'", 24 | p.host, p.port, p.user, p.database, p.sslmode) 25 | } 26 | 27 | func (p *pgsqlConn) connect() error { 28 | if p.db == nil { 29 | db, err := sql.Open("postgres", p.connectionString()) 30 | if err != nil { 31 | return err 32 | } 33 | p.db = db 34 | } 35 | if err := p.db.Ping(); err != nil { 36 | p.db = nil 37 | return err 38 | } 39 | return nil 40 | } 41 | 42 | // получение connection из lua-state 43 | func checkPgsqlConn(L *lua.LState) *pgsqlConn { 44 | ud := L.CheckUserData(1) 45 | if v, ok := ud.Value.(*pgsqlConn); ok { 46 | return v 47 | } 48 | L.ArgError(1, "postgres expected") 49 | return nil 50 | } 51 | 52 | // создание коннекта 53 | func (c *dslConfig) dslNewPgsqlConn(L *lua.LState) int { 54 | 55 | setStringValue := func(c *pgsqlConn, t *lua.LTable, key string) { 56 | luaVal := t.RawGetString(key) 57 | if val, ok := luaVal.(lua.LString); ok { 58 | switch key { 59 | case "host": 60 | c.host = string(val) 61 | case "user", "username": 62 | c.user = string(val) 63 | case "passwd", "password": 64 | c.passwd = string(val) 65 | case "db", "database": 66 | c.database = string(val) 67 | case "sslmode", "ssl": 68 | c.sslmode = string(val) 69 | default: 70 | L.RaiseError("unknown option key: %s", key) 71 | } 72 | } 73 | } 74 | 75 | conn := &pgsqlConn{ 76 | host: "127.0.0.1", 77 | database: "postgres", 78 | user: "postgres", 79 | passwd: "", 80 | port: 5432, 81 | sslmode: "disable", 82 | } 83 | tbl := L.CheckTable(1) 84 | setStringValue(conn, tbl, "host") 85 | setStringValue(conn, tbl, "database") 86 | setStringValue(conn, tbl, "user") 87 | setStringValue(conn, tbl, "passwd") 88 | setStringValue(conn, tbl, "sslmode") 89 | luaPort := tbl.RawGetString("port") 90 | if port, ok := luaPort.(lua.LNumber); ok { 91 | conn.port = int(port) 92 | } 93 | 94 | if err := conn.connect(); err != nil { 95 | L.Push(lua.LNil) 96 | L.Push(lua.LString(err.Error())) 97 | return 2 98 | } 99 | ud := L.NewUserData() 100 | ud.Value = conn 101 | L.SetMetatable(ud, L.GetTypeMetatable("postgres")) 102 | L.Push(ud) 103 | log.Printf("[INFO] New postgres connection `%s`\n", conn.connectionString()) 104 | return 1 105 | } 106 | 107 | // выполнение запроса 108 | func (c *dslConfig) dslPgsqlQuery(L *lua.LState) int { 109 | conn := checkPgsqlConn(L) 110 | query := L.CheckString(2) 111 | sqlRows, err := conn.db.Query(query) 112 | if err != nil { 113 | L.Push(lua.LNil) 114 | L.Push(lua.LString(err.Error())) 115 | return 2 116 | } 117 | defer sqlRows.Close() 118 | rows, err, column_count, row_count := parseRows(sqlRows, L) 119 | L.Push(rows) 120 | if err == nil { 121 | L.Push(lua.LNil) 122 | } else { 123 | L.Push(lua.LString(err.Error())) 124 | } 125 | L.Push(column_count) 126 | L.Push(row_count) 127 | return 4 128 | } 129 | 130 | // закрытие соединения 131 | func (c *dslConfig) dslPgsqlClose(L *lua.LState) int { 132 | conn := checkPgsqlConn(L) 133 | if conn.db != nil { 134 | conn.db.Close() 135 | } 136 | return 0 137 | } 138 | -------------------------------------------------------------------------------- /src/zalua/dsl/prometheus.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | 7 | promhttp "github.com/prometheus/client_golang/prometheus/promhttp" 8 | lua "github.com/yuin/gopher-lua" 9 | ) 10 | 11 | type prometheusSrv struct { 12 | listenAddr string 13 | } 14 | 15 | func (p *prometheusSrv) start(L *lua.LState) { 16 | log.Printf("[INFO] start prometheus listener at %s\n", p.listenAddr) 17 | http.Handle("/metrics", promhttp.Handler()) 18 | if err := http.ListenAndServe(p.listenAddr, nil); err != nil { 19 | L.RaiseError("listen prometheus: %s", err.Error()) 20 | } 21 | } 22 | 23 | func (c *dslConfig) dslPrometheusListen(L *lua.LState) int { 24 | listenAddr := L.CheckString(1) 25 | p := &prometheusSrv{listenAddr: listenAddr} 26 | go p.start(L) 27 | ud := L.NewUserData() 28 | ud.Value = p 29 | L.SetMetatable(ud, L.GetTypeMetatable("prometheus")) 30 | L.Push(ud) 31 | return 1 32 | } 33 | 34 | func checkPrometheus(L *lua.LState) *prometheusSrv { 35 | ud := L.CheckUserData(1) 36 | if v, ok := ud.Value.(*prometheusSrv); ok { 37 | return v 38 | } 39 | L.ArgError(1, "prometheus expected") 40 | return nil 41 | } 42 | -------------------------------------------------------------------------------- /src/zalua/dsl/prometheus_counter.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | import ( 4 | "fmt" 5 | 6 | prometheus "github.com/prometheus/client_golang/prometheus" 7 | lua "github.com/yuin/gopher-lua" 8 | ) 9 | 10 | var regPrometheusCounter = make(map[string]*lua.LUserData, 0) 11 | 12 | type dslPrometheusCounter struct { 13 | prometheus.Counter 14 | } 15 | 16 | func (c *dslConfig) dslNewPrometheusCounter(L *lua.LState) int { 17 | config := L.CheckTable(1) 18 | 19 | namespace := "" 20 | if config.RawGetString(`namespace`).Type() != lua.LTNil { 21 | namespace = config.RawGetString(`namespace`).String() 22 | } 23 | 24 | subsystem := "" 25 | if config.RawGetString(`subsystem`).Type() != lua.LTNil { 26 | subsystem = config.RawGetString(`subsystem`).String() 27 | } 28 | 29 | name := config.RawGetString(`name`).String() 30 | 31 | fullName := fmt.Sprintf("%s_%s_%s", namespace, subsystem, name) 32 | 33 | if ud, ok := regPrometheusCounter[fullName]; ok { 34 | L.Push(ud) 35 | return 1 36 | } 37 | 38 | counter := prometheus.NewCounter(prometheus.CounterOpts{ 39 | Namespace: namespace, 40 | Subsystem: subsystem, 41 | Name: name, 42 | Help: config.RawGetString(`help`).String(), 43 | }) 44 | if err := prometheus.Register(counter); err != nil { 45 | L.Push(lua.LNil) 46 | L.Push(lua.LString(err.Error())) 47 | return 2 48 | } 49 | ud := L.NewUserData() 50 | ud.Value = &dslPrometheusCounter{counter} 51 | L.SetMetatable(ud, L.GetTypeMetatable("prometheus_counter")) 52 | L.Push(ud) 53 | regPrometheusCounter[fullName] = ud 54 | return 1 55 | } 56 | 57 | func checkPrometheusCounter(L *lua.LState) *dslPrometheusCounter { 58 | ud := L.CheckUserData(1) 59 | if v, ok := ud.Value.(*dslPrometheusCounter); ok { 60 | return v 61 | } 62 | L.ArgError(1, "prometheus_counter expected") 63 | return nil 64 | } 65 | 66 | func (c *dslConfig) dslPrometheusCounterInc(L *lua.LState) int { 67 | counter := checkPrometheusCounter(L) 68 | counter.Inc() 69 | return 0 70 | } 71 | 72 | func (c *dslConfig) dslPrometheusCounterAdd(L *lua.LState) int { 73 | counter := checkPrometheusCounter(L) 74 | value := L.CheckNumber(2) 75 | counter.Add(float64(value)) 76 | return 0 77 | } 78 | -------------------------------------------------------------------------------- /src/zalua/dsl/prometheus_counter_vec.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | prometheus "github.com/prometheus/client_golang/prometheus" 8 | lua "github.com/yuin/gopher-lua" 9 | ) 10 | 11 | var regPrometheusCounterVecUserData = make(map[string]*lua.LUserData, 0) 12 | 13 | type dslPrometheusCounterVec struct { 14 | counter *prometheus.CounterVec 15 | vectors []string 16 | } 17 | 18 | func (c *dslConfig) dslNewPrometheusCounterVec(L *lua.LState) int { 19 | 20 | config := L.CheckTable(1) 21 | vec := config.RawGetString(`labels`) 22 | vectors, err := luaTblToSlice(vec) 23 | if err != nil { 24 | L.Push(lua.LNil) 25 | L.Push(lua.LString(err.Error())) 26 | return 2 27 | } 28 | 29 | namespace := "" 30 | if config.RawGetString(`namespace`).Type() != lua.LTNil { 31 | namespace = config.RawGetString(`namespace`).String() 32 | } 33 | 34 | subsystem := "" 35 | if config.RawGetString(`subsystem`).Type() != lua.LTNil { 36 | subsystem = config.RawGetString(`subsystem`).String() 37 | } 38 | 39 | name := config.RawGetString(`name`).String() 40 | 41 | fullName := fmt.Sprintf("%s_%s_%s", namespace, subsystem, name) 42 | 43 | if ud, ok := regPrometheusCounterVecUserData[fullName]; ok { 44 | gauge := ud.Value.(*dslPrometheusCounterVec) 45 | if strings.Join(gauge.vectors, "_") != strings.Join(vectors, "_") { 46 | L.Push(lua.LNil) 47 | L.Push(lua.LString("can't change labels online")) 48 | return 2 49 | } 50 | L.Push(ud) 51 | return 1 52 | } 53 | 54 | counter := prometheus.NewCounterVec(prometheus.CounterOpts{ 55 | Namespace: namespace, 56 | Subsystem: subsystem, 57 | Name: name, 58 | Help: config.RawGetString(`help`).String(), 59 | }, vectors) 60 | if err := prometheus.Register(counter); err != nil { 61 | L.Push(lua.LNil) 62 | L.Push(lua.LString(err.Error())) 63 | return 2 64 | } 65 | ud := L.NewUserData() 66 | ud.Value = &dslPrometheusCounterVec{counter: counter, vectors: vectors} 67 | L.SetMetatable(ud, L.GetTypeMetatable("prometheus_counter_lables")) 68 | L.Push(ud) 69 | regPrometheusCounterVecUserData[fullName] = ud 70 | return 1 71 | } 72 | 73 | func checkPrometheusCounterVec(L *lua.LState) *dslPrometheusCounterVec { 74 | ud := L.CheckUserData(1) 75 | if v, ok := ud.Value.(*dslPrometheusCounterVec); ok { 76 | return v 77 | } 78 | L.ArgError(1, "prometheus_counter_lables expected") 79 | return nil 80 | } 81 | 82 | func (c *dslConfig) dslPrometheusCounterVecAdd(L *lua.LState) int { 83 | counter := checkPrometheusCounterVec(L) 84 | luaLabels := L.CheckTable(2) 85 | value := L.CheckNumber(3) 86 | labels, err := luaTblToPrometheusLabels(luaLabels) 87 | if err != nil { 88 | L.Push(lua.LString(err.Error())) 89 | } 90 | counter.counter.With(labels).Add(float64(value)) 91 | return 0 92 | } 93 | 94 | func (c *dslConfig) dslPrometheusCounterVecInc(L *lua.LState) int { 95 | counter := checkPrometheusCounterVec(L) 96 | luaLabels := L.CheckTable(2) 97 | labels, err := luaTblToPrometheusLabels(luaLabels) 98 | if err != nil { 99 | L.Push(lua.LString(err.Error())) 100 | } 101 | counter.counter.With(labels).Inc() 102 | return 0 103 | } 104 | -------------------------------------------------------------------------------- /src/zalua/dsl/prometheus_gauge.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | import ( 4 | "fmt" 5 | 6 | prometheus "github.com/prometheus/client_golang/prometheus" 7 | lua "github.com/yuin/gopher-lua" 8 | ) 9 | 10 | var regPrometheusGaugeUserData = make(map[string]*lua.LUserData, 0) 11 | 12 | type dslPrometheusGauge struct { 13 | prometheus.Gauge 14 | } 15 | 16 | func (c *dslConfig) dslNewPrometheusGauge(L *lua.LState) int { 17 | 18 | config := L.CheckTable(1) 19 | 20 | namespace := "" 21 | if config.RawGetString(`namespace`).Type() != lua.LTNil { 22 | namespace = config.RawGetString(`namespace`).String() 23 | } 24 | 25 | subsystem := "" 26 | if config.RawGetString(`subsystem`).Type() != lua.LTNil { 27 | subsystem = config.RawGetString(`subsystem`).String() 28 | } 29 | 30 | name := config.RawGetString(`name`).String() 31 | 32 | fullName := fmt.Sprintf("%s_%s_%s", namespace, subsystem, name) 33 | 34 | if ud, ok := regPrometheusGaugeUserData[fullName]; ok { 35 | L.Push(ud) 36 | return 1 37 | } 38 | 39 | gauge := prometheus.NewGauge(prometheus.GaugeOpts{ 40 | Namespace: namespace, 41 | Subsystem: subsystem, 42 | Name: name, 43 | Help: config.RawGetString(`help`).String(), 44 | }) 45 | if err := prometheus.Register(gauge); err != nil { 46 | L.Push(lua.LNil) 47 | L.Push(lua.LString(err.Error())) 48 | return 2 49 | } 50 | ud := L.NewUserData() 51 | ud.Value = &dslPrometheusGauge{gauge} 52 | L.SetMetatable(ud, L.GetTypeMetatable("prometheus_gauge")) 53 | L.Push(ud) 54 | regPrometheusGaugeUserData[fullName] = ud 55 | return 1 56 | } 57 | 58 | func checkPrometheusGauge(L *lua.LState) *dslPrometheusGauge { 59 | ud := L.CheckUserData(1) 60 | if v, ok := ud.Value.(*dslPrometheusGauge); ok { 61 | return v 62 | } 63 | L.ArgError(1, "prometheus_gauge expected") 64 | return nil 65 | } 66 | 67 | func (c *dslConfig) dslPrometheusGaugeAdd(L *lua.LState) int { 68 | gauge := checkPrometheusGauge(L) 69 | value := L.CheckNumber(2) 70 | gauge.Add(float64(value)) 71 | return 0 72 | } 73 | 74 | func (c *dslConfig) dslPrometheusGaugeSet(L *lua.LState) int { 75 | gauge := checkPrometheusGauge(L) 76 | value := L.CheckNumber(2) 77 | gauge.Set(float64(value)) 78 | return 0 79 | } 80 | -------------------------------------------------------------------------------- /src/zalua/dsl/prometheus_gauge_vec.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "strings" 7 | 8 | prometheus "github.com/prometheus/client_golang/prometheus" 9 | lua "github.com/yuin/gopher-lua" 10 | ) 11 | 12 | var regPrometheusGaugeVecUserData = make(map[string]*lua.LUserData, 0) 13 | 14 | type dslPrometheusGaugeVec struct { 15 | gauge *prometheus.GaugeVec 16 | vectors []string 17 | } 18 | 19 | func luaTblToSlice(val lua.LValue) ([]string, error) { 20 | result := make([]string, 0) 21 | tbl, ok := val.(*lua.LTable) 22 | if !ok { 23 | return nil, fmt.Errorf("bad value type: %s", val.Type().String()) 24 | } 25 | tbl.ForEach(func(k lua.LValue, v lua.LValue) { 26 | result = append(result, v.String()) 27 | }) 28 | sort.Strings(result) 29 | return result, nil 30 | } 31 | 32 | func luaTblToPrometheusLabels(val lua.LValue) (prometheus.Labels, error) { 33 | result := make(map[string]string, 0) 34 | tbl, ok := val.(*lua.LTable) 35 | if !ok { 36 | return nil, fmt.Errorf("bad value type: %s", val.Type().String()) 37 | } 38 | tbl.ForEach(func(k lua.LValue, v lua.LValue) { 39 | result[k.String()] = v.String() 40 | }) 41 | return result, nil 42 | } 43 | 44 | func (c *dslConfig) dslNewPrometheusGaugeVec(L *lua.LState) int { 45 | 46 | config := L.CheckTable(1) 47 | vec := config.RawGetString(`labels`) 48 | vectors, err := luaTblToSlice(vec) 49 | if err != nil { 50 | L.Push(lua.LNil) 51 | L.Push(lua.LString(err.Error())) 52 | return 2 53 | } 54 | 55 | namespace := "" 56 | if config.RawGetString(`namespace`).Type() != lua.LTNil { 57 | namespace = config.RawGetString(`namespace`).String() 58 | } 59 | 60 | subsystem := "" 61 | if config.RawGetString(`subsystem`).Type() != lua.LTNil { 62 | subsystem = config.RawGetString(`subsystem`).String() 63 | } 64 | 65 | name := config.RawGetString(`name`).String() 66 | 67 | fullName := fmt.Sprintf("%s_%s_%s", namespace, subsystem, name) 68 | 69 | if ud, ok := regPrometheusGaugeVecUserData[fullName]; ok { 70 | gauge := ud.Value.(*dslPrometheusGaugeVec) 71 | if strings.Join(gauge.vectors, "_") != strings.Join(vectors, "_") { 72 | L.Push(lua.LNil) 73 | L.Push(lua.LString("can't change labels online")) 74 | return 2 75 | } 76 | L.Push(ud) 77 | return 1 78 | } 79 | 80 | gauge := prometheus.NewGaugeVec(prometheus.GaugeOpts{ 81 | Namespace: namespace, 82 | Subsystem: subsystem, 83 | Name: name, 84 | Help: config.RawGetString(`help`).String(), 85 | }, vectors) 86 | if err := prometheus.Register(gauge); err != nil { 87 | L.Push(lua.LNil) 88 | L.Push(lua.LString(err.Error())) 89 | return 2 90 | } 91 | ud := L.NewUserData() 92 | ud.Value = &dslPrometheusGaugeVec{gauge: gauge, vectors: vectors} 93 | L.SetMetatable(ud, L.GetTypeMetatable("prometheus_gauge_labels")) 94 | L.Push(ud) 95 | regPrometheusGaugeVecUserData[fullName] = ud 96 | return 1 97 | } 98 | 99 | func checkPrometheusGaugeVec(L *lua.LState) *dslPrometheusGaugeVec { 100 | ud := L.CheckUserData(1) 101 | if v, ok := ud.Value.(*dslPrometheusGaugeVec); ok { 102 | return v 103 | } 104 | L.ArgError(1, "prometheus_gauge_labels expected") 105 | return nil 106 | } 107 | 108 | func (c *dslConfig) dslPrometheusGaugeVecAdd(L *lua.LState) int { 109 | gauge := checkPrometheusGaugeVec(L) 110 | luaLabels := L.CheckTable(2) 111 | value := L.CheckNumber(3) 112 | labels, err := luaTblToPrometheusLabels(luaLabels) 113 | if err != nil { 114 | L.Push(lua.LString(err.Error())) 115 | } 116 | gauge.gauge.With(labels).Add(float64(value)) 117 | return 0 118 | } 119 | 120 | func (c *dslConfig) dslPrometheusGaugeVecSet(L *lua.LState) int { 121 | gauge := checkPrometheusGaugeVec(L) 122 | luaLabels := L.CheckTable(2) 123 | value := L.CheckNumber(3) 124 | labels, err := luaTblToPrometheusLabels(luaLabels) 125 | if err != nil { 126 | L.Push(lua.LString(err.Error())) 127 | } 128 | gauge.gauge.With(labels).Set(float64(value)) 129 | return 0 130 | } 131 | -------------------------------------------------------------------------------- /src/zalua/dsl/prometheus_test.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "net/http" 7 | "testing" 8 | "time" 9 | 10 | lua "github.com/yuin/gopher-lua" 11 | ) 12 | 13 | func TestPluginPrometheus(t *testing.T) { 14 | testLua := ` 15 | p = plugin.new("prometheus_test.lua") 16 | p:run() 17 | time.sleep(1) 18 | err = p:error(); if err then error(err) end 19 | time.sleep(1) 20 | ` 21 | state := lua.NewState() 22 | Register(NewConfig(), state) 23 | go func() { 24 | time.Sleep(time.Second) 25 | resp, err := http.Get("http://127.0.0.1:1111/metrics") 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | defer resp.Body.Close() 30 | body, err := ioutil.ReadAll(resp.Body) 31 | log.Printf("body:\n%s\n", body) 32 | }() 33 | if err := state.DoString(testLua); err != nil { 34 | t.Fatalf("execute lua error: %s\n", err.Error()) 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/zalua/dsl/prometheus_test.lua: -------------------------------------------------------------------------------- 1 | prometheus.listen(":1111") 2 | 3 | -- simple gauge 4 | gauge_one, err = prometheus_gauge.new({help = "this is help", name = "one_gauge"}) 5 | if err then error(err) end 6 | 7 | gauge_one:add(10) 8 | gauge_one:set(1) 9 | 10 | -- vector gauge 11 | gauge_vec_one, err = prometheus_gauge_labels.new({help = "this is help", name = "one_gauge_vec", labels = {"user"}}) 12 | if err then error(err) end 13 | 14 | gauge_vec_two, err = prometheus_gauge_labels.new({help = "this is help", name = "one_gauge_vec", labels = {"user"}}) 15 | if err then error(err) end 16 | 17 | gauge_vec_one:add({user = "user_1"}, 10) 18 | gauge_vec_one:add({user = "user_2"}, 20) 19 | gauge_vec_two:add({user = "user_2"}, 2) 20 | gauge_vec_two:set({user = "user_3"}, 1) 21 | gauge_vec_two:set({user = "user_3"}, 2) 22 | 23 | -- simple counter 24 | counter_one, err = prometheus_counter.new({help = "this is help", name = "counter_one"}) 25 | if err then error(err) end 26 | 27 | counter_two, err = prometheus_counter.new({help = "this is help", name = "counter_one"}) 28 | if err then error(err) end 29 | 30 | counter_one:inc() 31 | counter_two:add(1000.2) 32 | 33 | 34 | -- vector counter 35 | counter_vec_one, err = prometheus_counter_lables.new({help = "this is help", name = "one_counter_vec", labels = {"user"}}) 36 | if err then error(err) end 37 | 38 | counter_vec_two, err = prometheus_counter_lables.new({help = "this is help", name = "one_counter_vec", labels = {"user"}}) 39 | if err then error(err) end 40 | 41 | counter_vec_one:add({user = "user_1"}, 10) 42 | counter_vec_one:add({user = "user_2"}, 20) 43 | counter_vec_two:add({user = "user_2"}, 2) 44 | 45 | -------------------------------------------------------------------------------- /src/zalua/dsl/regexp.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | import ( 4 | "regexp" 5 | 6 | lua "github.com/yuin/gopher-lua" 7 | ) 8 | 9 | func checkRegexp(L *lua.LState) *regexp.Regexp { 10 | ud := L.CheckUserData(1) 11 | if v, ok := ud.Value.(*regexp.Regexp); ok { 12 | return v 13 | } 14 | return nil 15 | } 16 | 17 | // создание regexp 18 | func (c *dslConfig) dslRegexpCompile(L *lua.LState) int { 19 | expr := L.CheckString(1) 20 | reg, err := regexp.Compile(expr) 21 | if err != nil { 22 | L.Push(lua.LNil) 23 | L.Push(lua.LString(err.Error())) 24 | return 2 25 | } 26 | ud := L.NewUserData() 27 | ud.Value = reg 28 | L.SetMetatable(ud, L.GetTypeMetatable(`regexp`)) 29 | L.Push(ud) 30 | return 1 31 | } 32 | 33 | func (c *dslConfig) dslRegexpIsMatch(L *lua.LState) int { 34 | str := L.CheckString(1) 35 | expr := L.CheckString(2) 36 | reg, err := regexp.Compile(expr) 37 | if err != nil { 38 | L.Push(lua.LNil) 39 | L.Push(lua.LString(err.Error())) 40 | return 2 41 | } 42 | L.Push(lua.LBool(reg.MatchString(str))) 43 | return 1 44 | } 45 | 46 | // match 47 | func (c *dslConfig) dslRegexpMatch(L *lua.LState) int { 48 | reg := checkRegexp(L) 49 | str := L.CheckString(2) 50 | if reg == nil { 51 | L.Push(lua.LNil) 52 | L.Push(lua.LString("regexp is nil")) 53 | return 2 54 | } 55 | L.Push(lua.LBool(reg.MatchString(str))) 56 | return 1 57 | } 58 | 59 | // FindAllString 60 | func (c *dslConfig) dslRegexpFindAllString(L *lua.LState) int { 61 | reg := checkRegexp(L) 62 | if reg == nil { 63 | L.Push(lua.LNil) 64 | L.Push(lua.LString("regexp is nil")) 65 | return 2 66 | } 67 | str := L.CheckString(2) 68 | count := -1 69 | if L.GetTop() > 2 { 70 | count = int(L.CheckNumber(3)) 71 | } 72 | luaList := L.NewTable() 73 | for _, str := range reg.FindAllString(str, count) { 74 | luaList.Append(lua.LString(str)) 75 | } 76 | L.Push(luaList) 77 | return 1 78 | } 79 | 80 | // FindString 81 | func (c *dslConfig) dslRegexpFindString(L *lua.LState) int { 82 | reg := checkRegexp(L) 83 | str := L.CheckString(2) 84 | if reg == nil { 85 | L.Push(lua.LNil) 86 | L.Push(lua.LString("regexp is nil")) 87 | return 2 88 | } 89 | L.Push(lua.LString(reg.FindString(str))) 90 | return 1 91 | } 92 | -------------------------------------------------------------------------------- /src/zalua/dsl/statfs_linux.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | import ( 4 | "fmt" 5 | "syscall" 6 | 7 | lua "github.com/yuin/gopher-lua" 8 | ) 9 | 10 | func (d *dslConfig) dslStatFs(L *lua.LState) int { 11 | path := L.CheckString(1) 12 | fs := syscall.Statfs_t{} 13 | err := syscall.Statfs(path, &fs) 14 | if err != nil { 15 | L.Push(lua.LNil) 16 | L.Push(lua.LString(fmt.Sprintf("syscall statfs error: %s\n", err.Error()))) 17 | return 2 18 | } 19 | result := L.NewTable() 20 | result.RawSetString("size", lua.LNumber(float64(fs.Blocks)*float64(fs.Bsize))) 21 | result.RawSetString("free", lua.LNumber(float64(fs.Bfree)*float64(fs.Bsize))) 22 | result.RawSetString("avail", lua.LNumber(float64(fs.Bavail)*float64(fs.Bsize))) 23 | result.RawSetString("files", lua.LNumber(float64(fs.Files))) 24 | result.RawSetString("files_free", lua.LNumber(float64(fs.Ffree))) 25 | L.Push(result) 26 | return 1 27 | } 28 | -------------------------------------------------------------------------------- /src/zalua/dsl/storage.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | import ( 4 | "encoding/json" 5 | "math" 6 | "strconv" 7 | "sync" 8 | "time" 9 | 10 | lua "github.com/yuin/gopher-lua" 11 | 12 | "zalua/storage" 13 | ) 14 | 15 | func (c *dslConfig) dslStorageGet(L *lua.LState) int { 16 | key := L.CheckString(1) 17 | tags := make(map[string]string, 0) 18 | if L.GetTop() > 1 { 19 | tbl := L.CheckTable(2) 20 | newTags, err := tblToStringMap(tbl) 21 | if err != nil { 22 | L.RaiseError("argument #2 must be table string:string") 23 | } 24 | tags = newTags 25 | } 26 | 27 | if value, ok := storage.Box.Get(key, tags); ok { 28 | L.Push(lua.LString(value.GetValue())) 29 | L.Push(stringMapsToTable(L, value.GetTags())) 30 | } else { 31 | L.Push(lua.LNil) 32 | L.Push(lua.LNil) 33 | } 34 | return 2 35 | } 36 | 37 | var speedCacheLastValue = make(map[string]float64, 0) 38 | var speedCacheLastTime = make(map[string]int64, 0) 39 | var speedCacheLock = &sync.Mutex{} 40 | 41 | func setSpeed(L *lua.LState, counter bool) int { 42 | speedCacheLock.Lock() 43 | defer speedCacheLock.Unlock() 44 | 45 | metric := L.CheckString(1) 46 | luaVal := L.CheckAny(2) 47 | val := float64(0) 48 | tags, ttl := getTagsTtlFromState(L) 49 | 50 | // парсим как строку 51 | if luaStr, ok := luaVal.(lua.LString); ok { 52 | if floatVal, err := strconv.ParseFloat(string(luaStr), 64); err == nil { 53 | val = floatVal 54 | } 55 | } else { 56 | // парсим как float 57 | if luaFloat, ok := luaVal.(lua.LNumber); ok { 58 | val = float64(luaFloat) 59 | } else { 60 | L.RaiseError("argument #2 must be string or number") 61 | } 62 | } 63 | 64 | metricKey := metric 65 | if len(tags) > 0 { 66 | data, err := json.Marshal(&tags) 67 | if err == nil { 68 | metricKey = metricKey + string(data) 69 | } 70 | } 71 | 72 | if lastValue, ok := speedCacheLastValue[metricKey]; ok { 73 | if lastTime, ok := speedCacheLastTime[metricKey]; ok { 74 | now := time.Now().UnixNano() 75 | diff := float64(val - lastValue) 76 | if counter && diff < 0 { 77 | // должны пропустить если counter и счетчик провернулся 78 | } else { 79 | value := float64(time.Second) * diff / float64(now-lastTime) 80 | valueStr := strconv.FormatFloat(value, 'f', 2, 64) 81 | if math.Abs(value) < 0.01 { 82 | valueStr = strconv.FormatFloat(value, 'f', 4, 64) 83 | } 84 | storage.Box.Set(metric, valueStr, tags, ttl) 85 | } 86 | } 87 | } 88 | speedCacheLastValue[metricKey] = val 89 | speedCacheLastTime[metricKey] = time.Now().UnixNano() 90 | 91 | return 0 92 | } 93 | 94 | func (c *dslConfig) dslStorageSetSpeed(L *lua.LState) int { 95 | return setSpeed(L, false) 96 | } 97 | 98 | func (c *dslConfig) dslStorageSetCounterSpeed(L *lua.LState) int { 99 | return setSpeed(L, true) 100 | } 101 | 102 | func (c *dslConfig) dslStorageSet(L *lua.LState) int { 103 | key := L.CheckString(1) 104 | luaVal := L.CheckAny(2) 105 | val := "" 106 | if luaStr, ok := luaVal.(lua.LString); ok { 107 | val = string(luaStr) 108 | } else { 109 | if luaFloat, ok := luaVal.(lua.LNumber); ok { 110 | value := float64(luaFloat) 111 | val = strconv.FormatFloat(value, 'f', 2, 64) 112 | if math.Abs(value) < 0.01 { 113 | val = strconv.FormatFloat(value, 'f', 6, 64) 114 | } 115 | } else { 116 | L.RaiseError("argument #2 must be string or number") 117 | } 118 | } 119 | tags, ttl := getTagsTtlFromState(L) 120 | storage.Box.Set(key, val, tags, ttl) 121 | return 0 122 | } 123 | 124 | func (c *dslConfig) dslStorageList(L *lua.LState) int { 125 | list := storage.Box.List() 126 | result := L.CreateTable(len(list), 0) 127 | for _, item := range list { 128 | t := L.CreateTable(4, 0) 129 | L.SetField(t, "metric", lua.LString(item.GetMetric())) 130 | L.SetField(t, "value", lua.LString(item.GetValue())) 131 | L.SetField(t, "tags", stringMapsToTable(L, item.GetTags())) 132 | L.SetField(t, "at", lua.LNumber(item.GetCreatedAt())) 133 | result.Append(t) 134 | } 135 | L.Push(result) 136 | return 1 137 | } 138 | 139 | func (c *dslConfig) dslStorageDelete(L *lua.LState) int { 140 | return 0 141 | } 142 | -------------------------------------------------------------------------------- /src/zalua/dsl/storage_helpers.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | import ( 4 | "fmt" 5 | 6 | lua "github.com/yuin/gopher-lua" 7 | ) 8 | 9 | // из LState забрать 3 или 4 аргумент: table tags && int64 10 | func getTagsTtlFromState(L *lua.LState) (map[string]string, int64) { 11 | 12 | ttl := int64(300) 13 | tags := make(map[string]string, 0) 14 | 15 | // check 3, 4 args (ttl or tags) 16 | if L.GetTop() > 2 { 17 | 18 | any3 := L.CheckAny(3) 19 | var any4 lua.LValue 20 | if L.GetTop() > 3 { 21 | any4 = L.CheckAny(4) 22 | } else { 23 | any4 = lua.LNil 24 | } 25 | var ttlSet, tagsSet bool 26 | 27 | // check 3 28 | switch any3.Type() { 29 | case lua.LTNumber: 30 | ttl = L.CheckInt64(3) 31 | ttlSet = true 32 | case lua.LTTable: 33 | newTags, err := tblToStringMap(L.CheckTable(3)) 34 | if err != nil { 35 | L.RaiseError(fmt.Sprintf("argument #3 to table: %s", err.Error())) 36 | } 37 | tagsSet = true 38 | tags = newTags 39 | default: 40 | L.RaiseError("argument #3 unknown type: %v", any3.Type()) 41 | } 42 | 43 | // check 4 44 | if any4.Type() != lua.LTNil { 45 | switch any4.Type() { 46 | case lua.LTNumber: 47 | if ttlSet { 48 | L.RaiseError("argument #4 must be table (ttl already geted)") 49 | } 50 | ttl = L.CheckInt64(4) 51 | case lua.LTTable: 52 | if tagsSet { 53 | L.RaiseError("argument #4 must be int (tags already geted)") 54 | } 55 | newTags, err := tblToStringMap(L.CheckTable(4)) 56 | if err != nil { 57 | L.RaiseError(fmt.Sprintf("argument #4 to table: %s", err.Error())) 58 | } 59 | tags = newTags 60 | default: 61 | L.RaiseError("argument #4 unknown type: %v", any3.Type()) 62 | } 63 | } 64 | 65 | } 66 | 67 | return tags, ttl 68 | } 69 | 70 | func stringMapsToTable(L *lua.LState, tags map[string]string) lua.LValue { 71 | if tags == nil { 72 | return lua.LNil 73 | } 74 | result := L.CreateTable(0, len(tags)) 75 | for key, value := range tags { 76 | result.RawSetString(key, lua.LString(value)) 77 | } 78 | return result 79 | } 80 | 81 | func tblToStringMap(tbl *lua.LTable) (result map[string]string, err error) { 82 | if tbl == nil { 83 | return nil, fmt.Errorf("table empty") 84 | } 85 | result = make(map[string]string, 0) 86 | cb := func(k, v lua.LValue) { 87 | if kStr, ok := k.(lua.LString); ok { 88 | if vStr, ok := v.(lua.LString); ok { 89 | result[string(kStr)] = string(vStr) 90 | return 91 | } 92 | if vFloat, ok := v.(lua.LNumber); ok { 93 | result[string(kStr)] = vFloat.String() 94 | return 95 | } 96 | } 97 | if err == nil { 98 | err = fmt.Errorf("can't convert map") 99 | } 100 | } 101 | tbl.ForEach(cb) 102 | return result, err 103 | } 104 | -------------------------------------------------------------------------------- /src/zalua/dsl/strings.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | import ( 4 | "strings" 5 | 6 | lua "github.com/yuin/gopher-lua" 7 | ) 8 | 9 | func (d *dslConfig) dslStringsSplit(L *lua.LState) int { 10 | str := L.CheckString(1) 11 | delim := L.CheckString(2) 12 | strSlice := strings.Split(str, delim) 13 | result := L.CreateTable(len(strSlice), 0) 14 | for _, str := range strSlice { 15 | result.Append(lua.LString(str)) 16 | } 17 | L.Push(result) 18 | return 1 19 | } 20 | 21 | func (d *dslConfig) dslStringsHasPrefix(L *lua.LState) int { 22 | str1 := L.CheckString(1) 23 | str2 := L.CheckString(2) 24 | result := strings.HasPrefix(str1, str2) 25 | L.Push(lua.LBool(result)) 26 | return 1 27 | } 28 | 29 | func (d *dslConfig) dslStringsHasSuffix(L *lua.LState) int { 30 | str1 := L.CheckString(1) 31 | str2 := L.CheckString(2) 32 | result := strings.HasSuffix(str1, str2) 33 | L.Push(lua.LBool(result)) 34 | return 1 35 | } 36 | 37 | func (d *dslConfig) dslStringsTrim(L *lua.LState) int { 38 | str1 := L.CheckString(1) 39 | str2 := L.CheckString(2) 40 | result := strings.Trim(str1, str2) 41 | L.Push(lua.LString(result)) 42 | return 1 43 | } 44 | -------------------------------------------------------------------------------- /src/zalua/dsl/tac.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "io" 7 | "log" 8 | "os" 9 | 10 | lua "github.com/yuin/gopher-lua" 11 | ) 12 | 13 | type tac struct { 14 | filename string 15 | fd *os.File 16 | scanner *TacScanner 17 | } 18 | 19 | func (t *tac) open() error { 20 | fd, err := os.Open(t.filename) 21 | if err != nil { 22 | return err 23 | } 24 | t.fd = fd 25 | t.scanner = newTacScanner(fd) 26 | return nil 27 | } 28 | 29 | func (d *dslConfig) dslTacOpen(L *lua.LState) int { 30 | t := &tac{filename: L.CheckString(1)} 31 | if err := t.open(); err != nil { 32 | L.RaiseError("open error: %s", err.Error()) 33 | return 0 34 | } 35 | log.Printf("[INFO] Create new tac scanner for file `%s`\n", t.filename) 36 | ud := L.NewUserData() 37 | ud.Value = t 38 | L.SetMetatable(ud, L.GetTypeMetatable("tac")) 39 | L.Push(ud) 40 | return 1 41 | } 42 | 43 | // получение tac из lua-state 44 | func checkTac(L *lua.LState) *tac { 45 | ud := L.CheckUserData(1) 46 | if v, ok := ud.Value.(*tac); ok { 47 | return v 48 | } 49 | L.ArgError(1, "tac expected") 50 | return nil 51 | } 52 | 53 | // получение строки 54 | func (c *dslConfig) dslTacLine(L *lua.LState) int { 55 | t := checkTac(L) 56 | if t.scanner == nil { 57 | L.RaiseError("tac not initialized") 58 | return 0 59 | } 60 | if t.scanner.Scan() { 61 | text := t.scanner.Text() 62 | L.Push(lua.LString(text)) 63 | return 1 64 | } 65 | L.Push(lua.LNil) 66 | return 1 67 | } 68 | 69 | // остановка 70 | func (c *dslConfig) dslTacClose(L *lua.LState) int { 71 | t := checkTac(L) 72 | if t.fd == nil { 73 | L.RaiseError("tac not initialized") 74 | return 0 75 | } 76 | t.fd.Close() 77 | log.Printf("[INFO] Close tac scanner for file `%s`\n", t.filename) 78 | t = nil 79 | return 0 80 | } 81 | 82 | const maxBufSize = 8 * 1024 83 | 84 | type TacScanner struct { 85 | r io.ReadSeeker 86 | split bufio.SplitFunc 87 | buf []byte 88 | offset int64 89 | atEOF bool 90 | tokens [][]byte 91 | partialToken int 92 | err error 93 | } 94 | 95 | func newTacScanner(r io.ReadSeeker) *TacScanner { 96 | b := &TacScanner{ 97 | r: r, 98 | buf: make([]byte, 4096), 99 | atEOF: true, 100 | split: bufio.ScanLines, 101 | } 102 | b.offset, b.err = r.Seek(0, 2) 103 | return b 104 | } 105 | 106 | func (b *TacScanner) fillbuf() error { 107 | b.tokens = b.tokens[:0] 108 | if b.offset == 0 { 109 | return io.EOF 110 | } 111 | space := len(b.buf) - b.partialToken 112 | if space == 0 { 113 | if len(b.buf) >= maxBufSize { 114 | return errors.New("token too long") 115 | } 116 | n := len(b.buf) * 2 117 | if n > maxBufSize { 118 | n = maxBufSize 119 | } 120 | newBuf := make([]byte, n) 121 | copy(newBuf, b.buf[0:b.partialToken]) 122 | b.buf = newBuf 123 | space = len(b.buf) - b.partialToken 124 | } 125 | if int64(space) > b.offset { 126 | b.buf = b.buf[0 : b.partialToken+int(b.offset)] 127 | space = len(b.buf) - b.partialToken 128 | } 129 | newOffset := b.offset - int64(space) 130 | copy(b.buf[space:], b.buf[0:b.partialToken]) 131 | _, err := b.r.Seek(newOffset, 0) 132 | if err != nil { 133 | return err 134 | } 135 | b.offset = newOffset 136 | if _, err := io.ReadFull(b.r, b.buf[0:space]); err != nil { 137 | return err 138 | } 139 | if b.offset > 0 { 140 | advance, _, err := b.split(b.buf, b.atEOF) 141 | if err != nil { 142 | return err 143 | } 144 | b.partialToken = advance 145 | if advance == 0 || advance == len(b.buf) { 146 | return b.fillbuf() 147 | } 148 | } else { 149 | b.partialToken = 0 150 | } 151 | for i := b.partialToken; i < len(b.buf); { 152 | advance, token, err := b.split(b.buf[i:], b.atEOF) 153 | if err != nil { 154 | b.tokens = b.tokens[:0] 155 | return err 156 | } 157 | if advance == 0 { 158 | break 159 | } 160 | b.tokens = append(b.tokens, token) 161 | i += advance 162 | } 163 | b.atEOF = false 164 | if len(b.tokens) == 0 { 165 | return b.fillbuf() 166 | } 167 | return nil 168 | } 169 | 170 | func (b *TacScanner) Scan() bool { 171 | if len(b.tokens) > 0 { 172 | b.tokens = b.tokens[0 : len(b.tokens)-1] 173 | } 174 | if len(b.tokens) > 0 { 175 | return true 176 | } 177 | if b.err != nil { 178 | return false 179 | } 180 | b.err = b.fillbuf() 181 | return len(b.tokens) > 0 182 | } 183 | 184 | func (b *TacScanner) Split(split bufio.SplitFunc) { 185 | b.split = split 186 | } 187 | 188 | func (b *TacScanner) Bytes() []byte { 189 | return b.tokens[len(b.tokens)-1] 190 | } 191 | 192 | func (b *TacScanner) Text() string { 193 | return string(b.Bytes()) 194 | } 195 | 196 | func (b *TacScanner) Err() error { 197 | if len(b.tokens) > 0 { 198 | return nil 199 | } 200 | if b.err == io.EOF { 201 | return nil 202 | } 203 | return b.err 204 | } 205 | -------------------------------------------------------------------------------- /src/zalua/dsl/tcp.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net" 7 | "time" 8 | 9 | lua "github.com/yuin/gopher-lua" 10 | ) 11 | 12 | type tcpConn struct { 13 | address string 14 | tcp net.Conn 15 | } 16 | 17 | func (c *tcpConn) connect() error { 18 | conn, err := net.DialTimeout("tcp", c.address, 5*time.Second) 19 | if err != nil { 20 | return err 21 | } 22 | c.tcp = conn 23 | return nil 24 | } 25 | 26 | // получение connection из lua-state 27 | func checkTCPConn(L *lua.LState) *tcpConn { 28 | ud := L.CheckUserData(1) 29 | if v, ok := ud.Value.(*tcpConn); ok { 30 | return v 31 | } 32 | L.ArgError(1, "tcp connection expected") 33 | return nil 34 | } 35 | 36 | // создание коннекта 37 | func (c *dslConfig) dslNewTCPConn(L *lua.LState) int { 38 | addr := L.CheckString(1) 39 | t := &tcpConn{address: addr} 40 | if err := t.connect(); err != nil { 41 | L.Push(lua.LNil) 42 | L.Push(lua.LString(err.Error())) 43 | return 2 44 | } 45 | ud := L.NewUserData() 46 | ud.Value = t 47 | L.SetMetatable(ud, L.GetTypeMetatable("tcp")) 48 | L.Push(ud) 49 | log.Printf("[INFO] New tcp connection to `%s`\n", t.address) 50 | return 1 51 | } 52 | 53 | // выполнение записи 54 | func (c *dslConfig) dslTCPWrite(L *lua.LState) int { 55 | conn := checkTCPConn(L) 56 | data := L.CheckString(2) 57 | conn.tcp.SetWriteDeadline(time.Now().Add(5 * time.Second)) 58 | count, err := conn.tcp.Write([]byte(data)) 59 | if err != nil { 60 | L.Push(lua.LString(fmt.Sprintf("write to `%s`: %s", conn.address, err.Error()))) 61 | return 1 62 | } 63 | if count != len(data) { 64 | L.Push(lua.LString(fmt.Sprintf("write to `%s` get: %d except: %d", conn.address, count, len(data)))) 65 | return 1 66 | } 67 | L.Push(lua.LNil) 68 | return 1 69 | } 70 | 71 | // закрытие соединения 72 | func (c *dslConfig) dslTCPClose(L *lua.LState) int { 73 | conn := checkTCPConn(L) 74 | if conn.tcp != nil { 75 | log.Printf("[INFO] Close tcp connection to `%s`\n", conn.address) 76 | conn.tcp.Close() 77 | } 78 | return 0 79 | } 80 | -------------------------------------------------------------------------------- /src/zalua/dsl/time.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | import ( 4 | "time" 5 | 6 | lua "github.com/yuin/gopher-lua" 7 | ) 8 | 9 | func (d *dslConfig) dslTimeUnix(L *lua.LState) int { 10 | L.Push(lua.LNumber(time.Now().Unix())) 11 | return 1 12 | } 13 | 14 | func (d *dslConfig) dslTimeUnixNano(L *lua.LState) int { 15 | L.Push(lua.LNumber(time.Now().UnixNano())) 16 | return 1 17 | } 18 | 19 | func (d *dslConfig) dslTimeSleep(L *lua.LState) int { 20 | val := L.CheckInt64(1) 21 | time.Sleep(time.Duration(val) * time.Second) 22 | return 0 23 | } 24 | 25 | func (d *dslConfig) dslTimeParse(L *lua.LState) int { 26 | layout, value := L.CheckString(1), L.CheckString(2) 27 | result, err := time.Parse(layout, value) 28 | if err != nil { 29 | L.Push(lua.LNil) 30 | L.Push(lua.LString(err.Error())) 31 | return 2 32 | } 33 | L.Push(lua.LNumber(result.UTC().Unix())) 34 | return 1 35 | } 36 | -------------------------------------------------------------------------------- /src/zalua/dsl/tls_util.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "net" 7 | "time" 8 | 9 | lua "github.com/yuin/gopher-lua" 10 | ) 11 | 12 | func (d *dslConfig) dslTLSUtilCertGetNotAfter(L *lua.LState) int { 13 | serverName, address := L.CheckString(1), "" 14 | if L.GetTop() > 1 { 15 | address = L.CheckString(2) 16 | } else { 17 | address = fmt.Sprintf("%s:443", serverName) 18 | } 19 | conn, err := net.DialTimeout(`tcp`, address, 5*time.Second) 20 | if err != nil { 21 | L.Push(lua.LNil) 22 | L.Push(lua.LString(err.Error())) 23 | return 2 24 | } 25 | 26 | client := tls.Client(conn, &tls.Config{ServerName: serverName}) 27 | handshakeErr := client.Handshake() 28 | 29 | var minNotAfter *time.Time 30 | for _, chain := range client.ConnectionState().VerifiedChains { 31 | for _, cert := range chain { 32 | if minNotAfter == nil || minNotAfter.Unix() > cert.NotAfter.Unix() { 33 | minNotAfter = &cert.NotAfter 34 | } 35 | } 36 | } 37 | 38 | L.Push(lua.LNumber(minNotAfter.Unix())) 39 | if handshakeErr == nil { 40 | L.Push(lua.LNil) 41 | } else { 42 | L.Push(lua.LString(handshakeErr.Error())) 43 | } 44 | return 2 45 | } 46 | -------------------------------------------------------------------------------- /src/zalua/dsl/xml.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | import ( 4 | "bytes" 5 | 6 | lua "github.com/yuin/gopher-lua" 7 | xmlpath "gopkg.in/xmlpath.v2" 8 | ) 9 | 10 | func (d *dslConfig) dslXmlParse(L *lua.LState) int { 11 | 12 | xmlData := L.CheckString(1) 13 | xmlPath := L.CheckString(2) 14 | 15 | r := bytes.NewReader([]byte(xmlData)) 16 | node, err := xmlpath.Parse(r) 17 | if err != nil { 18 | L.Push(lua.LNil) 19 | L.Push(lua.LString(err.Error())) 20 | return 2 21 | } 22 | 23 | path, err := xmlpath.Compile(xmlPath) 24 | if err != nil { 25 | L.Push(lua.LNil) 26 | L.Push(lua.LString(err.Error())) 27 | return 2 28 | } 29 | 30 | i := 1 31 | it, result := path.Iter(node), L.NewTable() 32 | for it.Next() { 33 | L.RawSetInt(result, i, lua.LString(it.Node().String())) 34 | i++ 35 | } 36 | L.Push(result) 37 | return 1 38 | } 39 | -------------------------------------------------------------------------------- /src/zalua/dsl/xml_test.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | import ( 4 | "testing" 5 | 6 | lua "github.com/yuin/gopher-lua" 7 | ) 8 | 9 | func TestDslXml(t *testing.T) { 10 | 11 | testXmlLua := ` 12 | 13 | data = [[ 14 | 15 | 16 | 17 | 18 | 19 | ]] 20 | 21 | t, err = xmlpath.parse(data, "/channels/channel/@id") 22 | if not(err == nil) then error(err) end 23 | x = 0 24 | for k,v in pairs(t) do 25 | x = x + 1 26 | print("node "..v) 27 | end 28 | 29 | if not(x == 3) then error("x: "..x) end 30 | 31 | ` 32 | 33 | state := lua.NewState() 34 | Register(NewConfig(), state) 35 | if err := state.DoString(testXmlLua); err != nil { 36 | t.Fatalf("execute lua error: %s\n", err.Error()) 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/zalua/dsl/yaml.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | import ( 4 | lua "github.com/yuin/gopher-lua" 5 | yaml "gopkg.in/yaml.v2" 6 | ) 7 | 8 | func (c *dslConfig) dslYamlDecode(L *lua.LState) int { 9 | str := L.CheckString(1) 10 | 11 | var value interface{} 12 | err := yaml.Unmarshal([]byte(str), &value) 13 | if err != nil { 14 | L.Push(lua.LNil) 15 | L.Push(lua.LString(err.Error())) 16 | return 2 17 | } 18 | L.Push(fromYAML(L, value)) 19 | return 1 20 | } 21 | 22 | func fromYAML(L *lua.LState, value interface{}) lua.LValue { 23 | switch converted := value.(type) { 24 | case bool: 25 | return lua.LBool(converted) 26 | case float64: 27 | return lua.LNumber(converted) 28 | case int: 29 | return lua.LNumber(converted) 30 | case int64: 31 | return lua.LNumber(converted) 32 | case string: 33 | return lua.LString(converted) 34 | case []interface{}: 35 | arr := L.CreateTable(len(converted), 0) 36 | for _, item := range converted { 37 | arr.Append(fromYAML(L, item)) 38 | } 39 | return arr 40 | case map[interface{}]interface{}: 41 | tbl := L.CreateTable(0, len(converted)) 42 | for key, item := range converted { 43 | tbl.RawSetH(fromYAML(L, key), fromYAML(L, item)) 44 | } 45 | return tbl 46 | case interface{}: 47 | if v, ok := converted.(bool); ok { 48 | return lua.LBool(v) 49 | } 50 | if v, ok := converted.(float64); ok { 51 | return lua.LNumber(v) 52 | } 53 | if v, ok := converted.(string); ok { 54 | return lua.LString(v) 55 | } 56 | } 57 | return lua.LNil 58 | } 59 | -------------------------------------------------------------------------------- /src/zalua/dsl/yaml_test.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | import ( 4 | "testing" 5 | 6 | lua "github.com/yuin/gopher-lua" 7 | ) 8 | 9 | func TestDslYaml(t *testing.T) { 10 | 11 | testYamlLua := ` 12 | data = [[ 13 | a: 14 | b: 1 15 | ]] 16 | 17 | t, err = yaml.decode(data) 18 | if not(err == nil) then error(err) end 19 | if not(t["a"]["b"] == 1) then error("not working, get: "..t["a"]["b"]) end 20 | ` 21 | 22 | state := lua.NewState() 23 | Register(NewConfig(), state) 24 | if err := state.DoString(testYamlLua); err != nil { 25 | t.Fatalf("execute lua error: %s\n", err.Error()) 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/zalua/logger/dup_linux.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "os" 5 | "syscall" 6 | ) 7 | 8 | func DupFdToStd(fd *os.File) { 9 | syscall.Dup2(int(fd.Fd()), 1) 10 | syscall.Dup2(int(fd.Fd()), 2) 11 | } 12 | -------------------------------------------------------------------------------- /src/zalua/logger/dup_windows.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | func DupFdToStd(fd *os.File) { 8 | return 9 | } 10 | -------------------------------------------------------------------------------- /src/zalua/logger/log.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "os" 5 | 6 | "zalua/settings" 7 | ) 8 | 9 | var logFileFd *os.File 10 | 11 | func GetLogFD() (*os.File, error) { 12 | if logFileFd != nil { 13 | return logFileFd, nil 14 | } 15 | fd, err := os.OpenFile(settings.LogPath(), os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) 16 | if err != nil { 17 | return nil, err 18 | } 19 | logFileFd = fd 20 | return logFileFd, nil 21 | } 22 | -------------------------------------------------------------------------------- /src/zalua/protocol/protocol.go: -------------------------------------------------------------------------------- 1 | package protocol 2 | 3 | const ( 4 | PING = "ping" 5 | PONG = "pong" 6 | LIST_OF_METRICS = "list-of-metrics" 7 | LIST_OF_PLUGINS = "list-of-plugins" 8 | GET_METRIC_VALUE = "get-metric-value" 9 | EMPTY = "zalua-empty-message" 10 | 11 | UNKNOWN_METRIC = "unknown-metric" 12 | UNKNOWN_COMMAND = "unknown-command" 13 | 14 | COMMAND_ERROR = "command-error" 15 | COMMAND_KILL = "command-kill" 16 | COMMAND_SERVER_ID = "server-id" 17 | ) 18 | -------------------------------------------------------------------------------- /src/zalua/server/handler.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "math/rand" 7 | "net" 8 | "os" 9 | "sort" 10 | "strings" 11 | "time" 12 | 13 | "zalua/dsl" 14 | "zalua/protocol" 15 | "zalua/settings" 16 | "zalua/storage" 17 | ) 18 | 19 | var random = rand.New(rand.NewSource(time.Now().Unix())) 20 | 21 | // генерируем уникальный номер 22 | func requestId() int64 { 23 | return random.Int63n(100000000) 24 | } 25 | 26 | // обслуживание клиента 27 | func ClientHandler(conn net.Conn) { 28 | 29 | defer conn.Close() 30 | 31 | defer func() { 32 | if r := recover(); r != nil { 33 | log.Printf("[ERROR] Recover fatal error: %v\n", r) 34 | } 35 | }() 36 | 37 | requestId := fmt.Sprintf("request-id-%d", requestId()) 38 | 39 | buf := make([]byte, settings.MaxSizeRequest()) 40 | conn.SetReadDeadline(time.Now().Add(settings.TimeoutRead())) 41 | n, err := conn.Read(buf[:]) 42 | if err != nil { 43 | log.Printf("[ERROR] %s: can't read request: %s\n", requestId, err.Error()) 44 | return 45 | } 46 | request := string(buf[0:n]) 47 | if request == protocol.COMMAND_SERVER_ID { 48 | // чтобы не засорять логи, через health-check проверяем ServerId 49 | conn.Write([]byte(settings.ServerId())) 50 | return 51 | } 52 | response := "" 53 | 54 | switch { 55 | 56 | // ping-pong 57 | case request == protocol.PING: 58 | response = protocol.PONG 59 | 60 | // command-kill 61 | case request == protocol.COMMAND_KILL: 62 | log.Printf("[FATAL] kill server now!\n") 63 | os.Exit(100) 64 | 65 | // list of metrics 66 | case request == protocol.LIST_OF_METRICS: 67 | stringList := []string{} 68 | 69 | for _, item := range storage.Box.List() { 70 | tagsArr := []string{} 71 | if len(item.GetTags()) > 0 { 72 | for k, v := range item.GetTags() { 73 | tagsArr = append(tagsArr, fmt.Sprintf("%s=%s", k, v)) 74 | } 75 | } 76 | str := fmt.Sprintf("%s\t%s\t\t%s\t\t%d", item.GetMetric(), strings.Join(tagsArr, " "), item.GetValue(), item.GetCreatedAt()) 77 | stringList = append(stringList, str) 78 | } 79 | sort.Strings(stringList) 80 | response = strings.Join(stringList, "\n") 81 | 82 | // list of running plugins 83 | case request == protocol.LIST_OF_PLUGINS: 84 | list := dsl.ListOfPlugins() 85 | sort.Strings(list) 86 | response = strings.Join(list, "\n") 87 | 88 | // get value of metric 89 | case strings.HasPrefix(request, protocol.GET_METRIC_VALUE): 90 | data := strings.Split(request, " ") 91 | if len(data) >= 2 { 92 | metric := strings.Trim(data[1], " ") 93 | tags := make(map[string]string, 0) 94 | if len(data) > 2 { 95 | // key1=val2 key2=val2 96 | for _, str := range data[2:] { 97 | strData := strings.Split(str, "=") 98 | if len(strData) == 2 { 99 | tags[strData[0]] = strData[1] 100 | } 101 | } 102 | } 103 | val, ok := storage.Box.Get(metric, tags) 104 | if ok { 105 | response = val.GetValue() 106 | } else { 107 | response = protocol.UNKNOWN_METRIC 108 | } 109 | } else { 110 | response = protocol.COMMAND_ERROR 111 | } 112 | 113 | // unknown metric 114 | default: 115 | response = protocol.UNKNOWN_COMMAND 116 | } 117 | 118 | // чтобы не получить EOF при чтении 119 | if response == "" { 120 | response = protocol.EMPTY 121 | } 122 | 123 | // записываем ответ 124 | conn.Write([]byte(response)) 125 | } 126 | -------------------------------------------------------------------------------- /src/zalua/server/init.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "time" 7 | 8 | lua "github.com/yuin/gopher-lua" 9 | 10 | "zalua/dsl" 11 | "zalua/settings" 12 | ) 13 | 14 | func DoInit() { 15 | go doInit() 16 | time.Sleep(100 * time.Millisecond) 17 | log.Printf("[INFO] Plugins loaded\n") 18 | } 19 | 20 | // сама конструкция подразумевает что нам не нужен супервизор 21 | func doInit() { 22 | log.Printf("[INFO] Load settings file %s\n", settings.InitPath()) 23 | state := lua.NewState() 24 | dsl.Register(dsl.NewConfig(), state) 25 | if err := state.DoFile(settings.InitPath()); err != nil { 26 | log.Printf("[FATAL] Settings file: %s\n", err.Error()) 27 | os.Exit(20) 28 | } 29 | log.Printf("[INFO] Settings file loaded, nothing to do, exit now\n") 30 | os.Exit(0) 31 | } 32 | -------------------------------------------------------------------------------- /src/zalua/settings/settings.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | ) 8 | 9 | var ( 10 | socketPath = "/tmp/zalua-mon.sock" 11 | initPath = "/etc/zalua/init.lua" 12 | 13 | logPath = "/var/log/zabbix/zalua.log" 14 | storagePath = "/tmp/zalua-storage.json" 15 | maxSizeRequest = 64 * 1024 16 | defaultReadTimeoutInMs = 100 17 | defaultWriteTimeoutInMs = 100 18 | ) 19 | 20 | // путь до сокета 21 | func SocketPath() string { 22 | return socketPath 23 | } 24 | 25 | // путь до плагинов 26 | func InitPath() string { 27 | if path := os.Getenv("INIT_FILE"); path != `` { 28 | initPath = path 29 | } 30 | return initPath 31 | } 32 | 33 | // путь до файла с логами 34 | func LogPath() string { 35 | if path := os.Getenv("LOG_PATH"); path != `` { 36 | logPath = path 37 | } 38 | return logPath 39 | } 40 | 41 | // путь до файла с временным стораджем 42 | func StoragePath() string { 43 | return storagePath 44 | } 45 | 46 | // чтение из сокета 47 | func TimeoutRead() time.Duration { 48 | return time.Duration(defaultReadTimeoutInMs) * time.Millisecond 49 | } 50 | 51 | // запись в сокет 52 | func TimeoutWrite() time.Duration { 53 | return time.Duration(defaultWriteTimeoutInMs) * time.Millisecond 54 | } 55 | 56 | // масксимальное сообщение в сокете 57 | func MaxSizeRequest() int64 { 58 | return int64(maxSizeRequest) 59 | } 60 | 61 | var random = time.Now().UnixNano() 62 | 63 | func ServerId() string { 64 | return fmt.Sprintf("server-id-%d", random) 65 | } 66 | -------------------------------------------------------------------------------- /src/zalua/socket/alive.go: -------------------------------------------------------------------------------- 1 | package socket 2 | 3 | import ( 4 | "os" 5 | 6 | "zalua/protocol" 7 | "zalua/settings" 8 | ) 9 | 10 | func exists() bool { 11 | if _, err := os.Stat(settings.SocketPath()); err == nil { 12 | return true 13 | } 14 | return false 15 | } 16 | 17 | // проверка живой или не живой сокет 18 | func Alive() bool { 19 | 20 | if !exists() { 21 | return false 22 | } 23 | 24 | client, err := GetClient() 25 | if err != nil { 26 | return false 27 | } 28 | defer client.Close() 29 | 30 | if response, err := client.SendMessage(protocol.PING); err != nil { 31 | return false 32 | } else { 33 | return response == protocol.PONG 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/zalua/socket/client.go: -------------------------------------------------------------------------------- 1 | package socket 2 | 3 | import ( 4 | "net" 5 | "time" 6 | 7 | "zalua/settings" 8 | ) 9 | 10 | type client struct { 11 | conn net.Conn 12 | } 13 | 14 | func GetClient() (*client, error) { 15 | conn, err := net.DialTimeout("unix", settings.SocketPath(), 200*time.Millisecond) 16 | if err != nil { 17 | return nil, err 18 | } 19 | result := &client{conn: conn} 20 | return result, nil 21 | } 22 | 23 | func (c *client) Close() error { 24 | return c.conn.Close() 25 | } 26 | 27 | func (c *client) write(msg string) (err error) { 28 | c.conn.SetWriteDeadline(time.Now().Add(settings.TimeoutWrite())) 29 | _, err = c.conn.Write([]byte(msg)) 30 | return 31 | } 32 | 33 | func (c *client) read() (string, error) { 34 | buf := make([]byte, settings.MaxSizeRequest()) 35 | c.conn.SetReadDeadline(time.Now().Add(time.Duration(settings.TimeoutWrite()))) 36 | n, err := c.conn.Read(buf[:]) 37 | if err != nil { 38 | return "", err 39 | } 40 | result := string(buf[0:n]) 41 | return result, nil 42 | } 43 | 44 | func (c *client) SendMessage(msg string) (string, error) { 45 | if err := c.write(msg); err != nil { 46 | return "", err 47 | } 48 | return c.read() 49 | } 50 | -------------------------------------------------------------------------------- /src/zalua/socket/listen_loop.go: -------------------------------------------------------------------------------- 1 | package socket 2 | 3 | import ( 4 | "log" 5 | "net" 6 | "os" 7 | "time" 8 | 9 | "zalua/protocol" 10 | "zalua/settings" 11 | ) 12 | 13 | type server struct { 14 | fd net.Listener 15 | } 16 | 17 | func ListenLoop(clientHandle func(net.Conn)) error { 18 | if exists() { 19 | if err := os.Remove(settings.SocketPath()); err != nil { 20 | return err 21 | } 22 | } 23 | fd, err := net.Listen("unix", settings.SocketPath()) 24 | if err != nil { 25 | return err 26 | } 27 | if err := os.Chmod(settings.SocketPath(), 0777); err != nil { 28 | return err 29 | } 30 | result := &server{fd: fd} 31 | go result.healthCheck() 32 | result.run(clientHandle) 33 | return nil 34 | } 35 | 36 | func (s *server) run(clientHandle func(net.Conn)) { 37 | log.Printf("[INFO] Start listen %s\n", settings.SocketPath()) 38 | for { 39 | clientFd, err := s.fd.Accept() 40 | if err != nil { 41 | log.Printf("[ERROR] handle client: %s\n", err.Error()) 42 | } 43 | go clientHandle(clientFd) 44 | } 45 | 46 | } 47 | 48 | func (s *server) healthCheck() { 49 | 50 | validServerId := func() bool { 51 | client, err := GetClient() 52 | if err != nil { 53 | return false 54 | } 55 | defer client.Close() 56 | 57 | if response, err := client.SendMessage(protocol.COMMAND_SERVER_ID); err != nil { 58 | return false 59 | } else { 60 | return response == settings.ServerId() 61 | } 62 | } 63 | 64 | healTickChan := time.NewTicker(time.Second).C 65 | for { 66 | select { 67 | case <-healTickChan: 68 | if !validServerId() { 69 | log.Printf("[ERROR] Socket %s is not alive or run with another server, exiting now...\n", settings.SocketPath()) 70 | os.Exit(0) 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/zalua/storage/storage.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "os" 9 | "sync" 10 | "time" 11 | 12 | "zalua/settings" 13 | ) 14 | 15 | var Box = newStorage() 16 | 17 | type storage struct { 18 | sync.Mutex 19 | data map[string]*StorageItem 20 | } 21 | 22 | // создание storage 23 | func newStorage() *storage { 24 | result := &storage{data: make(map[string]*StorageItem, 0)} 25 | // загрузка storage из файла 26 | if _, err := os.Stat(settings.StoragePath()); err == nil { 27 | if data, err := ioutil.ReadFile(settings.StoragePath()); err == nil { 28 | list := make(map[string]*StorageItem, 0) 29 | if json.Unmarshal(data, &list); err == nil { 30 | for key, item := range list { 31 | if !item.valid() { 32 | delete(list, key) 33 | } 34 | } 35 | result.data = list 36 | } else { 37 | fmt.Fprintf(os.Stderr, "[ERROR] Storage unmarshal: %s\n", err.Error()) 38 | } 39 | } 40 | } 41 | go result.junitor() 42 | go result.saver() 43 | return result 44 | } 45 | 46 | func (s *storage) junitor() { 47 | ticker := time.NewTicker(15 * time.Minute) 48 | for range ticker.C { 49 | log.Printf("[INFO] Start compact storage\n") 50 | s.Lock() 51 | for key, item := range s.data { 52 | if !item.valid() { 53 | delete(s.data, key) 54 | } 55 | } 56 | s.Unlock() 57 | log.Printf("[INFO] Compact storage done\n") 58 | } 59 | } 60 | 61 | func (s *storage) saver() { 62 | ticker := time.NewTicker(time.Minute) 63 | for range ticker.C { 64 | s.Lock() 65 | if data, err := json.Marshal(s.data); err == nil { 66 | tmpFile := settings.StoragePath() + ".tmp" 67 | if err := ioutil.WriteFile(tmpFile, data, 0644); err == nil { 68 | if err := os.Rename(tmpFile, settings.StoragePath()); err == nil { 69 | log.Printf("[INFO] Storage file saved\n") 70 | } 71 | } else { 72 | log.Printf("[ERROR] Storage write: %s\n", err.Error()) 73 | } 74 | } 75 | s.Unlock() 76 | } 77 | } 78 | 79 | // установить значение по ключу с default TTL 80 | func (p *storage) Set(metric, val string, tags map[string]string, ttl int64) { 81 | p.Lock() 82 | defer p.Unlock() 83 | 84 | item := &StorageItem{ 85 | Value: val, 86 | Tags: tags, 87 | Metric: metric, 88 | CreatedAt: time.Now().Unix(), 89 | TTL: ttl, 90 | } 91 | p.data[item.Key()] = item 92 | } 93 | 94 | // отдать значение по ключу 95 | func (p *storage) Get(metric string, tags map[string]string) (*StorageItem, bool) { 96 | p.Lock() 97 | defer p.Unlock() 98 | 99 | metricKey := storageKey(metric, tags) 100 | 101 | item := p.data[metricKey] 102 | if item == nil { 103 | return nil, false 104 | } 105 | if !item.valid() { 106 | delete(p.data, metricKey) 107 | return item, false 108 | } 109 | 110 | return item, true 111 | } 112 | 113 | // удалить значение по ключу 114 | func (p *storage) Delete(key string) { 115 | p.Lock() 116 | defer p.Unlock() 117 | 118 | delete(p.data, key) 119 | } 120 | 121 | // список всех ключей и значений 122 | func (p *storage) List() map[string]*StorageItem { 123 | p.Lock() 124 | defer p.Unlock() 125 | 126 | result := make(map[string]*StorageItem, 0) 127 | for key, item := range p.data { 128 | if item.valid() { 129 | result[key] = item 130 | } 131 | } 132 | return result 133 | } 134 | -------------------------------------------------------------------------------- /src/zalua/storage/storage_item.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | ) 7 | 8 | type StorageItem struct { 9 | Metric string `json:"metric"` 10 | Value string `json:"value"` 11 | Tags map[string]string `json:"tags"` 12 | CreatedAt int64 `json:"created_at"` 13 | TTL int64 `json:"ttl"` 14 | } 15 | 16 | func (s *StorageItem) GetMetric() string { 17 | return s.Metric 18 | } 19 | 20 | func (s *StorageItem) GetValue() string { 21 | return s.Value 22 | } 23 | 24 | func (s *StorageItem) GetTags() map[string]string { 25 | return s.Tags 26 | } 27 | 28 | func (s *StorageItem) GetCreatedAt() int64 { 29 | return s.CreatedAt 30 | } 31 | 32 | func storageKey(metric string, tags map[string]string) string { 33 | result := metric 34 | if len(tags) > 0 { 35 | data, err := json.Marshal(&tags) 36 | if err == nil { 37 | result = result + string(data) 38 | } 39 | } 40 | return result 41 | } 42 | 43 | func (s *StorageItem) Key() string { 44 | return storageKey(s.Metric, s.Tags) 45 | } 46 | 47 | func (s *StorageItem) valid() bool { 48 | return s.CreatedAt+s.TTL > time.Now().Unix() 49 | } 50 | --------------------------------------------------------------------------------