td-40: move 'alert' project into tdengine
This commit is contained in:
parent
a4e1157d39
commit
5dd0158eda
|
@ -0,0 +1,21 @@
|
|||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
vendor/
|
||||
|
||||
# Project specific files
|
||||
cmd/alert/alert
|
||||
cmd/alert/alert.log
|
||||
*.db
|
||||
*.gz
|
|
@ -0,0 +1,180 @@
|
|||
# Alert [DRAFT]
|
||||
|
||||
The Alert application reads data from [TDEngine](https://www.taosdata.com/), calculating according to predefined rules to generate alerts, and pushes alerts to downstream applications like [AlertManager](https://github.com/prometheus/alertmanager).
|
||||
|
||||
## Install
|
||||
|
||||
### From Binary
|
||||
|
||||
TODO: 安装包具体位置
|
||||
|
||||
Precompiled binaries is available at [taosdata website](https://www.taosdata.com/), please download and unpack it by below shell command.
|
||||
|
||||
```
|
||||
$ tar -xzf alert-$version-$OS-$ARCH.tar.gz
|
||||
```
|
||||
|
||||
### From Source Code
|
||||
|
||||
Two prerequisites are required to install from source.
|
||||
|
||||
1. TDEngine server or client must be installed.
|
||||
2. Latest [Go](https://golang.org) language must be installed.
|
||||
|
||||
When these two prerequisites are ready, please follow steps below to build the application:
|
||||
|
||||
```
|
||||
$ mkdir taosdata
|
||||
$ cd taosdata
|
||||
$ git clone https://github.com/taosdata/tdengine.git
|
||||
$ cd tdengine/alert/cmd/alert
|
||||
$ go build
|
||||
```
|
||||
|
||||
If `go build` fails because some of the dependency packages cannot be downloaded, please follow steps in [goproxy.io](https://goproxy.io) to configure `GOPROXY` and try `go build` again.
|
||||
|
||||
## Configure
|
||||
|
||||
The configuration file format of Alert application is standard `json`, below is its default content, please revise according to actual scenario.
|
||||
|
||||
```json
|
||||
{
|
||||
"port": 8100,
|
||||
"database": "file:alert.db",
|
||||
"tdengine": "root:taosdata@/tcp(127.0.0.1:0)/",
|
||||
"log": {
|
||||
"level": "production",
|
||||
"path": "alert.log"
|
||||
},
|
||||
"receivers": {
|
||||
"alertManager": "http://127.0.0.1:9093/api/v1/alerts",
|
||||
"console": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The use of each configuration item is:
|
||||
|
||||
* **port**: This is the `http` service port which enables other application to manage rules by `restful API`.
|
||||
* **database**: rules are stored in a `sqlite` database, this is the path of the database file (if the file does not exist, the alert application creates it automatically).
|
||||
* **tdengine**: connection string of `TDEngine` server, note in most cases the database information should be put in a rule, thus it should NOT be included here.
|
||||
* **log > level**: log level, could be `production` or `debug`.
|
||||
* **log > path**: log output file path.
|
||||
* **receivers > alertManager**: the alert application pushes alerts to `AlertManager` at this URL.
|
||||
* **receivers > console**: print out alerts to console (stdout) or not.
|
||||
|
||||
When the configruation file is ready, the alert application can be started with below command (`alert.cfg` is the path of the configuration file):
|
||||
|
||||
```
|
||||
$ ./alert -cfg alert.cfg
|
||||
```
|
||||
|
||||
## Prepare an alert rule
|
||||
|
||||
From technical aspect, an alert could be defined as: query and filter recent data from `TDEngine`, and calculating out a boolean value from these data according to a formula, and trigger an alert if the boolean value last for a certain duration.
|
||||
|
||||
This is a rule example in `json` format:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "rule1",
|
||||
"sql": "select sum(col1) as sumCol1 from test.meters where ts > now - 1h group by areaid",
|
||||
"expr": "sumCol1 > 10",
|
||||
"for": "10m",
|
||||
"period": "1m",
|
||||
"labels": {
|
||||
"ruleName": "rule1"
|
||||
},
|
||||
"annotations": {
|
||||
"summary": "sum of rule {{$labels.ruleName}} of area {{$values.areaid}} is {{$values.sumCol1}}"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The fields of the rule is explained below:
|
||||
|
||||
* **name**: the name of the rule, must be unique.
|
||||
* **sql**: this is the `sql` statement used to query data from `TDEngine`, columns of the query result are used in later processing, so please give the column an alias if aggregation functions are used.
|
||||
* **expr**: an expression whose result is a boolean value, arithmatic and logical calculations can be included in the expression, and builtin functions (see below) are also supported. Alerts are only triggered when the expression evaluates to `true`.
|
||||
* **for**: this item is a duration which default value is zero second. when `expr` evaluates to `true` and last at least this duration, an alert is triggered.
|
||||
* **period**: the interval for the alert application to check the rule, default is 1 minute.
|
||||
* **labels**: a label list, labels are used to generate alert information. note if the `sql` statement includes a `group by` clause, the `group by` columns are inserted into this list automatically.
|
||||
* **annotations**: the template of alert information which is in [go template](https://golang.org/pkg/text/template) syntax, labels can be referenced by `$labels.<label name>` and columns of the query result can be referenced by `$values.<column name>`.
|
||||
|
||||
### Operators
|
||||
|
||||
Operators which can be used in the `expr` field of a rule are list below, `()` can be to change precedence if default does not meet requirement.
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr> <td>Operator</td><td>Unary/Binary</td><td>Precedence</td><td>Effect</td> </tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr> <td>~</td><td>Unary</td><td>6</td><td>Bitwise Not</td> </tr>
|
||||
<tr> <td>!</td><td>Unary</td><td>6</td><td>Logical Not</td> </tr>
|
||||
<tr> <td>+</td><td>Unary</td><td>6</td><td>Positive Sign</td> </tr>
|
||||
<tr> <td>-</td><td>Unary</td><td>6</td><td>Negative Sign</td> </tr>
|
||||
<tr> <td>*</td><td>Binary</td><td>5</td><td>Multiplication</td> </tr>
|
||||
<tr> <td>/</td><td>Binary</td><td>5</td><td>Division</td> </tr>
|
||||
<tr> <td>%</td><td>Binary</td><td>5</td><td>Modulus</td> </tr>
|
||||
<tr> <td><<</td><td>Binary</td><td>5</td><td>Bitwise Left Shift</td> </tr>
|
||||
<tr> <td>>></td><td>Binary</td><td>5</td><td>Bitwise Right Shift</td> </tr>
|
||||
<tr> <td>&</td><td>Binary</td><td>5</td><td>Bitwise And</td> </tr>
|
||||
<tr> <td>+</td><td>Binary</td><td>4</td><td>Addition</td> </tr>
|
||||
<tr> <td>-</td><td>Binary</td><td>4</td><td>Subtraction</td> </tr>
|
||||
<tr> <td>|</td><td>Binary</td><td>4</td><td>Bitwise Or</td> </tr>
|
||||
<tr> <td>^</td><td>Binary</td><td>4</td><td>Bitwise Xor</td> </tr>
|
||||
<tr> <td>==</td><td>Binary</td><td>3</td><td>Equal</td> </tr>
|
||||
<tr> <td>!=</td><td>Binary</td><td>3</td><td>Not Equal</td> </tr>
|
||||
<tr> <td><</td><td>Binary</td><td>3</td><td>Less Than</td> </tr>
|
||||
<tr> <td><=</td><td>Binary</td><td>3</td><td>Less Than or Equal</td> </tr>
|
||||
<tr> <td>></td><td>Binary</td><td>3</td><td>Great Than</td> </tr>
|
||||
<tr> <td>>=</td><td>Binary</td><td>3</td><td>Great Than or Equal</td> </tr>
|
||||
<tr> <td>&&</td><td>Binary</td><td>2</td><td>Logical And</td> </tr>
|
||||
<tr> <td>||</td><td>Binary</td><td>1</td><td>Logical Or</td> </tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
### Built-in Functions
|
||||
|
||||
Built-in function can be used in the `expr` field of a rule.
|
||||
|
||||
* **min**: returns the minimum one of its arguments, for example: `min(1, 2, 3)` returns `1`.
|
||||
* **max**: returns the maximum one of its arguments, for example: `max(1, 2, 3)` returns `3`.
|
||||
* **sum**: returns the sum of its arguments, for example: `sum(1, 2, 3)` returns `6`.
|
||||
* **avg**: returns the average of its arguments, for example: `avg(1, 2, 3)` returns `2`.
|
||||
* **sqrt**: returns the square root of its argument, for example: `sqrt(9)` returns `3`.
|
||||
* **ceil**: returns the minimum integer which greater or equal to its argument, for example: `ceil(9.1)` returns `10`.
|
||||
* **floor**: returns the maximum integer which lesser or equal to its argument, for example: `floor(9.9)` returns `9`.
|
||||
* **round**: round its argument to nearest integer, for examples: `round(9.9)` returns `10` and `round(9.1)` returns `9`.
|
||||
* **log**: returns the natural logarithm of its argument, for example: `log(10)` returns `2.302585`.
|
||||
* **log10**: returns base 10 logarithm of its argument, for example: `log10(10)` return `1`.
|
||||
* **abs**: returns the absolute value of its argument, for example: `abs(-1)` returns `1`.
|
||||
* **if**: if the first argument is `true` returns its second argument, and returns its third argument otherwise, for examples: `if(true, 10, 100)` returns `10` and `if(false, 10, 100)` returns `100`.
|
||||
|
||||
## Rule Management
|
||||
|
||||
* Add / Update
|
||||
|
||||
* API address: http://\<server\>:\<port\>/api/update-rule
|
||||
* Method: POST
|
||||
* Body: the rule
|
||||
* Example:curl -d '@rule.json' http://localhost:8100/api/update-rule
|
||||
|
||||
* Delete
|
||||
|
||||
* API address: http://\<server\>:\<port\>/api/delete-rule?name=\<rule name\>
|
||||
* Method:DELETE
|
||||
* Example:curl -X DELETE http://localhost:8100/api/delete-rule?name=rule1
|
||||
|
||||
* Enable / Disable
|
||||
|
||||
* API address: http://\<server\>:\<port\>/api/enable-rule?name=\<rule name\>&enable=[true | false]
|
||||
* Method POST
|
||||
* Example:curl -X POST http://localhost:8100/api/enable-rule?name=rule1&enable=true
|
||||
|
||||
* Retrieve rule list
|
||||
|
||||
* API address: http://\<server\>:\<port\>/api/list-rule
|
||||
* Method: GET
|
||||
* Example:curl http://localhost:8100/api/list-rule
|
|
@ -0,0 +1,177 @@
|
|||
# Alert [草稿]
|
||||
|
||||
报警监测程序,从 [TDEngine](https://www.taosdata.com/) 读取数据后,根据预定义的规则计算和生成报警,并将它们推送到 [AlertManager](https://github.com/prometheus/alertmanager) 或其它下游应用。
|
||||
|
||||
## 安装
|
||||
|
||||
### 使用编译好的二进制文件
|
||||
|
||||
TODO: 安装包具体位置
|
||||
|
||||
您可以从 [涛思数据](https://www.taosdata.com/) 官网下载最新的安装包。下载完成后,使用以下命令解压:
|
||||
|
||||
```
|
||||
$ tar -xzf alert-$version-$OS-$ARCH.tar.gz
|
||||
```
|
||||
|
||||
### 从源码安装
|
||||
|
||||
从源码安装需要在您用于编译的计算机上提前安装好 TDEngine 的服务端或客户端,如果您还没有安装,可以参考 TDEngine 的文档。
|
||||
|
||||
报警监测程序使用 [Go语言](https://golang.org) 开发,请安装最新版的 Go 语言编译环境。
|
||||
|
||||
```
|
||||
$ mkdir taosdata
|
||||
$ cd taosdata
|
||||
$ git clone https://github.com/taosdata/tdengine.git
|
||||
$ cd tdengine/alert/cmd/alert
|
||||
$ go build
|
||||
```
|
||||
|
||||
如果由于部分包无法下载导致 `go build` 失败,请根据 [goproxy.io](https://goproxy.io) 上的说明配置好 `GOPROXY` 再重新执行 `go build`。
|
||||
|
||||
## 配置
|
||||
|
||||
报警监测程序的配置文件采用标准`json`格式,下面是默认的文件内容,请根据实际情况修改。
|
||||
|
||||
```json
|
||||
{
|
||||
"port": 8100,
|
||||
"database": "file:alert.db",
|
||||
"tdengine": "root:taosdata@/tcp(127.0.0.1:0)/",
|
||||
"log": {
|
||||
"level": "production",
|
||||
"path": "alert.log"
|
||||
},
|
||||
"receivers": {
|
||||
"alertManager": "http://127.0.0.1:9093/api/v1/alerts",
|
||||
"console": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
其中:
|
||||
|
||||
* **port**:报警监测程序支持使用 `restful API` 对规则进行管理,这个参数用于配置 `http` 服务的侦听端口。
|
||||
* **database**:报警监测程序将规则保存到了一个 `sqlite` 数据库中,这个参数用于指定数据库文件的路径(不需要提前创建这个文件,如果它不存在,程序会自动创建它)。
|
||||
* **tdengine**:`TDEngine` 的连接信息,一般来说,数据库信息应该在报警规则中指定,所以这里 **不** 应包含这一部分信息。
|
||||
* **log > level**:日志的记录级别,可选 `production` 或 `debug`。
|
||||
* **log > path**:日志文件的路径。
|
||||
* **receivers > alertManager**:报警监测程序会将报警推送到 `AlertManager`,在这里指定 `AlertManager` 的接收地址。
|
||||
* **receivers > console**:是否输出到控制台 (stdout)。
|
||||
|
||||
准备好配置文件后,可使用下面的命令启动报警监测程序( `alert.cfg` 是配置文件的路径):
|
||||
|
||||
```
|
||||
$ ./alert -cfg alert.cfg
|
||||
```
|
||||
|
||||
## 编写报警规则
|
||||
|
||||
从技术角度,可以将报警描述为:从 `TDEngine` 中查询最近一段时间、符合一定过滤条件的数据,并基于这些数据根据定义好的计算方法得出一个结果,当结果符合某个条件且持续一定时间后,触发报警。
|
||||
|
||||
根据上面的描述,可以很容易的知道报警规则中需要包含的大部分信息。 以下是一个完整的报警规则,采用标准 `json` 格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "rule1",
|
||||
"sql": "select sum(col1) as sumCol1 from test.meters where ts > now - 1h group by areaid",
|
||||
"expr": "sumCol1 > 10",
|
||||
"for": "10m",
|
||||
"period": "1m",
|
||||
"labels": {
|
||||
"ruleName": "rule1"
|
||||
},
|
||||
"annotations": {
|
||||
"summary": "sum of rule {{$labels.ruleName}} of area {{$values.areaid}} is {{$values.sumCol1}}"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
其中:
|
||||
|
||||
* **name**:用于为规则指定一个唯一的名字。
|
||||
* **sql**:从 `TDEngine` 中查询数据时使用的 `sql` 语句,查询结果中的列将被后续计算使用,所以,如果使用了聚合函数,请为这一列指定一个别名。
|
||||
* **expr**:一个计算结果为布尔型的表达式,支持算数运算、逻辑运算,并且内置了部分函数,也可以引用查询结果中的列。 当表达式计算结果为 `true` 时,进入报警状态。
|
||||
* **for**:当表达式计算结果为 `true` 的连续时长超过这个选项时,触发报警,否则报警处于“待定”状态。默认为0,表示一旦计算结果为 `true`,立即触发报警。
|
||||
* **period**:规则的检查周期,默认1分钟。
|
||||
* **labels**:人为指定的标签列表,标签可以在生成报警信息引用。如果 `sql` 中包含 `group by` 子句,则所有用于分组的字段会被自动加入这个标签列表中。
|
||||
* **annotations**:用于定义报警信息,使用 [go template](https://golang.org/pkg/text/template) 语法,其中,可以通过 `$labels.<label name>` 引用标签,也可以通过 `$values.<column name>` 引用查询结果中的列。
|
||||
|
||||
### 运算符
|
||||
|
||||
以下是 `expr` 字段中支持的运算符,您可以使用 `()` 改变运算的优先级。
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr> <td>运算符</td><td>单目/双目</td><td>优先级</td><td>作用</td> </tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr> <td>~</td><td>单目</td><td>6</td><td>按位取反</td> </tr>
|
||||
<tr> <td>!</td><td>单目</td><td>6</td><td>逻辑非</td> </tr>
|
||||
<tr> <td>+</td><td>单目</td><td>6</td><td>正号</td> </tr>
|
||||
<tr> <td>-</td><td>单目</td><td>6</td><td>负号</td> </tr>
|
||||
<tr> <td>*</td><td>双目</td><td>5</td><td>乘法</td> </tr>
|
||||
<tr> <td>/</td><td>双目</td><td>5</td><td>除法</td> </tr>
|
||||
<tr> <td>%</td><td>双目</td><td>5</td><td>取模(余数)</td> </tr>
|
||||
<tr> <td><<</td><td>双目</td><td>5</td><td>按位左移</td> </tr>
|
||||
<tr> <td>>></td><td>双目</td><td>5</td><td>按位右移</td> </tr>
|
||||
<tr> <td>&</td><td>双目</td><td>5</td><td>按位与</td> </tr>
|
||||
<tr> <td>+</td><td>双目</td><td>4</td><td>加法</td> </tr>
|
||||
<tr> <td>-</td><td>双目</td><td>4</td><td>减法</td> </tr>
|
||||
<tr> <td>|</td><td>双目</td><td>4</td><td>按位或</td> </tr>
|
||||
<tr> <td>^</td><td>双目</td><td>4</td><td>按位异或</td> </tr>
|
||||
<tr> <td>==</td><td>双目</td><td>3</td><td>等于</td> </tr>
|
||||
<tr> <td>!=</td><td>双目</td><td>3</td><td>不等于</td> </tr>
|
||||
<tr> <td><</td><td>双目</td><td>3</td><td>小于</td> </tr>
|
||||
<tr> <td><=</td><td>双目</td><td>3</td><td>小于或等于</td> </tr>
|
||||
<tr> <td>></td><td>双目</td><td>3</td><td>大于</td> </tr>
|
||||
<tr> <td>>=</td><td>双目</td><td>3</td><td>大于或等于</td> </tr>
|
||||
<tr> <td>&&</td><td>双目</td><td>2</td><td>逻辑与</td> </tr>
|
||||
<tr> <td>||</td><td>双目</td><td>1</td><td>逻辑或</td> </tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
### 内置函数
|
||||
|
||||
目前支持以下内置函数,可以在报警规则的 `expr` 字段中使用这些函数:
|
||||
|
||||
* **min**:取多个值中的最小值,例如 `min(1, 2, 3)` 返回 `1`。
|
||||
* **max**:取多个值中的最大值,例如 `max(1, 2, 3)` 返回 `3`。
|
||||
* **sum**:求和,例如 `sum(1, 2, 3)` 返回 `6`。
|
||||
* **avg**:求算术平均值,例如 `avg(1, 2, 3)` 返回 `2`。
|
||||
* **sqrt**:计算平方根,例如 `sqrt(9)` 返回 `3`。
|
||||
* **ceil**:上取整,例如 `ceil(9.1)` 返回 `10`。
|
||||
* **floor**:下取整,例如 `floor(9.9)` 返回 `9`。
|
||||
* **round**:四舍五入,例如 `round(9.9)` 返回 `10`, `round(9.1)` 返回 `9`。
|
||||
* **log**:计算自然对数,例如 `log(10)` 返回 `2.302585`。
|
||||
* **log10**:计算以10为底的对数,例如 `log10(10)` 返回 `1`。
|
||||
* **abs**:计算绝对值,例如 `abs(-1)` 返回 `1`。
|
||||
* **if**:如果第一个参数为 `true`,返回第二个参数,否则返回第三个参数,例如 `if(true, 10, 100)` 返回 `10`, `if(false, 10, 100)` 返回 `100`。
|
||||
|
||||
## 规则管理
|
||||
|
||||
* 添加或修改
|
||||
|
||||
* API地址:http://\<server\>:\<port\>/api/update-rule
|
||||
* Method:POST
|
||||
* Body:规则定义
|
||||
* 示例:curl -d '@rule.json' http://localhost:8100/api/update-rule
|
||||
|
||||
* 删除
|
||||
|
||||
* API地址:http://\<server\>:\<port\>/api/delete-rule?name=\<rule name\>
|
||||
* Method:DELETE
|
||||
* 示例:curl -X DELETE http://localhost:8100/api/delete-rule?name=rule1
|
||||
|
||||
* 挂起或恢复
|
||||
|
||||
* API地址:http://\<server\>:\<port\>/api/enable-rule?name=\<rule name\>&enable=[true | false]
|
||||
* Method:POST
|
||||
* 示例:curl -X POST http://localhost:8100/api/enable-rule?name=rule1&enable=true
|
||||
|
||||
* 获取列表
|
||||
|
||||
* API地址:http://\<server\>:\<port\>/api/list-rule
|
||||
* Method:GET
|
||||
* 示例:curl http://localhost:8100/api/list-rule
|
|
@ -0,0 +1,175 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/taosdata/alert/models"
|
||||
"github.com/taosdata/alert/utils"
|
||||
"github.com/taosdata/alert/utils/log"
|
||||
)
|
||||
|
||||
func Init() error {
|
||||
if e := initRule(); e != nil {
|
||||
return e
|
||||
}
|
||||
|
||||
http.HandleFunc("/api/list-rule", onListRule)
|
||||
http.HandleFunc("/api/list-alert", onListAlert)
|
||||
http.HandleFunc("/api/update-rule", onUpdateRule)
|
||||
http.HandleFunc("/api/enable-rule", onEnableRule)
|
||||
http.HandleFunc("/api/delete-rule", onDeleteRule)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Uninit() error {
|
||||
uninitRule()
|
||||
return nil
|
||||
}
|
||||
|
||||
func onListRule(w http.ResponseWriter, r *http.Request) {
|
||||
var res []*Rule
|
||||
rules.Range(func(k, v interface{}) bool {
|
||||
res = append(res, v.(*Rule))
|
||||
return true
|
||||
})
|
||||
|
||||
w.Header().Add("Content-Type", "application/json; charset=utf-8")
|
||||
json.NewEncoder(w).Encode(res)
|
||||
}
|
||||
|
||||
func onListAlert(w http.ResponseWriter, r *http.Request) {
|
||||
var alerts []*Alert
|
||||
rn := r.URL.Query().Get("rule")
|
||||
rules.Range(func(k, v interface{}) bool {
|
||||
if len(rn) > 0 && rn != k.(string) {
|
||||
return true
|
||||
}
|
||||
|
||||
rule := v.(*Rule)
|
||||
rule.Alerts.Range(func(k, v interface{}) bool {
|
||||
alert := v.(*Alert)
|
||||
// TODO: not go-routine safe
|
||||
if alert.State != AlertStateWaiting {
|
||||
alerts = append(alerts, v.(*Alert))
|
||||
}
|
||||
return true
|
||||
})
|
||||
return true
|
||||
})
|
||||
w.Header().Add("Content-Type", "application/json; charset=utf-8")
|
||||
json.NewEncoder(w).Encode(alerts)
|
||||
}
|
||||
|
||||
func onUpdateRule(w http.ResponseWriter, r *http.Request) {
|
||||
data, e := ioutil.ReadAll(r.Body)
|
||||
if e != nil {
|
||||
log.Error("failed to read request body: ", e.Error())
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
rule, e := newRule(string(data))
|
||||
if e != nil {
|
||||
log.Error("failed to parse rule: ", e.Error())
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if e = doUpdateRule(rule, string(data)); e != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func doUpdateRule(rule *Rule, ruleStr string) error {
|
||||
if _, ok := rules.Load(rule.Name); ok {
|
||||
if len(utils.Cfg.Database) > 0 {
|
||||
e := models.UpdateRule(rule.Name, ruleStr)
|
||||
if e != nil {
|
||||
log.Errorf("[%s]: update failed: %s", rule.Name, e.Error())
|
||||
return e
|
||||
}
|
||||
}
|
||||
log.Infof("[%s]: update succeeded.", rule.Name)
|
||||
} else {
|
||||
if len(utils.Cfg.Database) > 0 {
|
||||
e := models.AddRule(&models.Rule{
|
||||
Name: rule.Name,
|
||||
Content: ruleStr,
|
||||
})
|
||||
if e != nil {
|
||||
log.Errorf("[%s]: add failed: %s", rule.Name, e.Error())
|
||||
return e
|
||||
}
|
||||
}
|
||||
log.Infof("[%s]: add succeeded.", rule.Name)
|
||||
}
|
||||
|
||||
rules.Store(rule.Name, rule)
|
||||
return nil
|
||||
}
|
||||
|
||||
func onEnableRule(w http.ResponseWriter, r *http.Request) {
|
||||
var rule *Rule
|
||||
name := r.URL.Query().Get("name")
|
||||
enable := strings.ToLower(r.URL.Query().Get("enable")) == "true"
|
||||
|
||||
if x, ok := rules.Load(name); ok {
|
||||
rule = x.(*Rule)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if rule.isEnabled() == enable {
|
||||
return
|
||||
}
|
||||
|
||||
if len(utils.Cfg.Database) > 0 {
|
||||
if e := models.EnableRule(name, enable); e != nil {
|
||||
if enable {
|
||||
log.Errorf("[%s]: enable failed: ", name, e.Error())
|
||||
} else {
|
||||
log.Errorf("[%s]: disable failed: ", name, e.Error())
|
||||
}
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if enable {
|
||||
rule = rule.clone()
|
||||
rule.setNextRunTime(time.Now())
|
||||
rules.Store(rule.Name, rule)
|
||||
log.Infof("[%s]: enable succeeded.", name)
|
||||
} else {
|
||||
rule.setState(RuleStateDisabled)
|
||||
log.Infof("[%s]: disable succeeded.", name)
|
||||
}
|
||||
}
|
||||
|
||||
func onDeleteRule(w http.ResponseWriter, r *http.Request) {
|
||||
name := r.URL.Query().Get("name")
|
||||
if len(name) == 0 {
|
||||
return
|
||||
}
|
||||
if e := doDeleteRule(name); e != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func doDeleteRule(name string) error {
|
||||
if len(utils.Cfg.Database) > 0 {
|
||||
if e := models.DeleteRule(name); e != nil {
|
||||
log.Errorf("[%s]: delete failed: %s", name, e.Error())
|
||||
return e
|
||||
}
|
||||
}
|
||||
rules.Delete(name)
|
||||
log.Infof("[%s]: delete succeeded.", name)
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,792 @@
|
|||
package expr
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/scanner"
|
||||
)
|
||||
|
||||
var (
|
||||
// compile errors
|
||||
ErrorExpressionSyntax = errors.New("expression syntax error")
|
||||
ErrorUnrecognizedFunction = errors.New("unrecognized function")
|
||||
ErrorArgumentCount = errors.New("too many/few arguments")
|
||||
ErrorInvalidFloat = errors.New("invalid float")
|
||||
ErrorInvalidInteger = errors.New("invalid integer")
|
||||
|
||||
// eval errors
|
||||
ErrorUnsupportedDataType = errors.New("unsupported data type")
|
||||
ErrorInvalidOperationFloat = errors.New("invalid operation for float")
|
||||
ErrorInvalidOperationInteger = errors.New("invalid operation for integer")
|
||||
ErrorInvalidOperationBoolean = errors.New("invalid operation for boolean")
|
||||
ErrorOnlyIntegerAllowed = errors.New("only integers is allowed")
|
||||
ErrorDataTypeMismatch = errors.New("data type mismatch")
|
||||
)
|
||||
|
||||
// binary operator precedence
|
||||
// 5 * / % << >> &
|
||||
// 4 + - | ^
|
||||
// 3 == != < <= > >=
|
||||
// 2 &&
|
||||
// 1 ||
|
||||
const (
|
||||
opOr = -(iota + 1000) // ||
|
||||
opAnd // &&
|
||||
opEqual // ==
|
||||
opNotEqual // !=
|
||||
opGTE // >=
|
||||
opLTE // <=
|
||||
opLeftShift // <<
|
||||
opRightShift // >>
|
||||
)
|
||||
|
||||
type lexer struct {
|
||||
scan scanner.Scanner
|
||||
tok rune
|
||||
}
|
||||
|
||||
func (l *lexer) init(src io.Reader) {
|
||||
l.scan.Error = func(s *scanner.Scanner, msg string) {
|
||||
panic(errors.New(msg))
|
||||
}
|
||||
l.scan.Mode = scanner.ScanIdents | scanner.ScanInts | scanner.ScanFloats | scanner.ScanStrings
|
||||
l.scan.Init(src)
|
||||
l.tok = l.next()
|
||||
}
|
||||
|
||||
func (l *lexer) next() rune {
|
||||
l.tok = l.scan.Scan()
|
||||
|
||||
switch l.tok {
|
||||
case '|':
|
||||
if l.scan.Peek() == '|' {
|
||||
l.tok = opOr
|
||||
l.scan.Scan()
|
||||
}
|
||||
|
||||
case '&':
|
||||
if l.scan.Peek() == '&' {
|
||||
l.tok = opAnd
|
||||
l.scan.Scan()
|
||||
}
|
||||
|
||||
case '=':
|
||||
if l.scan.Peek() == '=' {
|
||||
l.tok = opEqual
|
||||
l.scan.Scan()
|
||||
} else {
|
||||
// TODO: error
|
||||
}
|
||||
|
||||
case '!':
|
||||
if l.scan.Peek() == '=' {
|
||||
l.tok = opNotEqual
|
||||
l.scan.Scan()
|
||||
} else {
|
||||
// TODO: error
|
||||
}
|
||||
|
||||
case '<':
|
||||
if tok := l.scan.Peek(); tok == '<' {
|
||||
l.tok = opLeftShift
|
||||
l.scan.Scan()
|
||||
} else if tok == '=' {
|
||||
l.tok = opLTE
|
||||
l.scan.Scan()
|
||||
}
|
||||
|
||||
case '>':
|
||||
if tok := l.scan.Peek(); tok == '>' {
|
||||
l.tok = opRightShift
|
||||
l.scan.Scan()
|
||||
} else if tok == '=' {
|
||||
l.tok = opGTE
|
||||
l.scan.Scan()
|
||||
}
|
||||
}
|
||||
return l.tok
|
||||
}
|
||||
|
||||
func (l *lexer) token() rune {
|
||||
return l.tok
|
||||
}
|
||||
|
||||
func (l *lexer) text() string {
|
||||
switch l.tok {
|
||||
case opOr:
|
||||
return "||"
|
||||
case opAnd:
|
||||
return "&&"
|
||||
case opEqual:
|
||||
return "=="
|
||||
case opNotEqual:
|
||||
return "!="
|
||||
case opLeftShift:
|
||||
return "<<"
|
||||
case opLTE:
|
||||
return "<="
|
||||
case opRightShift:
|
||||
return ">>"
|
||||
case opGTE:
|
||||
return ">="
|
||||
default:
|
||||
return l.scan.TokenText()
|
||||
}
|
||||
}
|
||||
|
||||
type Expr interface {
|
||||
Eval(env func(string) interface{}) interface{}
|
||||
}
|
||||
|
||||
type unaryExpr struct {
|
||||
op rune
|
||||
subExpr Expr
|
||||
}
|
||||
|
||||
func (ue *unaryExpr) Eval(env func(string) interface{}) interface{} {
|
||||
val := ue.subExpr.Eval(env)
|
||||
switch v := val.(type) {
|
||||
case float64:
|
||||
if ue.op != '-' {
|
||||
panic(ErrorInvalidOperationFloat)
|
||||
}
|
||||
return -v
|
||||
case int64:
|
||||
switch ue.op {
|
||||
case '-':
|
||||
return -v
|
||||
case '~':
|
||||
return ^v
|
||||
default:
|
||||
panic(ErrorInvalidOperationInteger)
|
||||
}
|
||||
case bool:
|
||||
if ue.op != '!' {
|
||||
panic(ErrorInvalidOperationBoolean)
|
||||
}
|
||||
return !v
|
||||
default:
|
||||
panic(ErrorUnsupportedDataType)
|
||||
}
|
||||
}
|
||||
|
||||
type binaryExpr struct {
|
||||
op rune
|
||||
lhs Expr
|
||||
rhs Expr
|
||||
}
|
||||
|
||||
func (be *binaryExpr) Eval(env func(string) interface{}) interface{} {
|
||||
lval := be.lhs.Eval(env)
|
||||
rval := be.rhs.Eval(env)
|
||||
|
||||
switch be.op {
|
||||
case '*':
|
||||
switch lv := lval.(type) {
|
||||
case float64:
|
||||
switch rv := rval.(type) {
|
||||
case float64:
|
||||
return lv * rv
|
||||
case int64:
|
||||
return lv * float64(rv)
|
||||
case bool:
|
||||
panic(ErrorInvalidOperationBoolean)
|
||||
}
|
||||
case int64:
|
||||
switch rv := rval.(type) {
|
||||
case float64:
|
||||
return float64(lv) * rv
|
||||
case int64:
|
||||
return lv * rv
|
||||
case bool:
|
||||
panic(ErrorInvalidOperationBoolean)
|
||||
}
|
||||
case bool:
|
||||
panic(ErrorInvalidOperationBoolean)
|
||||
}
|
||||
|
||||
case '/':
|
||||
switch lv := lval.(type) {
|
||||
case float64:
|
||||
switch rv := rval.(type) {
|
||||
case float64:
|
||||
if rv == 0 {
|
||||
return math.Inf(int(lv))
|
||||
} else {
|
||||
return lv / rv
|
||||
}
|
||||
case int64:
|
||||
if rv == 0 {
|
||||
return math.Inf(int(lv))
|
||||
} else {
|
||||
return lv / float64(rv)
|
||||
}
|
||||
case bool:
|
||||
panic(ErrorInvalidOperationBoolean)
|
||||
}
|
||||
case int64:
|
||||
switch rv := rval.(type) {
|
||||
case float64:
|
||||
if rv == 0 {
|
||||
return math.Inf(int(lv))
|
||||
} else {
|
||||
return float64(lv) / rv
|
||||
}
|
||||
case int64:
|
||||
if rv == 0 {
|
||||
return math.Inf(int(lv))
|
||||
} else {
|
||||
return lv / rv
|
||||
}
|
||||
case bool:
|
||||
panic(ErrorInvalidOperationBoolean)
|
||||
}
|
||||
case bool:
|
||||
panic(ErrorInvalidOperationBoolean)
|
||||
}
|
||||
|
||||
case '%':
|
||||
switch lv := lval.(type) {
|
||||
case float64:
|
||||
switch rv := rval.(type) {
|
||||
case float64:
|
||||
return math.Mod(lv, rv)
|
||||
case int64:
|
||||
return math.Mod(lv, float64(rv))
|
||||
case bool:
|
||||
panic(ErrorInvalidOperationBoolean)
|
||||
}
|
||||
case int64:
|
||||
switch rv := rval.(type) {
|
||||
case float64:
|
||||
return math.Mod(float64(lv), rv)
|
||||
case int64:
|
||||
if rv == 0 {
|
||||
return math.Inf(int(lv))
|
||||
} else {
|
||||
return lv % rv
|
||||
}
|
||||
case bool:
|
||||
panic(ErrorInvalidOperationBoolean)
|
||||
}
|
||||
case bool:
|
||||
panic(ErrorInvalidOperationBoolean)
|
||||
}
|
||||
|
||||
case opLeftShift:
|
||||
switch lv := lval.(type) {
|
||||
case int64:
|
||||
switch rv := rval.(type) {
|
||||
case int64:
|
||||
return lv << rv
|
||||
default:
|
||||
panic(ErrorOnlyIntegerAllowed)
|
||||
}
|
||||
default:
|
||||
panic(ErrorOnlyIntegerAllowed)
|
||||
}
|
||||
|
||||
case opRightShift:
|
||||
switch lv := lval.(type) {
|
||||
case int64:
|
||||
switch rv := rval.(type) {
|
||||
case int64:
|
||||
return lv >> rv
|
||||
default:
|
||||
panic(ErrorOnlyIntegerAllowed)
|
||||
}
|
||||
default:
|
||||
panic(ErrorOnlyIntegerAllowed)
|
||||
}
|
||||
|
||||
case '&':
|
||||
switch lv := lval.(type) {
|
||||
case int64:
|
||||
switch rv := rval.(type) {
|
||||
case int64:
|
||||
return lv & rv
|
||||
default:
|
||||
panic(ErrorOnlyIntegerAllowed)
|
||||
}
|
||||
default:
|
||||
panic(ErrorOnlyIntegerAllowed)
|
||||
}
|
||||
|
||||
case '+':
|
||||
switch lv := lval.(type) {
|
||||
case float64:
|
||||
switch rv := rval.(type) {
|
||||
case float64:
|
||||
return lv + rv
|
||||
case int64:
|
||||
return lv + float64(rv)
|
||||
case bool:
|
||||
panic(ErrorInvalidOperationBoolean)
|
||||
}
|
||||
case int64:
|
||||
switch rv := rval.(type) {
|
||||
case float64:
|
||||
return float64(lv) + rv
|
||||
case int64:
|
||||
return lv + rv
|
||||
case bool:
|
||||
panic(ErrorInvalidOperationBoolean)
|
||||
}
|
||||
case bool:
|
||||
panic(ErrorInvalidOperationBoolean)
|
||||
}
|
||||
|
||||
case '-':
|
||||
switch lv := lval.(type) {
|
||||
case float64:
|
||||
switch rv := rval.(type) {
|
||||
case float64:
|
||||
return lv - rv
|
||||
case int64:
|
||||
return lv - float64(rv)
|
||||
case bool:
|
||||
panic(ErrorInvalidOperationBoolean)
|
||||
}
|
||||
case int64:
|
||||
switch rv := rval.(type) {
|
||||
case float64:
|
||||
return float64(lv) - rv
|
||||
case int64:
|
||||
return lv - rv
|
||||
case bool:
|
||||
panic(ErrorInvalidOperationBoolean)
|
||||
}
|
||||
case bool:
|
||||
panic(ErrorInvalidOperationBoolean)
|
||||
}
|
||||
|
||||
case '|':
|
||||
switch lv := lval.(type) {
|
||||
case int64:
|
||||
switch rv := rval.(type) {
|
||||
case int64:
|
||||
return lv | rv
|
||||
default:
|
||||
panic(ErrorOnlyIntegerAllowed)
|
||||
}
|
||||
default:
|
||||
panic(ErrorOnlyIntegerAllowed)
|
||||
}
|
||||
|
||||
case '^':
|
||||
switch lv := lval.(type) {
|
||||
case int64:
|
||||
switch rv := rval.(type) {
|
||||
case int64:
|
||||
return lv ^ rv
|
||||
default:
|
||||
panic(ErrorOnlyIntegerAllowed)
|
||||
}
|
||||
default:
|
||||
panic(ErrorOnlyIntegerAllowed)
|
||||
}
|
||||
|
||||
case opEqual:
|
||||
switch lv := lval.(type) {
|
||||
case float64:
|
||||
switch rv := rval.(type) {
|
||||
case float64:
|
||||
return lv == rv
|
||||
case int64:
|
||||
return lv == float64(rv)
|
||||
case bool:
|
||||
panic(ErrorDataTypeMismatch)
|
||||
}
|
||||
case int64:
|
||||
switch rv := rval.(type) {
|
||||
case float64:
|
||||
return float64(lv) == rv
|
||||
case int64:
|
||||
return lv == rv
|
||||
case bool:
|
||||
panic(ErrorDataTypeMismatch)
|
||||
}
|
||||
case bool:
|
||||
switch rv := rval.(type) {
|
||||
case float64:
|
||||
case int64:
|
||||
case bool:
|
||||
return lv == rv
|
||||
}
|
||||
}
|
||||
|
||||
case opNotEqual:
|
||||
switch lv := lval.(type) {
|
||||
case float64:
|
||||
switch rv := rval.(type) {
|
||||
case float64:
|
||||
return lv != rv
|
||||
case int64:
|
||||
return lv != float64(rv)
|
||||
case bool:
|
||||
panic(ErrorDataTypeMismatch)
|
||||
}
|
||||
case int64:
|
||||
switch rv := rval.(type) {
|
||||
case float64:
|
||||
return float64(lv) != rv
|
||||
case int64:
|
||||
return lv != rv
|
||||
case bool:
|
||||
panic(ErrorDataTypeMismatch)
|
||||
}
|
||||
case bool:
|
||||
switch rv := rval.(type) {
|
||||
case float64:
|
||||
case int64:
|
||||
case bool:
|
||||
return lv != rv
|
||||
}
|
||||
}
|
||||
|
||||
case '<':
|
||||
switch lv := lval.(type) {
|
||||
case float64:
|
||||
switch rv := rval.(type) {
|
||||
case float64:
|
||||
return lv < rv
|
||||
case int64:
|
||||
return lv < float64(rv)
|
||||
case bool:
|
||||
panic(ErrorDataTypeMismatch)
|
||||
}
|
||||
case int64:
|
||||
switch rv := rval.(type) {
|
||||
case float64:
|
||||
return float64(lv) < rv
|
||||
case int64:
|
||||
return lv < rv
|
||||
case bool:
|
||||
panic(ErrorDataTypeMismatch)
|
||||
}
|
||||
case bool:
|
||||
panic(ErrorInvalidOperationBoolean)
|
||||
}
|
||||
|
||||
case opLTE:
|
||||
switch lv := lval.(type) {
|
||||
case float64:
|
||||
switch rv := rval.(type) {
|
||||
case float64:
|
||||
return lv <= rv
|
||||
case int64:
|
||||
return lv <= float64(rv)
|
||||
case bool:
|
||||
panic(ErrorDataTypeMismatch)
|
||||
}
|
||||
case int64:
|
||||
switch rv := rval.(type) {
|
||||
case float64:
|
||||
return float64(lv) <= rv
|
||||
case int64:
|
||||
return lv <= rv
|
||||
case bool:
|
||||
panic(ErrorDataTypeMismatch)
|
||||
}
|
||||
case bool:
|
||||
panic(ErrorInvalidOperationBoolean)
|
||||
}
|
||||
|
||||
case '>':
|
||||
switch lv := lval.(type) {
|
||||
case float64:
|
||||
switch rv := rval.(type) {
|
||||
case float64:
|
||||
return lv > rv
|
||||
case int64:
|
||||
return lv > float64(rv)
|
||||
case bool:
|
||||
panic(ErrorDataTypeMismatch)
|
||||
}
|
||||
case int64:
|
||||
switch rv := rval.(type) {
|
||||
case float64:
|
||||
return float64(lv) > rv
|
||||
case int64:
|
||||
return lv > rv
|
||||
case bool:
|
||||
panic(ErrorDataTypeMismatch)
|
||||
}
|
||||
case bool:
|
||||
panic(ErrorInvalidOperationBoolean)
|
||||
}
|
||||
|
||||
case opGTE:
|
||||
switch lv := lval.(type) {
|
||||
case float64:
|
||||
switch rv := rval.(type) {
|
||||
case float64:
|
||||
return lv >= rv
|
||||
case int64:
|
||||
return lv >= float64(rv)
|
||||
case bool:
|
||||
panic(ErrorDataTypeMismatch)
|
||||
}
|
||||
case int64:
|
||||
switch rv := rval.(type) {
|
||||
case float64:
|
||||
return float64(lv) >= rv
|
||||
case int64:
|
||||
return lv >= rv
|
||||
case bool:
|
||||
panic(ErrorDataTypeMismatch)
|
||||
}
|
||||
case bool:
|
||||
panic(ErrorInvalidOperationBoolean)
|
||||
}
|
||||
|
||||
case opAnd:
|
||||
switch lv := lval.(type) {
|
||||
case bool:
|
||||
switch rv := rval.(type) {
|
||||
case bool:
|
||||
return lv && rv
|
||||
default:
|
||||
panic(ErrorOnlyIntegerAllowed)
|
||||
}
|
||||
default:
|
||||
panic(ErrorOnlyIntegerAllowed)
|
||||
}
|
||||
|
||||
case opOr:
|
||||
switch lv := lval.(type) {
|
||||
case bool:
|
||||
switch rv := rval.(type) {
|
||||
case bool:
|
||||
return lv || rv
|
||||
default:
|
||||
panic(ErrorOnlyIntegerAllowed)
|
||||
}
|
||||
default:
|
||||
panic(ErrorOnlyIntegerAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type funcExpr struct {
|
||||
name string
|
||||
args []Expr
|
||||
}
|
||||
|
||||
func (fe *funcExpr) Eval(env func(string) interface{}) interface{} {
|
||||
argv := make([]interface{}, 0, len(fe.args))
|
||||
for _, arg := range fe.args {
|
||||
argv = append(argv, arg.Eval(env))
|
||||
}
|
||||
return funcs[fe.name].call(argv)
|
||||
}
|
||||
|
||||
type floatExpr struct {
|
||||
val float64
|
||||
}
|
||||
|
||||
func (fe *floatExpr) Eval(env func(string) interface{}) interface{} {
|
||||
return fe.val
|
||||
}
|
||||
|
||||
type intExpr struct {
|
||||
val int64
|
||||
}
|
||||
|
||||
func (ie *intExpr) Eval(env func(string) interface{}) interface{} {
|
||||
return ie.val
|
||||
}
|
||||
|
||||
type boolExpr struct {
|
||||
val bool
|
||||
}
|
||||
|
||||
func (be *boolExpr) Eval(env func(string) interface{}) interface{} {
|
||||
return be.val
|
||||
}
|
||||
|
||||
type stringExpr struct {
|
||||
val string
|
||||
}
|
||||
|
||||
func (se *stringExpr) Eval(env func(string) interface{}) interface{} {
|
||||
return se.val
|
||||
}
|
||||
|
||||
type varExpr struct {
|
||||
name string
|
||||
}
|
||||
|
||||
func (ve *varExpr) Eval(env func(string) interface{}) interface{} {
|
||||
return env(ve.name)
|
||||
}
|
||||
|
||||
func Compile(src string) (expr Expr, err error) {
|
||||
defer func() {
|
||||
switch x := recover().(type) {
|
||||
case nil:
|
||||
case error:
|
||||
err = x
|
||||
default:
|
||||
}
|
||||
}()
|
||||
|
||||
lexer := lexer{}
|
||||
lexer.init(strings.NewReader(src))
|
||||
expr = parseBinary(&lexer, 0)
|
||||
if lexer.token() != scanner.EOF {
|
||||
panic(ErrorExpressionSyntax)
|
||||
}
|
||||
return expr, nil
|
||||
}
|
||||
|
||||
func precedence(op rune) int {
|
||||
switch op {
|
||||
case opOr:
|
||||
return 1
|
||||
case opAnd:
|
||||
return 2
|
||||
case opEqual, opNotEqual, '<', '>', opGTE, opLTE:
|
||||
return 3
|
||||
case '+', '-', '|', '^':
|
||||
return 4
|
||||
case '*', '/', '%', opLeftShift, opRightShift, '&':
|
||||
return 5
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// binary = unary ('+' binary)*
|
||||
func parseBinary(lexer *lexer, lastPrec int) Expr {
|
||||
lhs := parseUnary(lexer)
|
||||
|
||||
for {
|
||||
op := lexer.token()
|
||||
prec := precedence(op)
|
||||
if prec <= lastPrec {
|
||||
break
|
||||
}
|
||||
lexer.next() // consume operator
|
||||
rhs := parseBinary(lexer, prec)
|
||||
lhs = &binaryExpr{op: op, lhs: lhs, rhs: rhs}
|
||||
}
|
||||
|
||||
return lhs
|
||||
}
|
||||
|
||||
// unary = '+|-' expr | primary
|
||||
func parseUnary(lexer *lexer) Expr {
|
||||
flag := false
|
||||
for tok := lexer.token(); ; tok = lexer.next() {
|
||||
if tok == '-' {
|
||||
flag = !flag
|
||||
} else if tok != '+' {
|
||||
break
|
||||
}
|
||||
}
|
||||
if flag {
|
||||
return &unaryExpr{op: '-', subExpr: parsePrimary(lexer)}
|
||||
}
|
||||
|
||||
flag = false
|
||||
for tok := lexer.token(); tok == '!'; tok = lexer.next() {
|
||||
flag = !flag
|
||||
}
|
||||
if flag {
|
||||
return &unaryExpr{op: '!', subExpr: parsePrimary(lexer)}
|
||||
}
|
||||
|
||||
flag = false
|
||||
for tok := lexer.token(); tok == '~'; tok = lexer.next() {
|
||||
flag = !flag
|
||||
}
|
||||
if flag {
|
||||
return &unaryExpr{op: '~', subExpr: parsePrimary(lexer)}
|
||||
}
|
||||
|
||||
return parsePrimary(lexer)
|
||||
}
|
||||
|
||||
// primary = id
|
||||
// | id '(' expr ',' ... ',' expr ')'
|
||||
// | num
|
||||
// | '(' expr ')'
|
||||
func parsePrimary(lexer *lexer) Expr {
|
||||
switch lexer.token() {
|
||||
case '+', '-', '!', '~':
|
||||
return parseUnary(lexer)
|
||||
|
||||
case '(':
|
||||
lexer.next() // consume '('
|
||||
node := parseBinary(lexer, 0)
|
||||
if lexer.token() != ')' {
|
||||
panic(ErrorExpressionSyntax)
|
||||
}
|
||||
lexer.next() // consume ')'
|
||||
return node
|
||||
|
||||
case scanner.Ident:
|
||||
id := strings.ToLower(lexer.text())
|
||||
if lexer.next() != '(' {
|
||||
if id == "true" {
|
||||
return &boolExpr{val: true}
|
||||
} else if id == "false" {
|
||||
return &boolExpr{val: false}
|
||||
} else {
|
||||
return &varExpr{name: id}
|
||||
}
|
||||
}
|
||||
node := funcExpr{name: id}
|
||||
for lexer.next() != ')' {
|
||||
arg := parseBinary(lexer, 0)
|
||||
node.args = append(node.args, arg)
|
||||
if lexer.token() != ',' {
|
||||
break
|
||||
}
|
||||
}
|
||||
if lexer.token() != ')' {
|
||||
panic(ErrorExpressionSyntax)
|
||||
}
|
||||
|
||||
if fn, ok := funcs[id]; !ok {
|
||||
panic(ErrorUnrecognizedFunction)
|
||||
} else if fn.minArgs >= 0 && len(node.args) < fn.minArgs {
|
||||
panic(ErrorArgumentCount)
|
||||
} else if fn.maxArgs >= 0 && len(node.args) > fn.maxArgs {
|
||||
panic(ErrorArgumentCount)
|
||||
}
|
||||
|
||||
lexer.next() // consume it
|
||||
return &node
|
||||
|
||||
case scanner.Int:
|
||||
val, e := strconv.ParseInt(lexer.text(), 0, 64)
|
||||
if e != nil {
|
||||
panic(ErrorInvalidFloat)
|
||||
}
|
||||
lexer.next()
|
||||
return &intExpr{val: val}
|
||||
|
||||
case scanner.Float:
|
||||
val, e := strconv.ParseFloat(lexer.text(), 0)
|
||||
if e != nil {
|
||||
panic(ErrorInvalidInteger)
|
||||
}
|
||||
lexer.next()
|
||||
return &floatExpr{val: val}
|
||||
|
||||
case scanner.String:
|
||||
panic(errors.New("strings are not allowed in expression at present"))
|
||||
val := lexer.text()
|
||||
lexer.next()
|
||||
return &stringExpr{val: val}
|
||||
|
||||
default:
|
||||
panic(ErrorExpressionSyntax)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,198 @@
|
|||
package expr
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestIntArithmetic(t *testing.T) {
|
||||
cases := []struct {
|
||||
expr string
|
||||
expected int64
|
||||
}{
|
||||
{"+10", 10},
|
||||
{"-10", -10},
|
||||
{"3 + 4 + 5 + 6 * 7 + 8", 62},
|
||||
{"3 + 4 + (5 + 6) * 7 + 8", 92},
|
||||
{"3 + 4 + (5 + 6) * 7 / 11 + 8", 22},
|
||||
{"3 + 4 + -5 * 6 / 7 % 8", 3},
|
||||
{"10 - 5", 5},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
expr, e := Compile(c.expr)
|
||||
if e != nil {
|
||||
t.Errorf("failed to compile expression '%s': %s", c.expr, e.Error())
|
||||
}
|
||||
if res := expr.Eval(nil); res.(int64) != c.expected {
|
||||
t.Errorf("result for expression '%s' is %v, but expected is %v", c.expr, res, c.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFloatArithmetic(t *testing.T) {
|
||||
cases := []struct {
|
||||
expr string
|
||||
expected float64
|
||||
}{
|
||||
{"+10.5", 10.5},
|
||||
{"-10.5", -10.5},
|
||||
{"3.1 + 4.2 + 5 + 6 * 7 + 8", 62.3},
|
||||
{"3.1 + 4.2 + (5 + 6) * 7 + 8.3", 92.6},
|
||||
{"3.1 + 4.2 + (5.1 + 5.9) * 7 / 11 + 8", 22.3},
|
||||
{"3.3 + 4.2 - 4.0 * 7.5 / 3", -2.5},
|
||||
{"3.3 + 4.2 - 4 * 7.0 / 2", -6.5},
|
||||
{"3.5/2.0", 1.75},
|
||||
{"3.5/2", 1.75},
|
||||
{"7 / 3.5", 2},
|
||||
{"3.5 % 2.0", 1.5},
|
||||
{"3.5 % 2", 1.5},
|
||||
{"7 % 2.5", 2},
|
||||
{"7.3 - 2", 5.3},
|
||||
{"7 - 2.3", 4.7},
|
||||
{"1 + 1.5", 2.5},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
expr, e := Compile(c.expr)
|
||||
if e != nil {
|
||||
t.Errorf("failed to compile expression '%s': %s", c.expr, e.Error())
|
||||
}
|
||||
if res := expr.Eval(nil); res.(float64) != c.expected {
|
||||
t.Errorf("result for expression '%s' is %v, but expected is %v", c.expr, res, c.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestVariable(t *testing.T) {
|
||||
variables := map[string]interface{}{
|
||||
"a": int64(6),
|
||||
"b": int64(7),
|
||||
}
|
||||
env := func(key string) interface{} {
|
||||
return variables[key]
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
expr string
|
||||
expected int64
|
||||
}{
|
||||
{"3 + 4 + (+5) + a * b + 8", 62},
|
||||
{"3 + 4 + (5 + a) * b + 8", 92},
|
||||
{"3 + 4 + (5 + a) * b / 11 + 8", 22},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
expr, e := Compile(c.expr)
|
||||
if e != nil {
|
||||
t.Errorf("failed to compile expression '%s': %s", c.expr, e.Error())
|
||||
}
|
||||
if res := expr.Eval(env); res.(int64) != c.expected {
|
||||
t.Errorf("result for expression '%s' is %v, but expected is %v", c.expr, res, c.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFunction(t *testing.T) {
|
||||
variables := map[string]interface{}{
|
||||
"a": int64(6),
|
||||
"b": 7.0,
|
||||
}
|
||||
|
||||
env := func(key string) interface{} {
|
||||
return variables[key]
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
expr string
|
||||
expected float64
|
||||
}{
|
||||
{"sum(3, 4, 5, a * b, 8)", 62},
|
||||
{"sum(3, 4, (5 + a) * b, 8)", 92},
|
||||
{"sum(3, 4, (5 + a) * b / 11, 8)", 22},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
expr, e := Compile(c.expr)
|
||||
if e != nil {
|
||||
t.Errorf("failed to compile expression '%s': %s", c.expr, e.Error())
|
||||
}
|
||||
if res := expr.Eval(env); res.(float64) != c.expected {
|
||||
t.Errorf("result for expression '%s' is %v, but expected is %v", c.expr, res, c.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogical(t *testing.T) {
|
||||
cases := []struct {
|
||||
expr string
|
||||
expected bool
|
||||
}{
|
||||
{"true", true},
|
||||
{"false", false},
|
||||
{"true == true", true},
|
||||
{"true == false", false},
|
||||
{"true != true", false},
|
||||
{"true != false", true},
|
||||
{"5 > 3", true},
|
||||
{"5 < 3", false},
|
||||
{"5.2 > 3", true},
|
||||
{"5.2 < 3", false},
|
||||
{"5 > 3.1", true},
|
||||
{"5 < 3.1", false},
|
||||
{"5.1 > 3.3", true},
|
||||
{"5.1 < 3.3", false},
|
||||
{"5 >= 3", true},
|
||||
{"5 <= 3", false},
|
||||
{"5.2 >= 3", true},
|
||||
{"5.2 <= 3", false},
|
||||
{"5 >= 3.1", true},
|
||||
{"5 <= 3.1", false},
|
||||
{"5.1 >= 3.3", true},
|
||||
{"5.1 <= 3.3", false},
|
||||
{"5 != 3", true},
|
||||
{"5.2 != 3.2", true},
|
||||
{"5.2 != 3", true},
|
||||
{"5 != 3.2", true},
|
||||
{"5 == 3", false},
|
||||
{"5.2 == 3.2", false},
|
||||
{"5.2 == 3", false},
|
||||
{"5 == 3.2", false},
|
||||
{"!(5 > 3)", false},
|
||||
{"5>3 && 3>1", true},
|
||||
{"5<3 || 3<1", false},
|
||||
{"4<=4 || 3<1", true},
|
||||
{"4<4 || 3>=1", true},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
expr, e := Compile(c.expr)
|
||||
if e != nil {
|
||||
t.Errorf("failed to compile expression '%s': %s", c.expr, e.Error())
|
||||
}
|
||||
if res := expr.Eval(nil); res.(bool) != c.expected {
|
||||
t.Errorf("result for expression '%s' is %v, but expected is %v", c.expr, res, c.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBitwise(t *testing.T) {
|
||||
cases := []struct {
|
||||
expr string
|
||||
expected int64
|
||||
}{
|
||||
{"0x0C & 0x04", 0x04},
|
||||
{"0x08 | 0x04", 0x0C},
|
||||
{"0x0C ^ 0x04", 0x08},
|
||||
{"0x01 << 2", 0x04},
|
||||
{"0x04 >> 2", 0x01},
|
||||
{"~0x04", ^0x04},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
expr, e := Compile(c.expr)
|
||||
if e != nil {
|
||||
t.Errorf("failed to compile expression '%s': %s", c.expr, e.Error())
|
||||
}
|
||||
if res := expr.Eval(nil); res.(int64) != c.expected {
|
||||
t.Errorf("result for expression '%s' is 0x%X, but expected is 0x%X", c.expr, res, c.expected)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,198 @@
|
|||
package expr
|
||||
|
||||
import (
|
||||
"math"
|
||||
)
|
||||
|
||||
type builtInFunc struct {
|
||||
minArgs, maxArgs int
|
||||
call func([]interface{}) interface{}
|
||||
}
|
||||
|
||||
func fnMin(args []interface{}) interface{} {
|
||||
res := args[0]
|
||||
for _, arg := range args[1:] {
|
||||
switch v1 := res.(type) {
|
||||
case int64:
|
||||
switch v2 := arg.(type) {
|
||||
case int64:
|
||||
if v2 < v1 {
|
||||
res = v2
|
||||
}
|
||||
case float64:
|
||||
res = math.Min(float64(v1), v2)
|
||||
default:
|
||||
panic(ErrorUnsupportedDataType)
|
||||
}
|
||||
case float64:
|
||||
switch v2 := arg.(type) {
|
||||
case int64:
|
||||
res = math.Min(v1, float64(v2))
|
||||
case float64:
|
||||
res = math.Min(v1, v2)
|
||||
default:
|
||||
panic(ErrorUnsupportedDataType)
|
||||
}
|
||||
default:
|
||||
panic(ErrorUnsupportedDataType)
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func fnMax(args []interface{}) interface{} {
|
||||
res := args[0]
|
||||
for _, arg := range args[1:] {
|
||||
switch v1 := res.(type) {
|
||||
case int64:
|
||||
switch v2 := arg.(type) {
|
||||
case int64:
|
||||
if v2 > v1 {
|
||||
res = v2
|
||||
}
|
||||
case float64:
|
||||
res = math.Max(float64(v1), v2)
|
||||
default:
|
||||
panic(ErrorUnsupportedDataType)
|
||||
}
|
||||
case float64:
|
||||
switch v2 := arg.(type) {
|
||||
case int64:
|
||||
res = math.Max(v1, float64(v2))
|
||||
case float64:
|
||||
res = math.Max(v1, v2)
|
||||
default:
|
||||
panic(ErrorUnsupportedDataType)
|
||||
}
|
||||
default:
|
||||
panic(ErrorUnsupportedDataType)
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func fnSum(args []interface{}) interface{} {
|
||||
res := float64(0)
|
||||
for _, arg := range args {
|
||||
switch v := arg.(type) {
|
||||
case int64:
|
||||
res += float64(v)
|
||||
case float64:
|
||||
res += v
|
||||
default:
|
||||
panic(ErrorUnsupportedDataType)
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func fnAvg(args []interface{}) interface{} {
|
||||
return fnSum(args).(float64) / float64(len(args))
|
||||
}
|
||||
|
||||
func fnSqrt(args []interface{}) interface{} {
|
||||
switch v := args[0].(type) {
|
||||
case int64:
|
||||
return math.Sqrt(float64(v))
|
||||
case float64:
|
||||
return math.Sqrt(v)
|
||||
default:
|
||||
panic(ErrorUnsupportedDataType)
|
||||
}
|
||||
}
|
||||
|
||||
func fnFloor(args []interface{}) interface{} {
|
||||
switch v := args[0].(type) {
|
||||
case int64:
|
||||
return v
|
||||
case float64:
|
||||
return math.Floor(v)
|
||||
default:
|
||||
panic(ErrorUnsupportedDataType)
|
||||
}
|
||||
}
|
||||
|
||||
func fnCeil(args []interface{}) interface{} {
|
||||
switch v := args[0].(type) {
|
||||
case int64:
|
||||
return v
|
||||
case float64:
|
||||
return math.Ceil(v)
|
||||
default:
|
||||
panic(ErrorUnsupportedDataType)
|
||||
}
|
||||
}
|
||||
|
||||
func fnRound(args []interface{}) interface{} {
|
||||
switch v := args[0].(type) {
|
||||
case int64:
|
||||
return v
|
||||
case float64:
|
||||
return math.Round(v)
|
||||
default:
|
||||
panic(ErrorUnsupportedDataType)
|
||||
}
|
||||
}
|
||||
|
||||
func fnLog(args []interface{}) interface{} {
|
||||
switch v := args[0].(type) {
|
||||
case int64:
|
||||
return math.Log(float64(v))
|
||||
case float64:
|
||||
return math.Log(v)
|
||||
default:
|
||||
panic(ErrorUnsupportedDataType)
|
||||
}
|
||||
}
|
||||
|
||||
func fnLog10(args []interface{}) interface{} {
|
||||
switch v := args[0].(type) {
|
||||
case int64:
|
||||
return math.Log10(float64(v))
|
||||
case float64:
|
||||
return math.Log10(v)
|
||||
default:
|
||||
panic(ErrorUnsupportedDataType)
|
||||
}
|
||||
}
|
||||
|
||||
func fnAbs(args []interface{}) interface{} {
|
||||
switch v := args[0].(type) {
|
||||
case int64:
|
||||
if v < 0 {
|
||||
return -v
|
||||
}
|
||||
return v
|
||||
case float64:
|
||||
return math.Abs(v)
|
||||
default:
|
||||
panic(ErrorUnsupportedDataType)
|
||||
}
|
||||
}
|
||||
|
||||
func fnIf(args []interface{}) interface{} {
|
||||
v, ok := args[0].(bool)
|
||||
if !ok {
|
||||
panic(ErrorUnsupportedDataType)
|
||||
}
|
||||
if v {
|
||||
return args[1]
|
||||
} else {
|
||||
return args[2]
|
||||
}
|
||||
}
|
||||
|
||||
var funcs = map[string]builtInFunc{
|
||||
"min": builtInFunc{minArgs: 1, maxArgs: -1, call: fnMin},
|
||||
"max": builtInFunc{minArgs: 1, maxArgs: -1, call: fnMax},
|
||||
"sum": builtInFunc{minArgs: 1, maxArgs: -1, call: fnSum},
|
||||
"avg": builtInFunc{minArgs: 1, maxArgs: -1, call: fnAvg},
|
||||
"sqrt": builtInFunc{minArgs: 1, maxArgs: 1, call: fnSqrt},
|
||||
"ceil": builtInFunc{minArgs: 1, maxArgs: 1, call: fnCeil},
|
||||
"floor": builtInFunc{minArgs: 1, maxArgs: 1, call: fnFloor},
|
||||
"round": builtInFunc{minArgs: 1, maxArgs: 1, call: fnRound},
|
||||
"log": builtInFunc{minArgs: 1, maxArgs: 1, call: fnLog},
|
||||
"log10": builtInFunc{minArgs: 1, maxArgs: 1, call: fnLog10},
|
||||
"abs": builtInFunc{minArgs: 1, maxArgs: 1, call: fnAbs},
|
||||
"if": builtInFunc{minArgs: 3, maxArgs: 3, call: fnIf},
|
||||
}
|
|
@ -0,0 +1,329 @@
|
|||
package expr
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMax(t *testing.T) {
|
||||
cases := []struct {
|
||||
args []interface{}
|
||||
expected float64
|
||||
}{
|
||||
{[]interface{}{int64(1), int64(2), int64(3), int64(4), int64(5)}, 5},
|
||||
{[]interface{}{int64(1), int64(2), float64(3), int64(4), float64(5)}, 5},
|
||||
{[]interface{}{int64(-1), int64(-2), float64(-3), int64(-4), float64(-5)}, -1},
|
||||
{[]interface{}{int64(-1), int64(-1), float64(-1), int64(-1), float64(-1)}, -1},
|
||||
{[]interface{}{int64(-1), int64(0), float64(-1), int64(-1), float64(-1)}, 0},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
r := fnMax(c.args)
|
||||
switch v := r.(type) {
|
||||
case int64:
|
||||
if v != int64(c.expected) {
|
||||
t.Errorf("max(%v) = %v, want %v", c.args, v, int64(c.expected))
|
||||
}
|
||||
case float64:
|
||||
if v != c.expected {
|
||||
t.Errorf("max(%v) = %v, want %v", c.args, v, c.expected)
|
||||
}
|
||||
default:
|
||||
t.Errorf("unknown result type max(%v)", c.args)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMin(t *testing.T) {
|
||||
cases := []struct {
|
||||
args []interface{}
|
||||
expected float64
|
||||
}{
|
||||
{[]interface{}{int64(1), int64(2), int64(3), int64(4), int64(5)}, 1},
|
||||
{[]interface{}{int64(5), int64(4), float64(3), int64(2), float64(1)}, 1},
|
||||
{[]interface{}{int64(-1), int64(-2), float64(-3), int64(-4), float64(-5)}, -5},
|
||||
{[]interface{}{int64(-1), int64(-1), float64(-1), int64(-1), float64(-1)}, -1},
|
||||
{[]interface{}{int64(1), int64(0), float64(1), int64(1), float64(1)}, 0},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
r := fnMin(c.args)
|
||||
switch v := r.(type) {
|
||||
case int64:
|
||||
if v != int64(c.expected) {
|
||||
t.Errorf("min(%v) = %v, want %v", c.args, v, int64(c.expected))
|
||||
}
|
||||
case float64:
|
||||
if v != c.expected {
|
||||
t.Errorf("min(%v) = %v, want %v", c.args, v, c.expected)
|
||||
}
|
||||
default:
|
||||
t.Errorf("unknown result type min(%v)", c.args)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSumAvg(t *testing.T) {
|
||||
cases := []struct {
|
||||
args []interface{}
|
||||
expected float64
|
||||
}{
|
||||
{[]interface{}{int64(1)}, 1},
|
||||
{[]interface{}{int64(1), int64(2), int64(3), int64(4), int64(5)}, 15},
|
||||
{[]interface{}{int64(5), int64(4), float64(3), int64(2), float64(1)}, 15},
|
||||
{[]interface{}{int64(-1), int64(-2), float64(-3), int64(-4), float64(-5)}, -15},
|
||||
{[]interface{}{int64(-1), int64(-1), float64(-1), int64(-1), float64(-1)}, -5},
|
||||
{[]interface{}{int64(1), int64(0), float64(1), int64(1), float64(1)}, 4},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
r := fnSum(c.args)
|
||||
switch v := r.(type) {
|
||||
case float64:
|
||||
if v != c.expected {
|
||||
t.Errorf("sum(%v) = %v, want %v", c.args, v, c.expected)
|
||||
}
|
||||
default:
|
||||
t.Errorf("unknown result type sum(%v)", c.args)
|
||||
}
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
r := fnAvg(c.args)
|
||||
expected := c.expected / float64(len(c.args))
|
||||
switch v := r.(type) {
|
||||
case float64:
|
||||
if v != expected {
|
||||
t.Errorf("avg(%v) = %v, want %v", c.args, v, expected)
|
||||
}
|
||||
default:
|
||||
t.Errorf("unknown result type avg(%v)", c.args)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSqrt(t *testing.T) {
|
||||
cases := []struct {
|
||||
arg interface{}
|
||||
expected float64
|
||||
}{
|
||||
{int64(0), 0},
|
||||
{int64(1), 1},
|
||||
{int64(256), 16},
|
||||
{10.0, math.Sqrt(10)},
|
||||
{10000.0, math.Sqrt(10000)},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
r := fnSqrt([]interface{}{c.arg})
|
||||
switch v := r.(type) {
|
||||
case float64:
|
||||
if v != c.expected {
|
||||
t.Errorf("sqrt(%v) = %v, want %v", c.arg, v, c.expected)
|
||||
}
|
||||
default:
|
||||
t.Errorf("unknown result type sqrt(%v)", c.arg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFloor(t *testing.T) {
|
||||
cases := []struct {
|
||||
arg interface{}
|
||||
expected float64
|
||||
}{
|
||||
{int64(0), 0},
|
||||
{int64(1), 1},
|
||||
{int64(-1), -1},
|
||||
{10.4, 10},
|
||||
{-10.4, -11},
|
||||
{10.8, 10},
|
||||
{-10.8, -11},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
r := fnFloor([]interface{}{c.arg})
|
||||
switch v := r.(type) {
|
||||
case int64:
|
||||
if v != int64(c.expected) {
|
||||
t.Errorf("floor(%v) = %v, want %v", c.arg, v, int64(c.expected))
|
||||
}
|
||||
case float64:
|
||||
if v != c.expected {
|
||||
t.Errorf("floor(%v) = %v, want %v", c.arg, v, c.expected)
|
||||
}
|
||||
default:
|
||||
t.Errorf("unknown result type floor(%v)", c.arg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCeil(t *testing.T) {
|
||||
cases := []struct {
|
||||
arg interface{}
|
||||
expected float64
|
||||
}{
|
||||
{int64(0), 0},
|
||||
{int64(1), 1},
|
||||
{int64(-1), -1},
|
||||
{10.4, 11},
|
||||
{-10.4, -10},
|
||||
{10.8, 11},
|
||||
{-10.8, -10},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
r := fnCeil([]interface{}{c.arg})
|
||||
switch v := r.(type) {
|
||||
case int64:
|
||||
if v != int64(c.expected) {
|
||||
t.Errorf("ceil(%v) = %v, want %v", c.arg, v, int64(c.expected))
|
||||
}
|
||||
case float64:
|
||||
if v != c.expected {
|
||||
t.Errorf("ceil(%v) = %v, want %v", c.arg, v, c.expected)
|
||||
}
|
||||
default:
|
||||
t.Errorf("unknown result type ceil(%v)", c.arg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRound(t *testing.T) {
|
||||
cases := []struct {
|
||||
arg interface{}
|
||||
expected float64
|
||||
}{
|
||||
{int64(0), 0},
|
||||
{int64(1), 1},
|
||||
{int64(-1), -1},
|
||||
{10.4, 10},
|
||||
{-10.4, -10},
|
||||
{10.8, 11},
|
||||
{-10.8, -11},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
r := fnRound([]interface{}{c.arg})
|
||||
switch v := r.(type) {
|
||||
case int64:
|
||||
if v != int64(c.expected) {
|
||||
t.Errorf("round(%v) = %v, want %v", c.arg, v, int64(c.expected))
|
||||
}
|
||||
case float64:
|
||||
if v != c.expected {
|
||||
t.Errorf("round(%v) = %v, want %v", c.arg, v, c.expected)
|
||||
}
|
||||
default:
|
||||
t.Errorf("unknown result type round(%v)", c.arg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLog(t *testing.T) {
|
||||
cases := []struct {
|
||||
arg interface{}
|
||||
expected float64
|
||||
}{
|
||||
{int64(1), math.Log(1)},
|
||||
{0.1, math.Log(0.1)},
|
||||
{10.4, math.Log(10.4)},
|
||||
{10.8, math.Log(10.8)},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
r := fnLog([]interface{}{c.arg})
|
||||
switch v := r.(type) {
|
||||
case float64:
|
||||
if v != c.expected {
|
||||
t.Errorf("log(%v) = %v, want %v", c.arg, v, c.expected)
|
||||
}
|
||||
default:
|
||||
t.Errorf("unknown result type log(%v)", c.arg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLog10(t *testing.T) {
|
||||
cases := []struct {
|
||||
arg interface{}
|
||||
expected float64
|
||||
}{
|
||||
{int64(1), math.Log10(1)},
|
||||
{0.1, math.Log10(0.1)},
|
||||
{10.4, math.Log10(10.4)},
|
||||
{10.8, math.Log10(10.8)},
|
||||
{int64(100), math.Log10(100)},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
r := fnLog10([]interface{}{c.arg})
|
||||
switch v := r.(type) {
|
||||
case float64:
|
||||
if v != c.expected {
|
||||
t.Errorf("log10(%v) = %v, want %v", c.arg, v, c.expected)
|
||||
}
|
||||
default:
|
||||
t.Errorf("unknown result type log10(%v)", c.arg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAbs(t *testing.T) {
|
||||
cases := []struct {
|
||||
arg interface{}
|
||||
expected float64
|
||||
}{
|
||||
{int64(1), 1},
|
||||
{int64(0), 0},
|
||||
{int64(-1), 1},
|
||||
{10.4, 10.4},
|
||||
{-10.4, 10.4},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
r := fnAbs([]interface{}{c.arg})
|
||||
switch v := r.(type) {
|
||||
case int64:
|
||||
if v != int64(c.expected) {
|
||||
t.Errorf("abs(%v) = %v, want %v", c.arg, v, int64(c.expected))
|
||||
}
|
||||
case float64:
|
||||
if v != c.expected {
|
||||
t.Errorf("abs(%v) = %v, want %v", c.arg, v, c.expected)
|
||||
}
|
||||
default:
|
||||
t.Errorf("unknown result type abs(%v)", c.arg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIf(t *testing.T) {
|
||||
cases := []struct {
|
||||
args []interface{}
|
||||
expected float64
|
||||
}{
|
||||
{[]interface{}{true, int64(10), int64(20)}, 10},
|
||||
{[]interface{}{false, int64(10), int64(20)}, 20},
|
||||
{[]interface{}{true, 10.3, 20.6}, 10.3},
|
||||
{[]interface{}{false, 10.3, 20.6}, 20.6},
|
||||
{[]interface{}{true, int64(10), 20.6}, 10},
|
||||
{[]interface{}{false, int64(10), 20.6}, 20.6},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
r := fnIf(c.args)
|
||||
switch v := r.(type) {
|
||||
case int64:
|
||||
if v != int64(c.expected) {
|
||||
t.Errorf("if(%v) = %v, want %v", c.args, v, int64(c.expected))
|
||||
}
|
||||
case float64:
|
||||
if v != c.expected {
|
||||
t.Errorf("if(%v) = %v, want %v", c.args, v, c.expected)
|
||||
}
|
||||
default:
|
||||
t.Errorf("unknown result type if(%v)", c.args)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type RouteMatchCriteria struct {
|
||||
Tag string `yaml:"tag"`
|
||||
Value string `yaml:"match"`
|
||||
Re *regexp.Regexp `yaml:"-"`
|
||||
}
|
||||
|
||||
func (c *RouteMatchCriteria) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
var v map[string]string
|
||||
if e := unmarshal(&v); e != nil {
|
||||
return e
|
||||
}
|
||||
|
||||
for k, a := range v {
|
||||
c.Tag = k
|
||||
c.Value = a
|
||||
if strings.HasPrefix(a, "re:") {
|
||||
re, e := regexp.Compile(a[3:])
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
c.Re = re
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type Route struct {
|
||||
Continue bool `yaml:"continue"`
|
||||
Receiver string `yaml:"receiver"`
|
||||
GroupWait Duration `yaml:"group_wait"`
|
||||
GroupInterval Duration `yaml:"group_interval"`
|
||||
RepeatInterval Duration `yaml:"repeat_interval"`
|
||||
GroupBy []string `yaml:"group_by"`
|
||||
Match []RouteMatchCriteria `yaml:"match"`
|
||||
Routes []*Route `yaml:"routes"`
|
||||
}
|
|
@ -0,0 +1,627 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"text/scanner"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/taosdata/alert/app/expr"
|
||||
"github.com/taosdata/alert/models"
|
||||
"github.com/taosdata/alert/utils"
|
||||
"github.com/taosdata/alert/utils/log"
|
||||
)
|
||||
|
||||
type Duration struct{ time.Duration }
|
||||
|
||||
func (d Duration) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(d.String())
|
||||
}
|
||||
|
||||
func (d *Duration) doUnmarshal(v interface{}) error {
|
||||
switch value := v.(type) {
|
||||
case float64:
|
||||
*d = Duration{time.Duration(value)}
|
||||
return nil
|
||||
case string:
|
||||
if duration, e := time.ParseDuration(value); e != nil {
|
||||
return e
|
||||
} else {
|
||||
*d = Duration{duration}
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return errors.New("invalid duration")
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Duration) UnmarshalJSON(b []byte) error {
|
||||
var v interface{}
|
||||
if e := json.Unmarshal(b, &v); e != nil {
|
||||
return e
|
||||
}
|
||||
return d.doUnmarshal(v)
|
||||
}
|
||||
|
||||
const (
|
||||
AlertStateWaiting = iota
|
||||
AlertStatePending
|
||||
AlertStateFiring
|
||||
)
|
||||
|
||||
type Alert struct {
|
||||
State uint8 `json:"-"`
|
||||
LastRefreshAt time.Time `json:"-"`
|
||||
StartsAt time.Time `json:"startsAt,omitempty"`
|
||||
EndsAt time.Time `json:"endsAt,omitempty"`
|
||||
Values map[string]interface{} `json:"values"`
|
||||
Labels map[string]string `json:"labels"`
|
||||
Annotations map[string]string `json:"annotations"`
|
||||
}
|
||||
|
||||
func (alert *Alert) doRefresh(firing bool, rule *Rule) bool {
|
||||
switch {
|
||||
case (!firing) && (alert.State == AlertStateWaiting):
|
||||
return false
|
||||
|
||||
case (!firing) && (alert.State == AlertStatePending):
|
||||
alert.State = AlertStateWaiting
|
||||
return false
|
||||
|
||||
case (!firing) && (alert.State == AlertStateFiring):
|
||||
alert.State = AlertStateWaiting
|
||||
alert.EndsAt = time.Now()
|
||||
|
||||
case firing && (alert.State == AlertStateWaiting):
|
||||
alert.StartsAt = time.Now()
|
||||
if rule.For.Nanoseconds() > 0 {
|
||||
alert.State = AlertStatePending
|
||||
return false
|
||||
}
|
||||
alert.State = AlertStateFiring
|
||||
|
||||
case firing && (alert.State == AlertStatePending):
|
||||
if time.Now().Sub(alert.StartsAt) < rule.For.Duration {
|
||||
return false
|
||||
}
|
||||
alert.StartsAt = alert.StartsAt.Add(rule.For.Duration)
|
||||
alert.State = AlertStateFiring
|
||||
|
||||
case firing && (alert.State == AlertStateFiring):
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (alert *Alert) refresh(rule *Rule, values map[string]interface{}) {
|
||||
alert.LastRefreshAt = time.Now()
|
||||
|
||||
defer func() {
|
||||
switch x := recover().(type) {
|
||||
case nil:
|
||||
case error:
|
||||
rule.setState(RuleStateError)
|
||||
log.Errorf("[%s]: failed to evaluate: %s", rule.Name, x.Error())
|
||||
default:
|
||||
rule.setState(RuleStateError)
|
||||
log.Errorf("[%s]: failed to evaluate: unknown error", rule.Name)
|
||||
}
|
||||
}()
|
||||
|
||||
alert.Values = values
|
||||
res := rule.Expr.Eval(func(key string) interface{} {
|
||||
// ToLower is required as column name in result is in lower case
|
||||
return alert.Values[strings.ToLower(key)]
|
||||
})
|
||||
|
||||
val, ok := res.(bool)
|
||||
if !ok {
|
||||
rule.setState(RuleStateError)
|
||||
log.Errorf("[%s]: result type is not bool", rule.Name)
|
||||
return
|
||||
}
|
||||
|
||||
if !alert.doRefresh(val, rule) {
|
||||
return
|
||||
}
|
||||
|
||||
buf := bytes.Buffer{}
|
||||
alert.Annotations = map[string]string{}
|
||||
for k, v := range rule.Annotations {
|
||||
if e := v.Execute(&buf, alert); e != nil {
|
||||
log.Errorf("[%s]: failed to generate annotation '%s': %s", rule.Name, k, e.Error())
|
||||
} else {
|
||||
alert.Annotations[k] = buf.String()
|
||||
}
|
||||
buf.Reset()
|
||||
}
|
||||
|
||||
buf.Reset()
|
||||
if e := json.NewEncoder(&buf).Encode(alert); e != nil {
|
||||
log.Errorf("[%s]: failed to serialize alert to JSON: %s", rule.Name, e.Error())
|
||||
} else {
|
||||
chAlert <- buf.String()
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
RuleStateNormal = iota
|
||||
RuleStateError
|
||||
RuleStateDisabled
|
||||
RuleStateRunning = 0x04
|
||||
)
|
||||
|
||||
type Rule struct {
|
||||
Name string `json:"name"`
|
||||
State uint32 `json:"state"`
|
||||
SQL string `json:"sql"`
|
||||
GroupByCols []string `json:"-"`
|
||||
For Duration `json:"for"`
|
||||
Period Duration `json:"period"`
|
||||
NextRunTime time.Time `json:"-"`
|
||||
RawExpr string `json:"expr"`
|
||||
Expr expr.Expr `json:"-"`
|
||||
Labels map[string]string `json:"labels"`
|
||||
RawAnnotations map[string]string `json:"annotations"`
|
||||
Annotations map[string]*template.Template `json:"-"`
|
||||
Alerts sync.Map `json:"-"`
|
||||
}
|
||||
|
||||
func (rule *Rule) clone() *Rule {
|
||||
return &Rule{
|
||||
Name: rule.Name,
|
||||
State: RuleStateNormal,
|
||||
SQL: rule.SQL,
|
||||
GroupByCols: rule.GroupByCols,
|
||||
For: rule.For,
|
||||
Period: rule.Period,
|
||||
NextRunTime: time.Time{},
|
||||
RawExpr: rule.RawExpr,
|
||||
Expr: rule.Expr,
|
||||
Labels: rule.Labels,
|
||||
RawAnnotations: rule.RawAnnotations,
|
||||
Annotations: rule.Annotations,
|
||||
// don't copy alerts
|
||||
}
|
||||
}
|
||||
|
||||
func (rule *Rule) setState(s uint32) {
|
||||
for {
|
||||
old := atomic.LoadUint32(&rule.State)
|
||||
new := old&0xffffffc0 | s
|
||||
if atomic.CompareAndSwapUint32(&rule.State, old, new) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (rule *Rule) state() uint32 {
|
||||
return atomic.LoadUint32(&rule.State) & 0xffffffc0
|
||||
}
|
||||
|
||||
func (rule *Rule) isEnabled() bool {
|
||||
state := atomic.LoadUint32(&rule.State)
|
||||
return state&RuleStateDisabled == 0
|
||||
}
|
||||
|
||||
func (rule *Rule) setNextRunTime(tm time.Time) {
|
||||
rule.NextRunTime = tm.Round(rule.Period.Duration)
|
||||
if rule.NextRunTime.Before(tm) {
|
||||
rule.NextRunTime = rule.NextRunTime.Add(rule.Period.Duration)
|
||||
}
|
||||
}
|
||||
|
||||
func parseGroupBy(sql string) (cols []string, err error) {
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
err = e.(error)
|
||||
}
|
||||
}()
|
||||
|
||||
s := scanner.Scanner{
|
||||
Error: func(s *scanner.Scanner, msg string) {
|
||||
panic(errors.New(msg))
|
||||
},
|
||||
Mode: scanner.ScanIdents | scanner.ScanInts | scanner.ScanFloats,
|
||||
}
|
||||
|
||||
s.Init(strings.NewReader(sql))
|
||||
if s.Scan() != scanner.Ident || strings.ToLower(s.TokenText()) != "select" {
|
||||
err = errors.New("only select statement is allowed.")
|
||||
return
|
||||
}
|
||||
|
||||
hasGroupBy := false
|
||||
for t := s.Scan(); t != scanner.EOF; t = s.Scan() {
|
||||
if t != scanner.Ident {
|
||||
continue
|
||||
}
|
||||
if strings.ToLower(s.TokenText()) != "group" {
|
||||
continue
|
||||
}
|
||||
if s.Scan() != scanner.Ident {
|
||||
continue
|
||||
}
|
||||
if strings.ToLower(s.TokenText()) == "by" {
|
||||
hasGroupBy = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasGroupBy {
|
||||
return
|
||||
}
|
||||
|
||||
for {
|
||||
if s.Scan() != scanner.Ident {
|
||||
err = errors.New("SQL statement syntax error.")
|
||||
return
|
||||
}
|
||||
col := strings.ToLower(s.TokenText())
|
||||
cols = append(cols, col)
|
||||
if s.Scan() != ',' {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (rule *Rule) parseGroupBy() (err error) {
|
||||
cols, e := parseGroupBy(rule.SQL)
|
||||
if e == nil {
|
||||
rule.GroupByCols = cols
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rule *Rule) getAlert(values map[string]interface{}) *Alert {
|
||||
sb := strings.Builder{}
|
||||
for _, name := range rule.GroupByCols {
|
||||
value := values[name]
|
||||
if value == nil {
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprint(value))
|
||||
}
|
||||
sb.WriteByte('_')
|
||||
}
|
||||
|
||||
var alert *Alert
|
||||
key := sb.String()
|
||||
|
||||
if v, ok := rule.Alerts.Load(key); ok {
|
||||
alert = v.(*Alert)
|
||||
}
|
||||
if alert == nil {
|
||||
alert = &Alert{Labels: map[string]string{}}
|
||||
for k, v := range rule.Labels {
|
||||
alert.Labels[k] = v
|
||||
}
|
||||
for _, name := range rule.GroupByCols {
|
||||
value := values[name]
|
||||
if value == nil {
|
||||
alert.Labels[name] = ""
|
||||
} else {
|
||||
alert.Labels[name] = fmt.Sprint(value)
|
||||
}
|
||||
}
|
||||
rule.Alerts.Store(key, alert)
|
||||
}
|
||||
|
||||
return alert
|
||||
}
|
||||
|
||||
func (rule *Rule) preRun(tm time.Time) bool {
|
||||
if tm.Before(rule.NextRunTime) {
|
||||
return false
|
||||
}
|
||||
rule.setNextRunTime(tm)
|
||||
|
||||
for {
|
||||
state := atomic.LoadUint32(&rule.State)
|
||||
if state != RuleStateNormal {
|
||||
return false
|
||||
}
|
||||
if atomic.CompareAndSwapUint32(&rule.State, state, RuleStateRunning) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (rule *Rule) run(db *sql.DB) {
|
||||
rows, e := db.Query(rule.SQL)
|
||||
if e != nil {
|
||||
log.Errorf("[%s]: failed to query TDengine: %s", rule.Name, e.Error())
|
||||
return
|
||||
}
|
||||
|
||||
cols, e := rows.ColumnTypes()
|
||||
if e != nil {
|
||||
log.Errorf("[%s]: unable to get column information: %s", rule.Name, e.Error())
|
||||
return
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
values := make([]interface{}, 0, len(cols))
|
||||
for range cols {
|
||||
var v interface{}
|
||||
values = append(values, &v)
|
||||
}
|
||||
rows.Scan(values...)
|
||||
|
||||
m := make(map[string]interface{})
|
||||
for i, col := range cols {
|
||||
name := strings.ToLower(col.Name())
|
||||
m[name] = *(values[i].(*interface{}))
|
||||
}
|
||||
|
||||
alert := rule.getAlert(m)
|
||||
alert.refresh(rule, m)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
rule.Alerts.Range(func(k, v interface{}) bool {
|
||||
alert := v.(*Alert)
|
||||
if now.Sub(alert.LastRefreshAt) > rule.Period.Duration*10 {
|
||||
rule.Alerts.Delete(k)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func (rule *Rule) postRun() {
|
||||
for {
|
||||
old := atomic.LoadUint32(&rule.State)
|
||||
new := old & ^uint32(RuleStateRunning)
|
||||
if atomic.CompareAndSwapUint32(&rule.State, old, new) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newRule(str string) (*Rule, error) {
|
||||
rule := Rule{}
|
||||
|
||||
e := json.NewDecoder(strings.NewReader(str)).Decode(&rule)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
|
||||
if rule.Period.Nanoseconds() <= 0 {
|
||||
rule.Period = Duration{time.Minute}
|
||||
}
|
||||
rule.setNextRunTime(time.Now())
|
||||
|
||||
if rule.For.Nanoseconds() < 0 {
|
||||
rule.For = Duration{0}
|
||||
}
|
||||
|
||||
if e = rule.parseGroupBy(); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
|
||||
if expr, e := expr.Compile(rule.RawExpr); e != nil {
|
||||
return nil, e
|
||||
} else {
|
||||
rule.Expr = expr
|
||||
}
|
||||
|
||||
rule.Annotations = map[string]*template.Template{}
|
||||
for k, v := range rule.RawAnnotations {
|
||||
v = reValue.ReplaceAllStringFunc(v, func(s string) string {
|
||||
// as column name in query result is always in lower case,
|
||||
// we need to convert value reference in annotations to
|
||||
// lower case
|
||||
return strings.ToLower(s)
|
||||
})
|
||||
text := "{{$labels := .Labels}}{{$values := .Values}}" + v
|
||||
tmpl, e := template.New(k).Parse(text)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
rule.Annotations[k] = tmpl
|
||||
}
|
||||
|
||||
return &rule, nil
|
||||
}
|
||||
|
||||
const (
|
||||
batchSize = 1024
|
||||
)
|
||||
|
||||
var (
|
||||
rules sync.Map
|
||||
wg sync.WaitGroup
|
||||
chStop = make(chan struct{})
|
||||
chAlert = make(chan string, batchSize)
|
||||
reValue = regexp.MustCompile(`\$values\.[_a-zA-Z0-9]+`)
|
||||
)
|
||||
|
||||
func runRules() {
|
||||
defer wg.Done()
|
||||
|
||||
db, e := sql.Open("taosSql", utils.Cfg.TDengine)
|
||||
if e != nil {
|
||||
log.Fatal("failed to connect to TDengine: ", e.Error())
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
ticker := time.NewTicker(time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
LOOP:
|
||||
for {
|
||||
var tm time.Time
|
||||
select {
|
||||
case <-chStop:
|
||||
close(chAlert)
|
||||
break LOOP
|
||||
case tm = <-ticker.C:
|
||||
}
|
||||
|
||||
rules.Range(func(k, v interface{}) bool {
|
||||
rule := v.(*Rule)
|
||||
if !rule.preRun(tm) {
|
||||
return true
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go func(rule *Rule) {
|
||||
defer wg.Done()
|
||||
defer rule.postRun()
|
||||
rule.run(db)
|
||||
}(rule)
|
||||
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func doPushAlerts(alerts []string) {
|
||||
defer wg.Done()
|
||||
|
||||
if len(utils.Cfg.Receivers.AlertManager) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
buf := bytes.Buffer{}
|
||||
buf.WriteByte('[')
|
||||
for i, alert := range alerts {
|
||||
if i > 0 {
|
||||
buf.WriteByte(',')
|
||||
}
|
||||
buf.WriteString(alert)
|
||||
}
|
||||
buf.WriteByte(']')
|
||||
|
||||
log.Debug(buf.String())
|
||||
|
||||
resp, e := http.DefaultClient.Post(utils.Cfg.Receivers.AlertManager, "application/json", &buf)
|
||||
if e != nil {
|
||||
log.Errorf("failed to push alerts to downstream: %s", e.Error())
|
||||
return
|
||||
}
|
||||
resp.Body.Close()
|
||||
}
|
||||
|
||||
func pushAlerts() {
|
||||
defer wg.Done()
|
||||
|
||||
ticker := time.NewTicker(time.Millisecond * 100)
|
||||
defer ticker.Stop()
|
||||
|
||||
alerts := make([]string, 0, batchSize)
|
||||
|
||||
LOOP:
|
||||
for {
|
||||
select {
|
||||
case alert := <-chAlert:
|
||||
if utils.Cfg.Receivers.Console {
|
||||
fmt.Print(alert)
|
||||
}
|
||||
if len(alert) == 0 {
|
||||
if len(alerts) > 0 {
|
||||
wg.Add(1)
|
||||
doPushAlerts(alerts)
|
||||
}
|
||||
break LOOP
|
||||
}
|
||||
if len(alerts) == batchSize {
|
||||
wg.Add(1)
|
||||
go doPushAlerts(alerts)
|
||||
alerts = make([]string, 0, batchSize)
|
||||
}
|
||||
alerts = append(alerts, alert)
|
||||
|
||||
case <-ticker.C:
|
||||
if len(alerts) > 0 {
|
||||
wg.Add(1)
|
||||
go doPushAlerts(alerts)
|
||||
alerts = make([]string, 0, batchSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func loadRuleFromDatabase() error {
|
||||
allRules, e := models.LoadAllRule()
|
||||
if e != nil {
|
||||
log.Error("failed to load rules from database:", e.Error())
|
||||
return e
|
||||
}
|
||||
|
||||
count := 0
|
||||
for _, r := range allRules {
|
||||
rule, e := newRule(r.Content)
|
||||
if e != nil {
|
||||
log.Errorf("[%s]: parse failed: %s", r.Name, e.Error())
|
||||
continue
|
||||
}
|
||||
if !r.Enabled {
|
||||
rule.setState(RuleStateDisabled)
|
||||
}
|
||||
rules.Store(rule.Name, rule)
|
||||
count++
|
||||
}
|
||||
log.Infof("total %d rules loaded", count)
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadRuleFromFile() error {
|
||||
f, e := os.Open(utils.Cfg.RuleFile)
|
||||
if e != nil {
|
||||
log.Error("failed to load rules from file:", e.Error())
|
||||
return e
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var allRules []Rule
|
||||
e = json.NewDecoder(f).Decode(&allRules)
|
||||
if e != nil {
|
||||
log.Error("failed to parse rule file:", e.Error())
|
||||
return e
|
||||
}
|
||||
|
||||
for i := 0; i < len(allRules); i++ {
|
||||
rule := &allRules[i]
|
||||
rules.Store(rule.Name, rule)
|
||||
}
|
||||
log.Infof("total %d rules loaded", len(allRules))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func initRule() error {
|
||||
if len(utils.Cfg.Database) > 0 {
|
||||
if e := loadRuleFromDatabase(); e != nil {
|
||||
return e
|
||||
}
|
||||
} else {
|
||||
if e := loadRuleFromFile(); e != nil {
|
||||
return e
|
||||
}
|
||||
}
|
||||
|
||||
wg.Add(2)
|
||||
go runRules()
|
||||
go pushAlerts()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func uninitRule() error {
|
||||
close(chStop)
|
||||
wg.Wait()
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/taosdata/alert/utils/log"
|
||||
)
|
||||
|
||||
func TestParseGroupBy(t *testing.T) {
|
||||
cases := []struct {
|
||||
sql string
|
||||
cols []string
|
||||
}{
|
||||
{
|
||||
sql: "select * from a",
|
||||
cols: []string{},
|
||||
},
|
||||
{
|
||||
sql: "select * from a group by abc",
|
||||
cols: []string{"abc"},
|
||||
},
|
||||
{
|
||||
sql: "select * from a group by abc, def",
|
||||
cols: []string{"abc", "def"},
|
||||
},
|
||||
{
|
||||
sql: "select * from a Group by abc, def order by abc",
|
||||
cols: []string{"abc", "def"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
cols, e := parseGroupBy(c.sql)
|
||||
if e != nil {
|
||||
t.Errorf("failed to parse sql '%s': %s", c.sql, e.Error())
|
||||
}
|
||||
for i := range cols {
|
||||
if i >= len(c.cols) {
|
||||
t.Errorf("count of group by columns of '%s' is wrong", c.sql)
|
||||
}
|
||||
if c.cols[i] != cols[i] {
|
||||
t.Errorf("wrong group by columns for '%s'", c.sql)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestManagement(t *testing.T) {
|
||||
const format = `{"name":"rule%d", "sql":"select count(*) as count from meters", "expr":"count>2"}`
|
||||
|
||||
log.Init()
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
s := fmt.Sprintf(format, i)
|
||||
rule, e := newRule(s)
|
||||
if e != nil {
|
||||
t.Errorf("failed to create rule: %s", e.Error())
|
||||
}
|
||||
e = doUpdateRule(rule, s)
|
||||
if e != nil {
|
||||
t.Errorf("failed to add or update rule: %s", e.Error())
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
name := fmt.Sprintf("rule%d", i)
|
||||
if _, ok := rules.Load(name); !ok {
|
||||
t.Errorf("rule '%s' does not exist", name)
|
||||
}
|
||||
}
|
||||
|
||||
name := "rule1"
|
||||
if e := doDeleteRule(name); e != nil {
|
||||
t.Errorf("failed to delete rule: %s", e.Error())
|
||||
}
|
||||
|
||||
if _, ok := rules.Load(name); ok {
|
||||
t.Errorf("rule '%s' should not exist any more", name)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"port": 8100,
|
||||
"database": "file:alert.db",
|
||||
"tdengine": "root:taosdata@/tcp(127.0.0.1:0)/",
|
||||
"log": {
|
||||
"level": "debug",
|
||||
"path": ""
|
||||
},
|
||||
"receivers": {
|
||||
"alertManager": "http://127.0.0.1:9093/api/v1/alerts",
|
||||
"console": true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,183 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/taosdata/alert/app"
|
||||
"github.com/taosdata/alert/models"
|
||||
"github.com/taosdata/alert/utils"
|
||||
"github.com/taosdata/alert/utils/log"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
_ "github.com/taosdata/driver-go/taosSql"
|
||||
)
|
||||
|
||||
type httpHandler struct {
|
||||
}
|
||||
|
||||
func (h *httpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
path := r.URL.Path
|
||||
http.DefaultServeMux.ServeHTTP(w, r)
|
||||
duration := time.Now().Sub(start)
|
||||
log.Debugf("[%s]\t%s\t%s", r.Method, path, duration)
|
||||
}
|
||||
|
||||
func serveWeb() *http.Server {
|
||||
log.Info("Listening at port: ", utils.Cfg.Port)
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: ":" + strconv.Itoa(int(utils.Cfg.Port)),
|
||||
Handler: &httpHandler{},
|
||||
}
|
||||
go func() {
|
||||
if e := srv.ListenAndServe(); e != nil {
|
||||
log.Error(e.Error())
|
||||
}
|
||||
}()
|
||||
|
||||
return srv
|
||||
}
|
||||
|
||||
func copyFile(dst, src string) error {
|
||||
if dst == src {
|
||||
return nil
|
||||
}
|
||||
|
||||
in, e := os.Open(src)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
defer in.Close()
|
||||
|
||||
out, e := os.Create(dst)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
_, e = io.Copy(out, in)
|
||||
return e
|
||||
}
|
||||
|
||||
func doSetup(cfgPath string) error {
|
||||
exePath, e := os.Executable()
|
||||
if e != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to get executable path: %s\n", e.Error())
|
||||
return e
|
||||
}
|
||||
|
||||
if !filepath.IsAbs(cfgPath) {
|
||||
dir := filepath.Dir(exePath)
|
||||
cfgPath = filepath.Join(dir, cfgPath)
|
||||
}
|
||||
|
||||
e = copyFile("/etc/taos/alert.cfg", cfgPath)
|
||||
if e != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed copy configuration file: %s\n", e.Error())
|
||||
return e
|
||||
}
|
||||
|
||||
f, e := os.Create("/etc/systemd/system/alert.service")
|
||||
if e != nil {
|
||||
fmt.Printf("failed to create alert service: %s\n", e.Error())
|
||||
return e
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
const content = `[Unit]
|
||||
Description=Alert (TDengine Alert Service)
|
||||
After=syslog.target
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
RestartSec=2s
|
||||
Type=simple
|
||||
WorkingDirectory=/var/lib/taos/
|
||||
ExecStart=%s -cfg /etc/taos/alert.cfg
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
`
|
||||
_, e = fmt.Fprintf(f, content, exePath)
|
||||
if e != nil {
|
||||
fmt.Printf("failed to create alert.service: %s\n", e.Error())
|
||||
return e
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
const version = "TDengine alert v1.0.0"
|
||||
|
||||
func main() {
|
||||
var (
|
||||
cfgPath string
|
||||
setup bool
|
||||
showVersion bool
|
||||
)
|
||||
flag.StringVar(&cfgPath, "cfg", "alert.cfg", "path of configuration file")
|
||||
flag.BoolVar(&setup, "setup", false, "setup the service as a daemon")
|
||||
flag.BoolVar(&showVersion, "version", false, "show version information")
|
||||
flag.Parse()
|
||||
|
||||
if showVersion {
|
||||
fmt.Println(version)
|
||||
return
|
||||
}
|
||||
|
||||
if setup {
|
||||
if runtime.GOOS == "linux" {
|
||||
doSetup(cfgPath)
|
||||
} else {
|
||||
fmt.Fprintln(os.Stderr, "can only run as a daemon mode in linux.")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if e := utils.LoadConfig(cfgPath); e != nil {
|
||||
fmt.Fprintln(os.Stderr, "failed to load configuration")
|
||||
return
|
||||
}
|
||||
|
||||
if e := log.Init(); e != nil {
|
||||
fmt.Fprintln(os.Stderr, "failed to initialize logger:", e.Error())
|
||||
return
|
||||
}
|
||||
defer log.Sync()
|
||||
|
||||
if e := models.Init(); e != nil {
|
||||
log.Fatal("failed to initialize database:", e.Error())
|
||||
}
|
||||
|
||||
if e := app.Init(); e != nil {
|
||||
log.Fatal("failed to initialize application:", e.Error())
|
||||
}
|
||||
// start web server
|
||||
srv := serveWeb()
|
||||
|
||||
// wait `Ctrl-C` or `Kill` to exit, `Kill` does not work on Windows
|
||||
interrupt := make(chan os.Signal)
|
||||
signal.Notify(interrupt, os.Interrupt)
|
||||
signal.Notify(interrupt, os.Kill)
|
||||
<-interrupt
|
||||
fmt.Println("'Ctrl + C' received, exiting...")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
srv.Shutdown(ctx)
|
||||
cancel()
|
||||
|
||||
app.Uninit()
|
||||
models.Uninit()
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"name": "CarTooFast",
|
||||
"period": "10s",
|
||||
"sql": "select avg(speed) as avgspeed from test.cars where ts > now - 5m group by id",
|
||||
"expr": "avgSpeed > 100",
|
||||
"for": "0s",
|
||||
"labels": {
|
||||
"ruleName": "CarTooFast"
|
||||
},
|
||||
"annotations": {
|
||||
"summary": "car {{$values.id}} is too fast, its average speed is {{$values.avgSpeed}}km/h"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
module github.com/taosdata/alert
|
||||
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
github.com/jmoiron/sqlx v1.2.0
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible
|
||||
github.com/taosdata/driver-go v0.0.0-20200311072652-8c58c512b6ac
|
||||
go.uber.org/zap v1.14.1
|
||||
google.golang.org/appengine v1.6.5 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c
|
||||
)
|
|
@ -0,0 +1,80 @@
|
|||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-sql-driver/mysql v1.4.0 h1:7LxgVwFb2hIQtMm87NdgAVfXjnt4OePseqT1tKx+opk=
|
||||
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA=
|
||||
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
|
||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/taosdata/driver-go v0.0.0-20200311072652-8c58c512b6ac h1:uZplMwObJj8mfgI4ZvYPNHRn+fNz2leiMPqShsjtEEc=
|
||||
github.com/taosdata/driver-go v0.0.0-20200311072652-8c58c512b6ac/go.mod h1:TuMZDpnBrjNO07rneM2C5qMYFqIro4aupL2cUOGGo/I=
|
||||
go.uber.org/atomic v1.5.0 h1:OI5t8sDa1Or+q8AeE+yKeB/SDYioSHAgcVljj9JIETY=
|
||||
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk=
|
||||
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/multierr v1.3.0 h1:sFPn2GLc3poCkfrpIXGhBD2X0CMIo4Q/zSULXrj/+uc=
|
||||
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
|
||||
go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A=
|
||||
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
|
||||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4=
|
||||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
|
||||
go.uber.org/zap v1.14.0 h1:/pduUoebOeeJzTDFuoMgC6nRkiasr1sBCIEorly7m4o=
|
||||
go.uber.org/zap v1.14.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
|
||||
go.uber.org/zap v1.14.1 h1:nYDKopTbvAPq/NrUVZwT15y2lpROBiLLyoRTbXOYWOo=
|
||||
go.uber.org/zap v1.14.1/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs=
|
||||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
|
@ -0,0 +1,131 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/taosdata/alert/utils"
|
||||
"github.com/taosdata/alert/utils/log"
|
||||
)
|
||||
|
||||
var db *sqlx.DB
|
||||
|
||||
func Init() error {
|
||||
xdb, e := sqlx.Connect("sqlite3", utils.Cfg.Database)
|
||||
if e == nil {
|
||||
db = xdb
|
||||
}
|
||||
return upgrade()
|
||||
}
|
||||
|
||||
func Uninit() error {
|
||||
db.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
func getStringOption(tx *sqlx.Tx, name string) (string, error) {
|
||||
const qs = "SELECT * FROM `option` WHERE `name`=?"
|
||||
|
||||
var (
|
||||
e error
|
||||
o struct {
|
||||
Name string `db:"name"`
|
||||
Value string `db:"value"`
|
||||
}
|
||||
)
|
||||
|
||||
if tx != nil {
|
||||
e = tx.Get(&o, qs, name)
|
||||
} else {
|
||||
e = db.Get(&o, qs, name)
|
||||
}
|
||||
|
||||
if e != nil {
|
||||
return "", e
|
||||
}
|
||||
|
||||
return o.Value, nil
|
||||
}
|
||||
|
||||
func getIntOption(tx *sqlx.Tx, name string) (int, error) {
|
||||
s, e := getStringOption(tx, name)
|
||||
if e != nil {
|
||||
return 0, e
|
||||
}
|
||||
v, e := strconv.ParseInt(s, 10, 64)
|
||||
return int(v), e
|
||||
}
|
||||
|
||||
func setOption(tx *sqlx.Tx, name string, value interface{}) error {
|
||||
const qs = "REPLACE INTO `option`(`name`, `value`) VALUES(?, ?);"
|
||||
|
||||
var (
|
||||
e error
|
||||
sv string
|
||||
)
|
||||
|
||||
switch v := value.(type) {
|
||||
case time.Time:
|
||||
sv = v.Format(time.RFC3339)
|
||||
default:
|
||||
sv = fmt.Sprint(value)
|
||||
}
|
||||
|
||||
if tx != nil {
|
||||
_, e = tx.Exec(qs, name, sv)
|
||||
} else {
|
||||
_, e = db.Exec(qs, name, sv)
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
var upgradeScripts = []struct {
|
||||
ver int
|
||||
stmts []string
|
||||
}{
|
||||
{
|
||||
ver: 0,
|
||||
stmts: []string{
|
||||
"CREATE TABLE `option`( `name` VARCHAR(63) PRIMARY KEY, `value` VARCHAR(255) NOT NULL) WITHOUT ROWID;",
|
||||
"CREATE TABLE `rule`( `name` VARCHAR(63) PRIMARY KEY, `enabled` TINYINT(1) NOT NULL, `created_at` DATETIME NOT NULL, `updated_at` DATETIME NOT NULL, `content` TEXT(65535) NOT NULL);",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func upgrade() error {
|
||||
const dbVersion = "database version"
|
||||
|
||||
ver, e := getIntOption(nil, dbVersion)
|
||||
if e != nil { // regards all errors as schema not created
|
||||
ver = -1 // set ver to -1 to execute all statements
|
||||
}
|
||||
|
||||
tx, e := db.Beginx()
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
|
||||
for _, us := range upgradeScripts {
|
||||
if us.ver <= ver {
|
||||
continue
|
||||
}
|
||||
log.Info("upgrading database to version: ", us.ver)
|
||||
for _, s := range us.stmts {
|
||||
if _, e = tx.Exec(s); e != nil {
|
||||
tx.Rollback()
|
||||
return e
|
||||
}
|
||||
}
|
||||
ver = us.ver
|
||||
}
|
||||
|
||||
if e = setOption(tx, dbVersion, ver); e != nil {
|
||||
tx.Rollback()
|
||||
return e
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
const (
|
||||
sqlSelectAllRule = "SELECT * FROM `rule`;"
|
||||
sqlSelectRule = "SELECT * FROM `rule` WHERE `name` = ?;"
|
||||
sqlInsertRule = "INSERT INTO `rule`(`name`, `enabled`, `created_at`, `updated_at`, `content`) VALUES(:name, :enabled, :created_at, :updated_at, :content);"
|
||||
sqlUpdateRule = "UPDATE `rule` SET `content` = :content, `updated_at` = :updated_at WHERE `name` = :name;"
|
||||
sqlEnableRule = "UPDATE `rule` SET `enabled` = :enabled, `updated_at` = :updated_at WHERE `name` = :name;"
|
||||
sqlDeleteRule = "DELETE FROM `rule` WHERE `name` = ?;"
|
||||
)
|
||||
|
||||
type Rule struct {
|
||||
Name string `db:"name"`
|
||||
Enabled bool `db:"enabled"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
Content string `db:"content"`
|
||||
}
|
||||
|
||||
func AddRule(r *Rule) error {
|
||||
r.CreatedAt = time.Now()
|
||||
r.Enabled = true
|
||||
r.UpdatedAt = r.CreatedAt
|
||||
_, e := db.NamedExec(sqlInsertRule, r)
|
||||
return e
|
||||
}
|
||||
|
||||
func UpdateRule(name string, content string) error {
|
||||
r := Rule{
|
||||
Name: name,
|
||||
UpdatedAt: time.Now(),
|
||||
Content: content,
|
||||
}
|
||||
_, e := db.NamedExec(sqlUpdateRule, &r)
|
||||
return e
|
||||
}
|
||||
|
||||
func EnableRule(name string, enabled bool) error {
|
||||
r := Rule{
|
||||
Name: name,
|
||||
Enabled: enabled,
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if res, e := db.NamedExec(sqlEnableRule, &r); e != nil {
|
||||
return e
|
||||
} else if n, e := res.RowsAffected(); n != 1 {
|
||||
return e
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func DeleteRule(name string) error {
|
||||
_, e := db.Exec(sqlDeleteRule, name)
|
||||
return e
|
||||
}
|
||||
|
||||
func GetRuleByName(name string) (*Rule, error) {
|
||||
r := Rule{}
|
||||
if e := db.Get(&r, sqlSelectRule, name); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
return &r, nil
|
||||
}
|
||||
|
||||
func LoadAllRule() ([]Rule, error) {
|
||||
var rules []Rule
|
||||
if e := db.Select(&rules, sqlSelectAllRule); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
return rules, nil
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
set -e
|
||||
|
||||
# releash.sh -c [arm | arm64 | amd64 | 386]
|
||||
# -o [linux | darwin | windows]
|
||||
|
||||
# set parameters by default value
|
||||
cpuType=amd64 # [arm | arm64 | amd64 | 386]
|
||||
osType=linux # [linux | darwin | windows]
|
||||
|
||||
while getopts "h:c:o:" arg
|
||||
do
|
||||
case $arg in
|
||||
c)
|
||||
#echo "cpuType=$OPTARG"
|
||||
cpuType=$(echo $OPTARG)
|
||||
;;
|
||||
o)
|
||||
#echo "osType=$OPTARG"
|
||||
osType=$(echo $OPTARG)
|
||||
;;
|
||||
h)
|
||||
echo "Usage: `basename $0` -c [arm | arm64 | amd64 | 386] -o [linux | darwin | windows]"
|
||||
exit 0
|
||||
;;
|
||||
?) #unknown option
|
||||
echo "unknown argument"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
|
||||
startdir=$(pwd)
|
||||
scriptdir=$(dirname $(readlink -f $0))
|
||||
cd ${scriptdir}/cmd/alert
|
||||
version=$(grep 'const version =' main.go | awk '{print $NF}')
|
||||
version=${version%\"}
|
||||
|
||||
echo "cpuType=${cpuType}"
|
||||
echo "osType=${osType}"
|
||||
echo "version=${version}"
|
||||
|
||||
GOOS=${osType} GOARCH=${cpuType} go build
|
||||
|
||||
GZIP=-9 tar -zcf ${startdir}/alert-${version}-${osType}-${cpuType}.tar.gz alert alert.cfg
|
|
@ -0,0 +1,17 @@
|
|||
sql connect
|
||||
sleep 100
|
||||
|
||||
sql drop database if exists test
|
||||
sql create database test
|
||||
sql use test
|
||||
|
||||
print ====== create super table
|
||||
sql create table cars (ts timestamp, speed int) tags(id int)
|
||||
|
||||
print ====== create tables
|
||||
$i = 0
|
||||
while $i < 5
|
||||
$tb = car . $i
|
||||
sql create table $tb using cars tags( $i )
|
||||
$i = $i + 1
|
||||
endw
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"name": "test1",
|
||||
"period": "10s",
|
||||
"sql": "select avg(speed) as avgspeed from test.cars group by id",
|
||||
"expr": "avgSpeed >= 3",
|
||||
"for": "0s",
|
||||
"labels": {
|
||||
"ruleName": "test1"
|
||||
},
|
||||
"annotations": {
|
||||
"summary": "speed of car(id = {{$labels.id}}) is too high: {{$values.avgSpeed}}"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
sql connect
|
||||
sleep 100
|
||||
|
||||
print ====== insert 10 records to table 0
|
||||
$i = 10
|
||||
while $i > 0
|
||||
$ms = $i . s
|
||||
sql insert into test.car0 values(now - $ms , 1)
|
||||
$i = $i - 1
|
||||
endw
|
|
@ -0,0 +1,5 @@
|
|||
sql connect
|
||||
sleep 100
|
||||
|
||||
print ====== insert another records table 0
|
||||
sql insert into test.car0 values(now , 100)
|
|
@ -0,0 +1,12 @@
|
|||
sql connect
|
||||
sleep 100
|
||||
|
||||
print ====== insert 10 records to table 0, 1, 2
|
||||
$i = 10
|
||||
while $i > 0
|
||||
$ms = $i . s
|
||||
sql insert into test.car0 values(now - $ms , 1)
|
||||
sql insert into test.car1 values(now - $ms , $i )
|
||||
sql insert into test.car2 values(now - $ms , 10)
|
||||
$i = $i - 1
|
||||
endw
|
|
@ -0,0 +1,91 @@
|
|||
# wait until $1 alerts are generates, and at most wait $2 seconds
|
||||
# return 0 if wait succeeded, 1 if wait timeout
|
||||
function waitAlert() {
|
||||
local i=0
|
||||
|
||||
while [ $i -lt $2 ]; do
|
||||
local c=$(wc -l alert.out | awk '{print $1}')
|
||||
|
||||
if [ $c -ge $1 ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
let "i=$i+1"
|
||||
sleep 1s
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# prepare environment
|
||||
kill -INT `ps aux | grep 'alert -cfg' | grep -v grep | awk '{print $2}'`
|
||||
|
||||
rm -f alert.db
|
||||
rm -f alert.out
|
||||
../cmd/alert/alert -cfg ../cmd/alert/alert.cfg > alert.out &
|
||||
|
||||
../../td/debug/build/bin/tsim -c /etc/taos -f ./prepare.sim
|
||||
|
||||
# add a rule to alert application
|
||||
curl -d '@rule.json' http://localhost:8100/api/update-rule
|
||||
|
||||
# step 1: add some data but not trigger an alert
|
||||
../../td/debug/build/bin/tsim -c /etc/taos -f ./step1.sim
|
||||
|
||||
# wait 20 seconds, should not get an alert
|
||||
waitAlert 1 20
|
||||
res=$?
|
||||
if [ $res -eq 0 ]; then
|
||||
echo 'should not have alerts here'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# step 2: trigger an alert
|
||||
../../td/debug/build/bin/tsim -c /etc/taos -f ./step2.sim
|
||||
|
||||
# wait 30 seconds for the alert
|
||||
waitAlert 1 30
|
||||
res=$?
|
||||
if [ $res -eq 1 ]; then
|
||||
echo 'there should be an alert now'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# compare whether the generate alert meet expectation
|
||||
diff <(uniq alert.out | sed -n 1p | jq -cS 'del(.startsAt, .endsAt)') <(jq -cSn '{"values":{"avgspeed":10,"id":0},"labels":{"id":"0","ruleName":"test1"},"annotations":{"summary":"speed of car(id = 0) is too high: 10"}}')
|
||||
if [ $? -ne 0 ]; then
|
||||
echo 'the generated alert does not meet expectation'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# step 3: add more data, trigger another 3 alerts
|
||||
../../td/debug/build/bin/tsim -c /etc/taos -f ./step3.sim
|
||||
|
||||
# wait 30 seconds for the alerts
|
||||
waitAlert 4 30
|
||||
res=$?
|
||||
if [ $res -eq 1 ]; then
|
||||
echo 'there should be 4 alerts now'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# compare whether the generate alert meet expectation
|
||||
diff <(uniq alert.out | sed -n 2p | jq -cS 'del(.startsAt, .endsAt)') <(jq -cSn '{"annotations":{"summary":"speed of car(id = 0) is too high: 5.714285714285714"},"labels":{"id":"0","ruleName":"test1"},"values":{"avgspeed":5.714285714285714,"id":0}}')
|
||||
if [ $? -ne 0 ]; then
|
||||
echo 'the generated alert does not meet expectation'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
diff <(uniq alert.out | sed -n 3p | jq -cS 'del(.startsAt, .endsAt)') <(jq -cSn '{"annotations":{"summary":"speed of car(id = 1) is too high: 5.5"},"labels":{"id":"1","ruleName":"test1"},"values":{"avgspeed":5.5,"id":1}}')
|
||||
if [ $? -ne 0 ]; then
|
||||
echo 'the generated alert does not meet expectation'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
diff <(uniq alert.out | sed -n 4p | jq -cS 'del(.startsAt, .endsAt)') <(jq -cSn '{"annotations":{"summary":"speed of car(id = 2) is too high: 10"},"labels":{"id":"2","ruleName":"test1"},"values":{"avgspeed":10,"id":2}}')
|
||||
if [ $? -ne 0 ]; then
|
||||
echo 'the generated alert does not meet expectation'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
kill -INT `ps aux | grep 'alert -cfg' | grep -v grep | awk '{print $2}'`
|
|
@ -0,0 +1,41 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Port uint16 `json:"port,omitempty" yaml:"port,omitempty"`
|
||||
Database string `json:"database,omitempty" yaml:"database,omitempty"`
|
||||
RuleFile string `json:"ruleFile,omitempty" yaml:"ruleFile,omitempty"`
|
||||
Log struct {
|
||||
Level string `json:"level,omitempty" yaml:"level,omitempty"`
|
||||
Path string `json:"path,omitempty" yaml:"path,omitempty"`
|
||||
} `json:"log" yaml:"log"`
|
||||
TDengine string `json:"tdengine,omitempty" yaml:"tdengine,omitempty"`
|
||||
Receivers struct {
|
||||
AlertManager string `json:"alertManager,omitempty" yaml:"alertManager,omitempty"`
|
||||
Console bool `json:"console"`
|
||||
} `json:"receivers" yaml:"receivers"`
|
||||
}
|
||||
|
||||
var Cfg Config
|
||||
|
||||
func LoadConfig(path string) error {
|
||||
f, e := os.Open(path)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
e = yaml.NewDecoder(f).Decode(&Cfg)
|
||||
if e != nil {
|
||||
f.Seek(0, 0)
|
||||
e = json.NewDecoder(f).Decode(&Cfg)
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
package log
|
||||
|
||||
import (
|
||||
"github.com/taosdata/alert/utils"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var logger *zap.SugaredLogger
|
||||
|
||||
func Init() error {
|
||||
var cfg zap.Config
|
||||
|
||||
if utils.Cfg.Log.Level == "debug" {
|
||||
cfg = zap.NewDevelopmentConfig()
|
||||
} else {
|
||||
cfg = zap.NewProductionConfig()
|
||||
}
|
||||
|
||||
if len(utils.Cfg.Log.Path) > 0 {
|
||||
cfg.OutputPaths = []string{utils.Cfg.Log.Path}
|
||||
}
|
||||
|
||||
l, e := cfg.Build()
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
|
||||
logger = l.Sugar()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Debug package logger
|
||||
func Debug(args ...interface{}) {
|
||||
logger.Debug(args...)
|
||||
}
|
||||
|
||||
// Debugf package logger
|
||||
func Debugf(template string, args ...interface{}) {
|
||||
logger.Debugf(template, args...)
|
||||
}
|
||||
|
||||
// Info package logger
|
||||
func Info(args ...interface{}) {
|
||||
logger.Info(args...)
|
||||
}
|
||||
|
||||
// Infof package logger
|
||||
func Infof(template string, args ...interface{}) {
|
||||
logger.Infof(template, args...)
|
||||
}
|
||||
|
||||
// Warn package logger
|
||||
func Warn(args ...interface{}) {
|
||||
logger.Warn(args...)
|
||||
}
|
||||
|
||||
// Warnf package logger
|
||||
func Warnf(template string, args ...interface{}) {
|
||||
logger.Warnf(template, args...)
|
||||
}
|
||||
|
||||
// Error package logger
|
||||
func Error(args ...interface{}) {
|
||||
logger.Error(args...)
|
||||
}
|
||||
|
||||
// Errorf package logger
|
||||
func Errorf(template string, args ...interface{}) {
|
||||
logger.Errorf(template, args...)
|
||||
}
|
||||
|
||||
// Fatal package logger
|
||||
func Fatal(args ...interface{}) {
|
||||
logger.Fatal(args...)
|
||||
}
|
||||
|
||||
// Fatalf package logger
|
||||
func Fatalf(template string, args ...interface{}) {
|
||||
logger.Fatalf(template, args...)
|
||||
}
|
||||
|
||||
// Panic package logger
|
||||
func Panic(args ...interface{}) {
|
||||
logger.Panic(args...)
|
||||
}
|
||||
|
||||
// Panicf package logger
|
||||
func Panicf(template string, args ...interface{}) {
|
||||
logger.Panicf(template, args...)
|
||||
}
|
||||
|
||||
func Sync() error {
|
||||
return logger.Sync()
|
||||
}
|
Loading…
Reference in New Issue