diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 176a458f94e0ea5272ce67c36bf30b6be9caf623..0000000000000000000000000000000000000000 --- a/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -* text=auto diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml deleted file mode 100644 index 8894223a385d9ca07d435df7ba3aed268ce8d5b8..0000000000000000000000000000000000000000 --- a/.github/workflows/go.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Go - -on: - push: - branches: [ master ] - pull_request: - branches: [ master ] - -jobs: - - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - - name: Set up Go - uses: actions/setup-go@v2 - with: - go-version: 1.14 - - - name: Build - run: go build -v ./... - - - name: Test - run: go test -v ./... diff --git a/.gitignore b/.gitignore index b07cea67e51630016757b2bfa258e2fc7a6927c6..e26e6518ea59ced00ff543977556714466621032 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,30 @@ +src/**/*.pyc +src/**/build +src/monitor_app/*.so +src/monitor_app/*.cpp +src/monitor_app/*.c +src/obagent/*.so +src/obagent/*.cpp +src/obagent/*.c +src/conf/config.conf + +# go Binary files +bin/* +go.work +go.work.sum + +# go test files and generated files +bindata/ +iostat +tests/testdata/* +tests/testdata/data/ +tests/mock*/* +tests/coverage.out +tests/coverage-report/ + +# macOS files .DS_Store -.vscode -.idea + + +.idea/* *.iml -*.swp -*.log -*.exe -*.exe~ -*.dll -*.so -*.dylib -*.test -*.out -bin/* diff --git a/.secignore b/.secignore index dc16e1cd933b6174d3e4e68350305639605f0976..1b31e1bbbedfcb35f9003c59ba825fa0e3e26097 100644 --- a/.secignore +++ b/.secignore @@ -9,12 +9,14 @@ ** http://license.coscl.org.cn/MulanPSL2 http://1.1.1.1:9093 + http://127.0.0.1:9093 http://127.0.0.1:9100/metrics http://127.0.0.1:9091/metrics/node http://127.0.0.1:9090/metrics/node https://mirrors.aliyun.com/goproxy/ https://facebook.github.io/watchman/ https://github.com/oceanbase/obagent + https://goproxy.cn https://github.com/krallin/tini/* https://bixense.com/clicolors/ -------------------------------------------------------- @@ -23,12 +25,16 @@ /README.md /README-CN.md /LICENCE -/plugins/inputs/oceanbase/log/log_utils.go -/plugins/inputs/prometheus/prometheus_test.go /docs/develop-guide.md /docs/install-and-deploy/install-obagent.md /docs/install-and-deploy/deploy-obagent-with-obd.md /docs/install-and-deploy/deploy-obagent-manually.md /.git/** + +/build_rpm_obagent.aci.yml +/Makefile.common +/packaging/** +**/**_test.go +/rpm/obagent.spec -------------------------------------------------------- -# Config the ignored fold to escape the Chinese scan by GLOB wildcard +# Config the ignored fold to escape the Chinese scan by GLOB wildcard \ No newline at end of file diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 2d1d5edf0a38c936fb51578e046500e085ff3f15..0000000000000000000000000000000000000000 --- a/Dockerfile +++ /dev/null @@ -1,38 +0,0 @@ -FROM golang:1.16 as builder - -WORKDIR /workspace - -RUN echo '----------- DATE_CHANGE: Add this line to disable cache during docker building -------------------' -# Copy the Go Modules manifests -RUN mkdir /workspace/obagent -ADD . /workspace/obagent - -# Build -RUN cd /workspace/obagent && make build-release - -FROM openanolis/anolisos:8.4-x86_64 - -RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime - -WORKDIR /home/admin - -ENV TINI_VERSION v0.19.0 -ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini -RUN chmod +x /tini - -RUN useradd -m admin - -RUN mkdir -p /home/admin/obagent/bin -RUN mkdir -p /home/admin/obagent/run -RUN mkdir -p /home/admin/obagent/log -RUN mkdir -p /home/admin/obagent/conf - -COPY --from=builder /workspace/obagent/bin/monagent ./obagent/bin -COPY --from=builder /workspace/obagent/etc ./obagent/etc - -RUN chown -R admin:admin /home/admin/obagent -ADD ./replace_yaml.sh /home/admin/obagent - - -ENTRYPOINT ["/tini", "--"] -CMD ["bash", "-c", " cd /home/admin/obagent && if [ \"`ls -A conf`\" == \"\" ]; then cp -r etc/* conf/ && ./replace_yaml.sh; fi && ./bin/monagent -c conf/monagent.yaml"] diff --git a/LICENCE b/LICENCE deleted file mode 100644 index 8c5060421198a28d04baaa4d856e00f4bd3e1c74..0000000000000000000000000000000000000000 --- a/LICENCE +++ /dev/null @@ -1,87 +0,0 @@ -木兰宽松许可证, 第2版 - -2020年1月 http://license.coscl.org.cn/MulanPSL2 - -您对“软件”的复制、使用、修改及分发受木兰宽松许可证,第2版(“本许可证”)的如下条款的约束: - -0. 定义 - -“软件” 是指由“贡献”构成的许可在“本许可证”下的程序和相关文档的集合。 - -“贡献” 是指由任一“贡献者”许可在“本许可证”下的受版权法保护的作品。 - -“贡献者” 是指将受版权法保护的作品许可在“本许可证”下的自然人或“法人实体”。 - -“法人实体” 是指提交贡献的机构及其“关联实体”。 - -“关联实体” 是指,对“本许可证”下的行为方而言,控制、受控制或与其共同受控制的机构,此处的控制是指有受控方或共同受控方至少50%直接或间接的投票权、资金或其他有价证券。 - -1. 授予版权许可 - -每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的版权许可,您可以复制、使用、修改、分发其“贡献”,不论修改与否。 - -2. 授予专利许可 - -每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的(根据本条规定撤销除外)专利许可,供您制造、委托制造、使用、许诺销售、销售、进口其“贡献”或以其他方式转移其“贡献”。前述专利许可仅限于“贡献者”现在或将来拥有或控制的其“贡献”本身或其“贡献”与许可“贡献”时的“软件”结合而将必然会侵犯的专利权利要求,不包括对“贡献”的修改或包含“贡献”的其他结合。如果您或您的“关联实体”直接或间接地,就“软件”或其中的“贡献”对任何人发起专利侵权诉讼(包括反诉或交叉诉讼)或其他专利维权行动,指控其侵犯专利权,则“本许可证”授予您对“软件”的专利许可自您提起诉讼或发起维权行动之日终止。 - -3. 无商标许可 - -“本许可证”不提供对“贡献者”的商品名称、商标、服务标志或产品名称的商标许可,但您为满足第4条规定的声明义务而必须使用除外。 - -4. 分发限制 - -您可以在任何媒介中将“软件”以源程序形式或可执行形式重新分发,不论修改与否,但您必须向接收者提供“本许可证”的副本,并保留“软件”中的版权、商标、专利及免责声明。 - -5. 免责声明与责任限制 - -“软件”及其中的“贡献”在提供时不带任何明示或默示的担保。在任何情况下,“贡献者”或版权所有者不对任何人因使用“软件”或其中的“贡献”而引发的任何直接或间接损失承担责任,不论因何种原因导致或者基于何种法律理论,即使其曾被建议有此种损失的可能性。 - -6. 语言 - -“本许可证”以中英文双语表述,中英文版本具有同等法律效力。如果中英文版本存在任何冲突不一致,以中文版为准。 - -条款结束 - -Mulan Permissive Software License,Version 2 (Mulan PSL v2) - -January 2020 http://license.coscl.org.cn/MulanPSL2 - -Your reproduction, use, modification and distribution of the Software shall be subject to Mulan PSL v2 (this License) with the following terms and conditions: - -0. Definition - -Software means the program and related documents which are licensed under this License and comprise all Contribution(s). - -Contribution means the copyrightable work licensed by a particular Contributor under this License. - -Contributor means the Individual or Legal Entity who licenses its copyrightable work under this License. - -Legal Entity means the entity making a Contribution and all its Affiliates. - -Affiliates means entities that control, are controlled by, or are under common control with the acting entity under this License, ‘control’ means direct or indirect ownership of at least fifty percent (50%) of the voting power, capital or other securities of controlled or commonly controlled entity. - -1. Grant of Copyright License - -Subject to the terms and conditions of this License, each Contributor hereby grants to you a perpetual, worldwide, royalty-free, non-exclusive, irrevocable copyright license to reproduce, use, modify, or distribute its Contribution, with modification or not. - -2. Grant of Patent License - -Subject to the terms and conditions of this License, each Contributor hereby grants to you a perpetual, worldwide, royalty-free, non-exclusive, irrevocable (except for revocation under this Section) patent license to make, have made, use, offer for sale, sell, import or otherwise transfer its Contribution, where such patent license is only limited to the patent claims owned or controlled by such Contributor now or in future which will be necessarily infringed by its Contribution alone, or by combination of the Contribution with the Software to which the Contribution was contributed. The patent license shall not apply to any modification of the Contribution, and any other combination which includes the Contribution. If you or your Affiliates directly or indirectly institute patent litigation (including a cross claim or counterclaim in a litigation) or other patent enforcement activities against any individual or entity by alleging that the Software or any Contribution in it infringes patents, then any patent license granted to you under this License for the Software shall terminate as of the date such litigation or activity is filed or taken. - -3. No Trademark License - -No trademark license is granted to use the trade names, trademarks, service marks, or product names of Contributor, except as required to fulfill notice requirements in section 4. - -4. Distribution Restriction - -You may distribute the Software in any medium with or without modification, whether in source or executable forms, provided that you provide recipients with a copy of this License and retain copyright, patent, trademark and disclaimer statements in the Software. - -5. Disclaimer of Warranty and Limitation of Liability - -THE SOFTWARE AND CONTRIBUTION IN IT ARE PROVIDED WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED. IN NO EVENT SHALL ANY CONTRIBUTOR OR COPYRIGHT HOLDER BE LIABLE TO YOU FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO ANY DIRECT, OR INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES ARISING FROM YOUR USE OR INABILITY TO USE THE SOFTWARE OR THE CONTRIBUTION IN IT, NO MATTER HOW IT’S CAUSED OR BASED ON WHICH LEGAL THEORY, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. - -6. Language - -THIS LICENSE IS WRITTEN IN BOTH CHINESE AND ENGLISH, AND THE CHINESE VERSION AND ENGLISH VERSION SHALL HAVE THE SAME LEGAL EFFECT. IN THE CASE OF DIVERGENCE BETWEEN THE CHINESE AND ENGLISH VERSIONS, THE CHINESE VERSION SHALL PREVAIL. - -END OF THE TERMS AND CONDITIONS diff --git a/Makefile b/Makefile index 366efa808f5b9b2ecc80c4a3c4c3a23178904391..2133bbd8301a14a2b72b46314d8b5a9af916ec3e 100644 --- a/Makefile +++ b/Makefile @@ -1,29 +1,89 @@ include Makefile.common -.PHONY: all test clean build monagent +.PHONY: pre-build build bindata mgragent monagent agentd agentctl mockgen rpm buildsucc -default: clean fmt build +default: clean fmt pre-build build + +pre-build: bindata + +pre-test: mockgen + +bindata: get + go-bindata -o bindata/bindata.go -pkg bindata assets/... + +mockgen: + mockgen -source=lib/http/http.go -destination=tests/mock/lib_http_http_mock.go -package mock + mockgen -source=lib/system/process.go -destination=tests/mock/lib_system_process_mock.go -package mock + mockgen -source=lib/system/disk.go -destination=tests/mock/lib_system_disk_mock.go -package mock + mockgen -source=lib/system/system.go -destination=tests/mock/lib_system_system_mock.go -package mock + mockgen -source=lib/shell/shell.go -destination=tests/mock/lib_shell_shell_mock.go -package mock + mockgen -source=lib/shell/command.go -destination=tests/mock/lib_shell_command_mock.go -package mock + mockgen -source=lib/shellf/shellf.go -destination=tests/mock/lib_shellf_shellf_mock.go -package mock + mockgen -source=lib/file/file.go -destination=tests/mock/lib_file_file_mock.go -package mock + mockgen -source=lib/pkg/package.go -destination=tests/mock2/lib_pkg_package_mock.go -package mock2 build: build-debug -build-debug: set-debug-flags monagent +build-debug: set-debug-flags mgragent monagent agentd agentctl buildsucc + +build-release: set-release-flags mgragent monagent agentd agentctl buildsucc -build-release: set-release-flags monagent +rpm: + cd ./rpm && RELEASE=`date +%Y%m%d%H%M%S` rpmbuild -bb ./obagent.spec set-debug-flags: @echo Build with debug flags $(eval LDFLAGS += $(LDFLAGS_DEBUG)) + $(eval BUILD_FLAG += $(GO_RACE_FLAG)) set-release-flags: @echo Build with release flags $(eval LDFLAGS += $(LDFLAGS_RELEASE)) monagent: - $(GOBUILD) $(GO_RACE_FLAG) -ldflags '$(MONAGENT_LDFLAGS)' -o bin/monagent cmd/monagent/main.go + $(GO) build $(BUILD_FLAG) -ldflags '$(MONAGENT_LDFLAGS)' -o bin/ob_monagent cmd/monagent/main.go + +mgragent: + $(GO) build $(BUILD_FLAG) -ldflags '$(MGRAGENT_LDFLAGS)' -o bin/ob_mgragent cmd/mgragent/main.go + +agentctl: + $(GO) build $(BUILD_FLAG) -ldflags '$(AGENTCTL_LDFLAGS)' -o bin/ob_agentctl cmd/agentctl/main.go + +agentd: + $(GO) build $(BUILD_FLAG) -ldflags '$(AGENTD_LDFLAGS)' -o bin/ob_agentd cmd/agentd/main.go + + +buildsucc: + @echo Build obagent successfully! -test: +runmgragent: + ./bin/ob_mgragent --config tests/testdata/mgragent.yaml + +runmonagent: + ./bin/ob_monagent --config tests/testdata/monagent.yaml + +test: pre-build pre-test $(GOTEST) $(GOTEST_PACKAGES) +test-cover-html: + go tool cover -html=$(GOCOVERAGE_FILE) + +test-cover-html-out: + mkdir -p $(GOCOVERAGE_REPORT) + go tool cover -html=$(GOCOVERAGE_FILE) -o $(GOCOVERAGE_REPORT)/index.html + +test-cover-profile: + go tool cover -func=$(GOCOVERAGE_FILE) + +test-cover-total: + go tool cover -func=$(GOCOVERAGE_FILE) | tail -1 | awk '{print "total line coverage: " $$3}' + +deploy: + mkdir /home/admin/obagent + cp -r bin /home/admin/obagent/bin + cp -r etc /home/admin/obagent/conf + mkdir -p /home/admin/obagent/{log,run,tmp,backup,pkg_store,task_store,position_store} + fmt: @gofmt -s -w $(filter-out , $(GOFILES)) @@ -40,11 +100,17 @@ fmt-check: tidy: $(GO) mod tidy +get: + $(GO) install github.com/go-bindata/go-bindata/...@v3.1.2+incompatible + $(GO) install github.com/golang/mock/mockgen@v1.6.0 + vet: go vet $$(go list ./...) clean: rm -rf $(GOCOVERAGE_FILE) rm -rf tests/mock/* - rm -rf bin/monagent + rm -rf bin/ob_mgragent bin/ob_monagent bin/ob_agentctl bin/ob_agentd $(GO) clean -i ./... + +init: pre-build pre-test tidy diff --git a/Makefile.common b/Makefile.common index 6e412cada2c26dec7a37ab4ec172e482827148a5..543891b8758ebe59c0373ccbfcc7a5c46a043669 100644 --- a/Makefile.common +++ b/Makefile.common @@ -1,9 +1,9 @@ PROJECT=obagent PROCESSOR=2 -VERSION=1.0 +VERSION=4.1.0-SNAPSHOT PWD ?= $(shell pwd) -GO := GO111MODULE=on GOPROXY=https://mirrors.aliyun.com/goproxy/,direct go +GO := GO111MODULE=on GOPROXY=https://goproxy.cn,direct go BUILD_FLAG := -p $(PROCESSOR) GOBUILD := $(GO) build $(BUILD_FLAG) GOBUILDCOVERAGE := $(GO) test -covermode=count -coverpkg="../..." -c . @@ -11,14 +11,20 @@ GOCOVERAGE_FILE := tests/coverage.out GOCOVERAGE_REPORT := tests/coverage-report GOTEST := OB_AGENT_CONFIG_PATH=$(PWD) $(GO) test -tags test -covermode=count -coverprofile=$(GOCOVERAGE_FILE) -p $(PROCESSOR) +GO_RACE_FLAG =-race LDFLAGS += -X "github.com/oceanbase/obagent/config.AgentVersion=${VERSION}" -#LDFLAGS += -X "github.com/oceanbase/obagent/config.ReleaseVersion=$(shell git describe --tags --dirty --always)" -LDFLAGS += -X "github.com/oceanbase/obagent/config.BuildTimestamp=$(shell date -u '+%Y-%m-%d %H:%M:%S')" +LDFLAGS += -X "github.com/oceanbase/obagent/config.BuildEpoch=$(shell date '+%s')" +LDFLAGS += -X "github.com/oceanbase/obagent/config.BuildGoVersion=$(shell go version)" LDFLAGS += -X "github.com/oceanbase/obagent/config.GitBranch=$(shell git rev-parse --abbrev-ref HEAD)" -LDFLAGS += -X "github.com/oceanbase/obagent/config.GitHash=$(shell git rev-parse HEAD)" +LDFLAGS += -X "github.com/oceanbase/obagent/config.GitCommitId=$(shell git rev-parse HEAD)" +LDFLAGS += -X "github.com/oceanbase/obagent/config.GitShortCommitId=$(shell git rev-parse --short HEAD)" +LDFLAGS += -X "github.com/oceanbase/obagent/config.GitCommitTime=$(shell git log -1 --format=%cd)" LDFLAGS_DEBUG = -X "github.com/oceanbase/obagent/config.Mode=debug" LDFLAGS_RELEASE = -X "github.com/oceanbase/obagent/config.Mode=release" -MONAGENT_LDFLAGS = $(LDFLAGS) -X "github.com/oceanbase/obagent/config.CurProcess=monagent" +MGRAGENT_LDFLAGS = $(LDFLAGS) -X "github.com/oceanbase/obagent/config.CurProcess=ob_mgragent" +MONAGENT_LDFLAGS = $(LDFLAGS) -X "github.com/oceanbase/obagent/config.CurProcess=ob_monagent" +AGENTCTL_LDFLAGS = $(LDFLAGS) -X "github.com/oceanbase/obagent/config.CurProcess=ob_agentctl" +AGENTD_LDFLAGS = $(LDFLAGS) -X "github.com/oceanbase/obagent/config.CurProcess=ob_agentd" GOFILES ?= $(shell git ls-files '*.go') GOTEST_PACKAGES = $(shell go list ./... | grep -v -f tests/excludes.txt) diff --git a/README-CN.md b/README-CN.md deleted file mode 100644 index 2ede2f1054fb63a1c004f3f7cb48bb8d711f22f5..0000000000000000000000000000000000000000 --- a/README-CN.md +++ /dev/null @@ -1,58 +0,0 @@ -# OBAgent - -OBAgent 是一个监控采集框架。OBAgent 支持推、拉两种数据采集模式,可以满足不同的应用场景。OBAgent 默认支持的插件包括主机数据采集、OceanBase 数据库指标的采集、监控数据标签处理和 Prometheus 协议的 HTTP 服务。要使 OBAgent 支持其他数据源的采集,或者自定义数据的处理流程,您只需要开发对应的插件即可。 - -## 许可证 - -OBAgent 使用 [MulanPSL - 2.0](http://license.coscl.org.cn/MulanPSL2) 许可证。您可以免费复制及使用源代码。当您修改或分发源代码时,请遵守木兰协议。 - -## 文档 - -参考 [OBAgent 文档](docs/about-obagent/what-is-obagent.md)。 - -## 如何获取 - -### 环境依赖 - -构建 OBAgent 需要 Go 1.14 版本及以上。 - -### RPM 包 - -OBAgent 提供 RPM 包,您可以去 [Release 页面](https://mirrors.aliyun.com/oceanbase/community/stable/el/7/x86_64/) 下载 RPM 包,然后使用以下命令安装: - -```bash -rpm -ivh obagent-1.0.0-1.el7.x86_64.rpm -``` - -### 通过源码构建 - -#### Debug 模式 - -```bash -make build // make build will be debug mode by default -make build-debug -``` - -#### Release 模式 - -```bash -make build-release -``` - -## 如何开发 - -您可以为 OBAgent 开发插件。更多信息,参考 [OBAgent 插件开发](docs/develop-guide.md)。 - -## 如何贡献 - -我们十分欢迎并感谢您为我们贡献。以下是您参与贡献的几种方式: - -- 向我们提 [Issue](https://github.com/oceanbase/obagent/issues)。 - -## 获取帮助 - -如果您在使用 OBAgent 时遇到任何问题,欢迎通过以下方式寻求帮助: - -- [GitHub Issue](https://github.com/oceanbase/obagent/issues) -- [官方网站](https://open.oceanbase.com/) -- 知识问答(Coming soon) diff --git a/README.md b/README.md index bacfb1ee3f5a10a8dfcacecae309c476a6cb3863..4072cc378348a4e39e5a0350a18d45d7fe91b0ca 100644 --- a/README.md +++ b/README.md @@ -1,58 +1,90 @@ -# OBAgent +# OB-Agent +OB Agent 是 OCP Express 远程访问主机和 OBServer 的入口,提供两个主要能力: +1. 运维主机和 OBServer; +2. 收集主机、OBServer的监控。 +此外,OB Agent 还承担 OB 日志查询和日志清理的职能。 -OBAgent is a monitor collection framework. OBAgent supplies pull and push mode data collection to meet different applications. By default, OBAgent supports these plugins: server data collection, OceanBase Database metrics collection, monitor data processing, and the HTTP service for Prometheus Protocol. To support data collection for other data sources, or customized data flow processes, you only need to develop plugins. +## 目录结构 -## Licencing - -OBAgent is under [MulanPSL - 2.0](http://license.coscl.org.cn/MulanPSL2) license. You can freely copy and use the source code. When you modify or distribute the source code, please obey the MulanPSL - 2.0 license. - -## Documentation +``` +obagent +├── cmd:程序主入口 +│ ├── mgragent: 运维进程主入口,后台运行的进程,不期望被用户使用;目标产物名称为 ob_mgragent。 +│ ├── monagent: 监控进程主入口,后台运行进程,不期望被用户使用;目标产物名称为 ob_monagent。 +│ ├── agentd: 守护进程,后台运行的进程,负责拉起异常退出的ob_mgragent和ob_monagent进程;目标产物名称为ob_agentd。 +│ └── agentctl: 黑屏运维工具主入口,命令行运维工具,可通过该工具运维 ob_mgragent 和 ob_monagent 进程;目标产物名称为 ob_agentctl 。 +├── api:请求处理、认证 +│ ├── monroute:监控模块route和handlers +│ ├── mgrroute:运维模块route和handlers +│ ├── web:http server +│ └── common:公共handlers和middleware +├── agentd:守护进程agentd模块 +│ └── api:进程状态信息 +├── config: 配置文件解析,回调函数注册 +│ ├── monconfig:监控配置 +│ ├── mgrconfig:运维配置 +│ ├── agentctl:黑屏运维工具配置 +│ └── sdk:配置管理,回调函数注册 +├── executor:提供运维能力,命令执行能力,支持 shell 跨平台执行 +│ ├── agent:agent进程运维 +│ ├── cleaner:日志清理 +│ ├── log_query:日志查询 +│ └── ... +├── lib:通用方法,比如加密、脱敏、重试、命令行执行等 +│ ├── command:命令执行,异步任务调度 +│ ├── process:进程启停 +│ ├── goroutinepool:任务池 +│ ├── log_analyzer:日志解析 +│ ├── retry:重试框架 +│ ├── shellf:命令模板配置解析,命令构建 +│ ├── shell:命令执行 +│ └── ... +├── monitor:监控模块,包含插件定义、流水线引擎、监控数据结构等 +│ ├── engine:流水线引擎 +│ ├── plugins:流水线插件 +│ ├── message:监控数据结构 +│ └── utils:监控通用函数 +├── stat:自监控模块,obagent、moniotr 都会依赖此模块实现自监控 +├── log:日志框架 +├── errors:错误处理 +├── rpm:rpm打包逻辑 +├── tests:测试脚本、数据、配置 +├── etc:发布的配置文件,均为 yaml 类型 +│ ├── config_properties:key-value配置 +│ ├── module_config:流水线等配置文件 +│ ├── prometheus_config:prometheus拉取配置 +│ ├── scripts:开机自启动脚本 +│ └── shell_templates:命令模板 +└── docs:文档,包括 obagent 的 README 文档,以及各个子模块的说明文档 +``` -See [OBAgent Document](docs/about-obagent/what-is-obagent.md). +# 安装 OBAgent -## How to get +您可以使用 RPM 包或者构建源码安装 OBAgent。 -### Dependencies +## 环境依赖 -To build OBAgent, make sure that your Go version is 1.14 or above. +构建 OBAgent 需要 Go 1.19 版本及以上。 -### From RPM package +## RPM 包 -OBAgent supplies RPM package. You can download it from the [Release page](https://mirrors.aliyun.com/oceanbase/community/stable/el/7/x86_64/) and install it by using this command: +OBAgent 提供 RPM 包,您可以去 [Release 页面](https://mirrors.aliyun.com/oceanbase/community/stable/el/7/x86_64/) 下载 RPM 包,然后使用以下命令安装: ```bash -rpm -ivh obagent-1.0.0-1.el7.x86_64.rpm +rpm -ivh obagent-4.1.0-1.el7.x86_64.rpm ``` -### From source code +## 构建源码 -### Debug mode +### Debug 模式 ```bash -make build // make build is debug mode by default +make build // make build will be debug mode by default make build-debug ``` -### Release mode +### Release 模式 ```bash make build-release ``` - -## How to develop - -You can develop plugins for OBAgent. For more information, see [Develop plugins for OBAgent](docs/develop-guide.md). - -## Contributing - -Contributions are warmly welcomed and greatly appreciated. Here are a few ways you can contribute: - -- Raise us an [Issue](https://github.com/oceanbase/obagent/issues). - -## Support - -In case you have any problems when using OBAgent, welcome to reach out for help: - -- [GitHub Issue](https://github.com/oceanbase/obagent/issues) -- [Official website](https://open.oceanbase.com/) -- Knowledge base [Coming soon] diff --git a/agentd/api/api.go b/agentd/api/api.go new file mode 100644 index 0000000000000000000000000000000000000000..e5e21ca4bf7dd63156b5bf799272bd3a6a11b135 --- /dev/null +++ b/agentd/api/api.go @@ -0,0 +1,41 @@ +package api + +import ( + "github.com/oceanbase/obagent/lib/http" +) + +type Status struct { + // agentd state + State http.State `json:"state"` + // whether agentd and all services are running + Ready bool `json:"ready"` + // agentd version + Version string `json:"version"` + // pid of agentd + Pid int `json:"pid"` + // socket file path + Socket string `json:"socket"` + // services (mgragent, monagent) status + Services map[string]ServiceStatus `json:"services"` + // services without agentd. maybe agentd dead, or service not exited expectedly + Dangling []DanglingService `json:"dangling"` + // StartAt is start time of agentd + StartAt int64 `json:"startAt"` +} + +type ServiceStatus struct { + http.Status + Socket string `json:"socket"` + EndAt int64 `json:"endAt"` +} + +type DanglingService struct { + Name string `json:"name"` + Pid int `json:"pid"` + PidFile string `json:"pidFile"` + Socket string `json:"socket"` +} + +type StartStopAgentParam struct { + Service string `json:"service"` +} diff --git a/agentd/config.go b/agentd/config.go new file mode 100644 index 0000000000000000000000000000000000000000..81787e06c75db4e95d2899e03a8c13e04e119334 --- /dev/null +++ b/agentd/config.go @@ -0,0 +1,55 @@ +package agentd + +import ( + "time" + + "github.com/alecthomas/units" +) + +// ServiceConfig sub service config +type ServiceConfig struct { + //Program can be absolute or relative path or just command name + Program string `yaml:"program"` + //Args program arguments + Args []string `yaml:"args"` + //Cwd current work dir of service + Cwd string `yaml:"cwd"` + //RunDir dir to put something like pid, socket, lock + RunDir string `yaml:"runDir"` + //Path to redirect Stdout + Stdout string `yaml:"stdout"` + //Path to redirect Stderr + Stderr string `yaml:"stderr"` + //When service quited too quickly for QuickExitLimit times, service will not restart even more. + QuickExitLimit int `yaml:"quickExitLimit"` + //Services lives less then MinLiveTime will treated as quited too quickly. + MinLiveTime time.Duration `yaml:"minLiveTime"` + //KillWait after stop service and process not exited, will send SIGKILL signal to it. + //0 means no wait and won't send SIGKILL signal. + KillWait time.Duration `yaml:"killWait"` + //FinalWait after stop service and process not exited, will not wait for it. and return an error + FinalWait time.Duration `yaml:"finalWait"` + //Limit cpu and memory usage + Limit LimitConfig `yaml:"limit"` +} + +// Config agentd config +type Config struct { + //RunDir dir to put something like pid, socket, lock + RunDir string `yaml:"runDir"` + //LogDir dir to write agentd log + LogDir string `yaml:"logDir"` + //LogLevel highest level to log + LogLevel string `yaml:"logLevel"` + //Services sub services config + Services map[string]ServiceConfig `yaml:"services"` + //CleanupDangling whether cleanup dangling service process or not + CleanupDangling bool +} + +type LimitConfig struct { + //CpuQuota max cpu usage percentage. 1.0 means 100%, 2.0 means 200% + CpuQuota float32 `yaml:"cpuQuota"` + //MemoryQuota max memory limit in bytes + MemoryQuota units.Base2Bytes `yaml:"memoryQuota"` +} diff --git a/agentd/config_test.go b/agentd/config_test.go new file mode 100644 index 0000000000000000000000000000000000000000..3cd49605c50db4476de6dee629158f16a174f153 --- /dev/null +++ b/agentd/config_test.go @@ -0,0 +1,31 @@ +package agentd + +import ( + "testing" + + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v3" +) + +func TestLimitConfigUnmarshal(t *testing.T) { + content := `cpuQuota: 1.0 +memoryQuota: 1024MB` + var conf LimitConfig + err := yaml.Unmarshal([]byte(content), &conf) + assert.Nil(t, err) + assert.Equal(t, int64(1024*1024*1024), int64(conf.MemoryQuota)) + + logrus.Infof("conf: %+v", conf) +} + +func TestEmptyQuotaLimitConfigUnmarshal(t *testing.T) { + content := `cpuQuota: +memoryQuota: ` + var conf LimitConfig + err := yaml.Unmarshal([]byte(content), &conf) + assert.Nil(t, err) + assert.Equal(t, int64(0), int64(conf.MemoryQuota)) + + logrus.Infof("conf: %+v", conf) +} diff --git a/agentd/error.go b/agentd/error.go new file mode 100644 index 0000000000000000000000000000000000000000..4861379164b02437a4a6930acb8a68f8d5949825 --- /dev/null +++ b/agentd/error.go @@ -0,0 +1,17 @@ +package agentd + +import "github.com/oceanbase/obagent/lib/errors" + +const module = "agentd" + +var ( + ServiceAlreadyStartedErr = errors.FailedPrecondition.NewCode(module, "service_already_started") + ServiceAlreadyStoppedErr = errors.FailedPrecondition.NewCode(module, "service_already_stopped") + ServiceNotFoundErr = errors.NotFound.NewCode(module, "service_not_round") + BadParamErr = errors.InvalidArgument.NewCode(module, "bad_param").WithMessageTemplate("invalid input parameter, maybe bad format: %v") + InvalidActionErr = errors.InvalidArgument.NewCode(module, "invalid_action").WithMessageTemplate("invalid action: %s") + InternalServiceErr = errors.Internal.NewCode(module, "internal_service_err") + AgentdNotRunningErr = errors.Internal.NewCode(module, "agentd_not_running") + WritePidFailedErr = errors.Internal.NewCode(module, "write_pid_failed") + RemovePidFailedErr = errors.Internal.NewCode(module, "remove_pid_failed") +) diff --git a/agentd/limit.go b/agentd/limit.go new file mode 100644 index 0000000000000000000000000000000000000000..bc6279e8aab3d52322fd599c3b7d5628ae03ab37 --- /dev/null +++ b/agentd/limit.go @@ -0,0 +1,16 @@ +package agentd + +type Limiter interface { + LimitPid(pid int) error +} + +func NewLimiter(name string, conf LimitConfig) (Limiter, error) { + return newLimiter(name, conf) +} + +type NopLimiter struct { +} + +func (l *NopLimiter) LimitPid(pid int) error { + return nil +} diff --git a/agentd/limit_linux.go b/agentd/limit_linux.go new file mode 100644 index 0000000000000000000000000000000000000000..1b1dd1c4b7b59540f79e2a767bf5a9e4d334ee9a --- /dev/null +++ b/agentd/limit_linux.go @@ -0,0 +1,69 @@ +//go:build linux +// +build linux + +package agentd + +import ( + "path/filepath" + + "github.com/containerd/cgroups" + "github.com/opencontainers/runtime-spec/specs-go" + log "github.com/sirupsen/logrus" +) + +type LinuxLimiter struct { + cgroup cgroups.Cgroup +} + +func newLimiter(name string, conf LimitConfig) (Limiter, error) { + if conf.CpuQuota <= 0 && conf.MemoryQuota <= 0 { + log.Infof("create service %s resource limit skipped, no limit in config", name) + return &LinuxLimiter{}, nil + } + cg, err := cgroups.New(cgroups.V1, cgroups.StaticPath(filepath.Join("/ocp_agent/", name)), toLinuxResources(conf)) + if err != nil { + log.Warnf("create cgroup for service %s failed, fallback to watch limiter. only memory quota will affect!", name) + return &WatchLimiter{ + name: name, + conf: conf, + }, nil + } + log.Infof("create service %s resource limit done, cpu: %v, memory: %v", name, conf.CpuQuota, conf.MemoryQuota) + return &LinuxLimiter{ + cgroup: cg, + }, nil +} + +func (l *LinuxLimiter) LimitPid(pid int) error { + if l.cgroup == nil { + return nil + } + err := l.cgroup.Add(cgroups.Process{Pid: pid}) + return err +} + +func toLinuxResources(conf LimitConfig) *specs.LinuxResources { + var period *uint64 = nil + var quota *int64 = nil + var memLimit *int64 = nil + + if conf.CpuQuota > 0 { + period = new(uint64) + *period = 1000000 + quota = new(int64) + *quota = int64(1000000 * conf.CpuQuota) + } + if conf.MemoryQuota > 0 { + memLimit = new(int64) + *memLimit = int64(conf.MemoryQuota) + } + return &specs.LinuxResources{ + CPU: &specs.LinuxCPU{ + Period: period, + Quota: quota, + }, + Memory: &specs.LinuxMemory{ + Limit: memLimit, + }, + } +} diff --git a/agentd/limit_nonlinux.go b/agentd/limit_nonlinux.go new file mode 100644 index 0000000000000000000000000000000000000000..f847bf5d2513a5aa7b8b6515d3339a0df0872c79 --- /dev/null +++ b/agentd/limit_nonlinux.go @@ -0,0 +1,13 @@ +//go:build !linux +// +build !linux + +package agentd + +import ( + log "github.com/sirupsen/logrus" +) + +func newLimiter(name string, conf LimitConfig) (Limiter, error) { + log.Infof("creating service %s resource limit done, WatchLimiter memory: %v", name, conf.MemoryQuota) + return &WatchLimiter{name: name, conf: conf}, nil +} diff --git a/agentd/limit_watch.go b/agentd/limit_watch.go new file mode 100644 index 0000000000000000000000000000000000000000..39f026132f6cb2039184b7e662b1a29f1da12dd9 --- /dev/null +++ b/agentd/limit_watch.go @@ -0,0 +1,41 @@ +package agentd + +import ( + "time" + + "github.com/shirou/gopsutil/v3/process" + + log "github.com/sirupsen/logrus" +) + +type WatchLimiter struct { + name string + conf LimitConfig +} + +func (l *WatchLimiter) LimitPid(pid int) error { + if l.conf.MemoryQuota <= 0 { + return nil + } + p, err := process.NewProcess(int32(pid)) + if err != nil { + return err + } + go func() { + ticker := time.NewTicker(time.Second) + defer ticker.Stop() + for { + <-ticker.C + m, mErr := p.MemoryInfo() + if mErr != nil { + log.Warnf("fetch memory info for service %s failed: %v, stop watch loop", l.name, mErr) + return // maybe process exited + } + if m.RSS > uint64(l.conf.MemoryQuota) { + log.Warnf("service %s exceed the memory quota: %d, kill process %d", l.name, l.conf.MemoryQuota, p.Pid) + _ = p.Kill() + } + } + }() + return nil +} diff --git a/agentd/service.go b/agentd/service.go new file mode 100644 index 0000000000000000000000000000000000000000..c309008086bcd66857754e066f837b6ee8048e40 --- /dev/null +++ b/agentd/service.go @@ -0,0 +1,357 @@ +package agentd + +import ( + "fmt" + "os" + "syscall" + "time" + + log "github.com/sirupsen/logrus" + + "github.com/oceanbase/obagent/agentd/api" + "github.com/oceanbase/obagent/executor/agent" + "github.com/oceanbase/obagent/lib/http" + "github.com/oceanbase/obagent/lib/path" + "github.com/oceanbase/obagent/lib/process" +) + +// serviceProc creates a proc via ServiceConfig +func serviceProc(conf ServiceConfig) *process.Proc { + return process.NewProc(toProcConfig(conf)) +} + +func toProcConfig(conf ServiceConfig) process.ProcessConfig { + if conf.Cwd == "" { + conf.Cwd = path.AgentDir() + } + return process.ProcessConfig{ + Program: conf.Program, + Args: conf.Args, + Stdout: conf.Stdout, + Stderr: conf.Stderr, + Cwd: conf.Cwd, + InheritEnv: true, + KillWait: conf.KillWait, + FinalWait: conf.FinalWait, + } +} + +type Service struct { + name string + conf ServiceConfig + proc *process.Proc + done chan struct{} + limiter Limiter + state *http.StateHolder +} + +type TaskParam struct { + Action string `json:"action"` +} + +func NewService(name string, conf ServiceConfig) *Service { + proc := serviceProc(conf) + limiter, err := NewLimiter(name, conf.Limit) + if err != nil { + log.WithError(err).Errorf("failed to create limiter for service '%s' with config %+v", name, conf.Limit) + limiter = &NopLimiter{} + } + ret := &Service{ + name: name, + conf: conf, + proc: proc, + done: nil, + limiter: limiter, + state: http.NewStateHolder(http.Stopped), + } + return ret +} + +func timeToMill(t time.Time) int64 { + ret := t.UnixNano() / time.Millisecond.Nanoseconds() + if ret > 0 { + return ret + } + return 0 +} + +type ServiceState struct { + //Running whether the process is running + Running bool `json:"running"` + //Success whether the process is exited with code 0 + Success bool `json:"success"` + //Exited whether the process is exited + Exited bool `json:"exited"` + + //Pid of the process + Pid int `json:"Pid"` + //ExitCode of the finished process + ExitCode int `json:"exit_code"` + + //StarAt time of the process started + StartAt int64 `json:"start_at"` + //EndAt time of the process ended + EndAt int64 `json:"end_at"` + + Status string `json:"status"` +} + +func (s *Service) Start() (err error) { + s.cleanup() + if s.state.Get() == http.Running { + return nil + } + if !s.state.Cas(http.Stopped, http.Starting) { + err = ServiceAlreadyStartedErr.NewError(s.name) + log.WithField("service", s.name).WithError(err).Warn("service already started") + return err + } + s.done = make(chan struct{}) + err = s.startProc() + if err != nil { + s.state.Set(http.Stopped) + return + } + go s.guard() + return nil +} + +func (s *Service) startProc() (err error) { + s.cleanup() + pidFile, err := createPid(s.pidPath()) + if err != nil { + return err + } + defer pidFile.Close() + params := map[string]string{ + "run_dir": s.conf.RunDir, + "conf_dir": path.ConfDir(), + "bin_dir": path.BinDir(), + "agent_dir": path.AgentDir(), + } + log.WithField("service", s.name).Infof("starting service") + err = s.proc.Start(params) + if err != nil { + err = InternalServiceErr.NewError(s.name).WithCause(err) + s.cleanup() + return err + } + log.WithField("service", s.name).Infof("service process started. pid: %d", s.Pid()) + err = writePid(pidFile, s.Pid()) + if err != nil { + log.WithField("service", s.name).WithError(err).Errorf("write pid file failed %s", s.pidPath()) + } + return +} + +func (s *Service) waitExit() { + select { + case <-s.proc.Done(): + case <-time.After(time.Second): + } +} + +func (s *Service) limitResource() { + err := s.limiter.LimitPid(s.Pid()) + if err != nil { + log.WithField("service", s.name).WithError(err).Errorf("limit service resource failed. pid=%d", s.Pid()) + } +} + +func (s *Service) guard() { + var err error + quickExitCount := 0 + defer func() { + s.state.Set(http.Stopped) + s.cleanup() + close(s.done) + }() + s.limitResource() + for { + //todo wait for s.ready() + s.state.Cas(http.Starting, http.Running) + + //s.queryStatus() + s.waitExit() + + svcState := s.state.Get() + state := s.proc.State() + + if state.Exited { + log.WithField("service", s.name).Warnf("service exited with code %d. service state: %v", state.ExitCode, svcState) + } + + // service is stopped by watchdog, exit guard + if svcState == http.Stopping || svcState == http.Stopped { + log.WithField("service", s.name).Infof("service stopped. service state: %s", svcState) + return + } + + if !state.Exited { + // still running... + continue + } + + // process exited + if state.ExitCode == 0 { + log.WithField("service", s.name).Info("service normally exited") + return + } + s.state.Set(http.Stopped) + liveTime := state.EndAt.Sub(state.StartAt) + if s.conf.MinLiveTime > 0 && liveTime < s.conf.MinLiveTime { + quickExitCount++ + log.WithField("service", s.name).Warnf("service exited too quickly. live time: %d, MinLiveTime: %d, count: %d", liveTime, s.conf.MinLiveTime, quickExitCount) + if quickExitCount >= s.conf.QuickExitLimit { + log.WithField("service", s.name).Errorf("service exited too quickly. live time: %d, MinLiveTime: %d, count: %d", liveTime, s.conf.MinLiveTime, quickExitCount) + return + } + } else { + quickExitCount = 0 + } + log.WithField("service", s.name).Info("recovering service") + s.state.Set(http.Starting) + err = s.startProc() + if err != nil { + s.state.Set(http.Stopped) + log.WithField("service", s.name).WithError(err).Error("start service got error") + return + } + s.limitResource() + } +} + +func (s *Service) Stop() (err error) { + if s.state.Get() == http.Stopped { + return nil + } + s.state.Set(http.Stopping) // state may in running, staring, stopping + log.WithField("service", s.name).Info("stopping service") + err = s.proc.Stop() + if err != nil { + err = InternalServiceErr.NewError(s.name).WithCause(err) + log.WithField("service", s.name).WithField("pid", s.Pid()).WithError(err).Warn("stop service got error") + state := s.State() + if state.State == http.Stopping || state.State == http.Stopped { + s.state.Set(state.State) + return nil + } else { + log.WithField("service", s.name).WithField("pid", s.Pid()).Warn("service did not handle TERM signal properly, try KILL it") + err = s.proc.Kill() + } + } + return +} + +func (s *Service) Pid() int { + if s.proc == nil { + return 0 + } + return s.proc.Pid() +} + +func (s *Service) cleanup() { + socketPath := s.socketPath() + if isSocket(socketPath) { + //log.WithField("service", s.name).Info("removing socket file %s", socketPath) + _ = os.Remove(socketPath) + } + _ = removePid(s.pidPath(), s.Pid()) + _ = removePid(s.backupPidPath(), s.Pid()) +} + +func (s *Service) socketPath() string { + return agent.SocketPath(s.conf.RunDir, s.name, s.Pid()) +} + +func (s *Service) pidPath() string { + return agent.PidPath(s.conf.RunDir, s.name) +} + +func (s *Service) backupPidPath() string { + return agent.BackupPidPath(s.conf.RunDir, s.name, s.Pid()) +} + +func (s *Service) State() api.ServiceStatus { + state := s.proc.State() + svcState := http.Unknown + if state.Running { + ret, err := s.queryStatus() + if err == nil { + return api.ServiceStatus{ + Status: ret, + Socket: agent.SocketPath(s.conf.RunDir, s.name, s.Pid()), + EndAt: state.EndAt.UnixNano(), + } + } + } else { + svcState = http.Stopped + } + return api.ServiceStatus{ + Status: http.Status{ + State: svcState, + Pid: state.Pid, + StartAt: state.StartAt.UnixNano(), + }, + Socket: agent.SocketPath(s.conf.RunDir, s.name, s.Pid()), + EndAt: state.EndAt.UnixNano(), + } +} + +func (s *Service) queryStatus() (http.Status, error) { + readyResult := http.Status{} + c := s.apiClient() + if c == nil { + return readyResult, http.NoApiClientErr.NewError() + } + err := c.Call("/api/v1/status", nil, &readyResult) + if err != nil { + return readyResult, err + } + return readyResult, nil +} + +func (s *Service) apiClient() *http.ApiClient { + socketPath := s.socketPath() + if isSocket(socketPath) { + return http.NewSocketApiClient(socketPath, time.Second*5) + } + return nil +} + +func createPid(pidPath string) (*os.File, error) { + ret, err := os.OpenFile(pidPath, os.O_RDWR|os.O_CREATE|os.O_EXCL|syscall.O_CLOEXEC, 0644) + if err != nil { + return nil, WritePidFailedErr.NewError(pidPath).WithCause(err) + } + return ret, err +} + +func writePid(f *os.File, pid int) error { + _, err := fmt.Fprintf(f, "%d\n", pid) + if err != nil { + return WritePidFailedErr.NewError(f.Name()).WithCause(err) + } + return nil +} + +func removePid(pidPath string, expectedPid int) error { + pid, err := agent.ReadPid(pidPath) + if err != nil { + return RemovePidFailedErr.NewError(pidPath).WithCause(err) + } + if pid != expectedPid { + return nil + } + log.Infof("remove pid file %s", pidPath) + err = os.Remove(pidPath) + if err != nil { + return RemovePidFailedErr.NewError(pidPath).WithCause(err) + } + return nil +} + +func isSocket(path string) bool { + stat, err := os.Stat(path) + return err == nil && (stat.Mode()&os.ModeSocket != 0) +} diff --git a/agentd/service_test.go b/agentd/service_test.go new file mode 100644 index 0000000000000000000000000000000000000000..5943c15a85ed99e10e057da52373ee2694d57394 --- /dev/null +++ b/agentd/service_test.go @@ -0,0 +1,17 @@ +package agentd + +import ( + "testing" + + "github.com/oceanbase/obagent/lib/path" +) + +func Test_serviceProc(t *testing.T) { + conf := toProcConfig(ServiceConfig{}) + if conf.Cwd != path.AgentDir() { + t.Error("default cwd wrong") + } + if !conf.InheritEnv { + t.Error("InheritEnv should be true") + } +} diff --git a/agentd/watchdog.go b/agentd/watchdog.go new file mode 100644 index 0000000000000000000000000000000000000000..4cb5e8923fb3e18d161173fe9ab64104bb22a533 --- /dev/null +++ b/agentd/watchdog.go @@ -0,0 +1,391 @@ +package agentd + +import ( + "fmt" + "net/http" + "net/http/pprof" + "os" + "os/signal" + "path" + "path/filepath" + "syscall" + "time" + + "github.com/shirou/gopsutil/v3/process" + log "github.com/sirupsen/logrus" + "golang.org/x/sys/unix" + + "github.com/oceanbase/obagent/agentd/api" + "github.com/oceanbase/obagent/config" + "github.com/oceanbase/obagent/executor/agent" + "github.com/oceanbase/obagent/lib/command" + http2 "github.com/oceanbase/obagent/lib/http" + path2 "github.com/oceanbase/obagent/lib/path" +) + +// Agentd supervisor process for agents +// start, stop sub services, view status of self and sub services +type Agentd struct { + config Config + services map[string]*Service + listener *http2.Listener + state *http2.StateHolder +} + +// NewAgentd create a new Agentd via config +func NewAgentd(config Config) *Agentd { + listener := http2.NewListener() + services := make(map[string]*Service) + for name, svcConf := range config.Services { + svc := NewService(name, svcConf) + services[name] = svc + } + ret := &Agentd{ + config: config, + services: services, + listener: listener, + state: http2.NewStateHolder(http2.Stopped), + } + ret.initRoutes() + return ret +} + +var startAt = time.Now().UnixNano() + +func (w *Agentd) initRoutes() { + rootPath := "/api/v1" + statusHandler := http2.NewHandler(command.WrapFunc(w.Status)) + startServiceHandler := http2.NewHandler(command.WrapFunc(func(param api.StartStopAgentParam) error { + return w.StartService(param.Service) + })) + stopServiceHandler := http2.NewHandler(command.WrapFunc(func(param api.StartStopAgentParam) error { + return w.StopService(param.Service) + })) + w.listener.AddHandler(path.Join(rootPath, "/status"), statusHandler) + w.listener.AddHandler(path.Join(rootPath, "/startService"), startServiceHandler) + w.listener.AddHandler(path.Join(rootPath, "/stopService"), stopServiceHandler) + + w.listener.AddHandler("/debug/pprof/", http.HandlerFunc(pprof.Index)) + w.listener.AddHandler("/debug/pprof/cmdline", http.HandlerFunc(pprof.Cmdline)) + w.listener.AddHandler("/debug/pprof/profile", http.HandlerFunc(pprof.Profile)) + w.listener.AddHandler("/debug/pprof/symbol", http.HandlerFunc(pprof.Symbol)) + w.listener.AddHandler("/debug/pprof/trace", http.HandlerFunc(pprof.Trace)) +} + +// Start agentd and sub services +func (w *Agentd) Start() error { + w.cleanup() + if w.state.Get() == http2.Running { + return nil + } + log.Info("starting agentd") + var err error + err = w.writePid() + if err != nil { + return err + } + w.state.Set(http2.Starting) + err = os.MkdirAll(w.config.RunDir, 0755) + if err != nil { + return err + } + + socketPath := w.socketPath() + log.Infof("starting socket listener on '%s'", socketPath) + err = w.listener.StartSocket(socketPath) + if err != nil { + log.Errorf("start socket listener '%s' failed: %v", socketPath, err) + _ = w.Stop() + return err + } + w.state.Set(http2.Running) + + for name, svc := range w.services { + log.Infof("starting service '%s'", name) + err = svc.Start() + if err != nil { + log.Errorf("start service '%s' failed: %s", name, err) + } + } + log.Info("agentd started") + return nil +} + +// Stop agentd and sub services +func (w *Agentd) Stop() error { + if w.state.Get() == http2.Stopped { + return nil + } + log.Info("stopping agentd") + if !w.state.Cas(http2.Running, http2.Stopping) { + return AgentdNotRunningErr.NewError(w.state.Get()) + } + + for name, svc := range w.services { + state := svc.State().State + log.Infof("stopping service '%s'. current state: %s", name, state) + if state == http2.Stopped { + log.Infof("service '%s' already stopped. State: %s", name, state) + continue + } + err := svc.Stop() + if err != nil { + log.Errorf("stop service '%s' got error: %v", name, err) + } + } + + w.state.Set(http2.Stopped) + log.Info("agentd stopped") + + w.listener.Close() + + err := w.removePid() + if err != nil { + log.Warn("remove pid file failed: ", err) + } + w.cleanup() + return nil +} + +// ListenSignal capture SIGTERM and SIGINT, do a normal Stop +func (w *Agentd) ListenSignal() { + ch := make(chan os.Signal, 1) + signal.Notify(ch, syscall.SIGTERM, syscall.SIGINT) + select { + case sig := <-ch: + log.Infof("signal '%s' received. exiting...", sig.String()) + _ = w.Stop() + } +} + +// Status returns agentd and sub services status +func (w *Agentd) Status() api.Status { + svcStates := make(map[string]api.ServiceStatus) + ready := w.state.Get() == http2.Running + + for name, svc := range w.services { + state := svc.State() + svcStates[name] = state + if state.State != http2.Running { + ready = false + } + } + dangling := w.danglingServices() + socket := w.socketPath() + if !isSocket(socket) { + socket = "" + } + return api.Status{ + State: w.state.Get(), + Ready: ready, + Pid: os.Getpid(), + Socket: socket, + Services: svcStates, + Dangling: dangling, + Version: config.AgentVersion, + StartAt: startAt, + } +} + +func (w *Agentd) StartService(name string) error { + service, ok := w.services[name] + if !ok { + return ServiceNotFoundErr.NewError(name) + } + return service.Start() +} + +func (w *Agentd) StopService(name string) error { + service, ok := w.services[name] + if !ok { + return ServiceNotFoundErr.NewError(name) + } + return service.Stop() +} + +func (w *Agentd) socketPath() string { + return agent.SocketPath(w.config.RunDir, path2.Agentd, os.Getpid()) +} + +func (w *Agentd) pidPath() string { + return agent.PidPath(w.config.RunDir, path2.Agentd) +} + +func (w *Agentd) writePid() error { + f, err := os.OpenFile(w.pidPath(), os.O_RDWR|os.O_CREATE|os.O_EXCL|os.O_SYNC|syscall.O_CLOEXEC, 0644) + if err != nil { + return err + } + defer f.Close() + _, err = fmt.Fprintf(f, "%d", os.Getpid()) + if err != nil { + return err + } + return nil +} + +func (w *Agentd) removePid() error { + return removePid(w.pidPath(), os.Getpid()) +} + +func (w *Agentd) cleanupPid(program, pidPath string) error { + if _, err := os.Stat(pidPath); err != nil && os.IsNotExist(err) { + return nil + } + pid, err := agent.ReadPid(pidPath) + if err != nil { + return err + } + _, err = process.NewProcess(int32(pid)) + if err != nil { + return removePid(pidPath, pid) + } + return nil +} + +func (w *Agentd) cleanupPidPattern(program, pattern string) { + pidPaths, err := filepath.Glob(filepath.Join(w.config.RunDir, pattern)) + if err != nil { + return + } + for _, pidPath := range pidPaths { + err = w.cleanupPid(program, pidPath) + if err != nil { + log.WithError(err).Errorf("cleanup pid file %s got error", pidPath) + } + } +} + +func (w *Agentd) cleanupSocketPattern(pattern string) { + sockPaths, err := filepath.Glob(filepath.Join(w.config.RunDir, pattern)) + if err != nil { + return + } + for _, sockPath := range sockPaths { + err = w.cleanupSocket(sockPath) + if err != nil { + log.WithError(err).Errorf("cleanup socket file %s got error", sockPath) + } + } +} + +func (w *Agentd) cleanupSocket(sockPath string) error { + if !isSocket(sockPath) { + return nil + } + if http2.CanConnect("unix", sockPath, time.Second) { + return nil + } + return os.Remove(sockPath) +} + +func (w *Agentd) cleanupDangling() { + if !w.config.CleanupDangling { + return + } + n := len(w.danglingServices()) + if n == 0 { + return + } + log.Infof("cleaning up dangling services, %d to cleanup", n) + for _, dangling := range w.danglingServices() { + log.Infof("cleaning up dangling service: %s %d", dangling.Name, dangling.Pid) + proc, err := process.NewProcess(int32(dangling.Pid)) + if err != nil { + log.Warnf("get process of dangling service: %s %d got error %v", dangling.Name, dangling.Pid, err) + continue + } + err = proc.SendSignal(unix.SIGTERM) + if err != nil { + log.Warnf("terminate process of dangling service: %s %d got error %v", dangling.Name, dangling.Pid, err) + continue + } + } +} + +func (w *Agentd) cleanup() { + var err error + w.cleanupDangling() + w.cleanupPidPattern(path2.Agentd, path2.Agentd+".pid") + w.cleanupPidPattern(path2.Agentd, path2.Agentd+".*.pid") + w.cleanupSocketPattern(path2.Agentd + ".*.sock") + + pidPath := filepath.Join(w.config.RunDir, path2.Agentd+".pid") + err = w.cleanupPid(path2.Agentd, pidPath) + if err != nil { + log.WithError(err).Errorf("cleanup pid file %s got error", pidPath) + } + + if err != nil { + log.WithError(err).Errorf("cleanup pid file %s got error", pidPath) + } + + for name, conf := range w.config.Services { + w.cleanupPidPattern(conf.Program, fmt.Sprintf("%s.pid", name)) + w.cleanupPidPattern(conf.Program, fmt.Sprintf("%s.*.pid", name)) + w.cleanupSocketPattern(fmt.Sprintf("%s.*.sock", name)) + } +} + +func (w *Agentd) danglingServices() []api.DanglingService { + var ret []api.DanglingService + for name := range w.config.Services { + pidPath := agent.PidPath(w.config.RunDir, name) + pid, err := agent.ReadPid(pidPath) + if err == nil { + if w.isDangling(name, pid) { + socket := agent.SocketPath(w.config.RunDir, name, pid) + if !isSocket(socket) { + socket = "" + } + ret = append(ret, api.DanglingService{ + Name: name, + Pid: pid, + PidFile: pidPath, + Socket: socket, + }) + } + } + pidPaths, err := filepath.Glob(filepath.Join(w.config.RunDir, fmt.Sprintf("%s.*.pid", name))) + if err == nil { + for _, pidPath = range pidPaths { + pid, err = agent.ReadPid(pidPath) + if err != nil { + continue + } + if w.isDangling(name, pid) { + socket := agent.SocketPath(w.config.RunDir, name, pid) + if !isSocket(socket) { + socket = "" + } + ret = append(ret, api.DanglingService{ + Name: name, + Pid: pid, + PidFile: pidPath, + Socket: socket, + }) + } + } + } + } + return ret +} + +func (w *Agentd) isDangling(program string, pid int) bool { + proc, err := process.NewProcess(int32(pid)) + if err != nil { + return false + } + name, err := proc.Name() + if err != nil { + return false + } + if name != program { + return false + } + ppid, err := proc.Ppid() + if err != nil { + return false + } + return ppid == 1 || ppid == 0 +} diff --git a/agentd/watchdog_test.go b/agentd/watchdog_test.go new file mode 100644 index 0000000000000000000000000000000000000000..1dc2e5c7b10ccb713856c50401e228b805dee960 --- /dev/null +++ b/agentd/watchdog_test.go @@ -0,0 +1,178 @@ +package agentd + +import ( + "fmt" + "os" + "syscall" + "testing" + "time" + + process2 "github.com/shirou/gopsutil/v3/process" + + "github.com/oceanbase/obagent/lib/http" + "github.com/oceanbase/obagent/tests/testutil" +) + +var mockAgentPath string + +func TestMain(m *testing.M) { + testutil.MakeDirs() + //err := testutil.BuildBins() + //if err != nil { + // os.Exit(2) + //} + err := testutil.BuildMockAgent() + if err != nil { + os.Exit(2) + } + ret := m.Run() + testutil.KillAll() + testutil.DelTestFiles() + os.Exit(ret) +} + +func TestNewWatchdog(t *testing.T) { + defer testutil.KillAll() + testutil.MakeDirs() + defer testutil.DelTestFiles() + + watchdog := NewAgentd(Config{ + RunDir: testutil.RunDir, + LogDir: testutil.LogDir, + LogLevel: "info", + CleanupDangling: true, + Services: map[string]ServiceConfig{ + "test": { + Program: testutil.MockAgentPath, + Args: []string{"test", testutil.RunDir, "1", "1"}, + RunDir: testutil.RunDir, + FinalWait: time.Second * 2, + MinLiveTime: time.Second * 5, + QuickExitLimit: 3, + Stdout: testutil.LogDir + "/test.output.log", + Stderr: testutil.LogDir + "/test.error.log", + }, + }, + }) + err := watchdog.Start() + if err != nil { + t.Error(err) + } + + err = watchdog.Start() + if err != nil { + t.Error("duplicate start should not fail") + } + + status := watchdog.Status() + if status.State != http.Running { + t.Error("watchdog state should be running") + } + + time.Sleep(1500 * time.Millisecond) + + status = watchdog.Status() + fmt.Printf("status: %+v\n", status) + if status.Services["test"].State != http.Running { + t.Errorf("service state should be running got '%s'", status.Services["test"].State) + } + err = watchdog.Stop() + if err != nil { + t.Error(err) + } + time.Sleep(time.Second) + for name, service := range watchdog.services { + _, err2 := process2.NewProcess(int32(service.Pid())) + if err2 == nil { + t.Errorf("service '%s' should be stopped", name) + } + } + err = watchdog.Stop() + if err != nil { + t.Error("duplicate stop should not fail") + } +} + +func TestWatchdog_ListenSignal(t *testing.T) { + defer testutil.KillAll() + testutil.MakeDirs() + defer testutil.DelTestFiles() + + watchdog := NewAgentd(Config{ + RunDir: testutil.RunDir, + LogDir: testutil.LogDir, + LogLevel: "info", + Services: map[string]ServiceConfig{ + "test": { + Program: testutil.MockAgentPath, + Args: []string{"test", testutil.RunDir, "1", "1"}, + RunDir: testutil.RunDir, + FinalWait: time.Second * 2, + MinLiveTime: time.Second * 5, + QuickExitLimit: 3, + Stdout: testutil.LogDir + "/test.output.log", + Stderr: testutil.LogDir + "/test.error.log", + }, + }, + }) + err := watchdog.Start() + if err != nil { + t.Error(err) + } + + p, err := process2.NewProcess(int32(watchdog.Status().Pid)) + if err != nil { + t.Error(err) + return + } + ch := make(chan struct{}) + go func() { + watchdog.ListenSignal() + close(ch) + }() + time.Sleep(100 * time.Millisecond) + _ = p.SendSignal(syscall.SIGTERM) + select { + case <-ch: + // ok + case <-time.After(time.Second * 5): + t.Error("wait stop by signal timeout") + } + status := watchdog.Status() + if status.State != http.Stopped { + t.Error("watchdog should be stopped") + } + +} + +func TestStartFail(t *testing.T) { + defer testutil.KillAll() + testutil.MakeDirs() + defer testutil.DelTestFiles() + + watchdog := NewAgentd(Config{ + RunDir: testutil.RunDir, + LogDir: testutil.LogDir, + LogLevel: "info", + Services: map[string]ServiceConfig{ + "test": { + Program: testutil.MockAgentPath, + Args: []string{"test", testutil.RunDir, "-1", "1"}, + RunDir: testutil.RunDir, + FinalWait: time.Second * 2, + MinLiveTime: time.Second * 5, + QuickExitLimit: 0, + Stdout: testutil.LogDir + "/test.output.log", + Stderr: testutil.LogDir + "/test.error.log", + }, + }, + }) + err := watchdog.Start() + if err != nil { + t.Error(err) + } + time.Sleep(2000 * time.Millisecond) + if watchdog.services["test"].State().State != http.Stopped { + t.Error("failed service should be 'stopped'") + } +} diff --git a/api/common/config_handler.go b/api/common/config_handler.go new file mode 100644 index 0000000000000000000000000000000000000000..18d41c7f716397945a8c1d971fd204416bcc4902 --- /dev/null +++ b/api/common/config_handler.go @@ -0,0 +1,138 @@ +package common + +import ( + "fmt" + "github.com/gin-gonic/gin" + log "github.com/sirupsen/logrus" + + "github.com/oceanbase/obagent/config" + "github.com/oceanbase/obagent/config/mgragent" + "github.com/oceanbase/obagent/executor/agent" +) + +// http handler: validate module config and save module config +func UpdateConfigPropertiesHandler(c *gin.Context) { + kvs := config.KeyValues{} + c.Bind(&kvs) + ctx := NewContextWithTraceId(c) + ctxlog := log.WithContext(ctx) + + configVersion, err := config.UpdateConfig(ctx, &kvs) + if err != nil { + ctxlog.Errorf("update config err:%s", err) + } + + SendResponse(c, &agent.AgentctlResponse{ + Successful: true, + Message: configVersion, + Error: "", + }, err) +} + +// http handler: notify module config +func NotifyConfigPropertiesHandler(c *gin.Context) { + nconfig := new(config.NotifyModuleConfig) + c.Bind(nconfig) + + ctx := NewContextWithTraceId(c) + ctxlog := log.WithContext(ctx).WithFields(log.Fields{ + "process": nconfig.Process, + "module": nconfig.Module, + "updated key values": nconfig.UpdatedKeyValues, + }) + + ctxlog.Infof("notify module config") + + err := config.NotifyModuleConfigForHttp(ctx, nconfig) + if err != nil { + ctxlog.Errorf("notify module config err:%+v", err) + } + + SendResponse(c, &agent.AgentctlResponse{ + Successful: true, + Message: "notify module config success", + Error: "", + }, err) +} + +func ValidateConfigPropertiesHandler(c *gin.Context) { + kvs := config.KeyValues{} + c.Bind(&kvs) + + ctx := NewContextWithTraceId(c) + ctxlog := log.WithContext(ctx) + ctxlog.Debugf("validate module config") + + err := config.ValidateConfigKeyValues(ctx, kvs.Configs) + if err != nil { + ctxlog.Errorf("validate configs failed, err:%+v", err) + } + + SendResponse(c, &agent.AgentctlResponse{ + Successful: true, + Message: "success", + Error: "", + }, err) +} + +// http handler: effect module config +func ConfigStatusHandler(c *gin.Context) { + needRestartModuleKeyValues := config.NeedRestartModuleKeyValues() + SendResponse(c, needRestartModuleKeyValues, nil) +} + +func ReloadConfigHandler(c *gin.Context) { + ctx := NewContextWithTraceId(c) + ctxlog := log.WithContext(ctx) + err := mgragent.GlobalConfigManager.ReloadModuleConfigs(ctx) + if err != nil { + ctxlog.Errorf("reload module config files failed, err: %+v", err) + SendResponse(c, nil, err) + return + } + err = config.NotifyAllModules(ctx) + if err != nil { + ctxlog.Errorf("notify config change after changing module config failed, err: %+v", err) + SendResponse(c, nil, err) + return + } + SendResponse(c, "reload config success", nil) +} + +func ChangeConfigHandler(c *gin.Context) { + req := mgragent.ModuleConfigChangeRequest{ + Reload: true, + } + ctx := NewContextWithTraceId(c) + ctxlog := log.WithContext(ctx) + + err := c.BindJSON(&req) + if err != nil { + ctxlog.Errorf("parse request failed, err: %+v", err) + SendResponse(c, nil, err) + return + } + ctxlog.Debugf("change module config %+v", req) + changedFileNames, err := mgragent.GlobalConfigManager.ChangeModuleConfigs(ctx, &req) + if err != nil { + ctxlog.Errorf("change module config failed, err: %+v", err) + SendResponse(c, nil, err) + return + } + ctxlog.Infof("module config files changed: %+v", changedFileNames) + if len(changedFileNames) > 0 && req.Reload { + err = mgragent.GlobalConfigManager.ReloadModuleConfigs(ctx) + if err != nil { + ctxlog.Errorf("reload module config files failed, err: %+v", err) + SendResponse(c, nil, err) + return + } + err = config.NotifyAllModules(ctx) + if err != nil { + ctxlog.Errorf("notify config change after changing module config failed, err: %+v", err) + SendResponse(c, nil, err) + return + } + } + SendResponse(c, fmt.Sprintf("%d files changed", len(req.ModuleConfigChanges)), nil) +} diff --git a/api/common/handler_common.go b/api/common/handler_common.go new file mode 100644 index 0000000000000000000000000000000000000000..ef884410bdb027db26a4df67bd989e595da82dd5 --- /dev/null +++ b/api/common/handler_common.go @@ -0,0 +1,32 @@ +package common + +import ( + "context" + + "github.com/gin-gonic/gin" + + "github.com/oceanbase/obagent/lib/http" + "github.com/oceanbase/obagent/log" +) + +// keys stored in gin.Context +const ( + OcpAgentResponseKey = "ocpAgentResponse" + TraceIdKey = "traceId" + OcpServerIpKey = "ocpServerIp" +) + +func NewContextWithTraceId(c *gin.Context) context.Context { + traceId := "" + if t, ok := c.Get(TraceIdKey); ok { + if ts, ok := t.(string); ok { + traceId = ts + } + } + return context.WithValue(context.Background(), log.TraceIdKey{}, traceId) +} + +func SendResponse(c *gin.Context, data interface{}, err error) { + resp := http.BuildResponse(data, err) + c.Set(OcpAgentResponseKey, resp) +} diff --git a/api/common/info_handler.go b/api/common/info_handler.go new file mode 100644 index 0000000000000000000000000000000000000000..c71d2e13b0c8b03f9cc80ff9fd017f5bb4486d0a --- /dev/null +++ b/api/common/info_handler.go @@ -0,0 +1,50 @@ +package common + +import ( + "os" + "time" + + "github.com/gin-gonic/gin" + log "github.com/sirupsen/logrus" + + "github.com/oceanbase/obagent/config" + "github.com/oceanbase/obagent/lib/http" + "github.com/oceanbase/obagent/lib/system" +) + +func TimeHandler(c *gin.Context) { + SendResponse(c, time.Now(), nil) +} + +func InfoHandler(c *gin.Context) { + SendResponse(c, config.GetAgentInfo(), nil) +} + +func GitInfoHandler(c *gin.Context) { + SendResponse(c, config.GetGitInfo(), nil) +} + +var StartAt = time.Now().UnixNano() +var libProcess system.Process = system.ProcessImpl{} + +func StatusHandler(s *http.StateHolder) gin.HandlerFunc { + return func(c *gin.Context) { + ports := make([]int, 0) + + pid := os.Getpid() + processInfo, err := libProcess.GetProcessInfoByPid(int32(pid)) + if err != nil { + log.Errorf("StatusHandler get processInfo failed, pid:%s", pid) + } else { + ports = processInfo.Ports + } + var info = http.Status{ + State: s.Get(), + Version: config.AgentVersion, + Pid: pid, + StartAt: StartAt, + Ports: ports, + } + SendResponse(c, info, nil) + } +} diff --git a/api/common/middleware.go b/api/common/middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..ed9a6e2426cc6396bce182fd07a52d0061531c21 --- /dev/null +++ b/api/common/middleware.go @@ -0,0 +1,208 @@ +package common + +import ( + "bytes" + "fmt" + "io/ioutil" + "regexp" + "strings" + "time" + + "github.com/gin-gonic/gin" + "github.com/go-playground/validator/v10" + prom "github.com/prometheus/client_golang/prometheus" + "github.com/sirupsen/logrus" + log "github.com/sirupsen/logrus" + + "github.com/oceanbase/obagent/errors" + "github.com/oceanbase/obagent/lib/http" + "github.com/oceanbase/obagent/lib/system" + "github.com/oceanbase/obagent/lib/trace" + "github.com/oceanbase/obagent/stat" +) + +var libSystem system.System = system.SystemImpl{} + +const statusURI = "/api/v1/status" + +const logQuerierURI = "/api/v1/log" + +// Before handlers, extract HTTP headers, and log the API request. +func PreHandlers(maskBodyRoutes ...string) func(*gin.Context) { + return func(c *gin.Context) { + if c.Request.RequestURI == statusURI { + c.Next() + return + } + // Use traceId passed from OCP-Server for logging. + traceId := trace.GetTraceId(c.Request) + c.Set(TraceIdKey, traceId) + + // Store OCP-Server's ip address for logging. + // c.ClientIP() may not be accurate if HTTP requests are forwarded by proxy server. + ocpServerIp := c.Request.Header.Get(trace.OcpServerIpHeader) + c.Set(OcpServerIpKey, ocpServerIp) + + ctx := NewContextWithTraceId(c) + + masked := false + for _, it := range maskBodyRoutes { + if strings.HasPrefix(c.Request.RequestURI, it) { + masked = true + } + } + if masked { + log.WithContext(ctx).Infof("API request: [%v %v, client=%v, ocpServerIp=%v, traceId=%v]", + c.Request.Method, c.Request.URL, c.ClientIP(), ocpServerIp, traceId) + } else { + body := readRequestBody(c) + log.WithContext(ctx).Infof("API request: [%v %v, client=%v, ocpServerIp=%v, traceId=%v, body=%v]", + c.Request.Method, c.Request.URL, c.ClientIP(), ocpServerIp, traceId, body) + } + + c.Next() + } +} + +var emptyRe = regexp.MustCompile(`\s+`) + +func readRequestBody(c *gin.Context) string { + body, _ := ioutil.ReadAll(c.Request.Body) + c.Request.Body = ioutil.NopCloser(bytes.NewReader(body)) + return emptyRe.ReplaceAllString(string(body), "") +} + +// If c.BindJSON fails (e.g. validation error), content-type will be set to text/plain. +// So set content-type before handlers. +func SetContentType(c *gin.Context) { + c.Writer.Header().Set("Content-Type", "application/json") + + c.Next() +} + +func getResponseFromContext(c *gin.Context) http.OcpAgentResponse { + ctx := NewContextWithTraceId(c) + + if len(c.Errors) > 0 { + var subErrors []interface{} + for _, e := range c.Errors { + switch e.Type { + case gin.ErrorTypeBind: + validationErrors := e.Err.(validator.ValidationErrors) + for _, fieldError := range validationErrors { + subErrors = append(subErrors, http.NewApiFieldError(fieldError)) + } + default: + subErrors = append(subErrors, http.ApiUnknownError{Error: e.Err}) + } + } + return http.NewSubErrorsResponse(subErrors) + } + + if r, ok := c.Get(OcpAgentResponseKey); ok { + if resp, ok := r.(http.OcpAgentResponse); ok { + return resp + } + } + + log.WithContext(ctx).Error("no response object found from gin context") + return http.NewErrorResponse(errors.Occur(errors.ErrUnexpected, "cannot build response body")) +} + +// After handlers, build the complete OcpAgentResponse object, +// log the API result, and send HTTP response. +func PostHandlers(excludeRoutes ...string) func(*gin.Context) { + localIpAddress, _ := libSystem.GetLocalIpAddress() + return func(c *gin.Context) { + for _, it := range excludeRoutes { + if strings.HasPrefix(c.Request.RequestURI, it) { + c.Next() + return + } + } + + startTime := time.Now() + + c.Next() + + ctx := NewContextWithTraceId(c) + resp := getResponseFromContext(c) + + duration := time.Now().Sub(startTime) + resp.Duration = int(duration / time.Millisecond) + + ocpServerIp, _ := c.Get(OcpServerIpKey) + if v, ok := c.Get(TraceIdKey); ok { + if traceId, ok := v.(string); ok { + resp.TraceId = traceId + } + } + + resp.Server = localIpAddress + if resp.Successful { + if c.Request.RequestURI != statusURI { + if strings.HasPrefix(c.Request.RequestURI, logQuerierURI) { + log.WithContext(ctx).Infof("API response OK: [%v %v, client=%v, ocpServerIp=%v, traceId=%v, duration=%v, status=%v]", + c.Request.Method, c.Request.URL, c.ClientIP(), ocpServerIp, resp.TraceId, duration, resp.Status) + } else { + log.WithContext(ctx).Infof("API response OK: [%v %v, client=%v, ocpServerIp=%v, traceId=%v, duration=%v, status=%v, data=%+v]", + c.Request.Method, c.Request.URL, c.ClientIP(), ocpServerIp, resp.TraceId, duration, resp.Status, resp.Data) + } + } else { + log.WithContext(ctx).Debugf("API response OK: [%v %v, client=%v, ocpServerIp=%v, traceId=%v, duration=%v, status=%v, data=%+v]", + c.Request.Method, c.Request.URL, c.ClientIP(), ocpServerIp, resp.TraceId, duration, resp.Status, resp.Data) + } + } else { + log.WithContext(ctx).Infof("API response error: [%v %v, client=%v, ocpServerIp=%v, traceId=%v, duration=%v, status=%v, error=%v]", + c.Request.Method, c.Request.URL, c.ClientIP(), ocpServerIp, resp.TraceId, duration, resp.Status, resp.Error.String()) + } + c.JSON(resp.Status, resp) + } +} + +func MonitorAgentPostHandler(c *gin.Context) { + startTime := time.Now() + + c.Next() + + duration := time.Now().Sub(startTime) + serverIp := c.Request.Header.Get(trace.OcpServerIpHeader) + ctx := NewContextWithTraceId(c) + + fields := log.Fields{ + "url": c.Request.URL, + "duration": duration, + "status": c.Writer.Status(), + "ocpServerIp": serverIp, + "client": c.ClientIP(), + } + if duration < 100*time.Millisecond { + fields[logrus.FieldKeyLevel] = logrus.DebugLevel + } + + log.WithContext(ctx).WithFields(fields).Info("request end") +} + +func HttpStatMiddleware(c *gin.Context) { + startTime := time.Now() + + // run other middleware + c.Next() + + stat.HttpRequestMillisecondsSummary.With(prom.Labels{ + stat.HttpMethod: c.Request.Method, + stat.HttpStatus: fmt.Sprintf("%d", c.Writer.Status()), + stat.HttpApiPath: c.Request.URL.Path, + }).Observe(float64(time.Now().Sub(startTime)) / float64(time.Millisecond)) +} + +func Recovery(c *gin.Context, err interface{}) { + // log err to file + log.WithContext(NewContextWithTraceId(c)).Errorf("request context %+v, err:%+v", c, err) +} + +func IgnoreFaviconHandler(c *gin.Context) { + if c.Request.URL.Path == "/favicon.ico" { + c.Abort() + } +} diff --git a/api/web/middleware_auth.go b/api/common/middleware_auth.go similarity index 60% rename from api/web/middleware_auth.go rename to api/common/middleware_auth.go index 2a26ea956fe3cf3854c226762e1ff95172155dd0..9117daf8c720ebdcacb42da82e1622386169386d 100644 --- a/api/web/middleware_auth.go +++ b/api/common/middleware_auth.go @@ -1,26 +1,17 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - -package web +package common import ( + "context" "encoding/base64" "net/http" "strings" + "github.com/gin-gonic/gin" "github.com/pkg/errors" log "github.com/sirupsen/logrus" "github.com/oceanbase/obagent/config" + http2 "github.com/oceanbase/obagent/lib/http" ) type Authorizer interface { @@ -28,8 +19,23 @@ type Authorizer interface { SetConf(conf config.BasicAuthConfig) } +func InitBasicAuthConf(ctx context.Context) { + httpAuthorizer = &BasicAuth{} + + module := config.ManagerAgentBasicAuthConfigModule + err := config.InitModuleConfig(ctx, module) + if err != nil { + log.WithContext(ctx).Fatalf("init module %s config err:%+v", module, err) + } + log.WithContext(ctx).Infof("init module %s config end", module) +} + var httpAuthorizer Authorizer +func NotifyConf(conf config.BasicAuthConfig) { + httpAuthorizer.SetConf(conf) +} + type BasicAuth struct { config config.BasicAuthConfig } @@ -39,6 +45,9 @@ func (auth *BasicAuth) SetConf(conf config.BasicAuthConfig) { } func (auth *BasicAuth) Authorize(req *http.Request) error { + if !auth.config.MetricAuthEnabled && strings.HasPrefix(req.RequestURI, "/metrics/") { + return nil + } // header: Authorization Basic base64-encoding-content authHeader := req.Header.Get("Authorization") authHeaders := strings.SplitN(authHeader, " ", 2) @@ -65,7 +74,24 @@ func (auth *BasicAuth) Authorize(req *http.Request) error { return nil } - log.Infof("auth:%+v", auth.config) - return errors.Errorf("auth failed for user: %s", contentStrs[0]) } + +func AuthorizeMiddleware(c *gin.Context) { + ctx := NewContextWithTraceId(c) + ctxlog := log.WithContext(ctx) + if httpAuthorizer == nil { + ctxlog.Warnf("basic auth is nil, please check the initial process.") + c.Next() + return + } + + err := httpAuthorizer.Authorize(c.Request) + if err != nil { + ctxlog.Errorf("basic auth Authorize failed, err:%+v", err) + c.Abort() + c.JSON(http.StatusUnauthorized, http2.BuildResponse(nil, err)) + return + } + c.Next() +} diff --git a/api/common/route_common.go b/api/common/route_common.go new file mode 100644 index 0000000000000000000000000000000000000000..143197cfa30a88a5c2125892bf9723319315c9a4 --- /dev/null +++ b/api/common/route_common.go @@ -0,0 +1,17 @@ +package common + +import ( + "net/http" + + "github.com/felixge/fgprof" + "github.com/gin-contrib/pprof" + "github.com/gin-gonic/gin" + adapter "github.com/gwatts/gin-adapter" +) + +func InitPprofRouter(r *gin.Engine) { + pprof.Register(r, "debug/pprof") + r.GET("/debug/fgprof", adapter.Wrap(func(_ http.Handler) http.Handler { + return fgprof.Handler() + })) +} diff --git a/api/mgragent/agent_handler.go b/api/mgragent/agent_handler.go new file mode 100644 index 0000000000000000000000000000000000000000..f20b366e432f262d1b3b7942781916e9dc5b2863 --- /dev/null +++ b/api/mgragent/agent_handler.go @@ -0,0 +1,27 @@ +package mgragent + +import ( + "github.com/gin-gonic/gin" + log "github.com/sirupsen/logrus" + + "github.com/oceanbase/obagent/api/common" + "github.com/oceanbase/obagent/executor/agent" + "github.com/oceanbase/obagent/lib/command" +) + +var restartCmd = command.WrapFunc(func(taskToken agent.TaskToken) error { + log.Info("restarting agent") + cmd := agent.NewAgentctlCmd() + return cmd.Restart(taskToken) +}) + +func agentStatusService(c *gin.Context) { + log.Info("query agent status") + admin := agent.NewAdmin(agent.DefaultAdminConf()) + status, err := admin.AgentStatus() + if err != nil { + common.SendResponse(c, nil, err) + return + } + common.SendResponse(c, status, nil) +} diff --git a/api/mgragent/file_handler.go b/api/mgragent/file_handler.go new file mode 100644 index 0000000000000000000000000000000000000000..d13ac7303aa773280c5387fbdc9b4bf0c4ceda46 --- /dev/null +++ b/api/mgragent/file_handler.go @@ -0,0 +1,24 @@ +package mgragent + +import ( + "github.com/gin-gonic/gin" + + "github.com/oceanbase/obagent/api/common" + "github.com/oceanbase/obagent/executor/file" +) + +func isFileExists(c *gin.Context) { + ctx := common.NewContextWithTraceId(c) + var param file.GetFileExistsParam + c.BindJSON(¶m) + data, err := file.IsFileExists(ctx, param) + common.SendResponse(c, data, err) +} + +func getRealStaticPath(c *gin.Context) { + ctx := common.NewContextWithTraceId(c) + var param file.GetRealStaticPathParam + c.BindJSON(¶m) + data, err := file.GetRealStaticPath(ctx, param) + common.SendResponse(c, data, err) +} diff --git a/api/mgragent/log_handler.go b/api/mgragent/log_handler.go new file mode 100644 index 0000000000000000000000000000000000000000..5bdbf8f98c825861375affb1ab84e60473a82ee1 --- /dev/null +++ b/api/mgragent/log_handler.go @@ -0,0 +1,252 @@ +package mgragent + +import ( + "archive/zip" + "context" + "fmt" + "io" + "strconv" + "sync" + "time" + + "github.com/gin-gonic/gin" + log "github.com/sirupsen/logrus" + + "github.com/oceanbase/obagent/api/common" + "github.com/oceanbase/obagent/executor/log_query" +) + +// QueryLogRequest log query request params +type QueryLogRequest struct { + StartTime time.Time `json:"startTime"` + EndTime time.Time `json:"endTime"` + LogType string `json:"logType"` + Keyword []string `json:"keyword"` + KeywordType string `json:"keywordType"` + ExcludeKeyword []string `json:"excludeKeyword"` + ExcludeKeywordType string `json:"excludeKeywordType"` + LogLevel []string `json:"logLevel"` + ReqId string `json:"reqId"` + LastQueryFileId string `json:"lastQueryFileId"` + LastQueryFileOffset int64 `json:"lastQueryFileOffset"` + Limit int64 `json:"limit"` +} + +// DownloadLogRequest log download request params +type DownloadLogRequest struct { + StartTime time.Time `json:"startTime"` + EndTime time.Time `json:"endTime"` + LogType []string `json:"logType"` + Keyword []string `json:"keyword"` + KeywordType string `json:"keywordType"` + ExcludeKeyword []string `json:"excludeKeyword"` + ExcludeKeywordType string `json:"excludeKeywordType"` + LogLevel []string `json:"logLevel"` + ReqId string `json:"reqId"` +} + +type LogEntryResponse struct { + LogAt time.Time `json:"logAt"` + LogLine string `json:"logLine"` + LogLevel string `json:"logLevel"` + FileName string `json:"fileName"` + FileId string `json:"fileId"` + FileOffset int64 `json:"fileOffset"` +} + +type QueryLogResponse struct { + LogEntries []LogEntryResponse `json:"logEntries"` + FileId string `json:"fileId"` + FileOffset int64 `json:"fileOffset"` +} + +func queryLogHandler(c *gin.Context) { + ctx := common.NewContextWithTraceId(c) + ctxLog := log.WithContext(ctx) + var param QueryLogRequest + err := c.BindJSON(¶m) + if err != nil { + ctxLog.WithError(err).Error("bindJson failed") + return + } + if log_query.GlobalLogQuerier.GetConf().QueryTimeout != 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, log_query.GlobalLogQuerier.GetConf().QueryTimeout) + defer cancel() + } + + ctxLog.WithField("param", param) + ctxLog.Info("invoke log query") + + // Limit the maximum number of queries at a time + if param.Limit == 0 { + param.Limit = 200 + } + + logQueryReqParam, err := buildLogQueryReqParams(param) + if err != nil { + ctxLog.WithError(err).Error("buildLogQueryReqParams failed") + return + } + + logEntryChan := make(chan log_query.LogEntry, 1) + logQuery, err := log_query.NewLogQuery(log_query.GlobalLogQuerier.GetConf(), logQueryReqParam, logEntryChan) + if err != nil { + ctxLog.WithError(err).Error("create NewLogQuery failed") + common.SendResponse(c, nil, err) + return + } + + logEntries := make([]LogEntryResponse, 0) + + wg := sync.WaitGroup{} + wg.Add(1) + go func() { + defer wg.Done() + for logEntry := range logEntryChan { + logEntries = append(logEntries, buildLogEntryResp(logEntry)) + } + }() + + lastPos, err := log_query.GlobalLogQuerier.Query(ctx, logQuery) + if err != nil { + ctxLog.WithError(err).Error("query failed") + common.SendResponse(c, nil, err) + return + } + wg.Wait() + + resp := QueryLogResponse{ + LogEntries: logEntries, + } + if lastPos != nil { + resp.FileId = fmt.Sprintf("%d", lastPos.FileId) + resp.FileOffset = lastPos.FileOffset + } + common.SendResponse(c, resp, nil) +} + +func downloadLogHandler(c *gin.Context) { + ctx := common.NewContextWithTraceId(c) + ctxLog := log.WithContext(ctx) + var param DownloadLogRequest + err := c.BindJSON(¶m) + if err != nil { + ctxLog.WithError(err).Error("bindJson failed") + return + } + if log_query.GlobalLogQuerier.GetConf().DownloadTimeout != 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, log_query.GlobalLogQuerier.GetConf().DownloadTimeout) + defer cancel() + } + + ctxLog.WithField("param", param) + ctxLog.Info("invoke log download") + + w := c.Writer + zipWriter := zip.NewWriter(w) + + prevLogFileName := "" + for _, logType := range param.LogType { + queryLogReq := QueryLogRequest{ + StartTime: param.StartTime, + EndTime: param.EndTime, + LogType: logType, + Keyword: param.Keyword, + ExcludeKeyword: param.ExcludeKeyword, + LogLevel: param.LogLevel, + ReqId: param.ReqId, + } + + logQueryReqParam, err := buildLogQueryReqParams(queryLogReq) + if err != nil { + ctxLog.WithError(err).Error("buildLogQueryReqParams failed") + return + } + + logEntryChan := make(chan log_query.LogEntry, 1) + logQuery, err := log_query.NewLogQuery(log_query.GlobalLogQuerier.GetConf(), logQueryReqParam, logEntryChan) + if err != nil { + ctxLog.WithError(err).Error("create NewLogQuery failed") + common.SendResponse(c, nil, err) + return + } + + wg := sync.WaitGroup{} + wg.Add(1) + go func() { + defer wg.Done() + var zipFileWriter io.Writer + + for logEntry := range logEntryChan { + if logEntry.FileName != prevLogFileName { + zipFileWriter, err = zipWriter.Create(logEntry.FileName) + if err != nil { + ctxLog.WithField("logEntry", logEntry).WithError(err).Warn("zipFileWriter.Create failed") + } + prevLogFileName = logEntry.FileName + } + logLineBytes := append(logEntry.LogLine, '\n') + _, err1 := zipFileWriter.Write(logLineBytes) + if err1 != nil { + ctxLog.WithField("logEntry", logEntry).WithError(err1).Warn("write log entry bytes failed") + continue + } + } + }() + + _, err = log_query.GlobalLogQuerier.Query(ctx, logQuery) + if err != nil { + ctxLog.WithError(err).Error("query failed") + common.SendResponse(c, nil, err) + return + } + wg.Wait() + } + err = zipWriter.Close() + if err != nil { + ctxLog.WithError(err).Error("close failed") + } + w.Flush() + + common.SendResponse(c, "END-OF-STREAM", nil) +} + +func buildLogQueryReqParams(req QueryLogRequest) (*log_query.QueryLogRequest, error) { + var ( + lastQueryFileId uint64 + err error + ) + if req.LastQueryFileId != "" { + lastQueryFileId, err = strconv.ParseUint(req.LastQueryFileId, 10, 64) + if err != nil { + return nil, err + } + } + return &log_query.QueryLogRequest{ + StartTime: req.StartTime, + EndTime: req.EndTime, + LogType: req.LogType, + Keyword: req.Keyword, + KeywordType: log_query.ConditionType(req.KeywordType), + ExcludeKeyword: req.ExcludeKeyword, + ExcludeKeywordType: log_query.ConditionType(req.ExcludeKeywordType), + LogLevel: req.LogLevel, + ReqId: req.ReqId, + LastQueryFileId: lastQueryFileId, + LastQueryFileOffset: req.LastQueryFileOffset, + Limit: req.Limit, + }, nil +} + +func buildLogEntryResp(logEntry log_query.LogEntry) LogEntryResponse { + return LogEntryResponse{ + LogAt: logEntry.LogAt, + LogLine: string(logEntry.LogLine), + LogLevel: logEntry.LogLevel, + FileName: logEntry.FileName, + FileId: fmt.Sprintf("%d", logEntry.FileId), + FileOffset: logEntry.FileOffset, + } +} diff --git a/api/mgragent/mgragent_route.go b/api/mgragent/mgragent_route.go new file mode 100644 index 0000000000000000000000000000000000000000..3613d14cf105e7d1e059b4988cb0a01a37d22cad --- /dev/null +++ b/api/mgragent/mgragent_route.go @@ -0,0 +1,68 @@ +package mgragent + +import ( + "github.com/gin-gonic/gin" + adapter "github.com/gwatts/gin-adapter" + + "github.com/oceanbase/obagent/api/common" + "github.com/oceanbase/obagent/errors" + "github.com/oceanbase/obagent/lib/http" + "github.com/oceanbase/obagent/stat" +) + +func InitManagerAgentRoutes(s *http.StateHolder, r *gin.Engine) { + r.Use(common.HttpStatMiddleware) + + // self stat metrics + r.GET("/metrics/stat", adapter.Wrap(stat.PromHandler)) + r.Use( + gin.CustomRecovery(common.Recovery), // gin's crash-free middleware + common.PreHandlers("/api/v1/module/config/update", "/api/v1/module/config/validate"), + common.SetContentType, + common.PostHandlers("/debug/pprof"), + ) + + v1 := r.Group("/api/v1") + v1.GET("/time", common.TimeHandler) + v1.GET("/info", common.InfoHandler) + v1.GET("/git-info", common.GitInfoHandler) + v1.GET("/status", common.StatusHandler(s)) + v1.POST("/status", common.StatusHandler(s)) + + // task routes + task := v1.Group("/task") + task.POST("/status", queryTaskHandler) + task.GET("/status", queryTaskHandler) + + // agent admin routes + agent := v1.Group("/agent") + agent.POST("/status", agentStatusService) + agent.GET("/status", agentStatusService) + agent.POST("/restart", asyncCommandHandler(restartCmd)) + + // file routes + file := v1.Group("/file") + file.POST("/exists", isFileExists) + file.POST("/getRealPath", getRealStaticPath) + + // system routes + system := v1.Group("/system") + system.POST("/hostInfo", getHostInfoHandler) + + // module config + v1.POST("/module/config/update", common.UpdateConfigPropertiesHandler) + v1.POST("/module/config/notify", common.NotifyConfigPropertiesHandler) + v1.POST("/module/config/validate", common.ValidateConfigPropertiesHandler) + v1.GET("/module/config/status", common.ConfigStatusHandler) + v1.POST("/module/config/change", common.ChangeConfigHandler) + v1.POST("/module/config/reload", common.ReloadConfigHandler) + + logGroup := v1.Group("/log") + logGroup.POST("/query", queryLogHandler) + logGroup.POST("/download", downloadLogHandler) + + r.NoRoute(func(c *gin.Context) { + err := errors.Occur(errors.ErrBadRequest, "404 not found") + common.SendResponse(c, nil, err) + }) +} diff --git a/api/mgragent/system_handler.go b/api/mgragent/system_handler.go new file mode 100644 index 0000000000000000000000000000000000000000..b7a05e2e83f876f2abd5565cc21364bc61e08dc5 --- /dev/null +++ b/api/mgragent/system_handler.go @@ -0,0 +1,14 @@ +package mgragent + +import ( + "github.com/gin-gonic/gin" + + "github.com/oceanbase/obagent/api/common" + "github.com/oceanbase/obagent/executor/system" +) + +func getHostInfoHandler(c *gin.Context) { + ctx := common.NewContextWithTraceId(c) + data, err := system.GetHostInfo(ctx) + common.SendResponse(c, data, err) +} diff --git a/api/mgragent/task_handler.go b/api/mgragent/task_handler.go new file mode 100644 index 0000000000000000000000000000000000000000..2b47dcb69e30173114f511b6a3026e4e522739e4 --- /dev/null +++ b/api/mgragent/task_handler.go @@ -0,0 +1,82 @@ +package mgragent + +import ( + "reflect" + + "github.com/gin-gonic/gin" + + "github.com/oceanbase/obagent/api/common" + "github.com/oceanbase/obagent/errors" + "github.com/oceanbase/obagent/executor/agent" + "github.com/oceanbase/obagent/lib/command" + path2 "github.com/oceanbase/obagent/lib/path" +) + +type QueryTaskParam struct { + TaskToken string `json:"taskToken"` +} + +type TaskTokenResult struct { + TaskToken string `json:"taskToken"` +} + +type TaskStatusResult struct { + Finished bool `json:"finished"` + Ok bool `json:"ok"` + Result interface{} `json:"result"` + Err string `json:"err"` + Progress interface{} `json:"progress"` +} + +var taskExecutor = command.NewExecutor(command.NewFileTaskStore(path2.TaskStoreDir())) + +func queryTaskHandler(c *gin.Context) { + //ctx := NewContextWithTraceId(c) + var param QueryTaskParam + c.BindJSON(¶m) + status, ok := taskExecutor.GetResult(command.ExecutionTokenFromString(param.TaskToken)) + if !ok { + common.SendResponse(c, nil, errors.Occur(errors.ErrTaskNotFound, param.TaskToken)) + return + } + common.SendResponse(c, TaskStatusResult{ + Finished: status.Finished, + Ok: status.Ok, + Result: status.Result, + Err: status.Err, + Progress: status.Progress, + }, nil) +} + +func TaskCount() int { + return len(taskExecutor.AllExecutions()) +} + +func asyncCommandHandler(task command.Command) gin.HandlerFunc { + return func(c *gin.Context) { + ctx := common.NewContextWithTraceId(c) + defaultParam := task.DefaultParam() + v := reflect.New(reflect.TypeOf(defaultParam)) + v.Elem().Set(reflect.ValueOf(defaultParam)) + param := v.Interface() + err := c.BindJSON(param) + if err != nil { + common.SendResponse(c, nil, err) + return + } + taskToken := param.(agent.TaskTokenParam) + if taskToken.GetTaskToken() == "" { + taskToken.SetTaskToken(command.GenerateTaskId()) + } + input := command.NewInput(ctx, reflect.ValueOf(param).Elem().Interface()) + input.WithRequestTaskToken(taskToken.GetTaskToken()) + token, err := taskExecutor.Execute(task, input) + if err != nil { + common.SendResponse(c, nil, err) + return + } + common.SendResponse(c, TaskTokenResult{ + TaskToken: token.String(), + }, nil) + } +} diff --git a/api/mgragent/task_handler_test.go b/api/mgragent/task_handler_test.go new file mode 100644 index 0000000000000000000000000000000000000000..b69e1ec5a4dc905984659a71f136318364f36796 --- /dev/null +++ b/api/mgragent/task_handler_test.go @@ -0,0 +1,93 @@ +package mgragent + +import ( + "context" + "net/http" + "net/http/httptest" + "os" + "strings" + "testing" + + "github.com/gin-gonic/gin" + + "github.com/oceanbase/obagent/api/common" + "github.com/oceanbase/obagent/executor/agent" + "github.com/oceanbase/obagent/lib/command" + http2 "github.com/oceanbase/obagent/lib/http" + path2 "github.com/oceanbase/obagent/lib/path" +) + +type S struct { + agent.TaskToken + A string +} + +func TestAsyncCommandHandler(t *testing.T) { + os.MkdirAll(path2.TaskStoreDir(), 0755) + defer os.RemoveAll(path2.TaskStoreDir()) + + h := asyncCommandHandler(command.WrapFunc(func(s S) S { + s.A = s.A + s.A + return s + })) + req, _ := http.NewRequest("POST", "/xxx", strings.NewReader(`{"A":"a", "taskToken":"token12345"}`)) + rec := httptest.NewRecorder() + ctx, _ := gin.CreateTestContext(rec) + ctx.Request = req + ctx.Keys = map[string]interface{}{common.TraceIdKey: "a"} + h(ctx) + resp := ctx.Keys[common.OcpAgentResponseKey].(http2.OcpAgentResponse) + if !resp.Successful || resp.Status != 200 { + t.Errorf("Fail %+v", resp) + return + } + tokenResult := resp.Data.(TaskTokenResult) + if tokenResult.TaskToken != "token12345" { + t.Errorf("bad result %+v", tokenResult) + return + } + result, ok := taskExecutor.WaitResult(command.ExecutionTokenFromString("token12345")) + if !ok { + t.Error("wait result failed") + return + } + s := result.Result.(S) + if s.A != "aa" { + t.Errorf("bad result %+v", s) + } +} + +func TestAsyncCommandHandler2(t *testing.T) { + os.MkdirAll(path2.TaskStoreDir(), 0755) + defer os.RemoveAll(path2.TaskStoreDir()) + + h := asyncCommandHandler(command.WrapFunc(func(ctx context.Context, s S) (S, error) { + s.A = s.A + s.A + return s, nil + })) + req, _ := http.NewRequest("POST", "/xxx", strings.NewReader(`{"A":"a", "taskToken":"token12345"}`)) + rec := httptest.NewRecorder() + ctx, _ := gin.CreateTestContext(rec) + ctx.Request = req + ctx.Keys = map[string]interface{}{common.TraceIdKey: "a"} + h(ctx) + resp := ctx.Keys[common.OcpAgentResponseKey].(http2.OcpAgentResponse) + if !resp.Successful || resp.Status != 200 { + t.Errorf("Fail %+v", resp) + return + } + tokenResult := resp.Data.(TaskTokenResult) + if tokenResult.TaskToken != "token12345" { + t.Errorf("bad result %+v", tokenResult) + return + } + result, ok := taskExecutor.WaitResult(command.ExecutionTokenFromString("token12345")) + if !ok { + t.Error("wait result failed") + return + } + s := result.Result.(S) + if s.A != "aa" { + t.Errorf("bad result %+v", s) + } +} diff --git a/api/monagent/monagent_route.go b/api/monagent/monagent_route.go new file mode 100644 index 0000000000000000000000000000000000000000..b9dd3d88255fef7852262af1c0a25537a89ce00d --- /dev/null +++ b/api/monagent/monagent_route.go @@ -0,0 +1,97 @@ +package monagent + +import ( + "context" + "net/http" + "os" + + "github.com/gin-gonic/gin" + adapter "github.com/gwatts/gin-adapter" + log "github.com/sirupsen/logrus" + + "github.com/oceanbase/obagent/api/common" + "github.com/oceanbase/obagent/config" + http2 "github.com/oceanbase/obagent/lib/http" + "github.com/oceanbase/obagent/lib/system" + "github.com/oceanbase/obagent/stat" +) + +func InitMonitorAgentRoutes(router *gin.Engine, localRouter *gin.Engine) { + router.GET("/metrics/stat", adapter.Wrap(stat.PromHandler)) + + v1 := router.Group("/api/v1") + v1.Use(common.PostHandlers()) + + v1.POST("/module/config/update", common.UpdateConfigPropertiesHandler) + v1.POST("/module/config/notify", common.NotifyConfigPropertiesHandler) + v1.POST("/module/config/validate", common.ValidateConfigPropertiesHandler) + + v1.GET("/time", common.TimeHandler) + v1.GET("/info", common.InfoHandler) + v1.GET("/git-info", common.GitInfoHandler) + v1.POST("/status", monitorStatusHandler) + v1.GET("/status", monitorStatusHandler) + + initMonagentLocalRoutes(localRouter) +} + +func initMonagentLocalRoutes(localRouter *gin.Engine) { + common.InitPprofRouter(localRouter) + + localRouter.GET("/metrics/stat", adapter.Wrap(stat.PromHandler)) + + group := localRouter.Group("/api/v1") + group.GET("/time", common.TimeHandler) + group.GET("/info", common.InfoHandler) + group.POST("/info", common.InfoHandler) + group.GET("/git-info", common.GitInfoHandler) + group.POST("/status", monitorStatusHandler) + group.GET("/status", monitorStatusHandler) + group.POST("/module/config/update", common.UpdateConfigPropertiesHandler) + group.POST("/module/config/notify", common.NotifyConfigPropertiesHandler) +} + +func UseLocalMonitorMiddleware(r *gin.Engine) { + r.Use( + common.HttpStatMiddleware, + gin.CustomRecovery(common.Recovery), // gin's crash-free middleware + common.PreHandlers("/api/v1/module/config/update", "/api/v1/module/config/validate"), + common.PostHandlers("/debug/pprof", "/debug/fgprof", "/metrics/", "/api/v1/log/alarms"), + ) +} + +func UseMonitorMiddleware(r *gin.Engine) { + r.Use( + common.HttpStatMiddleware, + gin.CustomRecovery(common.Recovery), // gin's crash-free middleware + common.PreHandlers("/api/v1/module/config/update", "/api/v1/module/config/validate"), + common.MonitorAgentPostHandler, + ) +} + +func RegisterPipelineRoute(ctx context.Context, r *gin.Engine, url string, fh func(http.Handler) http.Handler) { + log.WithContext(ctx).Infof("register route %s", url) + r.GET(url, adapter.Wrap(fh)) +} + +var libProcess system.Process = system.ProcessImpl{} + +func monitorStatusHandler(c *gin.Context) { + ports := make([]int, 0) + + pid := os.Getpid() + processInfo, err := libProcess.GetProcessInfoByPid(int32(pid)) + if err != nil { + log.Errorf("StatusHandler get processInfo failed, pid:%s", pid) + } else { + ports = processInfo.Ports + } + var info = http2.Status{ + State: http2.Running, + Version: config.AgentVersion, + Pid: pid, + StartAt: common.StartAt, + Ports: ports, + } + common.SendResponse(c, info, nil) +} diff --git a/api/response/status.go b/api/response/status.go deleted file mode 100644 index 679f310fb42747283f73cbf0b00411c00544aa1f..0000000000000000000000000000000000000000 --- a/api/response/status.go +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - -package response - -import "encoding/json" - -type State int32 - -const ( - Unknown State = 0 - Starting State = 1 - Running State = 2 - Stopping State = 3 - Stopped State = 4 - StoppedWait State = 5 -) - -var stateString = []string{"unknown", "starting", "running", "stopping", "stopped", "stopped_wait"} - -func (s State) String() string { - return stateString[int(s)] -} - -func (s State) MarshalJSON() ([]byte, error) { - return json.Marshal(s.String()) -} - -func (s *State) UnmarshalJSON(b []byte) error { - var str string - err := json.Unmarshal(b, &str) - if err != nil { - return err - } - for i := 0; i < len(stateString); i++ { - if stateString[i] == str { - *s = State(i) - return nil - } - } - *s = Unknown - return nil -} - -//Status info api response -type Status struct { - //service state - State State `json:"state"` - //service version - Version string `json:"version"` - //service pid - Pid int `json:"pid"` - //timestamp when service started - StartAt int64 `json:"startAt"` -} diff --git a/api/route/config_handler.go b/api/route/config_handler.go deleted file mode 100644 index bc219aab59889c29a36476bc2a5cc3d473e854c9..0000000000000000000000000000000000000000 --- a/api/route/config_handler.go +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - -package route - -import ( - "github.com/gin-gonic/gin" - log "github.com/sirupsen/logrus" - - "github.com/oceanbase/obagent/config" -) - -// http handler: validate module config and save module config -func updateModuleConfigHandler(c *gin.Context) { - kvs := config.KeyValues{} - c.Bind(&kvs) - ctx := NewContextWithTraceId(c) - ctxlog := log.WithContext(ctx) - - configVersion, err := config.UpdateConfig(ctx, &kvs, true) - if err != nil { - ctxlog.Errorf("update config err:%+v", err) - } - - sendResponse(c, configVersion, err) -} - -// http handler: notify module config -func notifyModuleConfigHandler(c *gin.Context) { - nconfig := new(config.NotifyModuleConfig) - c.Bind(nconfig) - - ctx := NewContextWithTraceId(c) - ctxlog := log.WithContext(ctx).WithFields(log.Fields{ - "process": nconfig.Process, - "module": nconfig.Module, - "updated key values": nconfig.UpdatedKeyValues, - }) - - ctxlog.Debugf("notify module config") - - err := config.NotifyModuleConfigForHttp(ctx, nconfig) - if err != nil { - ctxlog.Errorf("notify module config err:%+v", err) - } - - sendResponse(c, "notify module config success", err) -} diff --git a/api/route/handler_common.go b/api/route/handler_common.go deleted file mode 100644 index 95e26b1f42926ef979193a2623223d72b447f9f3..0000000000000000000000000000000000000000 --- a/api/route/handler_common.go +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - -package route - -import ( - "context" - - "github.com/gin-gonic/gin" - - "github.com/oceanbase/obagent/api/response" - "github.com/oceanbase/obagent/log" -) - -// keys stored in gin.Context -const ( - AgentResponseKey = "agentResponse" - TraceIdKey = "traceId" -) - -func NewContextWithTraceId(c *gin.Context) context.Context { - traceId := "" - if t, ok := c.Get(TraceIdKey); ok { - if ts, ok := t.(string); ok { - traceId = ts - } - } - return context.WithValue(context.Background(), log.TraceIdKey{}, traceId) -} - -func sendResponse(c *gin.Context, data interface{}, err error) { - resp := response.BuildResponse(data, err) - c.Set(AgentResponseKey, resp) -} diff --git a/api/route/monagent_route.go b/api/route/monagent_route.go deleted file mode 100644 index df48defba3a1cb8d0fa74d60ac697c3ef54c0744..0000000000000000000000000000000000000000 --- a/api/route/monagent_route.go +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - -package route - -import ( - "net/http" - - "github.com/gin-contrib/pprof" - "github.com/gin-gonic/gin" - adapter "github.com/gwatts/gin-adapter" - log "github.com/sirupsen/logrus" - - "github.com/oceanbase/obagent/stat" -) - -func InitMonagentRoutes(r *gin.Engine) { - r.Use( - gin.Recovery(), // gin's crash-free middleware - ) - - v1 := r.Group("/api/v1") - v1.POST("/module/config/update", updateModuleConfigHandler) - v1.POST("/module/config/notify", notifyModuleConfigHandler) - - metric := r.Group("/metrics") - metric.GET("/stat", adapter.Wrap(stat.PromGinWrapper)) -} - -func RegisterPipelineRoute(r *gin.Engine, url string, fh func(http.Handler) http.Handler) { - log.Infof("register route %s", url) - r.GET(url, adapter.Wrap(fh)) -} - -func InitPprofRouter(r *gin.Engine) { - pprof.Register(r, "debug/pprof") -} diff --git a/api/web/http_server.go b/api/web/http_server.go index 9a9f8611441139b876abc5a1c930f60af76ed7e5..b4971300d77837004456fe3049e952eec5516e30 100644 --- a/api/web/http_server.go +++ b/api/web/http_server.go @@ -1,15 +1,3 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - package web import ( @@ -23,9 +11,8 @@ import ( "github.com/pkg/errors" log "github.com/sirupsen/logrus" - "github.com/oceanbase/obagent/api/response" - "github.com/oceanbase/obagent/api/route" - server2 "github.com/oceanbase/obagent/api/server" + "github.com/oceanbase/obagent/api/common" + http2 "github.com/oceanbase/obagent/lib/http" ) type HttpServer struct { @@ -34,22 +21,24 @@ type HttpServer struct { // current session count, concurrent safely Counter *Counter // http routers - Router *gin.Engine + Router *gin.Engine + LocalRouter *gin.Engine // address Address string // socket Socket string // http server, call its Run, Shutdown methods - Server *http.Server + Server *http.Server + LocalServer *http.Server // stop the http.Server by calling cancel method Cancel context.CancelFunc // basic authorizer - BasicAuthorizer Authorizer + BasicAuthorizer common.Authorizer } func (server *HttpServer) AuthorizeMiddleware(c *gin.Context) { - ctx := route.NewContextWithTraceId(c) - ctxlog := log.WithContext(ctx) + ctx := common.NewContextWithTraceId(c) + ctxlog := log.WithContext(ctx).WithField("url", c.Request.URL) if server.BasicAuthorizer == nil { ctxlog.Warnf("basic auth is nil, please check the initial process.") c.Next() @@ -60,7 +49,7 @@ func (server *HttpServer) AuthorizeMiddleware(c *gin.Context) { if err != nil { ctxlog.Errorf("basic auth Authorize failed, err:%+v", err) c.Abort() - c.JSON(http.StatusUnauthorized, response.BuildResponse(nil, err)) + c.JSON(http.StatusUnauthorized, http2.BuildResponse(nil, err)) return } c.Next() @@ -81,13 +70,13 @@ func (server *HttpServer) UseBasicAuth() { ) } -// Run start a httpServer +// run start a httpServer // when ctx is cancelled, call shutdown to stop the httpServer func (server *HttpServer) Run(ctx context.Context) { server.Server.Handler = server.Router if server.Address != "" { - tcpListener, err := server2.NewTcpListener(server.Address) + tcpListener, err := http2.NewTcpListener(server.Address) if err != nil { log.WithError(err). Errorf("create tcp listener on address '%s' failed %v", server.Address, err) @@ -101,14 +90,19 @@ func (server *HttpServer) Run(ctx context.Context) { }() } if server.Socket != "" { - socketListener, err := server2.NewSocketListener(server.Socket) + socketListener, err := http2.NewSocketListener(server.Socket) if err != nil { log.WithError(err). Errorf("create socket listener on file '%s' failed %v", server.Socket, err) return } go func() { - if err = server.Server.Serve(socketListener); err != nil { + server.LocalServer = &http.Server{ + Handler: server.LocalRouter, + ReadTimeout: 5 * time.Minute, + WriteTimeout: 5 * time.Minute, + } + if err = server.LocalServer.Serve(socketListener); err != nil { log.WithError(err). Info("socket server exited") } @@ -147,7 +141,7 @@ func (server *HttpServer) Shutdown(ctx context.Context) error { func (server *HttpServer) counterPreHandlerFunc(c *gin.Context) { if atomic.LoadInt32(&(server.Stopping)) == 1 { c.Abort() - c.JSON(http.StatusServiceUnavailable, response.BuildResponse("server is shutdowning now.", nil)) + c.JSON(http.StatusServiceUnavailable, http2.BuildResponse("server is shutdowning now.", nil)) return } diff --git a/api/web/http_server_test.go b/api/web/http_server_test.go index f964d1ca19fceefbb479e7e5f96c801170e03d48..8de59c85363386572cb299073e43e6a55731702c 100644 --- a/api/web/http_server_test.go +++ b/api/web/http_server_test.go @@ -1,15 +1,3 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - package web import ( diff --git a/api/web/monagent_server.go b/api/web/monagent_server.go new file mode 100644 index 0000000000000000000000000000000000000000..b1d8158c106ecaab56bad34ec209ac750f4799d5 --- /dev/null +++ b/api/web/monagent_server.go @@ -0,0 +1,80 @@ +package web + +import ( + "context" + "net/http" + "os" + "sync" + + "github.com/gin-gonic/gin" + + "github.com/oceanbase/obagent/api/common" + monroute "github.com/oceanbase/obagent/api/monagent" + monconfig "github.com/oceanbase/obagent/config/monagent" + "github.com/oceanbase/obagent/executor/agent" + path2 "github.com/oceanbase/obagent/lib/path" + "github.com/oceanbase/obagent/monitor/engine" +) + +var monitorAgentServer *MonitorAgentServer + +func GetMonitorAgentServer() *MonitorAgentServer { + return monitorAgentServer +} + +type MonitorAgentServer struct { + // original configs + Config *monconfig.MonitorAgentConfig + // sever of monitor metrics, selfstat, monitor manager API + Server *HttpServer + // two servers concurrent waitGroup + wg *sync.WaitGroup +} + +// NewMonitorAgentServer init monagent server: init configs and logger, register routers +func NewMonitorAgentServer(conf *monconfig.MonitorAgentConfig) *MonitorAgentServer { + monagentServer := &MonitorAgentServer{ + Config: conf, + Server: &HttpServer{ + Counter: new(Counter), + Router: gin.New(), + LocalRouter: gin.New(), + BasicAuthorizer: new(common.BasicAuth), + Server: &http.Server{}, + Address: conf.Server.Address, + Socket: agent.SocketPath(conf.Server.RunDir, path2.ProgramName(), os.Getpid()), + }, + wg: &sync.WaitGroup{}, + } + // register middleware before register handlers + monroute.UseMonitorMiddleware(monagentServer.Server.Router) + monroute.UseLocalMonitorMiddleware(monagentServer.Server.LocalRouter) + monitorAgentServer = monagentServer + return monitorAgentServer +} + +// Run start monagent servers: admin server, monitor server +func (server *MonitorAgentServer) Run() { + server.wg.Add(1) + go func() { + defer server.wg.Done() + ctx, cancel := context.WithCancel(context.Background()) + server.Server.Cancel = cancel + server.Server.Run(ctx) + }() + server.wg.Wait() +} + +// registerRouter register routers such as adminServer router and monitor metrics router +func (server *MonitorAgentServer) RegisterRouter() { + server.wg.Add(1) + go func() { + defer server.wg.Done() + server.Server.UseCounter() + monroute.InitMonitorAgentRoutes(server.Server.Router, server.Server.LocalRouter) + for router := range engine.PipelineRouteChan { + monroute.RegisterPipelineRoute(router.Ctx, server.Server.Router, router.ExposeUrl, router.FuncHandler) + monroute.RegisterPipelineRoute(router.Ctx, server.Server.LocalRouter, router.ExposeUrl, router.FuncHandler) + } + }() +} diff --git a/api/web/monagent_server_test.go b/api/web/monagent_server_test.go new file mode 100644 index 0000000000000000000000000000000000000000..e8d8f13012b165840be0ec125dfbdd1b1d2d6e0b --- /dev/null +++ b/api/web/monagent_server_test.go @@ -0,0 +1,27 @@ +package web + +import ( + "context" + "testing" + + . "github.com/smartystreets/goconvey/convey" + + "github.com/oceanbase/obagent/config/monagent" +) + +func TestMonitorAgentServerShutdown(t *testing.T) { + server := NewMonitorAgentServer(&monagent.MonitorAgentConfig{ + Server: monagent.MonitorAgentHttpConfig{ + Address: ":62889", + }, + }) + go server.Run() + + t.Run("shutdown without any request", func(t *testing.T) { + err := server.Server.Shutdown(context.Background()) + Convey("shutdown err", t, func() { + So(err, ShouldBeNil) + }) + }) + +} diff --git a/api/web/route_test.go b/api/web/route_test.go new file mode 100644 index 0000000000000000000000000000000000000000..cc58337df1ad4f3dc1ad5c847c7d37d2a144df7d --- /dev/null +++ b/api/web/route_test.go @@ -0,0 +1,147 @@ +package web + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + . "github.com/smartystreets/goconvey/convey" + + "github.com/oceanbase/obagent/api/common" + "github.com/oceanbase/obagent/config" + "github.com/oceanbase/obagent/config/mgragent" + "github.com/oceanbase/obagent/errors" + http2 "github.com/oceanbase/obagent/lib/http" +) + +func Test_RouteHandler(t *testing.T) { + type args struct { + url string + } + type want struct { + successful bool + statusCode int + } + tests := []struct { + name string + args args + want want + }{ + { + name: "example1", + args: args{ + url: "http://127.0.0.1:62888/api/example/1", + }, + want: want{ + successful: true, + statusCode: http.StatusOK, + }, + }, + { + name: "example2", + args: args{ + url: "http://127.0.0.1:62888/api/example/2", + }, + want: want{ + successful: true, + statusCode: http.StatusOK, + }, + }, + { + name: "example3", + args: args{ + url: "http://127.0.0.1:62888/api/example/3", + }, + want: want{ + successful: true, + statusCode: http.StatusOK, + }, + }, + { + name: "example4", + args: args{ + url: "http://127.0.0.1:62888/api/example/4", + }, + want: want{ + successful: false, + statusCode: http.StatusBadRequest, + }, + }, + } + server := NewServer(config.AgentVersion, mgragent.ServerConfig{}) + InitExampleRoutes(server.Router) + handler := func(w http.ResponseWriter, r *http.Request) { + server.Router.ServeHTTP(w, r) + } + for _, tt := range tests { + Convey(tt.name, t, func() { + req := httptest.NewRequest("GET", tt.args.url, nil) + w := httptest.NewRecorder() + handler(w, req) + + resp := w.Result() + body, _ := ioutil.ReadAll(resp.Body) + + var successResponse http2.OcpAgentResponse + _ = json.Unmarshal(body, &successResponse) + + So(resp.StatusCode, ShouldEqual, tt.want.statusCode) + So(successResponse.Successful, ShouldEqual, tt.want.successful) + So(successResponse.Status, ShouldEqual, tt.want.statusCode) + }) + } +} + +// only for test +func InitExampleRoutes(r *gin.Engine) { + v1 := r.Group("/api/example") + + v1.GET("/1", exampleHandler1) + v1.GET("/2", exampleHandler2) + v1.GET("/3", exampleHandler3) + v1.GET("/4", exampleHandler4) +} + +var exampleHandler1 = func(c *gin.Context) { + data, err := singleExample() + sendResponse(c, data, err) +} + +var exampleHandler2 = func(c *gin.Context) { + data, err := iterableExample() + sendResponse(c, data, err) +} + +var exampleHandler3 = func(c *gin.Context) { + err := noDataExample() + sendResponse(c, nil, err) +} + +var exampleHandler4 = func(c *gin.Context) { + data, err := errorExample() + sendResponse(c, data, err) +} + +func singleExample() (string, *errors.OcpAgentError) { + return "this is data", nil +} + +func iterableExample() ([]string, *errors.OcpAgentError) { + return []string{"data1", "data2", "data3"}, nil +} + +func noDataExample() *errors.OcpAgentError { + return nil +} + +func errorExample() (string, *errors.OcpAgentError) { + return "", errors.Occur(errors.ErrBadRequest) +} + +func sendResponse(c *gin.Context, data interface{}, err error) { + resp := http2.BuildResponse(data, err) + c.Set(common.OcpAgentResponseKey, resp) +} diff --git a/api/web/server.go b/api/web/server.go new file mode 100644 index 0000000000000000000000000000000000000000..550ddd16c11f1ebc15fb2606cf1704c779fbbb0b --- /dev/null +++ b/api/web/server.go @@ -0,0 +1,111 @@ +package web + +import ( + "context" + "net/http" + "os" + "sync" + "time" + + "github.com/gin-gonic/gin" + log "github.com/sirupsen/logrus" + + "github.com/oceanbase/obagent/api/common" + mgrroute "github.com/oceanbase/obagent/api/mgragent" + "github.com/oceanbase/obagent/config" + mgrconfig "github.com/oceanbase/obagent/config/mgragent" + "github.com/oceanbase/obagent/executor/agent" + http2 "github.com/oceanbase/obagent/lib/http" + path2 "github.com/oceanbase/obagent/lib/path" +) + +type Server struct { + Config mgrconfig.ServerConfig + Router *gin.Engine + LocalRouter *gin.Engine + HttpServer *http.Server + LocalHttpServer *http.Server + state *http2.StateHolder +} + +func NewServer(mode config.AgentMode, conf mgrconfig.ServerConfig) *Server { + if mode == config.DebugMode { + gin.SetMode(gin.DebugMode) + } else { + gin.SetMode(gin.ReleaseMode) + } + router := gin.New() + localRouter := gin.New() + + // TODO use gin.Logger() only for debugging, remove it before 3.2.0 publishes. + if mode == config.DebugMode { + router.Use(gin.Logger()) + } + + ret := &Server{ + Router: router, + LocalRouter: localRouter, + Config: conf, + state: http2.NewStateHolder(http2.Running), + } + router.Use(common.IgnoreFaviconHandler) + router.Use(common.AuthorizeMiddleware) + mgrroute.InitManagerAgentRoutes(ret.state, router) + mgrroute.InitManagerAgentRoutes(ret.state, localRouter) + common.InitPprofRouter(localRouter) + return ret +} + +func (s *Server) Run() { + s.HttpServer = &http.Server{ + Handler: s.Router, + ReadTimeout: 60 * time.Minute, + WriteTimeout: 60 * time.Minute, + } + s.LocalHttpServer = &http.Server{ + Handler: s.LocalRouter, + ReadTimeout: 60 * time.Minute, + WriteTimeout: 60 * time.Minute, + } + tcpListener, err := http2.NewTcpListener(s.Config.Address) + if err != nil { + log.WithError(err).Fatalf("create tcp listener on %s", s.Config.Address) + } + socketPath := agent.SocketPath(s.Config.RunDir, path2.ProgramName(), os.Getpid()) + socketListener, err := http2.NewSocketListener(socketPath) + if err != nil { + log.WithError(err).Fatalf("create socket listener on %s", socketPath) + } + + wg := sync.WaitGroup{} + wg.Add(2) + go func() { + err = s.HttpServer.Serve(tcpListener) + if err != nil { + log.WithError(err).Fatal("serve on tcp listener failed") + } + wg.Done() + }() + go func() { + err = s.LocalHttpServer.Serve(socketListener) + if err != nil { + log.WithError(err).Fatal("serve on socket listener failed") + } + wg.Done() + }() + wg.Wait() +} + +func (s *Server) Stop() { + err := s.HttpServer.Shutdown(context.Background()) + log.WithError(err).Error("stop http server got error") + s.state.Set(http2.Stopped) + // TODO: wait command finished + for mgrroute.TaskCount() > 0 { + time.Sleep(time.Second) + } +} + +func (s *Server) State() http2.State { + return s.state.Get() +} diff --git a/api/web/server_test.go b/api/web/server_test.go new file mode 100644 index 0000000000000000000000000000000000000000..1b2a6d5e909579d701db89871c8efff56e1c1ab5 --- /dev/null +++ b/api/web/server_test.go @@ -0,0 +1,37 @@ +package web + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" + + . "github.com/smartystreets/goconvey/convey" + + "github.com/oceanbase/obagent/config" + "github.com/oceanbase/obagent/config/mgragent" + http2 "github.com/oceanbase/obagent/lib/http" +) + +func Test_NewServer(t *testing.T) { + Convey("time api", t, func() { + server := NewServer(config.AgentVersion, mgragent.ServerConfig{}) + handler := func(w http.ResponseWriter, r *http.Request) { + server.Router.ServeHTTP(w, r) + } + req := httptest.NewRequest("GET", "http://127.0.0.1:62888/api/v1/time", nil) + w := httptest.NewRecorder() + handler(w, req) + + resp := w.Result() + body, _ := ioutil.ReadAll(resp.Body) + + var successResponse http2.OcpAgentResponse + _ = json.Unmarshal(body, &successResponse) + + So(resp.StatusCode, ShouldEqual, http.StatusOK) + So(successResponse.Status, ShouldEqual, http.StatusOK) + So(successResponse.Successful, ShouldEqual, true) + }) +} diff --git a/assets/i18n/error/en.json b/assets/i18n/error/en.json new file mode 100644 index 0000000000000000000000000000000000000000..0ee33e15f448c21493ce5d4f485f65f2180cf09a --- /dev/null +++ b/assets/i18n/error/en.json @@ -0,0 +1,68 @@ +{ + "err.bad.request": "Bad request: %v", + "err.illegal.argument": "Illegal argument: %v", + "err.unexpected": "Unexpected error: %v", + + "err.execute.command": "Execute shell command failed: %v", + + "err.download.file": "Cannot download file from url: %s, reason: %s", + "err.invalid.checksum": "Invalid checksum", + "err.write.file": "Cannot write to file %v, reason: %v", + "err.find.file": "Cannot find file under %v, reason: %v", + "err.check.file.exists": "Cannot check whether file %v exists, reason: %v", + + "err.create.directory": "Cannot create directory %v, reason: %v", + "err.remove.directory": "Cannot remove directory %v, reason: %v", + "err.chown.directory": "Cannot chown for directory %v, reason: %v", + + "err.create.symlink": "Cannot create symbolic link from %v to %v, reason %v", + "err.process.cgroup": "Process cgroup failed: %v, reason: %v", + + "err.task.not.found": "Task specified by token not found %v", + + "err.query.package": "Query software package failed, reason: %v", + "err.install.package": "Install software package failed, reason: %v", + "err.uninstall.package": "Uninstall software package failed, reason: %v", + "err.extract.package": "Extract software package failed, reason: %v", + + "err.check.process.exists": "Cannot check process existence of %v, reason: %v", + "err.get.process.info": "Cannot get process info of %v, reason: %v", + "err.stop.process": "Cannot stop process %v, reason: %v", + "err.get.process.proc": "Cannot get /proc/%v of process %v by user %v, reason: %v", + + "err.system.disk.get.usage": "Cannot get disk usage of %v, reason: %v", + "err.system.disk.batch.get.disk.infos": "Cannot get disk infos, reason: %v", + + "err.ob.install.pre-check": "Install OB pre-check failed: %v", + "err.ob.io.bench": "Do io bench failed: %v", + "err.observer.start": "Start observer failed: %v", + "err.check.observer.accessible": "Check observer accessible failed: %v", + "err.ob.bootstrap": "Bootstrap OB failed: %v", + "err.clean.ob.data.files": "Clean OB data files failed: %v", + "err.clean.ob.all.files": "Clean OB all files failed: %v", + "err.run.upgrade.script": "Failed to run upgrade script:'%v'", + + "err.backup.start.backup.agent": "Start backup agent failed: %v", + "err.backup.stop.backup.agent": "Stop backup agent failed: %v", + "err.backup.check.backup.agent.online": "Check backup agent online failed: %v", + "err.backup.clean.backup.agent.files": "Clean backup agent files failed: %v", + "err.backup.clean.backup.data": "Clean backup data failed: %v", + + "err.dump.backup.file": "Dump backup file %v failed: %v", + + "err.backup.set.storage.client": "Set storage client failed,bucket: %v, reason: %v", + "err.backup.check.bucket": "check storage bucket failed, bucket: %v, reason: %v", + "err.backup.object.not.found": "object %v not found, bucket: %v, reason: %v", + "err.backup.get.storage.object": "get cos bucket %v object %v list failed, reason %v", + "err.backup.del.storage.object": "Delete cos bucket %v object %v failed, reason %v", + + "err.agent.agentd.already.running": "agentd already running with pid file", + "err.agent.agentd.not.running": "agentd is not running", + "err.agent.agentd.exited.quickly": "start agentd failed, agentd exited quickly", + + "err.monagent.pipeline.already.start": "monagent pipeling already start", + "err.monagent.pipeline.start.failed": "monagent pipeline start failed", + "err.monagent.remove.pipeline": "Remove monagent pipeline instance failed: %v", + + "err.last.message.stub": "Last message stub" +} diff --git a/bindata/bindata.go b/bindata/bindata.go deleted file mode 100644 index c80fe0bb9fa897127193894c907dbd608298dec8..0000000000000000000000000000000000000000 --- a/bindata/bindata.go +++ /dev/null @@ -1,259 +0,0 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - -package bindata - -import ( - "bytes" - "compress/gzip" - "fmt" - "io" - "io/ioutil" - "os" - "path/filepath" - "strings" - "time" -) - -func bindataRead(data []byte, name string) ([]byte, error) { - gz, err := gzip.NewReader(bytes.NewBuffer(data)) - if err != nil { - return nil, fmt.Errorf("Read %q: %v", name, err) - } - - var buf bytes.Buffer - _, err = io.Copy(&buf, gz) - clErr := gz.Close() - - if err != nil { - return nil, fmt.Errorf("Read %q: %v", name, err) - } - if clErr != nil { - return nil, err - } - - return buf.Bytes(), nil -} - -type asset struct { - bytes []byte - info os.FileInfo -} - -type bindataFileInfo struct { - name string - size int64 - mode os.FileMode - modTime time.Time -} - -// Name return file name -func (fi bindataFileInfo) Name() string { - return fi.name -} - -// Size return file size -func (fi bindataFileInfo) Size() int64 { - return fi.size -} - -// Mode return file mode -func (fi bindataFileInfo) Mode() os.FileMode { - return fi.mode -} - -// ModTime return file modify time -func (fi bindataFileInfo) ModTime() time.Time { - return fi.modTime -} - -// IsDir return file whether a directory -func (fi bindataFileInfo) IsDir() bool { - return fi.mode&os.ModeDir != 0 -} - -// Sys return file is sys mode -func (fi bindataFileInfo) Sys() interface{} { - return nil -} - -var _assetsI18nErrorEnJson = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x94\x96\x5d\x8e\xe3\x36\x0c\xc7\xdf\xe7\x14\xc4\x00\xc1\xbe\x6c\x75\x80\x79\xdc\x6e\x0b\x2c\x50\xa0\xe8\xc7\x1e\x40\x96\xe9\x44\xb0\x2c\x7a\x28\x29\x89\x51\xf4\xee\x0b\x49\x96\x62\x27\x76\x30\xfb\x32\x18\xfd\x49\xfe\x44\x91\x8c\xe4\xff\x5e\x00\x5e\x91\x59\x34\xb2\x15\x8c\xef\x01\x9d\x7f\x7d\x83\xd7\x2f\xb2\x85\x79\xf9\x06\x87\xf3\xeb\xe7\xe2\xa7\x8d\xc1\xa3\x34\x42\xf2\x31\x0c\x68\x93\xf3\xb7\xac\x41\xd1\xd6\x11\xc1\xe2\x75\x44\xe5\xb1\x8d\xbe\xdf\xeb\x0a\x90\x99\x78\xf6\x2d\xce\x2d\x5d\xac\x21\xd9\x8a\x4e\x1b\x8c\xfe\xbf\x4a\x6b\xc9\x43\xd1\x21\xea\xd0\x31\x0d\x10\xd8\xdc\xa5\x66\xcf\xd2\xe8\x56\xa8\x13\xaa\xde\x85\x21\xa5\x96\x35\xa8\x5a\xf5\xbe\xb0\xf6\x78\xbf\x4d\x12\xc1\x53\xde\xe6\x70\xfe\x0c\x8c\xd2\x91\x5d\x6f\xd4\x69\xfb\x90\x60\xd4\x72\x54\xb0\x2d\xf2\x6e\x6c\x4a\x24\x05\x0b\xbc\x6a\xe7\xdd\x82\x91\x6c\x70\x39\xa1\x3f\x21\x97\x14\x20\xbb\xdd\xd1\x2a\x8e\x51\x7a\x14\xad\x66\x54\x9e\x78\x5a\xd2\x92\x09\xaa\x69\x37\x25\xc6\x81\xce\xdb\x8c\x6c\xfa\x00\x43\x9d\xe8\x62\xb7\xd3\x88\x16\xe8\x88\x9f\x51\xee\x8e\xe3\xa6\xc1\x68\xdb\x3f\x1e\xc6\x4d\x43\x43\x46\x2b\x88\xe6\x3c\x06\x87\x73\xec\xd7\x8d\xb8\x06\x7a\xe9\x7a\x61\xc9\x8b\x8e\x82\x4d\x03\xf8\xaf\x74\x3d\xb8\x11\x95\xee\x34\xb6\xd0\x4c\xe0\xa9\x47\x0b\xa9\x89\xd1\x69\x0d\x78\x0f\xc8\x93\x18\xa5\xea\xe5\x31\xf5\xfb\xaf\x28\x80\xa3\xce\x5f\x24\x23\xcc\x16\xe8\xa4\x36\xd8\x6e\x17\x47\x5b\xe7\xa5\x31\x4b\xca\xb7\x2c\xfd\x1c\x27\xd8\x0d\xd2\xf7\x22\xfe\x1c\x0b\xaf\x9e\xa5\xf2\x4b\xd2\x6f\x59\xfa\x20\x67\x3d\xd0\x23\x93\x42\xe7\xf6\x66\x7a\x36\xe7\x59\x46\xab\x10\xa8\xdb\x9d\xa5\x23\xfa\xca\xd3\xb6\xa3\x05\xed\x88\xbe\xb2\xa2\xe9\x19\xc6\x79\x1a\x0b\x67\x81\x88\x72\x65\xec\x0e\xa2\x9b\x9c\xc7\x41\xb4\xda\xf5\x29\x9f\xe0\xe6\x22\x2d\x12\x89\x46\x48\x86\xcd\x34\x0a\x8a\x9a\xdb\x00\x30\xfe\x92\x0a\xb2\x1c\x81\x3f\xbf\x40\xd5\xe7\x52\xaf\x0f\x12\x01\x24\x1a\xb4\xea\x14\xe3\xbe\x12\x68\x82\xb4\xdc\x71\x77\xc8\x67\x8c\x05\x90\x9c\xee\xe7\x7f\xe2\x3f\x50\xf4\xcd\xa0\xdc\xc5\x1a\x2a\x55\x2c\x8f\x6e\xe6\x2b\x2e\xa5\x56\xe3\x6f\xc6\xbd\x74\x1b\x22\xef\x3c\xcb\x31\x3d\x25\x65\x11\x4f\xba\xb9\xb7\x41\x69\x63\x58\x2b\xbd\x4c\x57\x63\xee\x57\x94\x63\x4c\x94\xd3\x6d\xe8\x9e\x87\xc7\x0a\x3f\x46\xc7\x12\xef\x07\x73\xb0\x22\x8c\x47\x96\x2d\x0a\xa7\x58\x8f\xa9\x60\xbf\x27\xcf\x78\xaf\x70\xb0\x30\xdb\x21\xdb\xdf\x3e\x1d\xce\x9f\x16\xed\x6d\xa4\xea\xc3\x98\x8b\x5d\x16\xf2\x38\xbf\x8c\xb9\xf2\x59\x85\xa4\x6e\x66\x51\x19\x34\x6e\x20\x68\xfc\x30\x21\xb7\x71\x89\x10\x64\x8d\xb6\x8b\x36\xae\x50\xd9\xf8\x94\x98\xaa\xbb\x22\xde\xd5\x78\x9d\xdb\x6e\xa5\xb7\x78\xb1\xb3\x0f\xa0\xdc\xee\x25\xa1\x7e\x1c\x84\xa1\xd6\xa7\x3c\xbf\x5f\xc3\x50\xcb\x53\x5e\xcc\xcd\x60\x6a\x46\xa6\xeb\xf4\xf8\xab\x48\xf2\xce\x24\xe7\x10\xc6\xd1\x48\x85\x65\xdd\x2e\x26\xe5\xef\x6c\x82\x9b\xe9\x34\xcf\xc9\x53\x62\x9e\x96\x0d\xde\x2a\xa9\x44\x7b\x86\xc9\xc5\xbc\xeb\x47\x3d\xd1\x6e\x2b\x6a\x38\x8d\x93\x40\xaf\x84\x22\xeb\xd1\xce\xf7\x36\x8d\x13\xa0\x57\x50\xc4\xed\x72\xe6\x51\x48\x7f\x5b\x21\x0d\xa3\x6c\xa7\xf8\x6b\xb2\xda\x1e\x23\x26\x5b\x60\xb6\xc0\x6c\x81\x8b\xf6\x27\x18\x75\xfe\x54\xba\x65\xb4\xa2\xc5\xe7\xfa\x91\xa4\x5d\x7a\xa2\x8b\x61\x3b\x14\xaf\xda\x63\x2b\xde\x83\x56\xbd\x49\x5f\x21\xa9\xd2\x30\x33\xca\x43\x36\x2f\xb3\x37\x14\xef\xdb\xe1\x8c\x74\x5e\x0c\xe8\xe2\xdd\x2e\x9c\x0f\x4d\x24\xfd\x21\x9d\x87\x59\x84\x24\xbe\xfc\xff\xf2\x23\x00\x00\xff\xff\x72\x8b\x79\xab\x40\x0b\x00\x00") - -func assetsI18nErrorEnJsonBytes() ([]byte, error) { - return bindataRead( - _assetsI18nErrorEnJson, - "assets/i18n/error/en.json", - ) -} - -func assetsI18nErrorEnJson() (*asset, error) { - bytes, err := assetsI18nErrorEnJsonBytes() - if err != nil { - return nil, err - } - - info := bindataFileInfo{name: "assets/i18n/error/en.json", size: 2880, mode: os.FileMode(420), modTime: time.Unix(1630584784, 0)} - a := &asset{bytes: bytes, info: info} - return a, nil -} - -// Asset loads and returns the asset for the given name. -// It returns an error if the asset could not be found or -// could not be loaded. -func Asset(name string) ([]byte, error) { - cannonicalName := strings.Replace(name, "\\", "/", -1) - if f, ok := _bindata[cannonicalName]; ok { - a, err := f() - if err != nil { - return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err) - } - return a.bytes, nil - } - return nil, fmt.Errorf("Asset %s not found", name) -} - -// MustAsset is like Asset but panics when Asset would return an error. -// It simplifies safe initialization of global variables. -func MustAsset(name string) []byte { - a, err := Asset(name) - if err != nil { - panic("asset: Asset(" + name + "): " + err.Error()) - } - - return a -} - -// AssetInfo loads and returns the asset info for the given name. -// It returns an error if the asset could not be found or -// could not be loaded. -func AssetInfo(name string) (os.FileInfo, error) { - cannonicalName := strings.Replace(name, "\\", "/", -1) - if f, ok := _bindata[cannonicalName]; ok { - a, err := f() - if err != nil { - return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err) - } - return a.info, nil - } - return nil, fmt.Errorf("AssetInfo %s not found", name) -} - -// AssetNames returns the names of the assets. -func AssetNames() []string { - names := make([]string, 0, len(_bindata)) - for name := range _bindata { - names = append(names, name) - } - return names -} - -// _bindata is a table, holding each asset generator, mapped to its name. -var _bindata = map[string]func() (*asset, error){ - "assets/i18n/error/en.json": assetsI18nErrorEnJson, -} - -// AssetDir returns the file names below a certain -// directory embedded in the file by go-bindata. -// For example if you run go-bindata on data/... and data contains the -// following hierarchy: -// data/ -// foo.txt -// img/ -// a.png -// b.png -// then AssetDir("data") would return []string{"foo.txt", "img"} -// AssetDir("data/img") would return []string{"a.png", "b.png"} -// AssetDir("foo.txt") and AssetDir("notexist") would return an error -// AssetDir("") will return []string{"data"}. -func AssetDir(name string) ([]string, error) { - node := _bintree - if len(name) != 0 { - cannonicalName := strings.Replace(name, "\\", "/", -1) - pathList := strings.Split(cannonicalName, "/") - for _, p := range pathList { - node = node.Children[p] - if node == nil { - return nil, fmt.Errorf("Asset %s not found", name) - } - } - } - if node.Func != nil { - return nil, fmt.Errorf("Asset %s not found", name) - } - rv := make([]string, 0, len(node.Children)) - for childName := range node.Children { - rv = append(rv, childName) - } - return rv, nil -} - -type bintree struct { - Func func() (*asset, error) - Children map[string]*bintree -} - -var _bintree = &bintree{nil, map[string]*bintree{ - "assets": {nil, map[string]*bintree{ - "i18n": {nil, map[string]*bintree{ - "error": {nil, map[string]*bintree{ - "en.json": {assetsI18nErrorEnJson, map[string]*bintree{}}, - }}, - }}, - }}, -}} - -// RestoreAsset restores an asset under the given directory -func RestoreAsset(dir, name string) error { - data, err := Asset(name) - if err != nil { - return err - } - info, err := AssetInfo(name) - if err != nil { - return err - } - err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755)) - if err != nil { - return err - } - err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode()) - if err != nil { - return err - } - err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) - if err != nil { - return err - } - return nil -} - -// RestoreAssets restores an asset under the given directory recursively -func RestoreAssets(dir, name string) error { - children, err := AssetDir(name) - // File - if err != nil { - return RestoreAsset(dir, name) - } - // Dir - for _, child := range children { - err = RestoreAssets(dir, filepath.Join(name, child)) - if err != nil { - return err - } - } - return nil -} - -func _filePath(dir, name string) string { - cannonicalName := strings.Replace(name, "\\", "/", -1) - return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...) -} diff --git a/cmd/agentctl/main.go b/cmd/agentctl/main.go new file mode 100644 index 0000000000000000000000000000000000000000..f719120600042e2e61bea1b3c54cda6b2993f049 --- /dev/null +++ b/cmd/agentctl/main.go @@ -0,0 +1,533 @@ +package main + +import ( + "context" + "errors" + "fmt" + "os" + "path/filepath" + "runtime" + "sync" + + json "github.com/json-iterator/go" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "gopkg.in/yaml.v3" + + "github.com/oceanbase/obagent/agentd/api" + "github.com/oceanbase/obagent/config" + "github.com/oceanbase/obagent/config/agentctl" + "github.com/oceanbase/obagent/config/sdk" + "github.com/oceanbase/obagent/executor/agent" + "github.com/oceanbase/obagent/lib/mask" + "github.com/oceanbase/obagent/lib/path" + "github.com/oceanbase/obagent/lib/trace" + agentlog "github.com/oceanbase/obagent/log" +) + +const ( + commandNameInfo = "info" + commandNameGitInfo = "git-info" +) + +var ( + agentCtlConfig *agentctl.AgentctlConfig + // root command + agentCtlCommand = &cobra.Command{ + Use: "ob_agentctl", + Short: "ob_agentctl is a CLI for agent management.", + Long: `ob_agentctl is a command line tool for the agent. It provides operation, maintenance, and management functions for the agent.`, + PersistentPreRun: func(cmd *cobra.Command, args []string) { + if cmd.Use == commandNameInfo || cmd.Use == commandNameGitInfo { + return + } + ctx := trace.ContextWithRandomTraceId() + ctlConfig := cmd.Flag("config").Value.String() + var err error + agentCtlConfig, err = LoadConfig(ctlConfig) + if err != nil { + setResult(err) + os.Exit(1) + } + + // Replace the home.path in the configuration file with the actual value + pathMap := map[string]string{"obagent.home.path": path.AgentDir()} + _, err = config.ReplaceConfValues(agentCtlConfig, pathMap) + if err != nil { + setResult(err) + os.Exit(1) + } + + InitLog(agentCtlConfig.Log) + + err = sdk.InitSDK(ctx, agentCtlConfig.SDKConfig) + if err != nil { + setResult(err) + os.Exit(1) + } + + err = sdk.RegisterMgragentCallbacks(ctx) + if err != nil { + setResult(err) + os.Exit(1) + } + + err = sdk.RegisterMonagentCallbacks(ctx) + if err != nil { + setResult(err) + os.Exit(1) + } + // Initialize sock proxy + if err := config.InitModuleConfig(ctx, config.ManagerAgentProxyConfigModule); err != nil { + setResult(err) + os.Exit(1) + } + }, + } + setResultOnce sync.Once +) + +// Defines the configuration of subcommands and command line parameters +func defineConfigCommands() { + // config command + configCommand := &cobra.Command{ + Use: "config", + Short: "config management", + Long: "update key-value configs, save configs to config properties files, and notify configs to the module using these configs", + Example: "config --update key1=value1,key2=value2", + Run: func(cmd *cobra.Command, args []string) { + ctx := trace.ContextWithRandomTraceId() + // config meta + if err := config.InitModuleTypeConfig(ctx, config.ConfigMetaModuleType); err != nil { + log.WithContext(ctx).Fatal(err) + } + + updateConfigs, err := cmd.Flags().GetStringSlice("update") + if err != nil { + setResult(err) + log.WithContext(ctx).WithField("args", os.Args).Fatalf("agentctl config --update err:%s", err) + } + notifyModules, err := cmd.Flags().GetStringSlice("notify") + if err != nil { + setResult(err) + log.WithContext(ctx).WithField("args", os.Args).Fatalf("agentctl config --notify err:%s", err) + } + validateConfigs, err := cmd.Flags().GetStringSlice("validate") + if err != nil { + setResult(err) + log.WithContext(ctx).WithField("args", os.Args).Fatalf("agentctl config --validate err:%s", err) + } + + log.WithContext(ctx).Infof("agentctl config updates:%+v, notify modules:%+v, validate configs:%+v", mask.MaskSlice(updateConfigs), notifyModules, mask.MaskSlice(validateConfigs)) + + err = runUpdateConfigs(ctx, updateConfigs) + if err != nil { + log.WithContext(ctx).WithField("updateConfigs", mask.MaskSlice(updateConfigs)).Errorf("agentctl config update config err:%s", err) + setResult(err) + } + + err = runNotifyModules(ctx, notifyModules) + if err != nil { + log.WithContext(ctx).WithField("notifyModules", notifyModules).Errorf("agentctl config notify modules err:%s", err) + setResult(err) + } + + err = runValidateConfigs(ctx, validateConfigs) + if err != nil { + log.WithContext(ctx).WithField("runValidateConfigs", mask.MaskSlice(validateConfigs)).Errorf("agentctl config validate err:%s", err) + } + setResult(err) + }, + } + // update configuration: The key-value pair is used for update. + // You need to enter the complete configuration. + // The UPDATE verifies the configuration, saves the configuration, + // and notifies services to use the configuration. + configCommand.PersistentFlags().StringSliceP("update", "u", nil, "key-value pairs, e.g., key1=value1,key2=value2") + // Notification service configuration takes effect. + configCommand.PersistentFlags().StringSliceP("notify", "n", nil, "notify modules, e.g., mgragent.config,monagent") + // Verify that the configurations are consistent + configCommand.PersistentFlags().StringSliceP("validate", "v", nil, "validate config key-value pairs, e.g., key1=value1,key2=value2") + + configNotifyCommand := &cobra.Command{ + Use: "notify", + Short: "notify config change", + Long: "notify modules configs changes. omitting modules means notify all modules", + Example: "notify module1 module2", + Run: func(cmd *cobra.Command, args []string) { + ctx := trace.ContextWithRandomTraceId() + var err error + if len(args) > 0 { + err = config.NotifyModules(ctx, args) + } else { + err = config.NotifyAllModules(ctx) + } + if err != nil { + log.WithContext(ctx).WithField("args", os.Args).Fatalf("agentctl config notify err:%s", err) + } + setResult(err) + }, + } + configChangeCommand := &cobra.Command{ + Use: "change", + Short: "change config properties", + Long: "change config properties", + Example: "change k1=v1 k2=v2", + Run: func(cmd *cobra.Command, args []string) { + ctx := trace.ContextWithRandomTraceId() + err := runUpdateConfigs(ctx, args) + if err != nil { + log.WithContext(ctx).WithField("args", os.Args).Fatalf("agentctl config update err:%s", err) + } + setResult(err) + }, + } + validateChangeCommand := &cobra.Command{ + Use: "validate", + Short: "validate config properties", + Long: "validate config properties", + Example: "validate k1=v1 k2=v2", + Run: func(cmd *cobra.Command, args []string) { + ctx := trace.ContextWithRandomTraceId() + err := runValidateConfigs(ctx, args) + if err != nil { + log.WithContext(ctx).WithField("args", os.Args).Fatalf("agentctl config validate err:%s", err) + } + setResult(err) + }, + } + configCommand.AddCommand(configChangeCommand, configNotifyCommand, validateChangeCommand) + + agentCtlCommand.AddCommand(configCommand) +} + +func adminConf() agent.AdminConf { + return agent.AdminConf{ + RunDir: agentCtlConfig.RunDir, + LogDir: agentCtlConfig.LogDir, + ConfDir: agentCtlConfig.ConfDir, + BackupDir: agentCtlConfig.BackupDir, + TempDir: agentCtlConfig.TempDir, + PkgStoreDir: agentCtlConfig.PkgStoreDir, + TaskStoreDir: agentCtlConfig.TaskStoreDir, + AgentPkgName: agentCtlConfig.AgentPkgName, + PkgExt: agentCtlConfig.PkgExt, + StartWaitSeconds: 10, + StopWaitSeconds: 10, + AgentdPath: path.AgentdPath(), + } +} + +func defineInfoCommands() { + agentCtlCommand.AddCommand(&cobra.Command{ + Use: commandNameInfo, + Run: func(cmd *cobra.Command, args []string) { + onSuccess(config.GetAgentInfo()) + }, + }) + agentCtlCommand.AddCommand(&cobra.Command{ + Use: commandNameGitInfo, + Run: func(cmd *cobra.Command, args []string) { + onSuccess(config.GetGitInfo()) + }, + }) +} + +func defineOperationCommands() { + agentCtlCommand.AddCommand(&cobra.Command{ + Use: "status", + Run: func(cmd *cobra.Command, args []string) { + if len(args) != 0 { + onError(errors.New("too many arguments")) + return + } + admin := agent.NewAdmin(adminConf()) + status, err := admin.AgentStatus() + if err != nil { + onError(err) + } else { + onSuccess(status) + } + }, + }) + agentCtlCommand.AddCommand(&cobra.Command{ + Use: "start", + Run: func(cmd *cobra.Command, args []string) { + if len(args) != 0 { + onError(errors.New("too many arguments")) + return + } + admin := agent.NewAdmin(adminConf()) + err := admin.StartAgent() + if err != nil { + onError(err) + } else { + onSuccess("ok") + } + }, + }) + stopCommand := &cobra.Command{ + Use: "stop", + Run: func(cmd *cobra.Command, args []string) { + if len(args) != 0 { + onError(errors.New("too many arguments")) + return + } + taskToken := cmd.Flag("task-token").Value.String() + admin := agent.NewAdmin(adminConf()) + err := admin.StopAgent(agent.TaskToken{TaskToken: taskToken}) + if err != nil { + onError(err) + } else { + onSuccess("ok") + } + }, + } + stopCommand.PersistentFlags().String("task-token", "", "task token to store result") + agentCtlCommand.AddCommand(stopCommand) + + serviceCommand := &cobra.Command{ + Use: "service", + } + serviceStartCommand := &cobra.Command{ + Use: "start", + Run: func(cmd *cobra.Command, args []string) { + if len(args) != 1 { + onError(errors.New("missing service name")) + return + } + name := args[0] + taskToken := cmd.Flag("task-token").Value.String() + admin := agent.NewAdmin(adminConf()) + err := admin.StartService(agent.StartStopServiceParam{ + TaskToken: agent.TaskToken{ + TaskToken: taskToken, + }, + StartStopAgentParam: api.StartStopAgentParam{ + Service: name, + }, + }) + if err != nil { + onError(err) + } else { + onSuccess("ok") + } + }, + } + serviceStartCommand.PersistentFlags().String("task-token", "", "task token to store result") + + serviceStopCommand := &cobra.Command{ + Use: "stop", + Run: func(cmd *cobra.Command, args []string) { + if len(args) != 1 { + onError(errors.New("missing service name")) + return + } + name := args[0] + taskToken := cmd.Flag("task-token").Value.String() + admin := agent.NewAdmin(adminConf()) + err := admin.StopService(agent.StartStopServiceParam{ + TaskToken: agent.TaskToken{ + TaskToken: taskToken, + }, + StartStopAgentParam: api.StartStopAgentParam{ + Service: name, + }, + }) + if err != nil { + onError(err) + } else { + onSuccess("ok") + } + }, + } + serviceStopCommand.PersistentFlags().String("task-token", "", "task token to store result") + + serviceCommand.AddCommand(serviceStartCommand) + serviceCommand.AddCommand(serviceStopCommand) + agentCtlCommand.AddCommand(serviceCommand) + + restartCommand := &cobra.Command{ + Use: "restart", + Run: func(cmd *cobra.Command, args []string) { + if len(args) != 0 { + onError(errors.New("too many arguments")) + return + } + taskToken := cmd.Flag("task-token").Value.String() + admin := agent.NewAdmin(adminConf()) + err := admin.RestartAgent(agent.TaskToken{TaskToken: taskToken}) + if err != nil { + onError(err) + } else { + onSuccess("ok") + } + }, + } + restartCommand.PersistentFlags().String("task-token", "", "task token to store result") + agentCtlCommand.AddCommand(restartCommand) + + reinstallCommand := &cobra.Command{ + Use: "reinstall", + Run: func(cmd *cobra.Command, args []string) { + if len(args) != 0 { + onError(errors.New("too many arguments")) + return + } + taskToken := cmd.Flag("task-token").Value.String() + admin := agent.NewAdmin(adminConf()) + source := cmd.Flag("source").Value.String() + checksum := cmd.Flag("checksum").Value.String() + version := cmd.Flag("version").Value.String() + // todo validate args + err := admin.ReinstallAgent(agent.ReinstallParam{ + TaskToken: agent.TaskToken{TaskToken: taskToken}, + DownloadParam: agent.DownloadParam{ + Source: source, + Checksum: checksum, + Version: version, + }, + }) + if err != nil { + onError(err) + } else { + onSuccess("ok") + } + }, + } + reinstallCommand.PersistentFlags().String("source", "", "package source") + reinstallCommand.PersistentFlags().String("checksum", "", "package checksum") + reinstallCommand.PersistentFlags().String("version", "", "package version") + reinstallCommand.PersistentFlags().String("task-token", "", "task token to store result") + agentCtlCommand.AddCommand(reinstallCommand) + +} + +func defineVersionCommands() { + agentCtlCommand.AddCommand(&cobra.Command{ + Use: "version", + Run: func(cmd *cobra.Command, args []string) { + onSuccess(config.AgentVersion) + }, + }) +} + +func main() { + runtime.GOMAXPROCS(1) + confPath := filepath.Join(path.ConfDir(), "agentctl.yaml") + agentCtlCommand.PersistentFlags().StringP("config", "c", confPath, "config file") + defineInfoCommands() + defineOperationCommands() + defineConfigCommands() + defineVersionCommands() + + if err := agentCtlCommand.Execute(); err != nil { + log.WithField("args", os.Args).Fatal(err) + setResult(err) + } +} + +func LoadConfig(configFile string) (*agentctl.AgentctlConfig, error) { + f, err := os.Open(configFile) + if err != nil { + return nil, err + } + ret := &agentctl.AgentctlConfig{ + ConfDir: path.ConfDir(), + RunDir: path.RunDir(), + LogDir: path.LogDir(), + BackupDir: path.BackupDir(), + TempDir: path.TempDir(), + TaskStoreDir: path.TaskStoreDir(), + AgentPkgName: filepath.Join(path.AgentDir(), "obagent"), + PkgExt: "rpm", + PkgStoreDir: path.PkgStoreDir(), + } + err = yaml.NewDecoder(f).Decode(ret) + return ret, err +} + +// init log +func InitLog(conf config.LogConfig) { + agentlog.InitLogger(agentlog.LoggerConfig{ + Level: conf.Level, + Filename: conf.Filename, + MaxSize: conf.MaxSize, + MaxAge: conf.MaxAge, + MaxBackups: conf.MaxBackups, + LocalTime: conf.LocalTime, + Compress: conf.Compress, + }) +} + +// set the result to stdout +// logs will be written to log file +func setResult(err error) { + setResultOnce.Do(func() { + if err != nil { + onError(err) + return + } + onSuccess("success") + }) +} + +// the returned err will be written to stderr +func onError(err error) { + resp := &agent.AgentctlResponse{ + Successful: false, + Error: err.Error(), + } + data, jsonerr := json.Marshal(resp) + if jsonerr != nil { + log.WithField("error", err).Errorf("json marshal err:%s", jsonerr) + fmt.Fprintf(os.Stderr, "%s", err.Error()) + os.Exit(-1) + return + } + log.WithField("response", string(data)).Info("agentctl error") + fmt.Fprintf(os.Stderr, "%s", data) + os.Exit(-1) +} + +// success message will be written to stdout +func onSuccess(message interface{}) { + resp := &agent.AgentctlResponse{ + Successful: true, + Message: message, + } + data, jsonerr := json.Marshal(resp) + if jsonerr != nil { + log.WithField("message", message).Errorf("json marshal err:%s", jsonerr) + return + } + log.WithField("response", string(data)).Info("agentctl success") + fmt.Fprintf(os.Stdout, "%s", data) +} + +func runAgentctl(configfile string) error { + return nil +} + +// run validate commands: validate config is identical +func runNotifyModules(ctx context.Context, modules []string) error { + if len(modules) <= 0 { + return nil + } + return config.NotifyModules(ctx, modules) +} + +// run config command: update module config +func runUpdateConfigs(ctx context.Context, pairs []string) error { + if len(pairs) <= 0 { + return nil + } + return config.UpdateConfigPairs(ctx, pairs) +} + +func runValidateConfigs(ctx context.Context, pairs []string) error { + if len(pairs) <= 0 { + return nil + } + return config.ValidateConfigPairs(ctx, pairs) +} diff --git a/cmd/agentd/main.go b/cmd/agentd/main.go new file mode 100644 index 0000000000000000000000000000000000000000..259feb42fcb81c43912e9e86c86feab5e325fe47 --- /dev/null +++ b/cmd/agentd/main.go @@ -0,0 +1,94 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "runtime" + + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "gopkg.in/yaml.v3" + + "github.com/oceanbase/obagent/agentd" + config2 "github.com/oceanbase/obagent/config" + "github.com/oceanbase/obagent/lib/path" + agentLog "github.com/oceanbase/obagent/log" +) + +// command-line arguments +type arguments struct { + ConfigFile string +} + +func main() { + runtime.GOMAXPROCS(1) + confPath := filepath.Join(path.ConfDir(), "/agentd.yaml") + rootCmd := &cobra.Command{ + Use: "ob_agentd", + Short: "OB agent supervisor", + Long: "OB agentd is the daemon of ob-Agent. Responsible for starting and stopping," + + " guarding other agent processes, and status query.", + } + rootCmd.PersistentFlags().StringVarP(&confPath, "config", "c", confPath, "config file") + rootCmd.Run = func(cmd *cobra.Command, positionalArgs []string) { + run(confPath) + } + err := rootCmd.Execute() + if err != nil { + log.Error("start agentd failed: ", err) + os.Exit(-1) + } +} + +func run(confPath string) { + config := loadConfig(confPath) + agentLog.InitLogger(agentLog.LoggerConfig{ + Level: config.LogLevel, + Filename: filepath.Join(path.LogDir(), "agentd.log"), //fmt.Sprintf("agentd.%d.log", os.Getpid())), + MaxSize: 100 * 1024 * 1024, + MaxBackups: 10, + }) + log.Infof("starting agentd with config %s", confPath) + + // Replace the home.path in the configuration file with the actual value + pathMap := map[string]string{"obagent.home.path": path.AgentDir()} + _, err := config2.ReplaceConfValues(&config, pathMap) + if err != nil { + log.Errorf("start agentd with config file '%s' failed: %v", confPath, err) + os.Exit(-1) + return + } + + watchdog := agentd.NewAgentd(config) + err = watchdog.Start() + if err != nil { + log.Errorf("start agentd with config file '%s' failed: %v", confPath, err) + os.Exit(-1) + return + } + watchdog.ListenSignal() +} + +func loadConfig(confPath string) agentd.Config { + config := agentd.Config{ + LogLevel: "info", + LogDir: "/tmp", + CleanupDangling: true, + } + + confFile, err := os.Open(confPath) + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "open config file %s failed: %v\n", confPath, err) + os.Exit(1) + return config + } + defer confFile.Close() + err = yaml.NewDecoder(confFile).Decode(&config) + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "read config file %s failed: %v\n", confPath, err) + os.Exit(1) + return config + } + return config +} diff --git a/cmd/mgragent/main.go b/cmd/mgragent/main.go new file mode 100644 index 0000000000000000000000000000000000000000..e212544d16440c24795806eabea9c9f1b70c12ba --- /dev/null +++ b/cmd/mgragent/main.go @@ -0,0 +1,158 @@ +package main + +import ( + "context" + "os" + "os/signal" + "path/filepath" + "runtime" + "syscall" + + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + + "github.com/oceanbase/obagent/api/common" + "github.com/oceanbase/obagent/api/web" + "github.com/oceanbase/obagent/config" + "github.com/oceanbase/obagent/config/mgragent" + configsdk "github.com/oceanbase/obagent/config/sdk" + "github.com/oceanbase/obagent/lib/path" + "github.com/oceanbase/obagent/lib/shellf" + "github.com/oceanbase/obagent/lib/trace" +) + +const ( + ManagerPortKey = "ocp.agent.manager.http.port" + OcpAgentHomePath = "obagent.home.path" +) + +// command-line arguments +type arguments struct { + ConfigFile string +} + +func run(args arguments) { + ctx := trace.ContextWithRandomTraceId() + + // Set the umask of the agent process to 0022 so that the operations + // of the agent are not affected by the umask set by the system + syscall.Umask(0022) + + conf := mgragent.NewManagerAgentConfig(args.ConfigFile) + // Replace the home.path in the configuration file with the actual value + pathMap := map[string]string{OcpAgentHomePath: path.AgentDir()} + _, err := config.ReplaceConfValues(conf, pathMap) + if err != nil { + log.WithError(err).Fatalf("parse mgragent config file path fialed %s", err) + os.Exit(1) + } + + // Initialize configuration + initModuleConfigs(ctx, conf.SDKConfig) + + // Obtain the service startup port based on the port value in the config file + managerPort := config.GetConfigPropertiesByKey(ManagerPortKey) + portMap := map[string]string{ManagerPortKey: managerPort} + _, err = config.ReplaceConfValues(conf, portMap) + if err != nil { + log.WithError(err).Fatalf("parse mgragent config file port fialed %s", err) + os.Exit(1) + } + + shellf.InitShelf(conf.ShellfConfig.TemplatePath) + + log.WithContext(ctx).Infof("starting ocp manager agent, version %v", config.AgentVersion) + log.WithContext(ctx).Infof("agent running in %v mode", config.Mode) + + server := web.NewServer(config.Mode, conf.Server) + go server.Run() + + ch := make(chan os.Signal, 1) + signal.Notify(ch, syscall.SIGTERM, syscall.SIGINT) + select { + case sig := <-ch: + log.WithContext(ctx).Infof("signal '%s' received. exiting...", sig.String()) + server.Stop() + } +} + +func parseArgsAndRun() error { + var args arguments + rootCmd := &cobra.Command{ + Use: "ob_mgragent", + Short: "OB manager agent", + Long: "OB manager agent is an operation and maintenance process of OB-Agent," + + " providing basic host operation and maintenance commands, OB operation and maintenance commands", + } + confPath := filepath.Join(path.ConfDir(), "mgragent.yaml") + rootCmd.PersistentFlags().StringVarP(&args.ConfigFile, "config", "c", confPath, "config file") + rootCmd.Run = func(cmd *cobra.Command, positionalArgs []string) { + // The startup phase is set to debug + log.SetLevel(log.DebugLevel) + run(args) + } + return rootCmd.Execute() +} + +func main() { + runtime.GOMAXPROCS(1) + err := parseArgsAndRun() + if err != nil { + log.Fatal(err) + os.Exit(-1) + } +} + +func initModuleConfigs(ctx context.Context, sdkconf config.SDKConfig) { + // Initialize the SDK + mgragent.GlobalConfigManager = mgragent.NewManager(mgragent.ManagerConfig{ + ModuleConfigDir: sdkconf.ModuleConfigDir, + ConfigPropertiesDir: sdkconf.ConfigPropertiesDir, + }) + err := configsdk.InitSDK(ctx, sdkconf) + if err != nil { + log.WithContext(ctx).Fatal(err) + } + err = configsdk.RegisterMgragentCallbacks(ctx) + if err != nil { + log.WithContext(ctx).Fatal(err) + } + err = configsdk.RegisterMonagentCallbacks(ctx) + if err != nil { + log.WithContext(ctx).Fatal(err) + } + + // Initialize the log + if err := config.InitModuleConfig(ctx, config.ManagerLogConfigModule); err != nil { + log.WithContext(ctx).Fatal(err) + } + + // Initialize sock proxy + if err := config.InitModuleConfig(ctx, config.ManagerAgentProxyConfigModule); err != nil { + log.WithContext(ctx).Fatal(err) + } + + // Self-monitoring configuration + if err := config.InitModuleTypeConfig(ctx, config.StatConfigModuleType); err != nil { + log.WithContext(ctx).Fatalf("init module type %s, err:%+v", config.StatConfigModuleType, err) + } + + // basic authentication gets the configuration and initializes + common.InitBasicAuthConf(ctx) + + //Initialize the ob_logcleaner + if err := config.InitModuleConfig(ctx, config.OBLogcleanerModule); err != nil { + log.WithContext(ctx).Fatal(err) + } + + // Initialize the logQuerier + if err := config.InitModuleConfig(ctx, config.ManagerLogQuerierModule); err != nil { + log.WithContext(ctx).Fatal(err) + } + + // config meta + err = config.InitModuleConfig(ctx, config.ManagerAgentConfigMetaModule) + if err != nil { + log.WithContext(ctx).Fatal(err) + } +} diff --git a/cmd/monagent/main.go b/cmd/monagent/main.go index 0f79d479fdb292ca07378387687de7b03a9ec897..ada604ac715f2199951a12e241cf4ecf35da6846 100644 --- a/cmd/monagent/main.go +++ b/cmd/monagent/main.go @@ -1,46 +1,46 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - package main import ( "context" "os" + "os/signal" + "path/filepath" + "runtime" + "syscall" "time" - "runtime/debug" - "github.com/pkg/errors" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" + "github.com/oceanbase/obagent/api/web" "github.com/oceanbase/obagent/config" + "github.com/oceanbase/obagent/config/monagent" "github.com/oceanbase/obagent/config/sdk" - "github.com/oceanbase/obagent/engine" - agentlog "github.com/oceanbase/obagent/log" - _ "github.com/oceanbase/obagent/plugins/exporters" - _ "github.com/oceanbase/obagent/plugins/inputs" - _ "github.com/oceanbase/obagent/plugins/outputs" - _ "github.com/oceanbase/obagent/plugins/processors" + "github.com/oceanbase/obagent/lib/path" + "github.com/oceanbase/obagent/lib/trace" + "github.com/oceanbase/obagent/monitor/engine" + _ "github.com/oceanbase/obagent/monitor/plugins/exporters" + _ "github.com/oceanbase/obagent/monitor/plugins/inputs" + _ "github.com/oceanbase/obagent/monitor/plugins/outputs" + _ "github.com/oceanbase/obagent/monitor/plugins/processors" +) + +const ( + MonitorPortKey = "ocp.agent.monitor.http.port" + OcpAgentHomePath = "obagent.home.path" ) var ( // root command monagentCommand = &cobra.Command{ - Use: "monagent", - Short: "monagent is a monitoring agent.", - Long: `monagent is a monitoring agent for gathering, processing and pushing monitor metrics.`, + Use: "ob_monagent", + Short: "ob_monagent is a monitoring agent.", + Long: `ob_monagent is a monitoring agent for gathering, processing and pushing monitor metrics.`, Run: func(cmd *cobra.Command, args []string) { + // The startup phase is set to debug + log.SetLevel(log.DebugLevel) err := runMonitorAgent() if err != nil { log.WithField("args", args).Errorf("monagent execute err:%s", err) @@ -50,10 +50,9 @@ var ( ) func init() { - debug.SetGCPercent(config.GCPercent) - + confPath := filepath.Join(path.ConfDir(), "monagent.yaml") // monagent server config file - monagentCommand.PersistentFlags().StringP("config", "c", "conf/monagent.yaml", "config file") + monagentCommand.PersistentFlags().StringP("config", "c", confPath, "config file") // plugins config use dir, all yaml files in the dir will be used as plugin config file monagentCommand.PersistentFlags().StringP("pipelines_config_dir", "d", "conf/monagent/pipelines", "monitor pipelines config file dir") @@ -62,29 +61,47 @@ func init() { } func main() { + runtime.GOMAXPROCS(1) if err := monagentCommand.Execute(); err != nil { log.WithField("args", os.Args).Errorf("monagentCommand Execute failed %s", err.Error()) + os.Exit(-1) } } func runMonitorAgent() error { - monagentConfig, err := config.DecodeMonitorAgentServerConfig(viper.GetString("config")) + monagentConfig, err := monagent.DecodeMonitorAgentServerConfig(viper.GetString("config")) if err != nil { return errors.Wrap(err, "read monitor agent server config") } - // init log for monagent - agentlog.InitLogger(agentlog.LoggerConfig{ - Level: monagentConfig.Log.Level, - Filename: monagentConfig.Log.Filename, - MaxSize: monagentConfig.Log.MaxSize, - MaxAge: monagentConfig.Log.MaxAge, - MaxBackups: monagentConfig.Log.MaxBackups, - LocalTime: monagentConfig.Log.LocalTime, - Compress: monagentConfig.Log.Compress, + // Replace the home.path in the configuration file with the actual value + contextMap := map[string]string{OcpAgentHomePath: path.AgentDir()} + _, err = config.ReplaceConfValues(monagentConfig, contextMap) + if err != nil { + return errors.Wrap(err, "Failed to parse config file path") + } + + ctxlog := trace.ContextWithRandomTraceId() + err = sdk.InitSDK(ctxlog, config.SDKConfig{ + ModuleConfigDir: monagentConfig.ModulePath, + ConfigPropertiesDir: monagentConfig.PropertiesPath, + CryptoPath: monagentConfig.CryptoPath, + CryptoMethod: monagentConfig.CryptoMethod, }) + log.WithContext(ctxlog).Infof("sdk inited") + if err != nil { + return errors.Wrap(err, "init config sdk") + } + + // Obtain the service startup port based on the port value in the config file + monitorPort := config.GetConfigPropertiesByKey(MonitorPortKey) + portMap := map[string]string{MonitorPortKey: monitorPort} + _, err = config.ReplaceConfValues(monagentConfig, portMap) + if err != nil { + return errors.Wrap(err, "Failed to parse config file port") + } - monagentServer := engine.NewMonitorAgentServer(monagentConfig) + monagentServer := web.NewMonitorAgentServer(monagentConfig) ctx, cancel := context.WithCancel(context.Background()) defer func() { cancel() @@ -95,36 +112,45 @@ func runMonitorAgent() error { engine.GetPipelineManager().Schedule(ctx) engine.GetConfigManager().Schedule(ctx) - err = sdk.InitSDK(config.SDKConfig{ - ModuleConfigDir: monagentConfig.ModulePath, - ConfigPropertiesDir: monagentConfig.PropertiesPath, - CryptoPath: monagentConfig.CryptoPath, - CryptoMethod: monagentConfig.CryptoMethod, - }) - log.Infof("sdk inited") - if err != nil { - return errors.Wrap(err, "init config sdk") - } - err = sdk.RegisterMonagentCallbacks() + err = sdk.RegisterMonagentCallbacks(ctxlog) if err != nil { return errors.Wrap(err, "register monagent callbacks") } - err = config.InitModuleTypeConfig(ctx, config.MonitorAdminBasicAuthModuleType) - err = config.InitModuleTypeConfig(ctx, config.MonitorServerBasicAuthModuleType) - err = config.InitModuleTypeConfig(ctx, config.MonitorPipelineModuleType) - if err != nil { - log.WithError(err).Errorf("init pipeline config, err:%+v", err) + // Initialize the log + if err := config.InitModuleTypeConfig(ctxlog, config.MonitorLogConfigModuleType); err != nil { + log.WithContext(ctxlog).WithError(err).Errorf("init module type %s, err:%+v", config.MonitorLogConfigModuleType, err) } - - err = monagentServer.RegisterRouter() - if err != nil { - return errors.Wrap(err, "monitor agent server register route") + // Initialize self-monitoring + if err := config.InitModuleTypeConfig(ctxlog, config.StatConfigModuleType); err != nil { + log.WithContext(ctxlog).WithError(err).Errorf("init module type %s, err:%+v", config.StatConfigModuleType, err) + } + // Initialize basic authentication + if err := config.InitModuleTypeConfig(ctxlog, config.MonitorServerBasicAuthModuleType); err != nil { + log.WithContext(ctxlog).WithError(err).Errorf("init module type %s, err:%+v", config.MonitorServerBasicAuthModuleType, err) + } + // Initialize monitoring and collection pipeline configuration + if err := config.InitModuleTypeConfig(ctxlog, config.MonitorPipelineModuleType); err != nil { + log.WithContext(ctxlog).WithError(err).Errorf("init module type %s, err:%+v", config.MonitorPipelineModuleType, err) + } + // Initialize meta config + if err := config.InitModuleConfig(ctx, config.MonitorAgentConfigMetaModule); err != nil { + log.WithContext(ctxlog).WithError(err).Errorf("init module type %s, err:%+v", config.ConfigMetaModuleType, err) } - err = monagentServer.Run() - if err != nil { - return errors.Wrap(err, "start monitor agent server") + go func() { + monagentServer.RegisterRouter() + monagentServer.Run() + }() + + ch := make(chan os.Signal, 1) + signal.Notify(ch, syscall.SIGTERM, syscall.SIGINT) + select { + case sig := <-ch: + log.WithContext(ctx).Infof("signal '%s' received. exiting...", sig.String()) + engine.GetPipelineManager().Stop(ctx) + monagentServer.Server.Cancel() + close(engine.PipelineRouteChan) } return nil diff --git a/config/agentctl/agentctl_config.go b/config/agentctl/agentctl_config.go new file mode 100644 index 0000000000000000000000000000000000000000..5d9fe330969768790b7230437e7223902bf427f8 --- /dev/null +++ b/config/agentctl/agentctl_config.go @@ -0,0 +1,20 @@ +package agentctl + +import "github.com/oceanbase/obagent/config" + +// agentctl meta +type AgentctlConfig struct { + SDKConfig config.SDKConfig `yaml:"sdkConfig"` + // log config + Log config.LogConfig `yaml:"log"` + + RunDir string `yaml:"runDir"` + ConfDir string `yaml:"confDir"` + LogDir string `yaml:"logDir"` + BackupDir string `yaml:"backupDir"` + TempDir string `yaml:"tempDir"` + TaskStoreDir string `yaml:"taskStoreDir"` + AgentPkgName string `yaml:"agentPkgName"` + PkgExt string `yaml:"pkgExt"` + PkgStoreDir string `yaml:"pkgStoreDir"` +} diff --git a/config/config_common.go b/config/config_common.go index 6313ffda66727cb0f74d36899ced2831b899cb24..59a476408d4669adc03192eac80a54f3bcbae94e 100644 --- a/config/config_common.go +++ b/config/config_common.go @@ -1,34 +1,40 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - package config -import "github.com/oceanbase/obagent/lib/crypto" +import ( + "strconv" + "time" -var ( - CurProcess Process + "github.com/oceanbase/obagent/lib/crypto" +) - AgentVersion string +var ( + CurProcess Process + AgentVersion string + Mode AgentMode = ReleaseMode + BuildEpoch string + BuildGoVersion string +) - Mode AgentMode +var ( + GitBranch string + GitCommitId string + GitShortCommitId string + GitCommitTime string ) type AgentMode = string +const ( + DebugMode AgentMode = "debug" + ReleaseMode AgentMode = "release" +) + type Process = string const ( - ProcessMonitorAgent Process = "monagent" - GCPercent int = 500 + ProcessManagerAgent Process = "ob_mgragent" + ProcessMonitorAgent Process = "ob_monagent" + ProcessAgentCtl Process = "ob_agentctl" ) type InstallConfig struct { @@ -63,8 +69,37 @@ type SDKConfig struct { CryptoMethod crypto.CryptoMethod `yaml:"cryptoMethod"` } +type ShellfConfig struct { + // shell template config file path + TemplatePath string `yaml:"template"` +} + type BasicAuthConfig struct { - Auth string `yaml:"auth"` - Username string `yaml:"username"` - Password string `yaml:"password"` + Auth string `yaml:"auth"` + MetricAuthEnabled bool `yaml:"metricAuthEnabled"` + Username string `yaml:"username"` + Password string `yaml:"password"` +} + +func GetAgentInfo() map[string]interface{} { + var infoMap = map[string]interface{}{ + "name": CurProcess, + "version": AgentVersion, + "mode": Mode, + "buildGoVersion": BuildGoVersion, + } + if epoch, err := strconv.Atoi(BuildEpoch); err == nil { + buildTime := time.Unix(int64(epoch), 0) + infoMap["buildTime"] = buildTime + } + return infoMap +} + +func GetGitInfo() map[string]interface{} { + return map[string]interface{}{ + "branch": GitBranch, + "commitId": GitCommitId, + "shortCommitId": GitShortCommitId, + "commitTime": GitCommitTime, + } } diff --git a/config/config_crypto.go b/config/config_crypto.go index ebc37bd1d03f86cdf024d77118703763468a311d..aadb69bce2dfcb091bbd35e011638b1f6b62589f 100644 --- a/config/config_crypto.go +++ b/config/config_crypto.go @@ -1,15 +1,3 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - package config import ( diff --git a/config/config_module.go b/config/config_module.go index 748e3a93238cd6e91ca98b8bb8d48467c4faacdb..cd724b70cd745e5a51f14aa176f7713e03b0412b 100644 --- a/config/config_module.go +++ b/config/config_module.go @@ -1,15 +1,3 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - package config import ( @@ -19,13 +7,29 @@ import ( ) const ( - MonitorPipelineModuleType ModuleType = "monagent.pipeline" - - MonitorServerBasicAuthModuleType ModuleType = "monagent.server.basic.auth" - - MonitorAdminBasicAuthModuleType ModuleType = "monagent.admin.basic.auth" + ManagerAgentBasicAuthConfigModuleType ModuleType = "mgragent.basic.auth" + MonitorPipelineModuleType ModuleType = "monagent.pipeline" + MonitorServerBasicAuthModuleType ModuleType = "monagent.server.basic.auth" + ProxyConfigModuleType ModuleType = "proxy.config" + NotifyProcessConfigModuleType ModuleType = "module.config.notify" + OBLogcleanerModuleType ModuleType = "ob.logcleaner" + MonitorLogConfigModuleType ModuleType = "monagent.log.config" + ManagerLogConfigModuleType ModuleType = "mgragent.log.config" + ManagerLogQuerierModuleType ModuleType = "mgragent.logquerier" + ConfigMetaModuleType ModuleType = "config.meta" + StatConfigModuleType ModuleType = "stat.config" +) - NotifyProcessConfigModuleType ModuleType = "module.config.notify" +const ( + ManagerAgentBasicAuthConfigModule = "mgragent.basic.auth" + ManagerAgentProxyConfigModule = "mgragent.proxy.config" + NotifyProcessConfigModule = "module.config.notify" + OBLogcleanerModule = "ob.logcleaner" + MonitorLogConfigModule = "monagent.log.config" + ManagerLogConfigModule = "mgragent.log.config" + ManagerLogQuerierModule = "mgragent.logquerier" + ManagerAgentConfigMetaModule = "mgragent.config.meta" + MonitorAgentConfigMetaModule = "monagent.config.meta" ) var ( @@ -62,7 +66,7 @@ func GetFinalModuleConfig(module string) (*ModuleConfig, error) { } finalConf, err := getFinalModuleConfig(module, sample.Config, nil) if err != nil { - return nil, errors.Errorf("get module %s config err:%+v", module, err) + return nil, errors.Errorf("get module %s config err:%s", module, err) } sample.Config = finalConf return &sample, nil diff --git a/config/config_module_callback.go b/config/config_module_callback.go index ddeeca7dc422409f0c09b3995cc5662371fddb9d..efcb41ac078a3a4518600ddcc59b19b4e0898807 100644 --- a/config/config_module_callback.go +++ b/config/config_module_callback.go @@ -1,15 +1,3 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - package config import ( @@ -33,7 +21,7 @@ func init() { modules = make(map[string]ModuleType, 32) } -// Callback call Callback when revive a request +// Callback call Callback when receive a request type Callback func(ctx context.Context, nconfig *NotifyModuleConfig) error // ModuleCallback register a ModuleCallback, Callback will call it @@ -80,9 +68,9 @@ func getModuleTypeCallback(moduleType ModuleType) (callback *ConfigCallback, exi } func RegisterModule(module string, moduleType ModuleType) error { - existModyleType, ex := modules[module] - if ex && existModyleType != moduleType { - return errors.Errorf("module %s 's module type %s is already set as %s", module, moduleType, existModyleType) + existModuleType, ex := modules[module] + if ex && existModuleType != moduleType { + return errors.Errorf("module %s 's module type %s is already set as %s", module, moduleType, existModuleType) } modules[module] = moduleType log.Debugf("register module %s, moduleType %s", module, moduleType) @@ -98,7 +86,7 @@ func RegisterConfigCallback(moduleType ModuleType, log.Debugf("register moduleType %s config callback start", moduleType) if _, ex := getModuleTypeCallback(moduleType); ex { - return errors.Errorf("module %s callback has been registried!", moduleType) + return errors.Errorf("moduleType %s callback has been registred!", moduleType) } initConfigCallback := func(ctx context.Context, module string) error { @@ -113,12 +101,12 @@ func RegisterConfigCallback(moduleType ModuleType, moduleConf, err := getFinalModuleConfig(module, moduleConfigSample.Config, nil) if err != nil { - return errors.Errorf("get module %s final config err:%+v", module, err) + return errors.Errorf("get module %s final config err:%s", module, err) } err = initConfig(ctx, moduleConf) if err != nil { - return errors.Errorf("init module %s conf err:%+v", module, err) + return errors.Errorf("init module %s conf err:%s", module, err) } return nil } @@ -127,7 +115,7 @@ func RegisterConfigCallback(moduleType ModuleType, log.WithContext(ctx).Infof("notify module %s config, process:%s", econfig.Module, econfig.Process) err := receiveUpdatedConfig(ctx, econfig.Config) if err != nil { - return errors.Errorf("notify module %s config, process %s, err:%+v", econfig.Module, econfig.Process, err) + return errors.Errorf("notify module %s config, process %s, err:%s", econfig.Module, econfig.Process, err) } return nil } diff --git a/config/config_module_test.go b/config/config_module_test.go index fbe89616701043d480fbbefbfe4a5336b2fe14ef..e2ecea07f1553bd1de0643bf735ad4e2aec9abc3 100644 --- a/config/config_module_test.go +++ b/config/config_module_test.go @@ -1,15 +1,3 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - package config import ( @@ -72,7 +60,7 @@ configs: - module: foo moduleType: fooType - process: ocp_mgragent + process: ob_mgragent config: foo: ${foo.foo} fooNoDefine: ${foo.not.defined} @@ -120,8 +108,8 @@ func _init(t *testing.T) string { Valid: nil, }) - croptoErr := InitCrypto("", crypto.PLAIN) - assert.Nil(t, croptoErr) + cryptoErr := InitCrypto("", crypto.PLAIN) + assert.Nil(t, cryptoErr) tempDir := os.TempDir() @@ -137,18 +125,27 @@ func _init(t *testing.T) string { err = ioutil.WriteFile(filepath.Join(configPropertiesDir, "foo.yaml"), []byte(fooKVYaml), 0755) assert.Nil(t, err) - err = InitModuleConfigs(moduleConfigDir) + err = InitModuleConfigs(context.Background(), moduleConfigDir) assert.Nil(t, err) - err = InitConfigProperties(configPropertiesDir) + err = InitConfigProperties(context.Background(), configPropertiesDir) assert.Nil(t, err) agentlog.InitLogger(agentlog.LoggerConfig{ - Output: os.Stdout, - Level: "debug", + Level: "debug", + Filename: "../tests/test.log", + MaxSize: 10, // 10M + MaxAge: 3, // 3days + MaxBackups: 3, + LocalTime: false, + Compress: false, }) + CurProcess = ProcessManagerAgent + registerFooCallback() + SetProcessModuleConfigNotifyAddress(ProcessConfigNotifyAddress{Process: ProcessManagerAgent}) + return tempDir } @@ -175,7 +172,7 @@ func registerFooCallback() { } log.WithField("module", foo).Info("init foo config") fooServer.Foo = foo - log.Infof("init module %s config sucessfully", testFooModule) + log.Infof("init module %s config successfully", testFooModule) return nil }, // notify updated config @@ -187,7 +184,7 @@ func registerFooCallback() { } log.WithField("module", foo).Info("update foo config") fooServer.Foo = foo - log.Infof("update module %s config sucessfully", testFooModule) + log.Infof("update module %s config successfully", testFooModule) return nil }, ) @@ -211,6 +208,17 @@ func TestModuleConfigCallback_Success(t *testing.T) { So(fooServer.Foo.Bar.Bar, ShouldEqual, 3306) So(fooServer.Foo.Bar.Duration, ShouldEqual, "10s") }) + + err = callback.NotifyConfigCallback(context.Background(), &NotifyModuleConfig{ + Process: ProcessManagerAgent, + Module: testFooModule, + Config: &Foo{Foo: "foofoo", Bar: Bar{Bar: 2883}}, + }) + Convey("NotifyConfigCallback", t, func() { + So(err, ShouldBeNil) + So(fooServer.Foo.Foo, ShouldEqual, "foofoo") + So(fooServer.Foo.Bar.Bar, ShouldEqual, 2883) + }) }) } diff --git a/config/config_notify.go b/config/config_notify.go index 53ecee7b74fdc830d1a2f7b2025d88dd88f0f731..dbd0056e2d625475b5e52cf93d3386ca97b83968 100644 --- a/config/config_notify.go +++ b/config/config_notify.go @@ -1,30 +1,14 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - package config import ( - "bytes" "context" - "encoding/json" - "net/http" "strings" "sync" - "time" "github.com/pkg/errors" log "github.com/sirupsen/logrus" - "github.com/oceanbase/obagent/api/response" + "github.com/oceanbase/obagent/executor/agent" ) var ( @@ -36,6 +20,8 @@ func init() { processConfigNotifyAddresses = make(map[string]ProcessConfigNotifyAddress, 4) } +// ProcessConfigNotifyAddress business process configuration information. +// This information serves as sdk source data for the mgragent and agentctl. type ProcessConfigNotifyAddress struct { Local bool `yaml:"local"` Process string `yaml:"process"` @@ -72,7 +58,7 @@ func InitModuleTypeConfig(ctx context.Context, moduleType ModuleType) error { if t == moduleType { err := InitModuleConfig(ctx, m) if err != nil { - errs = append(errs, errors.Errorf("init module %s err:%+v", m, err).Error()) + errs = append(errs, errors.Errorf("init module %s err:%s", m, err).Error()) } } } @@ -87,19 +73,24 @@ func NotifyModuleConfigs(ctx context.Context, verifyConfigResult *VerifyConfigRe if verifyConfigResult == nil { return nil } - log.WithContext(ctx).Infof("notify moduel configs length:%d", len(verifyConfigResult.UpdatedConfigs)) + log.WithContext(ctx).Infof("notify module configs length:%d", len(verifyConfigResult.UpdatedConfigs)) + var errs []string for _, conf := range verifyConfigResult.UpdatedConfigs { err := notifyModuleConfig(ctx, conf) if err != nil { - return err + log.WithContext(ctx).Errorf("notify module %s err:%+v", conf.Module, err) + errs = append(errs, err.Error()) } } + if len(errs) > 0 { + return errors.Errorf("notify modules err:%s", strings.Join(errs, ",")) + } return nil } // notify module config changed func notifyModuleConfig(ctx context.Context, econfig *NotifyModuleConfig) error { - log.Infof("module %s, notify process %s, current process %s", econfig.Module, econfig.Process, CurProcess) + log.WithContext(ctx).Infof("module %s, notify process %s, current process %s", econfig.Module, econfig.Process, CurProcess) notifyAddress, ex := getProcessModuleConfigNotifyAddress(string(econfig.Process)) if !ex { return errors.Errorf("process %s notify config is not found", econfig.Process) @@ -107,11 +98,7 @@ func notifyModuleConfig(ctx context.Context, econfig *NotifyModuleConfig) error if !notifyAddress.Local && econfig.Process != CurProcess { return NotifyRemoteModuleConfig(ctx, econfig) } - callback, ex := getModuleCallback(econfig.Module) - if !ex { - return errors.Errorf("module %s not found", econfig.Module) - } - return callback.NotifyConfigCallback(ctx, econfig) + return NotifyLocalModuleConfig(ctx, econfig) } // NotifyLocalModuleConfig notify local process's module config changed @@ -134,90 +121,90 @@ func NotifyRemoteModuleConfig(ctx context.Context, econfig *NotifyModuleConfig) "process": econfig.Process, "notify address": notifyAddress.NotifyAddress, }) - data, err := json.Marshal(econfig) - if err != nil { - err = errors.Errorf("module %s, process %s, json marshal err:%+v", econfig.Module, econfig.Process, err) - ctxlog.Errorf("json marshal err:%+v", err) - return err - } ctxlog.Infof("notify module config") - client := &http.Client{Timeout: time.Second * 1} - req, err := http.NewRequest("POST", notifyAddress.NotifyAddress, bytes.NewReader(data)) - req.Header.Add("Content-Type", "application/json") - if err != nil { - ctxlog.Errorf("notify config new request err:%+v", err) - return err - } - req.SetBasicAuth(notifyAddress.AuthConfig.Username, notifyAddress.AuthConfig.Password) - resp, err := client.Do(req) + admin := agent.NewAdmin(agent.DefaultAdminConf()) + client, err := admin.NewClient(notifyAddress.Process) if err != nil { - ctxlog.Errorf("notify config do request err:%+v", err) + ctxlog.Errorf("new client err:%+v", err) return err } - defer resp.Body.Close() - r := new(response.AgentResponse) - err = json.NewDecoder(resp.Body).Decode(r) + resp := new(agent.AgentctlResponse) + err = client.Call(notifyAddress.NotifyAddress, econfig, resp) if err != nil { - ctxlog.Errorf("decode resp err:%+v", err) + ctxlog.Errorf("notify config err:%+v", err) return err } + return nil +} - if !r.Successful || r.Error != nil { - return errors.Errorf("errorCode:%d, errorMessage:%s", r.Error.Code, r.Error.Message) - } +func NotifyAllModules(ctx context.Context) error { + return notifyModules(ctx, nil, true) +} - return nil +func NotifyModules(ctx context.Context, modules []string) error { + return notifyModules(ctx, modules, false) } -func NotifyModules(modules []string) error { +func notifyModules(ctx context.Context, modules []string, all bool) error { moduleConfigs := GetModuleConfigs() - notifyModules := make([]*NotifyModuleConfig, 0, len(modules)) - for _, moduleConf := range moduleConfigs { - for _, module := range modules { - if moduleConf.Module == module { - process := moduleConf.Process - moduleConf, err := GetFinalModuleConfig(module) - if err != nil { - log.Error(err) - return err - } - if moduleConf.Disabled { - log.Warnf("module %s config is disabled", module) - continue + modulesToNotify := make([]*NotifyModuleConfig, 0, len(moduleConfigs)) + for _, moduleConfTpl := range moduleConfigs { + if moduleConfTpl.Disabled { + log.WithContext(ctx).Infof("module %s config is disabled", moduleConfTpl.Module) + continue + } + affected := false + if all { + affected = true + } else { + for _, module := range modules { + if moduleConfTpl.Module == module { + affected = true + break } - notifyModules = append(notifyModules, &NotifyModuleConfig{ - Process: Process(process), - Module: module, - Config: moduleConf.Config, - }) } } + if !affected { + continue + } + + process := moduleConfTpl.Process + moduleConf, err := GetFinalModuleConfig(moduleConfTpl.Module) + if err != nil { + log.WithContext(ctx).Error(err) + return err + } + modulesToNotify = append(modulesToNotify, &NotifyModuleConfig{ + Process: process, + Module: moduleConfTpl.Module, + Config: moduleConf.Config, + }) } - err := NotifyModuleConfigs(context.Background(), &VerifyConfigResult{ + err := NotifyModuleConfigs(ctx, &VerifyConfigResult{ ConfigVersion: nil, - UpdatedConfigs: notifyModules, + UpdatedConfigs: modulesToNotify, }) if err != nil { - log.Errorf("notify module configs %+v, err:%+v", notifyModules, err) + log.WithContext(ctx).Errorf("notify module configs %+v, err:%+v", modulesToNotify, err) } return err } func NotifyModuleConfigForHttp(ctx context.Context, nconfig *NotifyModuleConfig) error { ctxlog := log.WithContext(ctx) - err := ReloadConfigFromFiles() + err := ReloadConfigFromFiles(ctx) if err != nil { - return errors.Errorf("reload config err:%+v", err) + return errors.Errorf("reload config err:%s", err) } moduleConf, err := GetFinalModuleConfig(nconfig.Module) if err != nil { - return errors.Errorf("get module config err:%+v", err) + return errors.Errorf("get module config err:%s", err) } - if nconfig.Process != Process(moduleConf.Process) { + if nconfig.Process != moduleConf.Process { ctxlog.Warnf("module %s process should be %s, not %s", nconfig.Module, moduleConf.Process, nconfig.Process) } @@ -232,7 +219,7 @@ func NotifyModuleConfigForHttp(ctx context.Context, nconfig *NotifyModuleConfig) nconfig.Config = moduleConf.Config err = NotifyLocalModuleConfig(ctx, nconfig) if err != nil { - return errors.Errorf("notify module config err:%+v", err) + return errors.Errorf("notify module config err:%s", err) } return nil diff --git a/config/config_notify_test.go b/config/config_notify_test.go index 97029b7282b0b99fc12b0fd8dd55aba828dba277..29eac7039b5ecf89ac0453abbba898a6fa0d4885 100644 --- a/config/config_notify_test.go +++ b/config/config_notify_test.go @@ -1,15 +1,3 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - package config import ( @@ -32,6 +20,42 @@ func TestCallNotifyModuleConfigs_Success(t *testing.T) { So(fooServer.Foo.Bar.Bar, ShouldEqual, 3306) }) + err = NotifyModuleConfigs(context.Background(), &VerifyConfigResult{ + UpdatedConfigs: []*NotifyModuleConfig{ + { + Process: ProcessManagerAgent, + Module: testFooModule, + Config: &Foo{Foo: "foofoo", Bar: Bar{Bar: 2883}}, + }, + }, + }) + Convey("NotifyModuleConfigs", t, func() { + So(err, ShouldBeNil) + So(fooServer.Foo.Foo, ShouldEqual, "foofoo") + So(fooServer.Foo.Bar.Bar, ShouldEqual, 2883) + }) + + err = NotifyLocalModuleConfig(context.Background(), &NotifyModuleConfig{ + Process: ProcessManagerAgent, + Module: testFooModule, + Config: &Foo{Foo: "foofoo2", Bar: Bar{Bar: 2881}}, + }) + Convey("NotifyLocalModuleConfig", t, func() { + So(err, ShouldBeNil) + So(fooServer.Foo.Foo, ShouldEqual, "foofoo2") + So(fooServer.Foo.Bar.Bar, ShouldEqual, 2881) + }) + + err = NotifyModuleConfigForHttp(context.Background(), &NotifyModuleConfig{ + Process: ProcessManagerAgent, + Module: testFooModule, + Config: &Foo{Foo: "foofoo3", Bar: Bar{Bar: 2882}}, + }) + Convey("NotifyModuleConfigForHttp", t, func() { + So(err, ShouldBeNil) + So(fooServer.Foo.Foo, ShouldEqual, "foo_value") + So(fooServer.Foo.Bar.Bar, ShouldEqual, 3306) + }) }) } @@ -84,6 +108,13 @@ func TestNotifyConfigModule(t *testing.T) { _init(t) defer cleanup() + t.Run("notify config by modules (exist)", func(t *testing.T) { + err := NotifyModules(context.Background(), []string{"foo"}) + Convey("NotifyModules", t, func() { + So(err, ShouldBeNil) + }) + }) + t.Run("notify config by modules (not exist)", func(t *testing.T) { noExistModuleReq := &NotifyModuleConfig{ Module: "foo-not-exist", @@ -106,7 +137,7 @@ func TestUpdateModuleConfigs(t *testing.T) { defer cleanup() t.Run("update module configs by pair", func(t *testing.T) { - err := UpdateConfigPairs([]string{"foo.foo=foo1"}) + err := UpdateConfigPairs(context.Background(), []string{"foo.foo=foo1"}) Convey("UpdateConfigPairs", t, func() { So(err, ShouldBeNil) }) diff --git a/config/config_property.go b/config/config_property.go index 22672cd5a48b1e2a6c3f97f222a572cf283b8b3c..2c5343b4c609aa3a2b15e3ecd001051ab02b2eaf 100644 --- a/config/config_property.go +++ b/config/config_property.go @@ -1,22 +1,13 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - package config import ( + "context" "fmt" "os" + "path/filepath" "reflect" "sort" + "syscall" "github.com/pkg/errors" log "github.com/sirupsen/logrus" @@ -29,7 +20,7 @@ var ( ) const ( - maskedString = "****" + maskedResult = "xxx" ) type ConfigPropertiesMain struct { @@ -60,8 +51,8 @@ func (c *ConfigPropertiesMain) ContainsRestartNeededKeyValues(module string, pro continue } c.needRestartModules[module].RestartKeyValues[key] = value - masedKeyValues := MaskedKeyValues(c.needRestartModules[module].RestartKeyValues) - c.needRestartModules[module].RestartKeyValues = masedKeyValues + maskedKeyValues := MaskedKeyValues(c.needRestartModules[module].RestartKeyValues) + c.needRestartModules[module].RestartKeyValues = maskedKeyValues log.Warnf("config key %s is changed, need restart agent process", key) restatNeeded = true } @@ -76,7 +67,7 @@ func (c *ConfigPropertiesMain) MaskedKeyValues(kvs map[string]interface{}) map[s continue } if property.Masked { - ret[key] = maskedString + ret[key] = maskedResult } else { ret[key] = value } @@ -92,6 +83,24 @@ func (c *ConfigPropertiesMain) GetConfigPropertiesKeyValues() map[string]interfa return ret } +func GetConfigPropertieByName(name string) interface{} { + for key, property := range mainConfigProperties.allConfigProperties { + if key == name { + return property.Val() + } + } + + return nil +} + +func (c *ConfigPropertiesMain) GetConfigPropertiesMaskedKeys() map[string]bool { + ret := make(map[string]bool, len(c.allConfigProperties)) + for key := range c.allConfigProperties { + ret[key] = true + } + return ret +} + func (c *ConfigPropertiesMain) GetDiffConfigProperties(keyValues map[string]interface{}, fatal bool) (map[string]interface{}, error) { properties := make(map[string]interface{}, 10) for key, value := range keyValues { @@ -104,10 +113,11 @@ func (c *ConfigPropertiesMain) GetDiffConfigProperties(keyValues map[string]inte } val, err := property.Parse(value) if err != nil { - return nil, errors.Errorf("pase key %s failed, err:%+v", key, err) + return nil, errors.Errorf("pase key %s failed, err:%s", key, err) } // diff configs - if property.Value != val { + // config may be decrypted, use Val() + if property.Val() != val { properties[key] = val } } @@ -136,14 +146,28 @@ func (c *ConfigPropertiesMain) GetCurrentConfigVersion() *ConfigVersion { } } -func (c *ConfigPropertiesMain) saveIncrementalConfig(kvs map[string]interface{}) (*ConfigVersion, error) { +func (c *ConfigPropertiesMain) saveIncrementalConfig(ctx context.Context, kvs map[string]interface{}) (*ConfigVersion, error) { groups := map[*ConfigPropertiesGroup]bool{} changed := false var err error + defaultConfigFile := filepath.Join(c.configPropertiesDir, "default_config.yaml") + var defaultGroup *ConfigPropertiesGroup + for _, group := range c.ConfigGroups { + if group.ConfigFile == defaultConfigFile { + defaultGroup = group + } + } + if defaultGroup == nil { + defaultGroup = &ConfigPropertiesGroup{ + Configs: []*ConfigProperty{}, + ConfigFile: defaultConfigFile, + } + } + for key, val := range kvs { property, ex := c.allConfigProperties[key] if !ex { - log.WithField("config key", key).Errorf("config key not exist") + log.WithContext(ctx).WithField("config key", key).Errorf("config key not exist") continue } if property.Value != val { @@ -151,7 +175,7 @@ func (c *ConfigPropertiesMain) saveIncrementalConfig(kvs map[string]interface{}) } finalVal := val if property.Encrypted { - log.Debugf("encrypt config key %s", property.Key) + log.WithContext(ctx).Debugf("encrypt config key %s", property.Key) rawVal := cast.ToString(val) finalVal, err = configCrypto.Encrypt(rawVal) if err != nil { @@ -160,17 +184,24 @@ func (c *ConfigPropertiesMain) saveIncrementalConfig(kvs map[string]interface{}) } c.allConfigProperties[key].Value = finalVal + configFile := "" for _, group := range c.ConfigGroups { for _, property := range group.Configs { if property.Key == key { property.Value = finalVal groups[group] = true + configFile = group.ConfigFile } } } + if configFile == "" { + defaultGroup.Configs = append(defaultGroup.Configs, c.allConfigProperties[key]) + groups[defaultGroup] = true + } } if !changed { - return nil, errors.Errorf("all configs is not changed") + log.WithContext(ctx).Info("all configs is not changed") + return c.GetCurrentConfigVersion(), nil } var reterr error @@ -179,15 +210,15 @@ func (c *ConfigPropertiesMain) saveIncrementalConfig(kvs map[string]interface{}) group.ConfigVersion = configVersion.ConfigVersion err := group.SaveConfig() if err != nil { - log.WithField("ConfigGroup", group).Errorf("save config to file err:%+v", err) + log.WithContext(ctx).WithField("ConfigGroup", group).Errorf("save config to file err:%+v", err) reterr = err } else { - log.Infof("save config %s to file %s", group.ConfigVersion, group.ConfigFile) + log.WithContext(ctx).Infof("save config %s to file %s", group.ConfigVersion, group.ConfigFile) } } - err = snapshotForConfigVersion(configVersion.ConfigVersion) + err = snapshotForConfigVersion(ctx, configVersion.ConfigVersion) if err != nil { - log.Errorf("save config snapshot %s err:%+v", configVersion.ConfigVersion, err) + log.WithContext(ctx).Errorf("save config snapshot %s err:%+v", configVersion.ConfigVersion, err) reterr = err } @@ -213,12 +244,27 @@ type ConfigPropertiesGroup struct { } func (g *ConfigPropertiesGroup) SaveConfig() error { + var err error file, err := os.OpenFile(g.ConfigFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) if err != nil { return err } defer file.Close() - return yaml.NewEncoder(file).Encode(g) + defer func(file *os.File) { + err = syscall.Flock(int(file.Fd()), syscall.LOCK_UN) + if err != nil { + log.Errorf("unLock config file failed, err: %s", err) + } + }(file) + err = syscall.Flock(int(file.Fd()), syscall.LOCK_EX|syscall.LOCK_NB) + if err != nil { + return err + } + err = yaml.NewEncoder(file).Encode(g) + if err != nil { + return err + } + return nil } type ConfigProperty struct { @@ -236,18 +282,20 @@ type ConfigProperty struct { } func (c *ConfigProperty) Val() interface{} { - var val interface{} - val = c.Value + val := c.Value + if c.Value == nil { + val = c.DefaultValue + } if c.Encrypted { rawVal := cast.ToString(val) // val is nil, no need to decrypt if len(rawVal) == 0 { return rawVal } - log.Debugf("decrypt config key %s", c.Key) + log.Debugf("decrypt config key %s rawVal-length:%d", c.Key, len(rawVal)) finalVal, err := configCrypto.Decrypt(rawVal) if err != nil { - log.Errorf("Decrypt config key %s, value %s err:%+v", c.Key, c.Value, err) + log.Errorf("Decrypt config key %s, err:%+v", c.Key, err) } return finalVal } @@ -268,29 +316,29 @@ func (c *ConfigProperty) Parse(value interface{}) (val interface{}, err error) { case ValueBool: val, err = cast.ToBoolE(value) if err != nil { - return nil, errors.Errorf("assert %s %+v (%s) to bool, err:%+v", c.Key, value, reflect.TypeOf(value), err) + return nil, errors.Errorf("assert %s %+v (%s) to bool, err:%s", c.Key, value, reflect.TypeOf(value), err) } return val, nil case ValueInt64: val, err = cast.ToInt64E(value) if err != nil { - return nil, errors.Errorf("assert %s %+v (%s) to int64, err:%+v", c.Key, value, reflect.TypeOf(value), err) + return nil, errors.Errorf("assert %s %+v (%s) to int64, err:%s", c.Key, value, reflect.TypeOf(value), err) } return val, nil case ValueFloat64: val, err = cast.ToFloat64E(value) if err != nil { - return nil, errors.Errorf("assert %s %+v (%s) to numberic float64, err:%+v", c.Key, value, reflect.TypeOf(value), err) + return nil, errors.Errorf("assert %s %+v (%s) to numeric float64, err:%s", c.Key, value, reflect.TypeOf(value), err) } return val, nil case ValueString: val, err = cast.ToStringE(value) if err != nil { - return nil, errors.Errorf("assert %s %+v (%s) to string, err:%+v", c.Key, value, reflect.TypeOf(value), err) + return nil, errors.Errorf("assert %s %+v (%s) to string, err:%s", c.Key, value, reflect.TypeOf(value), err) } return val, nil default: - return nil, errors.Errorf("key %s unsurported valueType %s", c.Key, c.ValueType) + return nil, errors.Errorf("key %s unsuported valueType %s", c.Key, c.ValueType) } } @@ -307,6 +355,14 @@ func MaskedKeyValues(kvs map[string]interface{}) map[string]interface{} { return mainConfigProperties.MaskedKeyValues(kvs) } +func MaskedStringKeyValues(kvs map[string]string) map[string]interface{} { + kvs2 := make(map[string]interface{}, len(kvs)) + for k, v := range kvs { + kvs2[k] = fmt.Sprint(v) + } + return mainConfigProperties.MaskedKeyValues(kvs2) +} + func GetConfigPropertiesKeyValues() map[string]string { kvs := mainConfigProperties.GetConfigPropertiesKeyValues() ret := make(map[string]string, len(kvs)) @@ -316,6 +372,15 @@ func GetConfigPropertiesKeyValues() map[string]string { return ret } +func GetConfigPropertiesByKey(key string) string { + ret := GetConfigPropertiesKeyValues() + return ret[key] +} + +func GetConfigPropertiesMaskedKeys() map[string]bool { + return mainConfigProperties.GetConfigPropertiesMaskedKeys() +} + func GetDiffConfigProperties(keyValues map[string]interface{}, fatal bool) (map[string]interface{}, error) { return mainConfigProperties.GetDiffConfigProperties(keyValues, fatal) } @@ -330,6 +395,10 @@ type RestartModuleKeyValues struct { RestartKeyValues map[string]interface{} } +func NeedRestartModuleKeyValues() map[string]*RestartModuleKeyValues { + return mainConfigProperties.needRestartModules +} + func ContainsRestartNeededKeyValues(module string, process string, kvs map[string]interface{}) bool { return mainConfigProperties.ContainsRestartNeededKeyValues(module, process, kvs) } diff --git a/config/config_property_meta.go b/config/config_property_meta.go index f0d0c46b8570870f11bf45b1898af4208e254559..bfc2cc1759025b51fa8815171c68c9117394cbbc 100644 --- a/config/config_property_meta.go +++ b/config/config_property_meta.go @@ -1,18 +1,8 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - package config import ( + "context" + log "github.com/sirupsen/logrus" ) @@ -30,7 +20,7 @@ func SetConfigPropertyMeta(property *ConfigProperty) { configPropertyMetas[property.Key] = property } -func mergeConfigProperties() { +func mergeConfigProperties(ctx context.Context) { for key, meta := range configPropertyMetas { property, ex := mainConfigProperties.allConfigProperties[key] if !ex { @@ -39,13 +29,12 @@ func mergeConfigProperties() { } if property.Value == meta.DefaultValue { if meta.Masked { - // The data needs to be desensitized, and the content will not be printed - log.Warnf("config key %s is still set as default value", key) + log.WithContext(ctx).Warnf("config key %s is still set as default value", key) } else { - log.Warnf("config key %s is still set as default value:%+v", key, property.DefaultValue) + log.WithContext(ctx).Debugf("config key %s is still set as default value:%+v", key, property.DefaultValue) } } - log.Debugf("merge config meta and configfile config, config key %s", key) + log.WithContext(ctx).Debugf("merge config meta and configfile config, config key %s", key) property.DefaultValue = meta.DefaultValue property.ValueType = meta.ValueType property.Encrypted = meta.Encrypted diff --git a/config/config_property_test.go b/config/config_property_test.go index e02dbcb6f79cade6a51ca2359224d4d5ea69cc00..b480f0d5ebe9059bceb38c068f0ae352568db808 100644 --- a/config/config_property_test.go +++ b/config/config_property_test.go @@ -1,15 +1,3 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - package config import ( diff --git a/config/config_reader_writer.go b/config/config_reader_writer.go index 20f54b1b78c340ae8f974c47c7833eb8cdab11c6..d3459dfaf520351bb59ca988519260b4d0603d68 100644 --- a/config/config_reader_writer.go +++ b/config/config_reader_writer.go @@ -1,23 +1,11 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - package config import ( "bytes" + "context" "io/ioutil" "os" "path/filepath" - "reflect" "strings" "github.com/pkg/errors" @@ -25,7 +13,7 @@ import ( ) // ReloadConfigFromFiles Reload all configurations: configuration items, module configuration templates -func ReloadConfigFromFiles() error { +func ReloadConfigFromFiles(ctx context.Context) error { if mainConfigProperties == nil { return errors.Errorf("config properties meta is lost, can not reload config properties.") } @@ -33,40 +21,42 @@ func ReloadConfigFromFiles() error { return errors.Errorf("module config meta is lost, can not reload module config.") } - err := InitConfigProperties(mainConfigProperties.configPropertiesDir) + err := InitConfigProperties(ctx, mainConfigProperties.configPropertiesDir) if err != nil { - return errors.Errorf("reload config properties from path %s, err:%+v", mainConfigProperties.configPropertiesDir, err) + return errors.Errorf("reload config properties from path %s, err:%s", mainConfigProperties.configPropertiesDir, err) } - err = InitModuleConfigs(mainModuleConfig.moduleConfigDir) + err = InitModuleConfigs(ctx, mainModuleConfig.moduleConfigDir) if err != nil { - return errors.Errorf("reload module config from path %s, err:%+v", mainModuleConfig.moduleConfigDir, err) + return errors.Errorf("reload module config from path %s, err:%s", mainModuleConfig.moduleConfigDir, err) } + log.WithContext(ctx).Info("reload config success.") return nil } // InitConfigProperties Initialize configuration items and parse yaml files in the entire directory -func InitConfigProperties(configPropertiesDir string) error { +func InitConfigProperties(ctx context.Context, configPropertiesDir string) error { - tmpConfigMain, err := decodeConfigPropertiesGroups(configPropertiesDir) + tmpConfigMain, err := decodeConfigPropertiesGroups(ctx, configPropertiesDir) if err != nil { - err = errors.Errorf("decode config properties from path %s, err:%+v", configPropertiesDir, err) - log.Error(err) + err = errors.Errorf("decode config properties from path %s, err:%s", configPropertiesDir, err) + log.WithContext(ctx).Error(err) return err } mainConfigProperties = tmpConfigMain - mergeConfigProperties() + mergeConfigProperties(ctx) + log.WithContext(ctx).Info("init config properties success.") return nil } // InitModuleConfigs Initialize the module configuration: the yaml file in the entire directory will be parsed -func InitModuleConfigs(moduleConfigDir string) error { - tmpMainModuleConfig, err := decodeModuleConfigGroups(moduleConfigDir) +func InitModuleConfigs(ctx context.Context, moduleConfigDir string) error { + tmpMainModuleConfig, err := decodeModuleConfigGroups(ctx, moduleConfigDir) if err != nil { - err = errors.Errorf("decode module config from path %s, err:%+v", moduleConfigDir, err) - log.Error(err) + err = errors.Errorf("decode module config from path %s, err:%s", moduleConfigDir, err) + log.WithContext(ctx).Error(err) return err } mainModuleConfig = tmpMainModuleConfig @@ -74,10 +64,11 @@ func InitModuleConfigs(moduleConfigDir string) error { return nil } -func decodeConfigPropertiesGroups(configPropertiesDir string) (*ConfigPropertiesMain, error) { +// Parse the entire directory of configuration items: All yaml configuration files are parsed +func decodeConfigPropertiesGroups(ctx context.Context, configPropertiesDir string) (*ConfigPropertiesMain, error) { absconfigPropertiesDir, err := filepath.Abs(configPropertiesDir) if err != nil { - return nil, errors.Errorf("get absolute path of %s err:%+v", configPropertiesDir, err) + return nil, errors.Errorf("get absolute path of %s err:%s", configPropertiesDir, err) } configMain := &ConfigPropertiesMain{ ConfigGroups: make([]*ConfigPropertiesGroup, 0, 4), @@ -85,23 +76,25 @@ func decodeConfigPropertiesGroups(configPropertiesDir string) (*ConfigProperties needRestartModules: map[string]*RestartModuleKeyValues{}, configPropertiesDir: absconfigPropertiesDir, } - err = loadYamlFilesFromPath(absconfigPropertiesDir, func(filename string) error { + err = loadYamlFilesFromPath(ctx, absconfigPropertiesDir, func(filename string) error { bs, err := ioutil.ReadFile(filename) if err != nil { - return errors.Errorf("read config properties file %s, err:%+v", filename, err) + return errors.Errorf("read config properties file %s, err:%s", filename, err) } - group, err := decodeConfigPropertiesGroup(bs) + group, err := decodeConfigPropertiesGroup(ctx, bs) if err != nil { - return errors.Errorf("decode config properties from config file %s, err:%+v", filename, err) + return errors.Errorf("decode config properties from config file %s, err:%s", filename, err) } group.ConfigFile = filename - configMain.addConfigs(group) + if err := configMain.addConfigs(group); err != nil { + return err + } return nil }) return configMain, err } -func decodeConfigPropertiesGroup(bs []byte) (*ConfigPropertiesGroup, error) { +func decodeConfigPropertiesGroup(ctx context.Context, bs []byte) (*ConfigPropertiesGroup, error) { configGroup := new(ConfigPropertiesGroup) err := Decode(bytes.NewReader(bs), configGroup) if err != nil { @@ -114,45 +107,42 @@ func decodeConfigPropertiesGroup(bs []byte) (*ConfigPropertiesGroup, error) { if !ex { val, err := property.Parse(v) if err != nil { - return nil, errors.Errorf("parse config key %s, err:%+v", property.Key, err) + return nil, errors.Errorf("parse config key %s, err:%s", property.Key, err) } - log.Debugf("config key %s is not defined in sdk, use valueType %s", property.Key, property.ValueType) + log.WithContext(ctx).Infof("config key %s is not defined in sdk, use valueType %s", property.Key, property.ValueType) property.Value = val continue } if meta.ValueType != property.ValueType && property.ValueType != "" { - log.Warnf("config key %s valueType is defined as %s, not %s", property.Key, meta.ValueType, property.ValueType) - } - if property.Value == nil || reflect.ValueOf(property.Value).IsZero() { - v = meta.DefaultValue + log.WithContext(ctx).Warnf("config key %s valueType is defined as %s, not %s", property.Key, meta.ValueType, property.ValueType) } val, err := meta.Parse(v) if err != nil { - return nil, errors.Errorf("parse config key %s, err:%+v", property.Key, err) + return nil, errors.Errorf("parse config key %s, err:%s", property.Key, err) } property.Value = val } return configGroup, nil } -func decodeModuleConfigGroups(moduleConfigDir string) (*moduleConfigMain, error) { +func decodeModuleConfigGroups(ctx context.Context, moduleConfigDir string) (*moduleConfigMain, error) { absModuleConfigDir, err := filepath.Abs(moduleConfigDir) if err != nil { - return nil, errors.Errorf("get absolute path of %s err:%+v", moduleConfigDir, err) + return nil, errors.Errorf("get absolute path of %s err:%s", moduleConfigDir, err) } mainModuleConfig := &moduleConfigMain{ moduleConfigDir: absModuleConfigDir, moduleConfigGroups: make([]*ModuleConfigGroup, 0, 4), allModuleConfigs: make(map[string]ModuleConfig, 10), } - err = loadYamlFilesFromPath(absModuleConfigDir, func(filename string) error { + err = loadYamlFilesFromPath(ctx, absModuleConfigDir, func(filename string) error { bs, err := ioutil.ReadFile(filename) if err != nil { - return errors.Errorf("read module config file %s, err:%+v", filename, err) + return errors.Errorf("read module config file %s, err:%s", filename, err) } - group, err := decodeModuleConfigGroup(bs) + group, err := decodeModuleConfigGroup(ctx, bs) if err != nil { - return errors.Errorf("decode module config from config file %s, err:%+v", filename, err) + return errors.Errorf("decode module config from config file %s, err:%s", filename, err) } group.ConfigFile = filename for _, moduleConfigGroup := range group.Modules { @@ -170,35 +160,39 @@ func decodeModuleConfigGroups(moduleConfigDir string) (*moduleConfigMain, error) return mainModuleConfig, err } -func decodeModuleConfigGroup(bs []byte) (*ModuleConfigGroup, error) { +func decodeModuleConfigGroup(ctx context.Context, bs []byte) (*ModuleConfigGroup, error) { moduleConfigs := new(ModuleConfigGroup) err := Decode(bytes.NewReader(bs), moduleConfigs) return moduleConfigs, err } -func snapshotForPath(snapshotPath string, currentPath string) error { +// snapshotPath is the target directory of the snapshot. currentPath is the current configuration directory. +// You need to create a subdirectory under snapshotPath and back up files in currentPath to the subdirectory. +// If an error occurs in the middle of the process, you should keep the error for the time being and +// return to it altogether: backup all files as much as possible. +func snapshotForPath(ctx context.Context, snapshotPath string, currentPath string) error { basePath := filepath.Base(currentPath) snapshotSubPath := filepath.Join(snapshotPath, basePath) err := os.MkdirAll(snapshotSubPath, 0755) if err != nil { - err = errors.Errorf("snapshot path %s, create path %s, err:%+v", snapshotPath, snapshotSubPath, err) - log.Error(err) + err = errors.Errorf("snapshot path %s, create path %s, err:%s", snapshotPath, snapshotSubPath, err) + log.WithContext(ctx).Error(err) return err } var errs error - err = loadYamlFilesFromPath(currentPath, func(srcFilename string) error { + err = loadYamlFilesFromPath(ctx, currentPath, func(srcFilename string) error { _, filename := filepath.Split(srcFilename) bs, err := ioutil.ReadFile(srcFilename) if err != nil { - log.Errorf("snapshot path %s, read config file %s, err:%+v", snapshotPath, srcFilename, err) + log.WithContext(ctx).Errorf("snapshot path %s, read config file %s, err:%+v", snapshotPath, srcFilename, err) errs = errors.Errorf("%s ,another err:%s", errs, err) return nil } dstFilename := filepath.Join(snapshotSubPath, filename) err = ioutil.WriteFile(dstFilename, bs, 0644) if err != nil { - log.Errorf("snapshot path %s, write config file to %s to snapshot file %s, err:%+v", + log.WithContext(ctx).Errorf("snapshot path %s, write config file to %s to snapshot file %s, err:%+v", snapshotPath, srcFilename, dstFilename, err) errs = errors.Errorf("%s ,another err:%s", errs, err) return nil @@ -214,28 +208,28 @@ func snapshotForPath(snapshotPath string, currentPath string) error { return errs } -func loadYamlFilesFromPath(configPath string, operator func(filename string) error) error { - log.Infof("read config files from path %s", configPath) +func loadYamlFilesFromPath(ctx context.Context, configPath string, operator func(filename string) error) error { + log.WithContext(ctx).Infof("read config files from path %s", configPath) files, err := ioutil.ReadDir(configPath) if err != nil { - return errors.Errorf("read config files from dir:%s, err:%+v", configPath, err) + return errors.Errorf("read config files from dir:%s, err:%s", configPath, err) } - log.Infof("read config from path %s, files length %d", configPath, len(files)) + log.WithContext(ctx).Infof("read config from path %s, files length %d", configPath, len(files)) if len(files) <= 0 { return errors.Errorf("no config file exists in path %s", configPath) } for _, file := range files { - log.Debugf("read config from path %s, file %s", configPath, file.Name()) + log.WithContext(ctx).Debugf("read config from path %s, file %s", configPath, file.Name()) if !isYamlFile(file) { - log.Debugf("read config from path %s, file %s is not yaml, skip it.", configPath, file) + log.WithContext(ctx).Debugf("read config from path %s, file %s is not yaml, skip it.", configPath, file.Name()) continue } filename := filepath.Join(configPath, file.Name()) err := operator(filename) if err != nil { - return errors.Errorf("decode config file %s err:%+v", filename, err) + return errors.Errorf("decode config file %s err:%s", filename, err) } } return nil diff --git a/config/config_reader_writer_test.go b/config/config_reader_writer_test.go new file mode 100644 index 0000000000000000000000000000000000000000..8fb6011ae6754f7b317c92aab2954c6507fcdb2c --- /dev/null +++ b/config/config_reader_writer_test.go @@ -0,0 +1,98 @@ +package config + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDecodeConfig(t *testing.T) { + configContent := `configVersion: "" +configs: + - key: string.not.empty + value: foo + valueType: string + - key: string.empty + value: + valueType: string + - key: bool.true + value: true + valueType: bool + - key: bool.empty + value: + valueType: bool + - key: bool.false + value: false + valueType: bool + - key: bool.false.default-true + value: false + valueType: bool + - key: int64.not.zore + value: 200 + valueType: int64 + - key: int64.empty + value: + valueType: int64 + - key: int64.zore + value: 0 + valueType: int64` + configPropertyMetas["string.not.empty"] = &ConfigProperty{ + DefaultValue: "bar", + ValueType: ValueString, + } + configPropertyMetas["string.empty"] = &ConfigProperty{ + DefaultValue: "bar for empty", + ValueType: ValueString, + } + configPropertyMetas["bool.true"] = &ConfigProperty{ + DefaultValue: "false", + ValueType: ValueBool, + } + configPropertyMetas["bool.empty"] = &ConfigProperty{ + DefaultValue: "false", + ValueType: ValueBool, + } + configPropertyMetas["bool.false"] = &ConfigProperty{ + DefaultValue: "false", + ValueType: ValueBool, + } + configPropertyMetas["bool.false.default-true"] = &ConfigProperty{ + DefaultValue: "true", + ValueType: ValueBool, + } + configPropertyMetas["int64.not.zore"] = &ConfigProperty{ + DefaultValue: "0", + ValueType: ValueInt64, + } + configPropertyMetas["int64.empty"] = &ConfigProperty{ + DefaultValue: "1", + ValueType: ValueInt64, + } + configPropertyMetas["int64.zore"] = &ConfigProperty{ + DefaultValue: "1", + ValueType: ValueInt64, + } + asserts := map[string]interface{}{ + "string.not.empty": "foo", + "string.empty": "", + "bool.true": true, + "bool.empty": false, + "bool.false": false, + "bool.false.default-true": false, + "int64.not.zore": int64(200), + "int64.empty": int64(0), + "int64.zore": int64(0), + } + group, err := decodeConfigPropertiesGroup(context.Background(), []byte(configContent)) + assert.Nil(t, err) + configs := group.Configs + for _, config := range configs { + ass, ex := asserts[config.Key] + if !ex { + continue + } + assert.Equalf(t, ass, config.Val(), "key %s", config.Key) + assert.Equalf(t, ass, config.Value, "key %s", config.Key) + } +} diff --git a/config/config_verify_save.go b/config/config_verify_save.go index 4971c9d63a2fdfdec29ce03c32ec4cba4f53c92b..bbc29b3092a6079bb181f09e3d10a987e3f726e0 100644 --- a/config/config_verify_save.go +++ b/config/config_verify_save.go @@ -1,15 +1,3 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - package config import ( @@ -17,23 +5,17 @@ import ( "fmt" "path/filepath" "strings" - "time" "github.com/pkg/errors" log "github.com/sirupsen/logrus" + + "github.com/oceanbase/obagent/lib/mask" ) -// ConfigVersion Configuration version number type ConfigVersion struct { ConfigVersion string } -func generateNewConfigVersion() *ConfigVersion { - return &ConfigVersion{ - ConfigVersion: time.Now().Format("2006-01-02T15:04:05.9999Z07:00"), - } -} - type KeyValue struct { Key string `json:"key"` Value interface{} `json:"value"` @@ -55,7 +37,7 @@ type NotifyModuleConfig struct { UpdatedKeyValues map[string]interface{} `json:"updatedKeyValues"` } -func UpdateConfig(ctx context.Context, kvs *KeyValues, async bool) (*ConfigVersion, error) { +func UpdateConfig(ctx context.Context, kvs *KeyValues) (*ConfigVersion, error) { keyValues := make(map[string]interface{}) for _, kv := range kvs.Configs { keyValues[kv.Key] = kv.Value @@ -66,57 +48,96 @@ func UpdateConfig(ctx context.Context, kvs *KeyValues, async bool) (*ConfigVersi result, err := verifyAndSaveConfig(ctx, keyValues) if err != nil { - log.WithContext(ctx).Errorf("VerifyAndSaveConfig err:%+v", err) + log.WithContext(ctx).Errorf("VerifyAndSaveConfig err:%s", err) return nil, err } - notifyFunc := func() error { - err := NotifyModuleConfigs(ctx, result) - if err != nil { - ctxlog.Errorf("update module config failed, config version:%s, changed modules length:%d, err:%+v", - result.ConfigVersion.ConfigVersion, len(result.UpdatedConfigs), err) - } + notifyErr := NotifyModuleConfigs(ctx, result) + if notifyErr != nil { + ctxlog.Errorf("update module config failed, config version:%s, changed modules length:%d, err:%+v", + result.ConfigVersion.ConfigVersion, len(result.UpdatedConfigs), notifyErr) + } + + return result.ConfigVersion, nil +} + +func UpdateConfigPairs(ctx context.Context, pairs []string) error { + maskedPairs := mask.MaskSlice(pairs) + log.WithContext(ctx).WithField("pairs", maskedPairs).Info("update module configs") + kvs, err := getKeyValues(pairs) + if err != nil { + log.WithContext(ctx).Errorf("getKeyValues, err:%+v", err) return err } - if async { - go notifyFunc() - } else { - err := notifyFunc() - return nil, err + + configVersion, err := UpdateConfig(context.Background(), &KeyValues{Configs: kvs}) + if err != nil { + log.WithContext(ctx).Errorf("update config, err:%+v", err) + return err } + log.WithContext(ctx).Infof("update config success, version:%+v", configVersion) + return nil +} - return result.ConfigVersion, nil +func ValidateConfigPairs(ctx context.Context, pairs []string) error { + maskedPairs := mask.MaskSlice(pairs) + log.WithContext(ctx).WithField("pairs", maskedPairs).Info("validate module configs") + kvs, err := getKeyValues(pairs) + if err != nil { + log.WithContext(ctx).Errorf("getKeyValues, err:%+v", err) + return err + } + + return ValidateConfigKeyValues(ctx, kvs) +} + +func ValidateConfigKeyValues(ctx context.Context, kvs []KeyValue) error { + log.WithContext(ctx).Info("validate module configs") + + configMain, err := decodeConfigPropertiesGroups(ctx, mainConfigProperties.configPropertiesDir) + if err != nil { + err = errors.Errorf("decode config properties from path %s, err:%s", mainConfigProperties.configPropertiesDir, err) + log.WithContext(ctx).Error(err) + return err + } + for _, kv := range kvs { + property, ex := configMain.allConfigProperties[kv.Key] + if !ex { + return errors.Errorf("key %s is not exist in config.", kv.Key) + } + val := fmt.Sprintf("%+v", property.Val()) + if val != kv.Value { + if !property.Masked { + log.WithContext(ctx).Warnf("key %s is not equal with config, current value is %+v", kv.Key, val) + } + return errors.Errorf("key %s is not equal with config.", kv.Key) + } + } + + return nil } -func UpdateConfigPairs(pairs []string) error { - log.WithField("pairs", pairs).Debugf("update module configs") +func getKeyValues(pairs []string) ([]KeyValue, error) { kvs := make([]KeyValue, 0, len(pairs)) for _, pair := range pairs { key, value, err := parseKeyValue(pair) if err != nil { - log.Errorf("parse pair %s, err:%+v", pair, err) - return err + log.Errorf("parse pair %s, err:%+v", mask.Mask(pair), err) + return nil, err } kvs = append(kvs, KeyValue{ Key: key, Value: value, }) } - log.WithField("key-values", kvs).Debugf("update module configs") - configVersion, err := UpdateConfig(context.Background(), &KeyValues{Configs: kvs}, true) - if err != nil { - log.Errorf("update config, err:%+v", err) - return err - } - log.Infof("update config success, version:%+v", configVersion) - return nil + return kvs, nil } func parseKeyValue(pairStr string) (string, string, error) { pair := strings.Split(pairStr, "=") if len(pair) != 2 { - return "", "", errors.Errorf("key-value pair %s is not a valid key=value formatted.", pairStr) + return "", "", errors.Errorf("key-value pair %s is not a valid key=value formatted.", mask.Mask(pairStr)) } return pair[0], pair[1], nil } @@ -147,7 +168,7 @@ func verifyAndSaveConfig(ctx context.Context, keyValues map[string]interface{}) } if len(diffkvs) <= 0 { currentVersion := GetCurrentConfigVersion() - log.Infof("configs do not change, no need to modify, current config version:%+v.", currentVersion) + ctxlog.Infof("configs do not change, no need to modify, current config version:%+v.", currentVersion) return &VerifyConfigResult{ ConfigVersion: currentVersion, }, nil @@ -161,12 +182,12 @@ func verifyAndSaveConfig(ctx context.Context, keyValues map[string]interface{}) return nil, err } - configVersion, err := saveIncrementalConfig(diffkvs) + configVersion, err := saveIncrementalConfig(ctx, diffkvs) if err != nil { ctxlog.WithFields(log.Fields{ "configVersion": configVersion, "updated configs": MaskedKeyValues(diffkvs), - }).Errorf("save config failed, please try again later or contant administrator. err:%+v", err) + }).Errorf("save config failed, please try again later or content administrator. err:%+v", err) return nil, err } @@ -210,7 +231,7 @@ func getUpdatedConfigs(ctx context.Context, diffkvs map[string]interface{}) ([]* return nil, err } changedConfig := &NotifyModuleConfig{ - Process: Process(sample.Process), + Process: sample.Process, Module: module, Config: finalConf, } @@ -219,7 +240,7 @@ func getUpdatedConfigs(ctx context.Context, diffkvs map[string]interface{}) ([]* } ctxlog.WithField("change config modules", moduleKeys).Info("changed module") if len(changedConfigs) <= 0 { - return nil, errors.Errorf("no config changed") + return changedConfigs, nil } for _, conf := range changedConfigs { moduleChangedKeys, ex := moduleKeys[conf.Module] @@ -240,23 +261,24 @@ func getUpdatedConfigs(ctx context.Context, diffkvs map[string]interface{}) ([]* return changedConfigs, nil } -func saveIncrementalConfig(kvs map[string]interface{}) (*ConfigVersion, error) { - return mainConfigProperties.saveIncrementalConfig(kvs) +func saveIncrementalConfig(ctx context.Context, kvs map[string]interface{}) (*ConfigVersion, error) { + return mainConfigProperties.saveIncrementalConfig(ctx, kvs) } -func snapshotForConfigVersion(configVersion string) error { +func snapshotForConfigVersion(ctx context.Context, configVersion string) error { snapshotPath := filepath.Join(filepath.Dir(mainConfigProperties.configPropertiesDir), configVersion) var errs error - err := snapshotForPath(snapshotPath, mainConfigProperties.configPropertiesDir) + err := snapshotForPath(ctx, snapshotPath, mainConfigProperties.configPropertiesDir) if err != nil { - log.Errorf("save config snapshot %s err:%+v", configVersion, err) + log.WithContext(ctx).Errorf("save config snapshot %s err:%+v", configVersion, err) errs = errors.Errorf("%s, err:%s", errs, err) } - err = snapshotForPath(snapshotPath, mainModuleConfig.moduleConfigDir) + err = snapshotForPath(ctx, snapshotPath, mainModuleConfig.moduleConfigDir) if err != nil { - log.Errorf("save module config snapshot %s to dir %s err:%+v", configVersion, snapshotPath, err) + log.WithContext(ctx).Errorf("save module config snapshot %s to dir %s err:%+v", configVersion, snapshotPath, err) errs = errors.Errorf("%s, err:%s", errs, err) } + configMetaBackupWorker.checkOnce(ctx) return errs } diff --git a/config/config_verify_save_test.go b/config/config_verify_save_test.go index 5072295208a6799e242014e47f5344fbae3bb734..cb81ed9fddf5e8b6a1716b5857561ecb0b625bed 100644 --- a/config/config_verify_save_test.go +++ b/config/config_verify_save_test.go @@ -1,15 +1,3 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - package config import ( @@ -34,7 +22,7 @@ func TestSaveConfigSnapshot(t *testing.T) { t.Run("save config snapshot with foo (path not exist before save)", func(t *testing.T) { os.RemoveAll(snapshotPath) - err := snapshotForConfigVersion(configVersion) + err := snapshotForConfigVersion(context.Background(), configVersion) Convey("saveConfigSnapshot", t, func() { So(err, ShouldBeNil) }) @@ -45,7 +33,7 @@ func TestSaveConfigSnapshot(t *testing.T) { So(version.ConfigVersion, ShouldEqual, configVersion) }) - tmpModuleConfigs, err := decodeModuleConfigGroups(filepath.Join(snapshotPath, "module_config")) + tmpModuleConfigs, err := decodeModuleConfigGroups(context.Background(), filepath.Join(snapshotPath, "module_config")) Convey("DecodeModuleConfig", t, func() { So(err, ShouldBeNil) assert.NotSame(t, mainModuleConfig.allModuleConfigs, tmpModuleConfigs.allModuleConfigs) @@ -76,7 +64,7 @@ func TestSaveConfigSnapshot(t *testing.T) { }) t.Run("save config snapshot with foo (path already exist before save)", func(t *testing.T) { - err := snapshotForConfigVersion(configVersion) + err := snapshotForConfigVersion(context.Background(), configVersion) Convey("saveConfigSnapshot", t, func() { So(err, ShouldBeNil) }) @@ -94,14 +82,14 @@ func TestSaveConfigProperties(t *testing.T) { "foo.bar.duration": "100h", "key-not-exist": "xxx", } - configVersion, err := saveIncrementalConfig(diffKVs) + configVersion, err := saveIncrementalConfig(context.Background(), diffKVs) Convey("saveIncrementalConfig", t, func() { So(err, ShouldBeNil) }) log.Infof("configVersion %s", configVersion) properties := GetConfigPropertiesKeyValues() - Convey("GetConfigProtertiesKeyValues", t, func() { + Convey("GetConfigPropertiesKeyValues", t, func() { So(properties["foo.foo"], ShouldEqual, "diff-foo") So(properties["foo.bar.duration"], ShouldEqual, "100h") _, ex := properties["key-not-exist"] @@ -110,13 +98,13 @@ func TestSaveConfigProperties(t *testing.T) { }) t.Run("reload config", func(t *testing.T) { - err := ReloadConfigFromFiles() + err := ReloadConfigFromFiles(context.Background()) Convey("ReloadConfigFromFiles", t, func() { So(err, ShouldBeNil) }) properties := GetConfigPropertiesKeyValues() - Convey("GetConfigProtertiesKeyValues", t, func() { + Convey("GetConfigPropertiesKeyValues", t, func() { So(properties["foo.foo"], ShouldEqual, "diff-foo") So(properties["foo.bar.duration"], ShouldEqual, "100h") _, ex := properties["key-not-exist"] @@ -140,6 +128,7 @@ func TestGetUpdatedConfigs(t *testing.T) { So(err, ShouldBeNil) So(len(updatedModules), ShouldEqual, 1) So(updatedModules[0].Module, ShouldEqual, testFooModule) + So(updatedModules[0].Process, ShouldEqual, ProcessManagerAgent) So(reflect.DeepEqual(updatedModules[0].UpdatedKeyValues, map[string]interface{}{ "foo.foo": "diff-foo", "foo.bar.duration": "100h", @@ -162,6 +151,7 @@ func TestVerifyAndSaveConfig(t *testing.T) { So(err, ShouldBeNil) So(len(result.UpdatedConfigs), ShouldEqual, 1) So(result.UpdatedConfigs[0].Module, ShouldEqual, testFooModule) + So(result.UpdatedConfigs[0].Process, ShouldEqual, ProcessManagerAgent) So(reflect.DeepEqual(result.UpdatedConfigs[0].UpdatedKeyValues, map[string]interface{}{ "foo.foo": "diff-foo", "foo.bar.duration": "100h", @@ -169,7 +159,7 @@ func TestVerifyAndSaveConfig(t *testing.T) { }) properties := GetConfigPropertiesKeyValues() - Convey("GetConfigProtertiesKeyValues", t, func() { + Convey("GetConfigPropertiesKeyValues", t, func() { So(properties["foo.foo"], ShouldEqual, "diff-foo") So(properties["foo.bar.duration"], ShouldEqual, "100h") _, ex := properties["key-not-exist"] @@ -217,19 +207,43 @@ func TestReloadConfigFromFiles_Fail(t *testing.T) { t.Run("reload config from no exist file", func(t *testing.T) { noExistPath := filepath.Join(tempDir, "no-exist-path") mainModuleConfig.moduleConfigDir = noExistPath - err := ReloadConfigFromFiles() + err := ReloadConfigFromFiles(context.Background()) Convey("ReloadConfigFromFiles", t, func() { So(err, ShouldNotBeNil) }) mainConfigProperties.configPropertiesDir = noExistPath - err = ReloadConfigFromFiles() + err = ReloadConfigFromFiles(context.Background()) Convey("ReloadConfigFromFiles", t, func() { So(err, ShouldNotBeNil) }) }) } +func TestValidateConfig(t *testing.T) { + _init(t) + defer cleanup() + + t.Run("validate config success", func(t *testing.T) { + err := ValidateConfigPairs(context.Background(), []string{"foo.foo=foo_value", "foo.bar.bar=3306"}) + Convey("ValidateConfigPairs", t, func() { + So(err, ShouldBeNil) + }) + }) + + t.Run("validate config failed", func(t *testing.T) { + err := ValidateConfigPairs(context.Background(), []string{"foo.foo=wrong-value"}) + Convey("ValidateConfigPairs with wrong value", t, func() { + So(err, ShouldNotBeNil) + }) + + err2 := ValidateConfigPairs(context.Background(), []string{"key-not-exist=foo"}) + Convey("ValidateConfigPairs with wrong value", t, func() { + So(err2, ShouldNotBeNil) + }) + }) +} + func Test_parseKeyValue(t *testing.T) { tests := []struct { name string diff --git a/config/config_version.go b/config/config_version.go new file mode 100644 index 0000000000000000000000000000000000000000..67a6567a5594d671215af826b6fe3913d3bd0f2f --- /dev/null +++ b/config/config_version.go @@ -0,0 +1,119 @@ +package config + +import ( + "context" + "io/ioutil" + "os" + "path/filepath" + "sort" + "sync/atomic" + "time" + + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" +) + +const ( + configVersionFormat = "2006-01-02T15:04:05.9999Z07:00" +) + +var configMetaBackupWorker *checkConfigVersionBackupWorker + +func init() { + configMetaBackupWorker = &checkConfigVersionBackupWorker{} +} + +type ConfigMetaBackup struct { + MaxBackups int64 `yaml:"maxbackups"` +} + +func SetConfigMetaModuleConfigNotify(ctx context.Context, conf ConfigMetaBackup) error { + if conf.MaxBackups < 0 { + return errors.Errorf("maxbuckups must be bigger than 0.") + } + log.WithContext(ctx).Infof("receive version backup conf: %+v", conf) + configMetaBackupWorker.configMetaBackup = conf + + atomic.StoreInt32(&configMetaBackupWorker.working, 1) + + go configMetaBackupWorker.checkOnce(ctx) + return nil +} + +type checkConfigVersionBackupWorker struct { + working int32 + + configMetaBackup ConfigMetaBackup +} + +func (worker *checkConfigVersionBackupWorker) checkOnce(ctx context.Context) { + if atomic.LoadInt32(&worker.working) <= 0 { + log.WithContext(ctx).Infof("config version backup worker is not working in current process.") + return + } + log.WithContext(ctx).Info("check config version backups once.") + configDir := filepath.Dir(mainConfigProperties.configPropertiesDir) + err := checkConfigVersionBackups(ctx, int(worker.configMetaBackup.MaxBackups), configDir) + if err != nil { + log.WithContext(ctx).Error(err) + } +} + +func generateNewConfigVersion() *ConfigVersion { + return &ConfigVersion{ + ConfigVersion: time.Now().Format(configVersionFormat), + } +} + +func isConfigVersionDir(file os.FileInfo) (bool, time.Time) { + if !file.IsDir() { + return false, time.Now() + } + t, err := time.Parse(configVersionFormat, file.Name()) + return err == nil, t +} + +type configVersionFile struct { + fileName string + version time.Time +} + +func checkConfigVersionBackups(ctx context.Context, versionStayCount int, configPath string) error { + files, err := ioutil.ReadDir(configPath) + if err != nil { + return errors.Errorf("read config files from dir:%s, err:%s", configPath, err) + } + log.WithContext(ctx).Infof("read config from path %s, files length %d", configPath, len(files)) + if len(files) <= 0 { + log.WithContext(ctx).Infof("no config file exists in path %s", configPath) + return nil + } + configVersionFiles := make([]configVersionFile, 0, 20) + for _, file := range files { + log.WithContext(ctx).Debugf("read config from path %s, file %s", configPath, file.Name()) + if ok, tm := isConfigVersionDir(file); ok { + configVersionFiles = append(configVersionFiles, configVersionFile{ + fileName: filepath.Join(configPath, file.Name()), + version: tm, + }) + } + } + if len(configVersionFiles) <= versionStayCount { + log.WithContext(ctx).Infof("config stay count:%d, config version count:%d, no need to rotate.", versionStayCount, len(configVersionFiles)) + return nil + } + + sort.Slice(configVersionFiles, func(i, j int) bool { + return configVersionFiles[i].version.After(configVersionFiles[j].version) + }) + + for i := versionStayCount; i < len(configVersionFiles); i++ { + log.WithContext(ctx).Infof("remove config version file:%s", configVersionFiles[i].fileName) + err := os.RemoveAll(configVersionFiles[i].fileName) + if err != nil { + log.WithContext(ctx).Errorf("remove config file %s failed, err:%+v", configVersionFiles[i].fileName, err) + } + } + + return nil +} diff --git a/config/config_version_test.go b/config/config_version_test.go new file mode 100644 index 0000000000000000000000000000000000000000..9160e284e308b3b1755edd3726753a2245d1cfcd --- /dev/null +++ b/config/config_version_test.go @@ -0,0 +1,77 @@ +package config + +import ( + "context" + "os" + "path/filepath" + "testing" + "time" + + "github.com/sirupsen/logrus" + log "github.com/sirupsen/logrus" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestVersion(t *testing.T) { + log.SetLevel(logrus.DebugLevel) + + tempDir := _init(t) + defer cleanup() + + configVersion1 := generateNewConfigVersion() + snapshotPath1 := filepath.Join(tempDir, configVersion1.ConfigVersion) + t.Run("save config snapshot version 1", func(t *testing.T) { + os.RemoveAll(snapshotPath1) + + err := snapshotForConfigVersion(context.Background(), configVersion1.ConfigVersion) + Convey("saveConfigSnapshot", t, func() { + So(err, ShouldBeNil) + }) + }) + + time.Sleep(time.Millisecond) + + configVersion2 := generateNewConfigVersion() + snapshotPath2 := filepath.Join(tempDir, configVersion2.ConfigVersion) + t.Run("save config snapshot version 2", func(t *testing.T) { + os.RemoveAll(snapshotPath2) + + err := snapshotForConfigVersion(context.Background(), configVersion2.ConfigVersion) + Convey("saveConfigSnapshot", t, func() { + So(err, ShouldBeNil) + }) + }) + + t.Run("rotate config version", func(t *testing.T) { + err := checkConfigVersionBackups(context.Background(), 2, tempDir) + Convey("rotate 2 with 2 versions", t, func() { + So(err, ShouldBeNil) + }) + + err2 := checkConfigVersionBackups(context.Background(), 1, tempDir) + Convey("rotate 1 with 2 versions", t, func() { + So(err2, ShouldBeNil) + + _, version1Err := os.Stat(snapshotPath1) + _, version2Err := os.Stat(snapshotPath2) + So(version1Err, ShouldNotBeNil) + So(version2Err, ShouldBeNil) + }) + + }) + + t.Run("rotate config version", func(t *testing.T) { + SetConfigMetaModuleConfigNotify(context.Background(), ConfigMetaBackup{ + MaxBackups: 0, + }) + configMetaBackupWorker.checkOnce(context.Background()) + Convey("rotate 0 with 1 versions", t, func() { + _, version1Err := os.Stat(snapshotPath1) + _, version2Err := os.Stat(snapshotPath2) + So(version1Err, ShouldNotBeNil) + So(version2Err, ShouldNotBeNil) + }) + }) + +} diff --git a/config/mgragent/cleaner_config.go b/config/mgragent/cleaner_config.go new file mode 100644 index 0000000000000000000000000000000000000000..b214dd04d280f8d0ea74ad5100acfbe1399a8a14 --- /dev/null +++ b/config/mgragent/cleaner_config.go @@ -0,0 +1,37 @@ +package mgragent + +import "time" + +type Rule struct { + // FileRegex Matches the regular expression of the file + FileRegex string `json:"fileRegex" yaml:"fileRegex"` + // RetentionDays Retention days + RetentionDays uint64 `json:"retentionDays" yaml:"retentionDays"` + // KeepPercentage Retention ratio, unit percentage, range: [0,100] + KeepPercentage uint64 `json:"keepPercentage" yaml:"keepPercentage"` +} + +type LogCleanerRules struct { + // LogName Name of the log to be cleared + LogName string `json:"logName" yaml:"logName"` + // Path Log directory address + Path string `json:"path" yaml:"path"` + // DiskThreshold Disk clearing threshold (unit percentage) Range: [0,100] + DiskThreshold uint64 `json:"diskThreshold" yaml:"diskThreshold"` + // Rules Cleaning rule + Rules []*Rule `json:"rules" yaml:"rules"` +} + +// CleanerConfig Save the configuration of the cleaner +type CleanerConfig struct { + LogCleaners []*LogCleanerRules `json:"logCleaners" yaml:"logCleaners"` +} + +// ObCleaner Clear logs +type ObCleanerConfig struct { + // RunInterval Running interval + RunInterval time.Duration `json:"runInterval" yaml:"runInterval"` + Enabled bool `json:"enabled" yaml:"enabled"` + // CleanerConf The configuration required to run + CleanerConf *CleanerConfig `json:"cleanerConfig" yaml:"cleanerConfig"` +} diff --git a/config/mgragent/config_manager.go b/config/mgragent/config_manager.go new file mode 100644 index 0000000000000000000000000000000000000000..214e7fb72ee63e4f5be5846da885d2e78409447b --- /dev/null +++ b/config/mgragent/config_manager.go @@ -0,0 +1,249 @@ +package mgragent + +import ( + "context" + "fmt" + "io/ioutil" + "os" + "path/filepath" + + log "github.com/sirupsen/logrus" + "gopkg.in/yaml.v3" + + "github.com/oceanbase/obagent/config" + "github.com/oceanbase/obagent/lib/file" +) + +type Manager struct { + config ManagerConfig + + //propertyDefinitions map[string][]*ConfigProperty + // + //propertyValues map[string][]interface{} + //lock sync.RWMutex + //configGroups []*ConfigPropertiesGroup + //allConfigProperties map[string]*ConfigProperty + //needRestartModules map[string]*RestartModuleKeyValues + //moduleConfigGroups []*ModuleConfigGroup + //allModuleConfigs map[string]ModuleConfig +} + +type ManagerConfig struct { + ModuleConfigDir string + ConfigPropertiesDir string + //CryptoPath string + //CryptoMethod string +} + +var GlobalConfigManager *Manager +var fileImpl = &file.FileImpl{} + +func NewManager(config ManagerConfig) *Manager { + return &Manager{ + config: config, + } +} + +type ModuleConfigUpdateOp string + +const ( + ModuleConfigSet ModuleConfigUpdateOp = "set" + ModuleConfigDel ModuleConfigUpdateOp = "delete" + ModuleConfigRestore ModuleConfigUpdateOp = "restore" +) + +type ModuleConfigChange struct { + Operation ModuleConfigUpdateOp `json:"operation"` + FileName string `json:"fileName"` + Content string `json:"content"` +} + +type ModuleConfigChangeRequest struct { + ModuleConfigChanges []ModuleConfigChange `json:"moduleConfigChanges"` + Reload bool `json:"reload"` +} + +// ChangeModuleConfigs change module_config files +// req contains multiple change operation. +// set means write a file to module_config dir. +// del means delete a file from module_config dir. +func (m *Manager) ChangeModuleConfigs(ctx context.Context, req *ModuleConfigChangeRequest) ([]string, error) { + var changedFileNames []string + if req == nil || len(req.ModuleConfigChanges) == 0 { + log.WithContext(ctx).Info("no module configs to changes") + return changedFileNames, nil + } + log.WithContext(ctx).Infof("%d files to change", len(req.ModuleConfigChanges)) + fileMap := make(map[string]bool) + for _, change := range req.ModuleConfigChanges { + if _, ok := fileMap[change.FileName]; ok { + return nil, fmt.Errorf("fileName '%s' in changes duplicated", change.FileName) + } + fileMap[change.FileName] = true + } + + var toApply []ModuleConfigChange + for _, change := range req.ModuleConfigChanges { + if change.Operation == ModuleConfigSet { + err := m.validateYaml(change.Content) + if err != nil { + return nil, fmt.Errorf("validate new content of '%s' failed: %v", change.FileName, err) + } + changed, err := m.changed(change.FileName, change.Content) + if err != nil { + return nil, fmt.Errorf("check content changed of '%s' failed: %v", change.FileName, err) + } + if !changed { + log.WithContext(ctx).Infof("content of '%s' not changed, nothing to do", change.FileName) + continue + } + err = m.backupFile(ctx, change) + if err != nil { + return nil, err + } + } else if change.Operation == ModuleConfigDel { + origExists, err := m.originExists(change.FileName) + if err != nil { + return nil, fmt.Errorf("check origin file '%s' exists failed: %v", change.FileName, err) + } + if !origExists { + log.WithContext(ctx).Infof("file '%s' not exists, nothing to do", change.FileName) + continue + } + err = m.backupFile(ctx, change) + if err != nil { + return nil, err + } + } else if change.Operation == ModuleConfigRestore { + backupExists, err := m.backupExists(change.FileName) + if err != nil { + return nil, fmt.Errorf("check backup file '%s' exists failed %v", change.FileName, err) + } + if !backupExists { + log.WithContext(ctx).Infof("backup file for '%s' not exists, nothing to do", change.FileName) + continue + } + } else { + return nil, fmt.Errorf("invalid change operation: %s", change.Operation) + } + toApply = append(toApply, change) + } + for _, change := range toApply { + filePath := m.moduleConfigFilePath(change.FileName) + var err error + if change.Operation == ModuleConfigSet { + log.WithContext(ctx).Infof("setting module config file %s", change.FileName) + err = ioutil.WriteFile(filePath, []byte(change.Content), 0644) + } else if change.Operation == ModuleConfigDel { + log.WithContext(ctx).Infof("deleting module config file %s", change.FileName) + err = m.deleteFile(ctx, change.FileName) + } else if change.Operation == ModuleConfigRestore { + err = m.restoreBackupFile(ctx, change.FileName) + } + if err != nil { + return changedFileNames, err + } + changedFileNames = append(changedFileNames, change.FileName) + } + return changedFileNames, nil +} + +func (m *Manager) changed(fileName string, newContent string) (bool, error) { + origFile := m.moduleConfigFilePath(fileName) + bytesContent, err := ioutil.ReadFile(origFile) + if err != nil { + if os.IsNotExist(err) { + return true, nil + } + return false, err + } + return string(bytesContent) != newContent, nil +} + +func (m *Manager) deleteFile(ctx context.Context, fileName string) error { + origFile := m.moduleConfigFilePath(fileName) + err := os.Remove(origFile) + if err != nil && os.IsNotExist(err) { + // Allows to delete nonexistent files + log.WithContext(ctx).Warnf("delete module config file '%s' got file not exists: %v", fileName, err) + return nil + } + return err +} + +func (m *Manager) backupFile(ctx context.Context, change ModuleConfigChange) error { + origFile := m.moduleConfigFilePath(change.FileName) + backupFile := m.moduleConfigBackupFilePath(change.FileName) + backupExists, err := m.backupExists(change.FileName) + if err != nil { + return fmt.Errorf("check backup file %s exists failed %v", backupFile, err) + } + origExists, err := m.originExists(change.FileName) + if err != nil { + return fmt.Errorf("check origin file %s exists failed %v", origFile, err) + } + if !backupExists { + if origExists { + log.WithContext(ctx).Infof("backup module config file %s", change.FileName) + err = fileImpl.CopyFile(origFile, backupFile, 0644) + } else { + log.WithContext(ctx).Infof("create empty backup module config file %s", change.FileName) + err = ioutil.WriteFile(backupFile, []byte{}, 0644) + + } + if err != nil { + return err + } + } + return nil +} + +func (m *Manager) backupExists(fileName string) (bool, error) { + backupFile := m.moduleConfigBackupFilePath(fileName) + return fileImpl.FileExists(backupFile) +} + +func (m *Manager) originExists(fileName string) (bool, error) { + origFile := m.moduleConfigFilePath(fileName) + return fileImpl.FileExists(origFile) +} + +func (m *Manager) restoreBackupFile(ctx context.Context, fileName string) error { + backupFile := m.moduleConfigBackupFilePath(fileName) + targetFile := m.moduleConfigFilePath(fileName) + content, err := ioutil.ReadFile(backupFile) + if err != nil { + return err + } + if len(content) == 0 { + // An empty backup file indicates that the configuration file does not exist. restore should delete it + err = os.Remove(targetFile) + if err == nil { + err = os.Remove(backupFile) + } + } else { + log.WithContext(ctx).Infof("restoring module config file %s", fileName) + err = os.Rename(backupFile, targetFile) + } + if err != nil { + return err + } + return nil +} + +func (m *Manager) ReloadModuleConfigs(ctx context.Context) error { + return config.InitModuleConfigs(ctx, m.config.ModuleConfigDir) +} + +func (m *Manager) moduleConfigFilePath(fileName string) string { + return filepath.Join(m.config.ModuleConfigDir, fileName) +} + +func (m *Manager) moduleConfigBackupFilePath(fileName string) string { + return m.moduleConfigFilePath(fileName) + ".bak" +} + +func (m *Manager) validateYaml(content string) error { + var t = &yaml.Node{} + return yaml.Unmarshal([]byte(content), &t) +} diff --git a/config/mgragent/config_manager_test.go b/config/mgragent/config_manager_test.go new file mode 100644 index 0000000000000000000000000000000000000000..5fdee804d069b3d4dd509a3ec2b285337494f885 --- /dev/null +++ b/config/mgragent/config_manager_test.go @@ -0,0 +1,241 @@ +package mgragent + +import ( + "context" + "io/ioutil" + "path/filepath" + "testing" + + "github.com/oceanbase/obagent/tests/testutil" +) + +func TestValidateYaml(t *testing.T) { + m := &Manager{} + err := m.validateYaml(` +test: + - a: 1 + - a: 2 +`) + if err != nil { + t.Errorf("should success") + } + + err = m.validateYaml(` +test~ +xxxxx +12345$ +`) + if err == nil { + t.Errorf("should fail") + } + + err = m.validateYaml(``) + if err != nil { + t.Errorf("should success") + } +} + +func TestNewChangeModuleConfigs(t *testing.T) { + testutil.MakeDirs() + defer testutil.DelTestFiles() + + m := NewManager(ManagerConfig{ + ModuleConfigDir: testutil.ModuleConfigDir, + }) + ctx := context.Background() + req := &ModuleConfigChangeRequest{ + ModuleConfigChanges: []ModuleConfigChange{ + { + Operation: ModuleConfigSet, + FileName: "new_file.yaml", + Content: ` +test: + k1: v1 + k2: v2 +`, + }, + }, + } + changed, err := m.ChangeModuleConfigs(ctx, req) + if err != nil { + t.Error(err) + } + if len(changed) != 1 && changed[0] != "new_file.yaml" { + t.Errorf("changed files wrong") + } + content, err := ioutil.ReadFile(filepath.Join(m.config.ModuleConfigDir, "new_file.yaml")) + if err != nil { + t.Error(err) + } + if string(content) != req.ModuleConfigChanges[0].Content { + t.Errorf("content wrong") + } + content, err = ioutil.ReadFile(filepath.Join(m.config.ModuleConfigDir, "new_file.yaml.bak")) + if err != nil { + t.Error(err) + } + if string(content) != "" { + t.Errorf("content wrong") + } + + changed, err = m.ChangeModuleConfigs(ctx, &ModuleConfigChangeRequest{ + ModuleConfigChanges: []ModuleConfigChange{ + { + Operation: ModuleConfigRestore, + FileName: "new_file.yaml", + }, + }, + }) + if err != nil { + t.Error(err) + } + if testutil.FileExists(filepath.Join(m.config.ModuleConfigDir, "new_file.yaml")) || + testutil.FileExists(filepath.Join(m.config.ModuleConfigDir, "new_file.yaml.bak")) { + t.Errorf("restore should del files") + } +} + +func TestSetChangeModuleConfigs(t *testing.T) { + testutil.MakeDirs() + defer testutil.DelTestFiles() + + m := NewManager(ManagerConfig{ + ModuleConfigDir: testutil.ModuleConfigDir, + }) + orig := ` +test: + k1: v0 + k2: v0 +` + _ = ioutil.WriteFile(filepath.Join(m.config.ModuleConfigDir, "test.yaml"), []byte(orig), 0644) + + ctx := context.Background() + req := &ModuleConfigChangeRequest{ + ModuleConfigChanges: []ModuleConfigChange{ + { + Operation: ModuleConfigSet, + FileName: "test.yaml", + Content: ` +test: + k1: v1 + k2: v2 +`, + }, + }, + } + changed, err := m.ChangeModuleConfigs(ctx, req) + if err != nil { + t.Error(err) + } + if len(changed) != 1 && changed[0] != "test.yaml" { + t.Errorf("changed files wrong") + } + content, err := ioutil.ReadFile(filepath.Join(m.config.ModuleConfigDir, "test.yaml")) + if err != nil { + t.Error(err) + } + if string(content) != req.ModuleConfigChanges[0].Content { + t.Errorf("content wrong") + } + content, err = ioutil.ReadFile(filepath.Join(m.config.ModuleConfigDir, "test.yaml.bak")) + if err != nil { + t.Error(err) + } + if string(content) != orig { + t.Errorf("content wrong") + } + + changed, err = m.ChangeModuleConfigs(ctx, &ModuleConfigChangeRequest{ + ModuleConfigChanges: []ModuleConfigChange{ + { + Operation: ModuleConfigRestore, + FileName: "test.yaml", + }, + }, + }) + if err != nil { + t.Error(err) + } + if len(changed) != 1 && changed[0] != "test.yaml" { + t.Errorf("changed files wrong") + } + content, err = ioutil.ReadFile(filepath.Join(m.config.ModuleConfigDir, "test.yaml")) + if err != nil { + t.Error(err) + } + if string(content) != orig { + t.Errorf("content wrong") + } + if testutil.FileExists(filepath.Join(m.config.ModuleConfigDir, "new_file.yaml.bak")) { + t.Errorf("restore should del bak file") + } +} + +func TestDelChangeModuleConfigs(t *testing.T) { + testutil.MakeDirs() + defer testutil.DelTestFiles() + + m := NewManager(ManagerConfig{ + ModuleConfigDir: testutil.ModuleConfigDir, + }) + orig := ` +test: + k1: v0 + k2: v0 +` + _ = ioutil.WriteFile(filepath.Join(m.config.ModuleConfigDir, "test.yaml"), []byte(orig), 0644) + + ctx := context.Background() + req := &ModuleConfigChangeRequest{ + ModuleConfigChanges: []ModuleConfigChange{ + { + Operation: ModuleConfigDel, + FileName: "test.yaml", + Content: "", + }, + }, + } + changed, err := m.ChangeModuleConfigs(ctx, req) + if err != nil { + t.Error(err) + } + if len(changed) != 1 && changed[0] != "test.yaml" { + t.Errorf("changed files wrong") + } + if testutil.FileExists(filepath.Join(m.config.ModuleConfigDir, "new_file.yaml.bak")) { + t.Errorf("file should be deleted") + } + + content, err := ioutil.ReadFile(filepath.Join(m.config.ModuleConfigDir, "test.yaml.bak")) + if err != nil { + t.Error(err) + } + if string(content) != orig { + t.Errorf("content wrong") + } + + changed, err = m.ChangeModuleConfigs(ctx, &ModuleConfigChangeRequest{ + ModuleConfigChanges: []ModuleConfigChange{ + { + Operation: ModuleConfigRestore, + FileName: "test.yaml", + }, + }, + }) + if err != nil { + t.Error(err) + } + if len(changed) != 1 && changed[0] != "test.yaml" { + t.Errorf("changed files wrong") + } + content, err = ioutil.ReadFile(filepath.Join(m.config.ModuleConfigDir, "test.yaml")) + if err != nil { + t.Error(err) + } + if string(content) != orig { + t.Errorf("content wrong") + } + if testutil.FileExists(filepath.Join(m.config.ModuleConfigDir, "new_file.yaml.bak")) { + t.Errorf("restore should del bak file") + } +} diff --git a/config/mgragent/log_query_config.go b/config/mgragent/log_query_config.go new file mode 100644 index 0000000000000000000000000000000000000000..76b220909e397c0918a83ac644eb8d780e6d6538 --- /dev/null +++ b/config/mgragent/log_query_config.go @@ -0,0 +1,29 @@ +package mgragent + +import "time" + +type LogQueryConfig struct { + // ErrCountLimit Upper limit for the total number of row errors in single-file processing logs + ErrCountLimit int `json:"errCountLimit" yaml:"errCountLimit"` + QueryTimeout time.Duration `json:"queryTimeout" yaml:"queryTimeout"` + DownloadTimeout time.Duration `json:"downloadTimeout" yaml:"downloadTimeout"` + LogTypeQueryConfigs []LogTypeQueryConfig `json:"logTypeQueryConfigs" yaml:"logTypeQueryConfigs"` +} + +type LogTypeQueryConfig struct { + LogType string `json:"logType" yaml:"logType"` + // IsOverrideByPriority Specifies whether logs of the ERROR level, WARN level, + // and INFO level are used to override logs of the ERROR level. + // For example, if INFO and ERROR are selected at the same time, + // you only need to check the log file corresponding to INFO, and ERROR is also + // included in the log file, so you do not need to check the two files + IsOverrideByPriority bool `json:"isOverrideByPriority" yaml:"isOverrideByPriority"` + LogLevelAndFilePatterns []LogLevelAndFilePattern `json:"logLevelAndFilePatterns" yaml:"logLevelAndFilePatterns"` +} + +type LogLevelAndFilePattern struct { + LogLevel string `json:"logLevel" yaml:"logLevel"` + Dir string `json:"dir" yaml:"dir"` + FilePatterns []string `json:"filePatterns" yaml:"filePatterns"` + LogParserCategory string `json:"logParserCategory" yaml:"logParserCategory"` +} diff --git a/config/mgragent/mgragent_basic_auth_test.go b/config/mgragent/mgragent_basic_auth_test.go new file mode 100644 index 0000000000000000000000000000000000000000..985e2fba859694acd4f9310968fbc386451d87a0 --- /dev/null +++ b/config/mgragent/mgragent_basic_auth_test.go @@ -0,0 +1,74 @@ +package mgragent + +import ( + "bytes" + "fmt" + "testing" + + "gopkg.in/yaml.v3" + + "github.com/oceanbase/obagent/config" +) + +var ( + basicAuthConfigGroup = &config.ConfigPropertiesGroup{ + Configs: []*config.ConfigProperty{ + { + Key: "agent.http.basic.auth.username", + Value: "mgragent", + ValueType: config.ValueString, + Description: "mgragent basic auth username", + Unit: "", + }, + { + Key: "agent.http.basic.auth.password", + Value: "root@123", + ValueType: config.ValueString, + Description: "mgragent basic auth password", + Unit: "", + }, + }, + } + + basicAuthModuleConfig = config.ModuleConfig{ + Module: "mgragent.basic.auth", + Process: "mgragent", + Disabled: false, + Config: &config.BasicAuthConfig{ + Auth: "basic", + Username: "agent.http.basic.auth.username", + Password: "agent.http.basic.auth.password", + }, + } +) + +func TestBasicAuthConfigPropertyExample(t *testing.T) { + w := bytes.NewBuffer(make([]byte, 0, 10)) + err := yaml.NewEncoder(w).Encode(basicAuthConfigGroup) + if err != nil { + t.Failed() + } + fmt.Printf("%s\n", w.Bytes()) +} + +func TestBasicAuthModuleConfigExample(t *testing.T) { + w := bytes.NewBuffer(make([]byte, 0, 10)) + err := yaml.NewEncoder(w).Encode(basicAuthModuleConfig) + if err != nil { + t.Failed() + } + fmt.Printf("%s\n", w.Bytes()) +} + +func TestModuleConfigTemplates(t *testing.T) { + templates := &config.ModuleConfigGroup{ + Modules: []config.ModuleConfig{basicAuthModuleConfig}, + } + + w := bytes.NewBuffer(make([]byte, 0, 10)) + err := yaml.NewEncoder(w).Encode(templates) + if err != nil { + t.Failed() + } + fmt.Printf("%s\n", w.Bytes()) +} diff --git a/config/mgragent/mgragent_config.go b/config/mgragent/mgragent_config.go new file mode 100644 index 0000000000000000000000000000000000000000..a2616a77ce1258fe4d702637570681c5e2fc3158 --- /dev/null +++ b/config/mgragent/mgragent_config.go @@ -0,0 +1,52 @@ +package mgragent + +import ( + "bytes" + "io/ioutil" + "os" + + log "github.com/sirupsen/logrus" + "gopkg.in/yaml.v3" + + "github.com/oceanbase/obagent/config" + "github.com/oceanbase/obagent/lib/crypto" +) + +// Config for ob_mgragent process +type ManagerAgentConfig struct { + Server ServerConfig `yaml:"server"` + SDKConfig config.SDKConfig `yaml:"sdkConfig"` + CryptoMethod crypto.CryptoMethod `yaml:"cryptoMethod"` + Install config.InstallConfig `yaml:"install"` + ShellfConfig config.ShellfConfig `yaml:"shellf"` +} + +type AgentProxyConfig struct { + ProxyAddress string `yaml:"proxyAddress"` + ProxyEnabled bool `yaml:"proxyEnabled"` +} + +type ServerConfig struct { + //Port int `yaml:"port"` + Address string `yaml:"address"` + RunDir string `yaml:"runDir"` +} + +func NewManagerAgentConfig(configFile string) *ManagerAgentConfig { + _, err := os.Stat(configFile) + if err != nil { + log.WithField("file", configFile).WithError(err).Fatal("config file not found") + } + + content, err := ioutil.ReadFile(configFile) + if err != nil { + log.WithField("file", configFile).WithError(err).Fatal("fail to read config file") + } + + config := new(ManagerAgentConfig) + err = yaml.NewDecoder(bytes.NewReader(content)).Decode(config) + if err != nil { + log.WithField("file", configFile).WithError(err).Fatal("fail to decode config file") + } + return config +} diff --git a/config/monagent/log_tailer_config.go b/config/monagent/log_tailer_config.go new file mode 100644 index 0000000000000000000000000000000000000000..381a42760ca8a4065a3d601053f253cf8e5c46d5 --- /dev/null +++ b/config/monagent/log_tailer_config.go @@ -0,0 +1,36 @@ +package monagent + +import ( + "fmt" + "time" +) + +type LogTailerConfig struct { + TailConfigs []TailConfig `json:"tailConfigs" yaml:"tailConfigs"` + RecoveryConfig RecoveryConfig `json:"recoveryConfig" yaml:"recoveryConfig"` + // ProcessQueueCapacity Maximum capacity of file processing queue + ProcessQueueCapacity int `json:"processQueueCapacity" yaml:"processQueueCapacity"` +} + +type TailConfig struct { + // LogDir Directory of files to be parsed (do not include file names) + LogDir string `json:"logDir" yaml:"logDir"` + LogFileName string `json:"logFileName" yaml:"logFileName"` + // ProcessLogInterval The interval at which logs are processed + ProcessLogInterval time.Duration `json:"processLogInterval" yaml:"processLogInterval"` + LogSourceType string `json:"logSourceType" yaml:"logSourceType"` + LogAnalyzerType string `json:"logAnalyzerType" yaml:"logAnalyzerType"` +} + +func (t TailConfig) GetLogFileRealPath() string { + return fmt.Sprintf("%s/%s", t.LogDir, t.LogFileName) +} + +type RecoveryConfig struct { + // Enabled The function to restore the last tail location from a file is enabled + Enabled bool `json:"enabled" yaml:"enabled"` + // LastPositionStoreDir Persist the last queried file and the queried location information directory + LastPositionStoreDir string `json:"lastPositionStoreDir" yaml:"lastPositionStoreDir"` + // TriggerStoreThreshold How many lines of tail actively trigger the store action + TriggerStoreThreshold uint64 `json:"triggerThreshold" yaml:"triggerThreshold"` +} diff --git a/config/monagent_config.go b/config/monagent/monagent_config.go similarity index 62% rename from config/monagent_config.go rename to config/monagent/monagent_config.go index f2d86b7f16fac5b262f51fe8f0ac8363190650f7..f052151285e49185f15ddc33ca895991139b8fee 100644 --- a/config/monagent_config.go +++ b/config/monagent/monagent_config.go @@ -1,16 +1,4 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - -package config +package monagent import ( "bytes" @@ -23,9 +11,7 @@ import ( ) type MonitorAgentConfig struct { - // log config - Log *LogConfig `yaml:"log"` - // mongagent server config + // monagent server config Server MonitorAgentHttpConfig `yaml:"server"` // pipeline config file dir ModulePath string `yaml:"modulePath"` @@ -40,8 +26,6 @@ type MonitorAgentConfig struct { type MonitorAgentHttpConfig struct { // monitor metrics server address Address string `yaml:"address"` - // admin server (pprof) address - AdminAddress string `yaml:"adminAddress"` RunDir string `yaml:"runDir"` } diff --git a/config/monagent_config_test.go b/config/monagent/monagent_config_test.go similarity index 56% rename from config/monagent_config_test.go rename to config/monagent/monagent_config_test.go index 7566ce4d2dc2b4bf6d8b0ffbb8d31c0a323a0861..d07376e1a04fd548e4e828ac7e27caa422a5d220 100644 --- a/config/monagent_config_test.go +++ b/config/monagent/monagent_config_test.go @@ -1,16 +1,4 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - -package config +package monagent import ( "testing" @@ -21,10 +9,11 @@ import ( func TestDecodeMonitorAgentServerConfig(t *testing.T) { t.Run("DecodeMonitorAgentServerConfig success", func(t *testing.T) { - serverConfig, err := DecodeMonitorAgentServerConfig("../etc/monagent.yaml") + serverConfig, err := DecodeMonitorAgentServerConfig("../../etc/monagent.yaml") Convey("DecodeMonitorAgentServerConfig success", t, func() { So(err, ShouldBeNil) So(serverConfig, ShouldNotBeNil) + So(serverConfig.Server.Address, ShouldEqual, "0.0.0.0:${ocp.agent.monitor.http.port}") }) }) diff --git a/config/pipeline_config.go b/config/monagent/pipeline_config.go similarity index 67% rename from config/pipeline_config.go rename to config/monagent/pipeline_config.go index 5018cb929a3be3666fc9f19f42c7df300fbf7bfd..e09cdb3d7dafc89f5825a6682f112c55a0149e67 100644 --- a/config/pipeline_config.go +++ b/config/monagent/pipeline_config.go @@ -1,16 +1,4 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - -package config +package monagent import ( "time" @@ -20,8 +8,7 @@ type ScheduleStrategy string type PipelineModuleStatus string const ( - Trigger ScheduleStrategy = "trigger" - Periodic ScheduleStrategy = "periodic" + BySource ScheduleStrategy = "bySource" ) const ( @@ -29,6 +16,10 @@ const ( INACTIVE PipelineModuleStatus = "inactive" ) +func (p PipelineModuleStatus) Validate() bool { + return p == ACTIVE || p == INACTIVE +} + type PluginConfig struct { Timeout time.Duration `yaml:"timeout"` PluginInnerConfig map[string]interface{} `yaml:"pluginConfig"` diff --git a/config/monagent/pipeline_config_test.go b/config/monagent/pipeline_config_test.go new file mode 100644 index 0000000000000000000000000000000000000000..5e527210d5c9d663b9c5ea642daa5f07504d9bd2 --- /dev/null +++ b/config/monagent/pipeline_config_test.go @@ -0,0 +1,14 @@ +package monagent + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestPipelineModuleStatus_Validate(t *testing.T) { + var pms PipelineModuleStatus = "false" + assert.False(t, pms.Validate()) + pms = "active" + assert.True(t, pms.Validate()) +} diff --git a/config/placeholder.go b/config/placeholder.go index 4cd7d40055ccffa6b8b25f91bde6b0c1c0d5439f..6dfed237a13b3b88e054df208bf8140afa8e5db7 100644 --- a/config/placeholder.go +++ b/config/placeholder.go @@ -1,15 +1,3 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - package config import ( diff --git a/config/placeholder_test.go b/config/placeholder_test.go index b851a16805a506fa0dde214260697d283e3ed90b..ffbae36837b8a67c13f1802ede9b6a944e801098 100644 --- a/config/placeholder_test.go +++ b/config/placeholder_test.go @@ -1,15 +1,3 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - package config import ( diff --git a/config/sdk/log_config_properties_meta.go b/config/sdk/log_config_properties_meta.go new file mode 100644 index 0000000000000000000000000000000000000000..793b72887400e86014675107a73455451e5a26da --- /dev/null +++ b/config/sdk/log_config_properties_meta.go @@ -0,0 +1,187 @@ +package sdk + +import ( + "github.com/oceanbase/obagent/config" +) + +func setLogtailerConfigPropertyMeta() { + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "ob.logtailer.enabled", + DefaultValue: "false", + ValueType: config.ValueBool, + }) + + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "ob.logtailer.log.filter.rules.json.content", + DefaultValue: "", + ValueType: config.ValueString, + }) + + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "logtailer.log.filter.rules.json.content", + DefaultValue: "[]", + ValueType: config.ValueString, + }) + + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "ob.logtailer.keyword.alarm.rules", + DefaultValue: "", + ValueType: config.ValueString, + }) + +} + +func setLogCleanerConfigPropertyMeta() { + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "ob.logcleaner.enabled", + DefaultValue: "false", + ValueType: config.ValueBool, + }) + + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "ob.logcleaner.run.internal", + DefaultValue: "5m", + ValueType: config.ValueString, + }) + + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "ob.logcleaner.ob_log.disk.threshold", + DefaultValue: "80", + ValueType: config.ValueInt64, + }) + + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "ob.logcleaner.ob_log.rule0.retention.days", + DefaultValue: "8", + ValueType: config.ValueInt64, + }) + + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "ob.logcleaner.ob_log.rule0.keep.percentage", + DefaultValue: "60", + ValueType: config.ValueInt64, + }) + + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "ob.logcleaner.ob_log.rule1.retention.days", + DefaultValue: "30", + ValueType: config.ValueInt64, + }) + + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "ob.logcleaner.ob_log.rule1.keep.percentage", + DefaultValue: "80", + ValueType: config.ValueInt64, + }) + + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "ob.logcleaner.core_log.disk.threshold", + DefaultValue: "80", + ValueType: config.ValueInt64, + }) + + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "ob.logcleaner.core_log.rule0.retention.days", + DefaultValue: "8", + ValueType: config.ValueInt64, + }) + + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "ob.logcleaner.core_log.rule0.keep.percentage", + DefaultValue: "60", + ValueType: config.ValueInt64, + }) +} + +func setLogConfigPropertyMeta() { + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "monagent.log.level", + DefaultValue: "info", + ValueType: config.ValueString, + }) + + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "monagent.log.maxsize.mb", + DefaultValue: "100", + ValueType: config.ValueInt64, + }) + + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "monagent.log.maxage.days", + DefaultValue: "30", + ValueType: config.ValueInt64, + }) + + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "monagent.log.maxbackups", + DefaultValue: "10", + ValueType: config.ValueInt64, + }) + + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "monagent.log.compress", + DefaultValue: "true", + ValueType: config.ValueBool, + }) + + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "mgragent.log.level", + DefaultValue: "info", + ValueType: config.ValueString, + }) + + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "mgragent.log.maxsize.mb", + DefaultValue: "100", + ValueType: config.ValueInt64, + }) + + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "mgragent.log.maxage.days", + DefaultValue: "30", + ValueType: config.ValueInt64, + }) + + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "mgragent.log.maxbackups", + DefaultValue: "10", + ValueType: config.ValueInt64, + }) + + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "mgragent.log.compress", + DefaultValue: "true", + ValueType: config.ValueBool, + }) + + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "config.version.maxbackups", + DefaultValue: "30", + ValueType: config.ValueInt64, + }) +} diff --git a/config/sdk/mgragent_callbacks.go b/config/sdk/mgragent_callbacks.go new file mode 100644 index 0000000000000000000000000000000000000000..04dce728c80eb96d92c6572685c7056dbaf60eb3 --- /dev/null +++ b/config/sdk/mgragent_callbacks.go @@ -0,0 +1,170 @@ +package sdk + +import ( + "context" + "reflect" + + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + + "github.com/oceanbase/obagent/api/common" + "github.com/oceanbase/obagent/config" + "github.com/oceanbase/obagent/config/mgragent" + "github.com/oceanbase/obagent/executor/cleaner" + "github.com/oceanbase/obagent/executor/log_query" + "github.com/oceanbase/obagent/lib/http" + agentlog "github.com/oceanbase/obagent/log" +) + +// RegisterMgragentCallbacks When the service callback module is loaded, +// the service needs to provide the callback function, +// and the configuration can be obtained when the service is initialized or changed. +func RegisterMgragentCallbacks(ctx context.Context) error { + err := config.RegisterConfigCallback( + config.ManagerLogConfigModuleType, + func() interface{} { + return agentlog.LoggerConfig{} + }, + setLogger, + setLogger, + ) + if err != nil { + return err + } + + err = config.RegisterConfigCallback( + config.ManagerAgentBasicAuthConfigModuleType, + func() interface{} { + return config.BasicAuthConfig{} + }, + // Initialize the configuration callback + func(ctx context.Context, moduleConf interface{}) error { + basicConf, ok := moduleConf.(config.BasicAuthConfig) + if !ok { + return errors.Errorf("init module %s conf %s is not config.BasicAuthConfig", config.ManagerAgentBasicAuthConfigModuleType, reflect.TypeOf(moduleConf)) + } + common.NotifyConf(basicConf) + log.WithContext(ctx).Infof("module %s init config successfully", config.ManagerAgentBasicAuthConfigModuleType) + return nil + }, + // Configure update callbacks + func(ctx context.Context, moduleConf interface{}) error { + basicConf, ok := moduleConf.(config.BasicAuthConfig) + if !ok { + return errors.Errorf("update module %s conf %s is not config.BasicAuthConfig", config.ManagerAgentBasicAuthConfigModuleType, reflect.TypeOf(moduleConf)) + } + common.NotifyConf(basicConf) + log.WithContext(ctx).Infof("module %s update config successfully", config.ManagerAgentBasicAuthConfigModuleType) + return nil + }, + ) + if err != nil { + return err + } + + err = config.RegisterConfigCallback( + config.OBLogcleanerModuleType, + func() interface{} { + return new(mgragent.ObCleanerConfig) + }, + func(ctx context.Context, moduleConf interface{}) error { + conf, ok := moduleConf.(*mgragent.ObCleanerConfig) + if !ok { + return errors.Errorf("module %s init conf %s is not *config.ObCleanerConfig", config.OBLogcleanerModule, reflect.TypeOf(moduleConf)) + } + err := cleaner.InitOBCleanerConf(conf) + if err != nil { + return errors.Errorf("init ob cleaner err:%s", err) + } + return nil + }, + func(ctx context.Context, moduleConf interface{}) error { + conf, ok := moduleConf.(*mgragent.ObCleanerConfig) + if !ok { + return errors.Errorf("module %s update conf %s is not *config.ObCleanerConfig", config.OBLogcleanerModule, reflect.TypeOf(moduleConf)) + } + err := cleaner.UpdateOBCleanerConf(conf) + if err != nil { + return errors.Errorf("update ob cleaner err:%s", err) + } + return nil + }, + ) + if err != nil { + return err + } + + err = config.RegisterConfigCallback( + config.ManagerLogQuerierModuleType, + func() interface{} { + return new(mgragent.LogQueryConfig) + }, + func(ctx context.Context, moduleConf interface{}) error { + conf, ok := moduleConf.(*mgragent.LogQueryConfig) + if !ok { + return errors.Errorf("module %s init conf %s is not *config.LogQueryConfig", config.ManagerLogQuerierModuleType, reflect.TypeOf(moduleConf)) + } + err := log_query.InitLogQuerierConf(conf) + if err != nil { + return errors.Errorf("init log querier conf err:%s", err) + } + return nil + }, + func(ctx context.Context, moduleConf interface{}) error { + conf, ok := moduleConf.(*mgragent.LogQueryConfig) + if !ok { + return errors.Errorf("module %s update conf %s is not *config.LogQueryConfig", config.ManagerLogQuerierModuleType, reflect.TypeOf(moduleConf)) + } + err := log_query.UpdateLogQuerierConf(conf) + if err != nil { + return errors.Errorf("update log querier conf err:%s", err) + } + return nil + }, + ) + if err != nil { + return err + } + + err = config.RegisterConfigCallback( + config.ProxyConfigModuleType, + func() interface{} { + return new(mgragent.AgentProxyConfig) + }, + func(ctx context.Context, moduleConf interface{}) error { + conf, ok := moduleConf.(*mgragent.AgentProxyConfig) + if !ok { + return errors.Errorf("module %s init conf %s is not *mgragent.AgentProxyConfig", config.ProxyConfigModuleType, reflect.TypeOf(moduleConf)) + } + if conf.ProxyEnabled { + err := http.SetSocksProxy(conf.ProxyAddress) + if err != nil { + return errors.Errorf("SetSocksProxy err:%s", err) + } + } + + return nil + }, + func(ctx context.Context, moduleConf interface{}) error { + conf, ok := moduleConf.(*mgragent.AgentProxyConfig) + if !ok { + return errors.Errorf("module %s update conf %s is not *mgragent.AgentProxyConfig", config.ProxyConfigModuleType, reflect.TypeOf(moduleConf)) + } + if conf.ProxyEnabled { + err := http.SetSocksProxy(conf.ProxyAddress) + if err != nil { + return errors.Errorf("SetSocksProxy err:%s", err) + } + } else { + http.UnsetSocksProxy() + } + + return nil + }, + ) + if err != nil { + return err + } + + return nil +} diff --git a/config/sdk/module_test.go b/config/sdk/module_test.go new file mode 100644 index 0000000000000000000000000000000000000000..eb14a517e6a243e8399a5f2674618633e4301e5e --- /dev/null +++ b/config/sdk/module_test.go @@ -0,0 +1,32 @@ +package sdk + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/oceanbase/obagent/api/common" + "github.com/oceanbase/obagent/config" +) + +func TestLogModule(t *testing.T) { + err := initSDK() + assert.Nil(t, err) + + ctx := context.Background() + // init log + err = config.InitModuleConfig(ctx, config.ManagerLogConfigModule) + assert.Nil(t, err) + + // init basic auth + common.InitBasicAuthConf(ctx) + + // init notify process + err = config.InitModuleConfig(ctx, config.NotifyProcessConfigModule) + assert.Nil(t, err) + + err = config.InitModuleConfig(ctx, config.OBLogcleanerModule) + assert.Nil(t, err) + +} diff --git a/config/sdk/monagent_callbacks.go b/config/sdk/monagent_callbacks.go index 1ec383bd4ccd24bf75dd6d18b50ac8d9ccbb0811..7460507216c78a5f1f4cdf7a4d796f959ace9e51 100644 --- a/config/sdk/monagent_callbacks.go +++ b/config/sdk/monagent_callbacks.go @@ -1,15 +1,3 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - package sdk import ( @@ -19,37 +7,52 @@ import ( "github.com/pkg/errors" log "github.com/sirupsen/logrus" + "github.com/oceanbase/obagent/api/web" "github.com/oceanbase/obagent/config" - "github.com/oceanbase/obagent/engine" + "github.com/oceanbase/obagent/config/monagent" + agentlog "github.com/oceanbase/obagent/log" + "github.com/oceanbase/obagent/monitor/engine" ) -//RegisterMonagentCallbacks To load the business callback module, -//the business needs to provide a callback function, -//and the configuration can be obtained when the business is initialized or changed. -func RegisterMonagentCallbacks() error { - // register pipeline module +// RegisterMonagentCallbacks When the service callback module is loaded, +// the service needs to provide the callback function, +// and the configuration can be obtained when the service is initialized or changed. +func RegisterMonagentCallbacks(ctx context.Context) error { err := config.RegisterConfigCallback( + config.MonitorLogConfigModuleType, + func() interface{} { + return agentlog.LoggerConfig{} + }, + setLogger, + setLogger, + ) + if err != nil { + return err + } + + // register pipeline module + err = config.RegisterConfigCallback( config.MonitorPipelineModuleType, func() interface{} { - return &config.PipelineModule{} + return &monagent.PipelineModule{} }, - // Initial configuration callback + // Initialize the configuration callback func(ctx context.Context, moduleConf interface{}) error { - conf, ok := moduleConf.(*config.PipelineModule) + conf, ok := moduleConf.(*monagent.PipelineModule) if !ok { return errors.Errorf("init module %s conf %s is not *config.PipelineModule", config.MonitorPipelineModuleType, reflect.TypeOf(moduleConf)) } - err := engine.InitPipelineModuleCallback(conf) + err := engine.InitPipelineModuleCallback(ctx, conf) return err }, - // Configuration update callback + // Configure update callbacks func(ctx context.Context, moduleConf interface{}) error { - conf, ok := moduleConf.(*config.PipelineModule) + conf, ok := moduleConf.(*monagent.PipelineModule) if !ok { return errors.Errorf("update module %s conf %s is not *config.PipelineModule", config.MonitorPipelineModuleType, reflect.TypeOf(moduleConf)) } - err := engine.UpdatePipelineModuleCallback(conf) + err := engine.UpdatePipelineModuleCallback(ctx, conf) return err }, ) @@ -63,24 +66,24 @@ func RegisterMonagentCallbacks() error { func() interface{} { return config.BasicAuthConfig{} }, - // Initial configuration callback + // Initialize the configuration callback func(ctx context.Context, moduleConf interface{}) error { basicConf, ok := moduleConf.(config.BasicAuthConfig) if !ok { return errors.Errorf("init module %s conf %s is not config.BasicAuthConfig", config.MonitorServerBasicAuthModuleType, reflect.TypeOf(moduleConf)) } - engine.NotifyServerBasicAuth(basicConf) - log.WithContext(ctx).Infof("module %s init config sucessfully", config.MonitorServerBasicAuthModuleType) + notifyServerBasicAuth(basicConf) + log.WithContext(ctx).Infof("module %s init config successfully", config.MonitorServerBasicAuthModuleType) return nil }, - // Configuration update callback + // Configure update callbacks func(ctx context.Context, moduleConf interface{}) error { basicConf, ok := moduleConf.(config.BasicAuthConfig) if !ok { return errors.Errorf("update module %s conf %s is not config.BasicAuthConfig", config.MonitorServerBasicAuthModuleType, reflect.TypeOf(moduleConf)) } - engine.NotifyServerBasicAuth(basicConf) - log.WithContext(ctx).Infof("module %s update config sucessfully", config.MonitorServerBasicAuthModuleType) + notifyServerBasicAuth(basicConf) + log.WithContext(ctx).Infof("module %s update config successfully", config.MonitorServerBasicAuthModuleType) return nil }, ) @@ -88,36 +91,12 @@ func RegisterMonagentCallbacks() error { return err } - // monagent admin server basic auth - err = config.RegisterConfigCallback( - config.MonitorAdminBasicAuthModuleType, - func() interface{} { - return config.BasicAuthConfig{} - }, - // Initial configuration callback - func(ctx context.Context, moduleConf interface{}) error { - basicConf, ok := moduleConf.(config.BasicAuthConfig) - if !ok { - return errors.Errorf("init module %s conf %s is not config.BasicAuthConfig", config.MonitorAdminBasicAuthModuleType, reflect.TypeOf(moduleConf)) - } - engine.NotifyAdminServerBasicAuth(basicConf) - log.WithContext(ctx).Infof("module %s init config sucessfully", config.MonitorAdminBasicAuthModuleType) - return nil - }, - // Configuration update callback - func(ctx context.Context, moduleConf interface{}) error { - basicConf, ok := moduleConf.(config.BasicAuthConfig) - if !ok { - return errors.Errorf("update module %s conf %s is not config.BasicAuthConfig", config.MonitorAdminBasicAuthModuleType, reflect.TypeOf(moduleConf)) - } - engine.NotifyAdminServerBasicAuth(basicConf) - log.WithContext(ctx).Infof("module %s update config sucessfully", config.MonitorAdminBasicAuthModuleType) - return nil - }, - ) - if err != nil { - return err - } + return nil +} +func notifyServerBasicAuth(basicConf config.BasicAuthConfig) error { + monagentServer := web.GetMonitorAgentServer() + monagentServer.Server.BasicAuthorizer.SetConf(basicConf) + monagentServer.Server.UseBasicAuth() return nil } diff --git a/config/sdk/monagent_config_properties_meta.go b/config/sdk/monagent_config_properties_meta.go new file mode 100644 index 0000000000000000000000000000000000000000..26ca0adcd53839d48caa54e34febabd7b816aa23 --- /dev/null +++ b/config/sdk/monagent_config_properties_meta.go @@ -0,0 +1,242 @@ +package sdk + +import ( + "github.com/oceanbase/obagent/config" + "github.com/oceanbase/obagent/lib/path" +) + +func setMonitorAgentConfigPropertyMeta() { + + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "monagent.ob.monitor.user", + DefaultValue: "ocp_monitor", + ValueType: config.ValueString, + }) + + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "monagent.ob.monitor.password", + DefaultValue: "", + ValueType: config.ValueString, + Encrypted: true, + Masked: true, + }) + + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "monagent.ob.sql.port", + DefaultValue: "2881", + ValueType: config.ValueInt64, + }) + + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "monagent.ob.rpc.port", + DefaultValue: "2882", + ValueType: config.ValueInt64, + }) + + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "monagent.host.ip", + DefaultValue: "127.0.0.1", + ValueType: config.ValueString, + }) + + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "monagent.cluster.id", + DefaultValue: "0", + ValueType: config.ValueInt64, + }) + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "monagent.ob.cluster.name", + DefaultValue: "", + ValueType: config.ValueString, + }) + + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "monagent.ob.cluster.id", + DefaultValue: "0", + ValueType: config.ValueInt64, + }) + + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "monagent.ob.zone.name", + DefaultValue: "", + ValueType: config.ValueString, + }) + + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "monagent.pipeline.ob.status", + DefaultValue: "inactive", + ValueType: config.ValueString, + }) + + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "monagent.pipeline.node.status", + DefaultValue: "active", + ValueType: config.ValueString, + }) + + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "ob.log.path", + DefaultValue: "/data/log1", + ValueType: config.ValueString, + }) + + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "ob.data.path", + DefaultValue: "/data/1", + ValueType: config.ValueString, + }) + + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "ob.install.path", + DefaultValue: "/home/admin/oceanbase", + ValueType: config.ValueString, + }) + + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "host.check.readonly.mountpoint", + DefaultValue: "/", + ValueType: config.ValueString, + }) + + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "monagent.node.custom.interval", + DefaultValue: "1s", + ValueType: config.ValueString, + }) + + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "monagent.pipeline.ob.log.status", + DefaultValue: "inactive", + ValueType: config.ValueString, + }) + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "es.client.addresses", + DefaultValue: "", + ValueType: config.ValueString, + }) + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "es.client.auth.username", + DefaultValue: "", + ValueType: config.ValueString, + }) + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "es.client.auth.password", + DefaultValue: "", + ValueType: config.ValueString, + Encrypted: true, + }) + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "observer.log.path", + DefaultValue: "/home/admin/oceanbase/log", + ValueType: config.ValueString, + }) + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "agent.log.path", + DefaultValue: path.LogDir(), + ValueType: config.ValueString, + }) + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "os.log.path", + DefaultValue: "/var/log", + ValueType: config.ValueString, + }) + + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "monagent.second.metric.cache.update.interval", + DefaultValue: "15s", + ValueType: config.ValueString, + }) + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "monagent.collector.prometheus.interval", + DefaultValue: "1s", + ValueType: config.ValueString, + }) + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "monagent.collector.ob.basic.interval", + DefaultValue: "1s", + ValueType: config.ValueString, + }) + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "monagent.collector.ob.extra.interval", + DefaultValue: "60s", + ValueType: config.ValueString, + }) + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "monagent.collector.ob.snapshot.interval", + DefaultValue: "1h", + ValueType: config.ValueString, + }) + + // obagent 1.2.0 + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "monagent.mysql.monitor.user", + DefaultValue: "mysql_monitor_user", + ValueType: config.ValueString, + }) + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "monagent.mysql.monitor.password", + DefaultValue: "mysql_monitor_password", + ValueType: config.ValueString, + Encrypted: true, + }) + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "monagent.mysql.sql.port", + DefaultValue: 3306, + ValueType: config.ValueInt64, + }) + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "monagent.mysql.host", + DefaultValue: "127.0.0.1", + ValueType: config.ValueString, + }) + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "monagent.pipeline.mysql.status", + DefaultValue: "inactive", + ValueType: config.ValueString, + }) + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "monagent.alertmanager.address", + DefaultValue: "", + ValueType: config.ValueString, + }) + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "monagent.pipeline.ob.alertmanager.status", + DefaultValue: "inactive", + ValueType: config.ValueString, + }) +} diff --git a/config/sdk/sdk_config_properties_meta.go b/config/sdk/sdk_config_properties_meta.go new file mode 100644 index 0000000000000000000000000000000000000000..cd2dc767de72b4b880aca094ab308b49dff748fb --- /dev/null +++ b/config/sdk/sdk_config_properties_meta.go @@ -0,0 +1,94 @@ +package sdk + +import ( + "github.com/oceanbase/obagent/config" + "github.com/oceanbase/obagent/lib/path" +) + +// Set the configuration item meta information. +// All configuration items have to be configured with meta information: otherwise, +// configuration items cannot be parsed according to their data type. +func setConfigPropertyMeta() { + setLogConfigPropertyMeta() + setLogCleanerConfigPropertyMeta() + setLogtailerConfigPropertyMeta() + setMonitorAgentConfigPropertyMeta() + setBasicAuthConfigPropertyMeta() + setCommonAgentConfigPropertyMeta() +} + +func setBasicAuthConfigPropertyMeta() { + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "agent.http.basic.auth.username", + DefaultValue: "ocp_agent", + ValueType: config.ValueString, + Fatal: false, + Masked: false, + NeedRestart: false, + Description: "basic auth username", + Unit: "", + Valid: nil, + }) + + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "agent.http.basic.auth.password", + DefaultValue: "", + ValueType: config.ValueString, + Encrypted: true, + Fatal: false, + Masked: true, + NeedRestart: false, + Description: "basic auth password", + Unit: "", + Valid: nil, + }) + + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "agent.http.basic.auth.metricAuthEnabled", + DefaultValue: "true", + ValueType: config.ValueBool, + Encrypted: false, + Fatal: false, + Masked: false, + NeedRestart: true, + Description: "basic auth disabled", + Unit: "", + Valid: nil, + }) +} + +func setCommonAgentConfigPropertyMeta() { + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "obagent.home.path", + DefaultValue: path.AgentDir(), + ValueType: config.ValueString, + }) + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "ocp.agent.http.socks.proxy.enabled", + DefaultValue: "false", + ValueType: config.ValueBool, + }) + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "ocp.agent.http.socks.proxy.address", + DefaultValue: "", + ValueType: config.ValueString, + }) + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "ocp.agent.manager.http.port", + DefaultValue: 62888, + ValueType: config.ValueInt64, + }) + config.SetConfigPropertyMeta( + &config.ConfigProperty{ + Key: "ocp.agent.monitor.http.port", + DefaultValue: 62889, + ValueType: config.ValueInt64, + }) +} diff --git a/config/sdk/sdk_init.go b/config/sdk/sdk_init.go index f7b583009a5c8f17a4c2b868ee9f5576093eb51a..96e6383a1f2d229ece3004f4e3dd95ee39204877 100644 --- a/config/sdk/sdk_init.go +++ b/config/sdk/sdk_init.go @@ -1,15 +1,3 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - package sdk import ( @@ -26,7 +14,6 @@ var ( configPropertiesMetaOnce sync.Once ) -//SDKConfig SDK configuration type SDKConfig struct { ConfigPropertiesDir string ModuleConfigDir string @@ -34,50 +21,58 @@ type SDKConfig struct { CryptoMethod crypto.CryptoMethod `yaml:"cryptoMethod"` } -//InitSDK SDK initialization: -//1. Load key-value meta information: only value is for users to change, and other content is put into the SDK. -//2. Initialize the key-value configuration and the configuration template of the business module. -//3. To load the business callback module, the business needs to provide a callback function, and the configuration can be obtained when the business is initialized or changed. -//4. The callback service of the configuration module may be cross-process, and the cross-process API can be configured. It itself serves as part of configuration management. -func InitSDK(conf config.SDKConfig) error { - log.Infof("init sdk conf %+v", conf) +// InitSDK SDK initialization: +// 1. Load key-value meta information: only value is for users to change, and other content is put into the SDK. +// 2. Initialize the key-value configuration and the configuration template of the business module. +// 3. To load the business callback module, the business needs to provide a callback function, +// and the configuration can be obtained when the business is initialized or changed. +// 4. The callback service of the configuration module may be cross-process, +// and the cross-process API can be configured. It itself serves as part of configuration management. +func InitSDK(ctx context.Context, conf config.SDKConfig) error { + log.WithContext(ctx).Infof("init sdk conf %+v", conf) + // init metas + configPropertiesMetaOnce.Do(func() { + setConfigPropertyMeta() + }) if err := config.InitCrypto(conf.CryptoPath, conf.CryptoMethod); err != nil { - log.Error(err) + log.WithContext(ctx).Error(err) return err } // init configs - err := initConfigs(conf.ConfigPropertiesDir, conf.ModuleConfigDir) + err := initConfigs(ctx, conf.ConfigPropertiesDir, conf.ModuleConfigDir) if err != nil { - log.Error(err) + log.WithContext(ctx).Error(err) return err } // init config callbacks - if err := RegisterConfigCallbacks(); err != nil { - log.Error(err) + if err := registerCallbacks(ctx); err != nil { + log.WithContext(ctx).Error(err) return err } // set process notify address - err = config.InitModuleTypeConfig(context.Background(), config.NotifyProcessConfigModuleType) + err = config.InitModuleConfig(context.Background(), config.NotifyProcessConfigModule) if err != nil { - log.Error(err) + log.WithContext(ctx).Error(err) + return err } - return err + return nil } -//initConfigs Initialize the key-value configuration and the configuration template of the business module. -func initConfigs(configPropertiesDir, moduleConfigDir string) error { - err := config.InitConfigProperties(configPropertiesDir) +// initConfigs Initialize the key-value configuration and the configuration template of the business module. +func initConfigs(ctx context.Context, configPropertiesDir, moduleConfigDir string) error { + err := config.InitConfigProperties(ctx, configPropertiesDir) if err != nil { - log.Error(err) + log.WithContext(ctx).Error(err) return err } - err = config.InitModuleConfigs(moduleConfigDir) + err = config.InitModuleConfigs(ctx, moduleConfigDir) if err != nil { - log.Error(err) + log.WithContext(ctx).Error(err) + return err } - return err + return nil } diff --git a/config/sdk/sdk_init_test.go b/config/sdk/sdk_init_test.go new file mode 100644 index 0000000000000000000000000000000000000000..730db8a91071d7d22662f87a127db07a950adad2 --- /dev/null +++ b/config/sdk/sdk_init_test.go @@ -0,0 +1,95 @@ +package sdk + +import ( + "context" + "sync" + "testing" + + . "github.com/smartystreets/goconvey/convey" + "github.com/stretchr/testify/assert" + + "github.com/oceanbase/obagent/api/common" + "github.com/oceanbase/obagent/config" + "github.com/oceanbase/obagent/lib/crypto" + agentlog "github.com/oceanbase/obagent/log" +) + +func init() { + agentlog.InitLogger(agentlog.LoggerConfig{ + Level: "debug", + Filename: "../tests/test.log", + MaxSize: 10, // 10M + MaxAge: 3, // 3days + MaxBackups: 3, + LocalTime: false, + Compress: false, + }) +} + +var sdkOnce sync.Once + +func initSDK() error { + var err error + sdkOnce.Do(func() { + ctx := context.Background() + err = InitSDK(ctx, config.SDKConfig{ + ConfigPropertiesDir: "../../etc/config_properties", + ModuleConfigDir: "../../etc/module_config", + CryptoPath: "../../etc/.config_secret.key", + CryptoMethod: crypto.PLAIN, + }) + RegisterMgragentCallbacks(ctx) + RegisterMonagentCallbacks(ctx) + }) + + return err +} + +func TestInitSDK_Example(t *testing.T) { + err := initSDK() + assert.Nil(t, err) + + config.CurProcess = config.ProcessManagerAgent + + Convey("mgragent config", t, func() { + + common.InitBasicAuthConf(context.Background()) + + err = config.NotifyModules(context.Background(), []string{config.ManagerAgentBasicAuthConfigModule}) + So(err, ShouldBeNil) + }) + + Convey("mgragent config", t, func() { + err = config.InitModuleConfig(context.Background(), config.NotifyProcessConfigModule) + So(err, ShouldBeNil) + + err = config.NotifyModules(context.Background(), []string{config.NotifyProcessConfigModule}) + So(err, ShouldBeNil) + }) +} + +func TestInitSDK_WithWrongPath_Fail(t *testing.T) { + Convey("init sdk with wrong config properties path", t, func() { + err := InitSDK(context.Background(), config.SDKConfig{ + ConfigPropertiesDir: "../../etc/no-exist-path/config_properties", + ModuleConfigDir: "../../etc/module_config", + CryptoPath: "../../etc/.config_secret.key", + CryptoMethod: crypto.PLAIN, + }) + So(err, ShouldNotBeNil) + }) + + Convey("init sdk with wrong module config path", t, func() { + err := InitSDK(context.Background(), config.SDKConfig{ + ConfigPropertiesDir: "../../etc/config_properties", + ModuleConfigDir: "../../etc/no-exist-path/module_config", + CryptoPath: "../../etc/.config_secret.key", + CryptoMethod: crypto.PLAIN, + }) + So(err, ShouldNotBeNil) + }) +} +func Test_processNotifyModuleConfigCallback(t *testing.T) { + err := processNotifyModuleConfigCallback(context.Background(), nil) + assert.NotNil(t, err) +} diff --git a/config/sdk/sdk_module_callback.go b/config/sdk/sdk_module_callback.go index afb45b5e75da10680b0f70a58f384041bffbaf62..dc6e70c1add0720d9f858d0fceaa0082c2aaed6e 100644 --- a/config/sdk/sdk_module_callback.go +++ b/config/sdk/sdk_module_callback.go @@ -1,15 +1,3 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - package sdk import ( @@ -19,12 +7,14 @@ import ( "github.com/pkg/errors" "github.com/oceanbase/obagent/config" + agentlog "github.com/oceanbase/obagent/log" + "github.com/oceanbase/obagent/stat" ) -//RegisterConfigCallbacks To load the business callback module, -//the business needs to provide a callback function, -//and the configuration can be obtained when the business is initialized or changed. -func RegisterConfigCallbacks() error { +// registerCallbacks To load the business callback module, +// the business needs to provide a callback function, +// and the configuration can be obtained when the business is initialized or changed. +func registerCallbacks(ctx context.Context) error { // register config module callbacks err := config.RegisterConfigCallback( config.NotifyProcessConfigModuleType, @@ -40,9 +30,40 @@ func RegisterConfigCallbacks() error { return err } + err = config.RegisterConfigCallback( + config.ConfigMetaModuleType, + func() interface{} { + return config.ConfigMetaBackup{} + }, + configNotifyModuleConfigCallback, + configNotifyModuleConfigCallback, + ) + if err != nil { + return err + } + + if err := config.RegisterConfigCallback( + config.StatConfigModuleType, + func() interface{} { + return stat.StatConfig{} + }, + setStatConfig, + setStatConfig, + ); err != nil { + return err + } + return nil } +func configNotifyModuleConfigCallback(ctx context.Context, moduleConf interface{}) error { + conf, ok := moduleConf.(config.ConfigMetaBackup) + if !ok { + return errors.Errorf("module %s conf %s is not NotifyAddress", config.NotifyProcessConfigModuleType, reflect.TypeOf(moduleConf)) + } + return config.SetConfigMetaModuleConfigNotify(ctx, conf) +} + func processNotifyModuleConfigCallback(ctx context.Context, moduleConf interface{}) error { confs, ok := moduleConf.([]config.ProcessConfigNotifyAddress) if !ok { @@ -53,3 +74,21 @@ func processNotifyModuleConfigCallback(ctx context.Context, moduleConf interface } return nil } + +func setLogger(ctx context.Context, moduleConf interface{}) error { + logconf, ok := moduleConf.(agentlog.LoggerConfig) + if !ok { + return errors.Errorf("conf is not agentlog.LoggerConfig") + } + agentlog.InitLogger(logconf) + return nil +} + +func setStatConfig(ctx context.Context, moduleConf interface{}) error { + conf, ok := moduleConf.(stat.StatConfig) + if !ok { + return errors.Errorf("conf is not stat.StatConfig") + } + stat.SetStatConfig(ctx, conf) + return nil +} diff --git a/config/yaml.go b/config/yaml.go index 9801d77e15a793b6637168ae85145607ace1efc1..cbe0392cada56c85a5788577d1a985e279f4cc73 100644 --- a/config/yaml.go +++ b/config/yaml.go @@ -1,15 +1,3 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - package config import ( @@ -96,3 +84,12 @@ func replaceValues(node *yaml.Node, replacer ExpandFunc) [][]string { recursivelyReplaceValues(node, replacer, []string{}, &p) return p } + +func ReplaceConfValues(conf interface{}, context map[string]string) (interface{}, error) { + expander := NewExpanderWithKeyValues( + DefaultExpanderPrefix, + DefaultExpanderSuffix, + context, + ) + return ToStructured(conf, conf, expander.Replace) +} diff --git a/config/yaml_test.go b/config/yaml_test.go index b0a0f5697c677bb35576d68557b8dc5ea6788a8a..db99b34bd1ccdcc53d14c9d91bba3d5ea9b62f36 100644 --- a/config/yaml_test.go +++ b/config/yaml_test.go @@ -1,15 +1,3 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - package config import ( @@ -18,7 +6,9 @@ import ( "strings" "testing" + "github.com/alecthomas/units" "github.com/huandu/go-clone" + "github.com/stretchr/testify/assert" "gopkg.in/yaml.v3" ) @@ -79,6 +69,12 @@ func TestParseStrut(t *testing.T) { } } +func TestParseBase2Bytes(t *testing.T) { + n, err := units.ParseBase2Bytes("2048MB") + assert.NoError(t, err) + assert.Equal(t, 2147483648, int(n)) +} + func TestToStructured(t *testing.T) { in := map[string]interface{}{ "a": "1", @@ -111,6 +107,32 @@ func TestToStructured(t *testing.T) { t.Error("ToStructured wrong") } } +func Test_ToStructured_Slice(t *testing.T) { + + configMap := map[string]interface{}{ + "A": "A", + "B": "${obagent.home.path}", + "C": map[string]interface{}{ + "Slice": []string{"-a", "${obagent.home.path}", "${ocp.agent.manager.http.port}"}, + "Z": "Z", + }, + } + + paramsMap := map[string]string{ + "obagent.home.path": "beixun_test", + "ocp.agent.manager.http.port": "62888", + "ocp.agent.monitor.http.port": "62889", + } + expander := NewExpanderWithKeyValues( + DefaultExpanderPrefix, + DefaultExpanderSuffix, + paramsMap, + ) + + ToStructured(&configMap, &configMap, expander.Replace) + + fmt.Printf("%+v\n", configMap) +} func TestWalk(t *testing.T) { node := yaml.Node{} diff --git a/docs/about-obagent/what-is-obagent.md b/docs/about-obagent/what-is-obagent.md index dec666fec2d728a1393cff118da5231a4515fa78..a888da7292ef10dd0ec6a8d038b683364f866623 100644 --- a/docs/about-obagent/what-is-obagent.md +++ b/docs/about-obagent/what-is-obagent.md @@ -1,6 +1,8 @@ # 什么是 OBAgent -OBAgent 是一个监控采集框架。OBAgent 支持推、拉两种数据采集模式,可以满足不同的应用场景。OBAgent 默认支持的插件包括主机数据采集、OceanBase 数据库指标的采集、监控数据标签处理和 Prometheus 协议的 HTTP 服务。要使 OBAgent 支持其他数据源的采集,或者自定义数据的处理流程,您只需要开发对应的插件即可。 +OBAgent 是一个监控采集和运维框架。 +监控部分,OBAgent 支持推、拉两种数据采集模式,可以满足不同的应用场景。OBAgent 默认支持的插件包括主机数据采集、OceanBase 数据库指标的采集、日志信息采集、监控数据标签处理和 Prometheus 协议的 HTTP 服务。支持推送数据到pushgateway、vmagent、es、sls、alertmanager。要使 OBAgent 支持其他数据源的采集,或者自定义数据的处理流程,您只需要开发对应的插件即可。 +运维部分,OBAagent 支持配置热更新,同时提供了对agent自身运维的接口,以及文件操作和rpm包操作的接口,您可以根据自身需求来使用这些接口。 ## 特性 @@ -8,3 +10,16 @@ OBAgent 具有以下特性: - 采用 Go 语言开发,无需外部依赖。 - 插件驱动,易开发。 + +# OBAgent 架构 + +OBAgent 由四部分组成,包含 agentd、mgragent、monagent 以及 agentctl。 + +进程名称 | 说明 +--- | --- +agentd | 作为守护进程,用于启停 mgragent & monagent 进程,如果 mgragent 或者 monagent 非正常退出,agentd 会负责将他们自动重新启动起来,保证服务可用;同时 agentd 也会运行一些资源限制任务,用于限制 mgragent / monagent 的资源使用情况(比如 CPU、内存等)。 +mgragent | 提供运维相关的功能。包括了重启、重装OBAgent,文件管理,安装包管理,以及配置管理等。 +monagent | 提供了监控功能。监控的内容范围包含了数据库性能参数、应用日志、主机性能/资源参数、租户资源/性能参数、SQL/事务参数等等。用户可以通过 推 和 拉 两种模式获取监控采集的数据内容。 +agentctl | 提供了黑屏运维功能。agentctl 本身不包含业务逻辑,以命令行的方式提供给用户一个操作入口。包含了进程启停,配置变更等。 + + diff --git a/docs/config-reference/kv-config.md b/docs/config-reference/kv-config.md index 381cc523527c2104eeb27da2c0c0fadce7bbb97c..ff4c15dc9cd28dc6ecd4d04c769aa180939a95bd 100644 --- a/docs/config-reference/kv-config.md +++ b/docs/config-reference/kv-config.md @@ -4,110 +4,272 @@ ```yaml # encrypted=true 的配置项,需要加密存储,目前仅支持 aes 加密。 -# 请将 {} 中的变量替换成您的真实值。如果您在 monagent 启动配置中将加密方法设置为 aes,您需要配置加密之后的值。 ## 基础认证相关 -# monagent_basic_auth.yaml -configVersion: "2021-08-20T07:52:28.5443+08:00" +# basic_auth.yaml +configVersion: "" configs: - - key: http.server.basic.auth.username - value: {http_basic_auth_user} + # agent http 接口用户名 + - key: agent.http.basic.auth.username + value: ocp_agent valueType: string - - key: http.server.basic.auth.password - value: {http_basic_auth_password} + # agent http 接口密码 + - key: agent.http.basic.auth.password + value: valueType: string encrypted: true - - key: http.admin.basic.auth.username - value: {pprof_basic_auth_user} + # 监控接口认证开关 + - key: agent.http.basic.auth.metricAuthEnabled + value: true valueType: string - - key: http.admin.basic.auth.password - value: {pprof_basic_auth_password} - valueType: string - encrypted: true + encrypted: false + +## 元信息配置项 +# common_meta.yaml +configVersion: "" +configs: + # 配置最大备份版本数量 + - key: config.version.maxbackups + value: 30 + valueType: int64 + # socks 代理开关 + - key: ocp.agent.http.socks.proxy.enabled + value: false + valueType: bool + # socks 代理地址 + - key: ocp.agent.http.socks.proxy.address + value: "" + valueType: string + # mgragent 进程端口 + - key: ocp.agent.manager.http.port + value: 62888 + valueType: int64 + # monagent 进程端口 + - key: ocp.agent.monitor.http.port + value: 62889 + valueType: int64 + +## agent日志相关 +# log.yaml +configVersion: "" +configs: + # monagent日志等级 + - key: monagent.log.level + value: info + valueType: string + # monagent单个日志文件大小 + - key: monagent.log.maxsize.mb + value: 200 + valueType: int64 + # monagent日志最大保留天数 + - key: monagent.log.maxage.days + value: 30 + valueType: int64 + # monagent日志文件最大数量 + - key: monagent.log.maxbackups + value: 15 + valueType: int64 + # monagent日志是否压缩 + - key: monagent.log.compress + value: true + valueType: bool + # mgragent日志等级 + - key: mgragent.log.level + value: info + valueType: string + # monagent单个日志文件大小 + - key: mgragent.log.maxsize.mb + value: 200 + valueType: int64 + # monagent日志最大保留天数 + - key: mgragent.log.maxage.days + value: 30 + valueType: int64 + # monagent日志文件最大数量 + - key: mgragent.log.maxbackups + value: 15 + valueType: int64 + # mgragent日志是否压缩 + - key: mgragent.log.compress + value: true + valueType: bool + +## observer日志清理相关 +# ob_logcleaner.yaml +configVersion: "" +configs: + # 是否清理 ob 日志 + - key: ob.logcleaner.enabled + value: false + valueType: bool + # ob 日志清理间隔 + - key: ob.logcleaner.run.internal + value: 5m + valueType: string + # ob 日志清理百分比阈值 + - key: ob.logcleaner.ob_log.disk.threshold + value: 80 + valueType: int64 + # ob 日志清理0级规则之保留天数 + - key: ob.logcleaner.ob_log.rule0.retention.days + value: 8 + valueType: int64 + # ob 日志清理0级规则之保留百分比 + - key: ob.logcleaner.ob_log.rule0.keep.percentage + value: 60 + valueType: int64 + # ob 日志清理1级规则之保留天数 + - key: ob.logcleaner.ob_log.rule1.retention.days + value: 30 + valueType: int64 + # ob 日志清理1级规则之保留百分比 + - key: ob.logcleaner.ob_log.rule1.keep.percentage + value: 80 + valueType: int64 + # core 文件清理百分比阈值 + - key: ob.logcleaner.core_log.disk.threshold + value: 80 + valueType: int64 + # core 文件清理保留天数 + - key: ob.logcleaner.core_log.rule0.retention.days + value: 8 + valueType: int64 + # core 文件清理保留百分比 + - key: ob.logcleaner.core_log.rule0.keep.percentage + value: 60 + valueType: int64 ## 流水线相关 # monagent_pipeline.yaml -configVersion: "2021-08-20T07:52:28.5443+08:00" +configVersion: "" configs: - # mysql 监控用户 - - key: monagent.mysql.monitor.user - value: mysql_monitor_user - valueType: string - # mysql 监控用户密码 - - key: monagent.mysql.monitor.password - value: mysql_monitor_password - valueType: string - encrypted: true - # mysql sql 端口 - - key: monagent.mysql.sql.port - value: 3306 - valueType: int64 - # mysql 地址 - - key: monagent.mysql.host - value: 127.0.0.1 - valueType: string - # ob 监控用户 - - key: monagent.ob.monitor.user - value: {monitor_user} - valueType: string - # ob 监控用户密码 - - key: monagent.ob.monitor.password - value: {monitor_password} - valueType: string - encrypted: true - # ob sql 端口 - - key: monagent.ob.sql.port - value: {sql_port} - valueType: int64 - # ob rpc 端口 - - key: monagent.ob.rpc.port - value: {rpc_port} - valueType: int64 - # ob 安装路径 - - key: monagent.ob.install.path - value: {ob_install_path} - valueType: string - # 主机 ip - - key: monagent.host.ip - value: {host_ip} - valueType: string - # ob 集群名 - - key: monagent.ob.cluster.name - value: {cluster_name} - valueType: string - # ob 集群 id - - key: monagent.ob.cluster.id - value: {cluster_id} - valueType: int64 - # ob zone 名字 - - key: monagent.ob.zone.name - value: {zone_name} - valueType: string - # ob 流水线开启状态 - - key: monagent.pipeline.ob.status - value: {ob_monitor_status} - valueType: string - # ob log 流水线开启状态 - - key: monagent.pipeline.ob.log.status - value: {ob_log_monitor_status} - valueType: string - # 主机流水线开启状态 - - key: monagent.pipeline.node.status - value: {host_monitor_status} - valueType: string - # alertmanager 地址 - - key: monagent.alertmanager.address - value: {alertmanager_address} - valueType: string - # mysql 流水线开启状态 - - key: monagent.pipeline.mysql.status - value: inactive - valueType: string + # ob 监控用户 + - key: monagent.ob.monitor.user + value: ocp_monitor + valueType: string + # ob 监控用户密码 + - key: monagent.ob.monitor.password + value: + valueType: string + encrypted: true + # ob sql 端口 + - key: monagent.ob.sql.port + value: 2881 + valueType: int64 + # ob rpc 端口 + - key: monagent.ob.rpc.port + value: 2882 + valueType: int64 + # 主机 ip + - key: monagent.host.ip + value: 127.0.0.1 + valueType: string + # ob 集群名 + - key: monagent.ob.cluster.name + value: "" + valueType: string + # ob 集群 id + - key: monagent.ob.cluster.id + value: 0 + valueType: int64 + # ob zone 名称 + - key: monagent.ob.zone.name + value: "" + valueType: string + # ob 监控采集流水线开关 + - key: monagent.pipeline.ob.status + value: inactive + valueType: string + # 主机监控采集流水线开关 + - key: monagent.pipeline.node.status + value: active + valueType: string + # ob 日志路径 + - key: ob.log.path + value: /data/log1 + valueType: string + # ob 数据路径 + - key: ob.data.path + value: /data/1 + valueType: string + # ob 安装路径 + - key: ob.install.path + value: /home/admin/oceanbase + valueType: string + # 主机只读挂载点 + - key: host.check.readonly.mountpoint + value: / + valueType: string + # ob 日志采集流水线开关 + - key: monagent.pipeline.ob.log.status + value: inactive + valueType: string + # es client 地址 + - key: es.client.addresses + value: "" + valueType: string + # es client 用户名 + - key: es.client.auth.username + value: "" + valueType: string + # es client 用户密码 + - key: es.client.auth.password + value: + valueType: string + encrypted: true + # ob 日志路径 + - key: observer.log.path + value: /home/admin/oceanbase/log + valueType: string + # agent 日志路径 + - key: agent.log.path + value: /home/admin/ocp_agent/log + valueType: string + # 系统日志路径 + - key: os.log.path + value: /var/log + valueType: string + # 秒级监控采集间隔 + - key: monagent.second.metric.cache.update.interval + value: 15s + valueType: string + # mysql 监控用户 + - key: monagent.mysql.monitor.user + value: mysql_monitor_user + valueType: string + # mysql 监控用户名密码 + - key: monagent.mysql.monitor.password + value: + valueType: string + encrypted: true + # mysql sql 端口 + - key: monagent.mysql.sql.port + value: 3306 + valueType: int64 + # mysql地址 + - key: monagent.mysql.host + value: 127.0.0.1 + valueType: string + # alertmanager 地址 + - key: monagent.alertmanager.address + value: + valueType: string + # alertmanager 推送流水线开关 + - key: monagent.pipeline.ob.alertmanager.status + value: inactive + valueType: string + # mysql 流水线开关 + - key: monagent.pipeline.mysql.status + value: inactive + valueType: string ``` ## 配置模版 KV 的相关配置文件模板如下: -- monagent_basic_auth.yaml,基础认证相关的 KV 配置项 -- monagent_pipeline.yaml,流水线相关的 KV 配置项 +- basic_auth.yaml, 基础认证相关的 KV 配置项 +- common_meta.yaml, 元信息的 KV 配置项 +- log.yaml, agent日志相关的 KV 配置项 +- ob_logcleaner.yaml, ob日志清理相关的 KV 配置项 +- monagent_pipeline.yaml, 流水线相关的 KV 配置项 diff --git a/docs/config-reference/mgragent-config.md b/docs/config-reference/mgragent-config.md new file mode 100644 index 0000000000000000000000000000000000000000..8bdd78ca21dce45d6ae536fdf5c0dd8a023497c4 --- /dev/null +++ b/docs/config-reference/mgragent-config.md @@ -0,0 +1,29 @@ +# mgragent 配置文件说明 + +本文介绍 mgragent.yaml 配置文件的相关配置项,并列出了配置文件模板供您参考。这里面的大部分配置项都是用了${config_key}来进行表示,${config_key}和config_properties下KV-config关联,并在启动agent时进行替换。 + +`monagent.yaml` 配置文件的示例如下: + +```yaml + +## 安装相关配置。指定 obagent 的home路径。 +install: + path: ${obagent.home.path} + +## 进程相关配置。其中,address 是默认的拉取 metrics 和管理相关接口,也是pprof调试端口。 +server: + address: 0.0.0.0:${ocp.agent.monitor.http.port} + runDir: ${obagent.home.path}/run + +## sdk 配置相关,加密方法支持 aes 和 plain。其中,aes 使用下面 key 文件中的 key 对需要加密的配置项进行加密。 +## moduleConfigDir 用来存放配置模版,configPropertiesDir 用来存放 KV 变量配置 +sdkConfig: + configPropertiesDir: ${obagent.home.path}/conf/config_properties + moduleConfigDir: ${obagent.home.path}/conf/module_config + cryptoPath: ${obagent.home.path}/conf/.config_secret.key + cryptoMethod: aes + +## 命令模板配置相关。指定mgragent的配置模板文件。 +shellf: + template: ${obagent.home.path}/conf/shell_templates/shell_template.yaml +``` diff --git a/docs/config-reference/module_config.md b/docs/config-reference/module_config.md new file mode 100644 index 0000000000000000000000000000000000000000..bb0112537e43521d8dae7a68c11d5850279e8538 --- /dev/null +++ b/docs/config-reference/module_config.md @@ -0,0 +1,21 @@ +# 配置模板文件说明 + +配置模板中定义了各个模块的配置模板,以及各个监控的流水线。这里面的大部分配置项都是用了${config_key}来进行表示,${config_key}和config_properties下KV-config关联,并在启动agent时进行替换。 +配置模板文件的说明见下表: + +配置文件名称 | 说明 +--- | --- +common_module.yaml | monagent和mgragent meta信息配置模块。 +log_module.yaml | mongagent和mgragent 日志相关配置。 +mgragent_logquerier_module.yaml | ob和agent日志采集相关配置。 +mgragent_module.yaml | mgragent认证和配置更新相关配置。 +monagent_basic_auth.yaml | monagent认证相关配置。 +monitor_host_log.yaml | 主机日志采集推送ES流水线配置模板。 +monitor_mysql.yaml | mysql监控采集流水线配置模板。 +monitor_node_host.yaml | 主机监控采集流水线配置模板。 +monitor_ob.yaml | ob性能监控采集流水线配置模板。 +monitor_ob_custom.yaml | ob连接和进程监控流水线配置模板。 +monitor_ob_log.yaml | ob error日志采集流水线配置模板。 +monitor_observer_log.yaml | ob日志采集推送ES流水线配置模板。 +ob_logcleaner_module.yaml | ob日志清理模块配置模板。 + diff --git a/docs/config-reference/monagent-config.md b/docs/config-reference/monagent-config.md index d941260ee4a5b302bf47a337d6b05dfd5d209701..a7623df5d92e99aff4bf54a0d0218a1d1628a65a 100644 --- a/docs/config-reference/monagent-config.md +++ b/docs/config-reference/monagent-config.md @@ -1,41 +1,21 @@ # monagent 配置文件说明 -本文介绍 monagent 配置文件中的相关配置项,并列出了配置文件模板供您参考。 +本文介绍 monagent.yaml 配置文件的相关配置项,并列出了配置文件模板供您参考。这里面的大部分配置项都是用了${config_key}来进行表示,${config_key}和config_properties下KV-config关联,并在启动agent时进行替换。 `monagent.yaml` 配置文件的示例如下: ```yaml -## 日志相关配置 -log: - level: debug - filename: log/monagent.log - maxsize: 30 - maxage: 7 - maxbackups: 10 - localtime: true - compress: true -## 进程相关配置。其中,address 是默认的拉取 metrics 和管理相关接口,adminAddress 是 pprof 调试端口。 +## 进程相关配置。其中,address 是默认的拉取 metrics 和管理相关接口,也是pprof调试端口。 server: - address: "0.0.0.0:8088" - adminAddress: "0.0.0.0:8089" - runDir: run + address: 0.0.0.0:${ocp.agent.monitor.http.port} + runDir: ${obagent.home.path}/run ## 配置相关,加密方法支持 aes 和 plain。其中,aes 使用下面 key 文件中的 key 对需要加密的配置项进行加密。 ## modulePath 用来存放配置模版,propertiesPath 用来存放 KV 变量配置 -cryptoMethod: plain -cryptoPath: conf/.config_secret.key -modulePath: conf/module_config -propertiesPath: conf/config_properties +cryptoMethod: aes +cryptoPath: ${obagent.home.path}/conf/.config_secret.key +modulePath: ${obagent.home.path}/conf/module_config +propertiesPath: ${obagent.home.path}/conf/config_properties ``` -## 配置模版 - -monagent 的相关配置文件模板见下表: - -配置文件名称 | 说明 ---- | --- -monagent_basic_auth.yaml | 基础认证的相关配置,用来配置两个端口的开启或者关闭,配置禁用后对应的变量为 {disable_http_basic_auth} 和 {disable_pprof_basic_auth}。 -monagent_config | 配置模块相关的配置,无需修改。 -monitor_node_host.yaml | 主机监控流水线配置模版,无需修改。 -monitor_ob.yaml | OceanBase 数据库监控流水线配置模版,无需修改。 diff --git a/docs/develop-guide.md b/docs/develop-guide.md index a88fe31249657539a47cbc6883a3329296661036..e336a32413cb7f0c38694ab80c395e5e00c646be 100644 --- a/docs/develop-guide.md +++ b/docs/develop-guide.md @@ -6,107 +6,66 @@ OBAgent 是一个插件驱动的监控采集框架。要扩展 OBAgent 的功能 ![OBAgent数据处理流程图](https://github.com/Xjxjy/obagent/blob/master/picture/OBAgent-Process.png) -OBAgent 的数据处理流程包括数据采集、处理和上报,需要用到的插件包含输入插件(Inputs)、处理插件(Process)、输出插件(OutPuts 和 Exporter)。插件详细信息,参考 [外部插件](#外部插件) 章节。 +OBAgent 的数据处理流程包括数据采集、处理和上报,需要用到的插件包含输入插件(Source)、处理插件(Processor)、输出插件(Sink)。插件详细信息,参考 [外部插件](#外部插件) 章节。 ## 外部插件 OBAgent 支持的插件类型见下表: -| 插件类型 | 功能描述 | -| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 输入插件(Input) | 收集各种时间序列性指标,包含各种系统信息和应用信息的插件。 | -| 处理插件(Process) | 串行进行数据处理。 | -| 输出插件(Output) | 仅适用于推模式。用来将 metrics 数据推送到远端。 | -输出插件(Exporter) | 仅适用于拉模式。通过 HTTP 服务暴露数据。用来为 metrics 做格式转换。 +| 插件类型 | 功能描述 | +|-----------------|--------------------------------------------| +| 输入插件(Source) | 收集各种时间序列性指标,包含各种系统信息和应用信息的插件。 | +| 处理插件(Processor) | 串行进行数据处理。 | +| 输出插件(Sink) | 数据输出,支持推和拉两种模式。 | ### 输入插件接口定义 ```go -type Input interface { - Collect() ([]metric.Metric, error) +type Source interface { + Start(out chan<- []*message.Message) (err error) + Stop() } ``` -输入插件需要实现 Collect 方法采集数据。输入插件返回一组 metrics 和 error。 +输入插件在Start中开始采集数据。并将数据写入out中。 ​ ### 处理插件接口定义 ```go type Processor interface { - Process(metrics ...metric.Metric) ([]metric.Metric, error) + Start(in <-chan []*message.Message, out chan<- []*message.Message) (err error) + Stop() } ``` -处理插件需要实现 Process 方法处理数据,输入参数是一组 metrics,输出一组 metrics 和 error。 +处理插件从in中读数据,处理后写到out中。 ​ -### 输出插件(Exporter)接口定义 +### 输出插件接口定义 ```go -type Exporter interface { - Export(metrics []metric.Metric) (*bytes.Buffer, error) +type Sink interface { + Start(in <-chan []*message.Message) error + Stop() } ``` -输出插件(Exporter)需要实现 Export 方法,输入参数是一组 metrcs,输出 byte buffer 和 error。 +输出插件从in中读取数据,如果是拉模式,则放入缓存中等待拉取;如果是推模式,则直接推给目的端。 -### 输出插件(Output)接口定义 +## message 数据结构 -```go -type Output interface { - Write(metrics []metric.Metric) error -} -``` - -输出插件(Output)需要实现 Write 方法,输入参数是一组 metrics,输出 error。 - -## Metric 接口定义 - -OBAgent 数据处理流程中流转的数据定义为统一的 Metric 接口。 +OBAgent 数据处理流程中流转的数据定义为统一的 message。 ```go -type Metric interface { - Clone() Metric - SetName(name string) - GetName() string - SetTime(time time.Time) - GetTime() time.Time - SetMetricType(metricType Type) - GetMetricType() Type - Fields() map[string]interface{} - Tags() map[string]string +type Message struct { + name string + fields []FieldEntry + tags []TagEntry + timestamp time.Time + msgType Type + tagSorted bool + fieldSorted bool + id string } ``` - -## 插件基本接口定义 - -所有的 OBAgent 插件都必须实现以下的基本接口: - -```go -//Initializer 包含 Init 函数 -type Initializer interface { - Init(config map[string]interface{}) error -} - -//Closer 包含 Close 函数 -type Closer interface { - Close() error -} - -//Describer 包含 SampleConfig 和 Description -type Describer interface { - SampleConfig() string - Description() string -} - -``` - -函数详情见下表: - -函数名 | 说明 ---- | --- -Init | 初始化插件。 -Close | 在插件退出时调用,用来关闭一些资源。 -SampleConfig | 用来返回插件的配置样例。 -Description | 用来返回插件的描述信息。 diff --git a/docs/install-and-deploy/deploy-obagent-manually.md b/docs/install-and-deploy/deploy-obagent-manually.md index dcec598ff12b3c453cfe1470aa1a40db63bfe10d..36158d34d986d54eb1c317ec7cd39f94d94c3098 100644 --- a/docs/install-and-deploy/deploy-obagent-manually.md +++ b/docs/install-and-deploy/deploy-obagent-manually.md @@ -14,32 +14,20 @@ OBAgent 提供使用 OBD 部署和手动部署。要手动部署 OBAgent,您 按以下步骤部署 OBAgent: -### 步骤1:部署 monagent +### 步骤1:部署 obagent 1. 修改配置文件,详细信息,参考 [monagent 配置](../config-reference/monagent-config.md) 和 [KV 配置](../config-reference/kv-config.md)。 -2. 启动 monagent 进程。推荐您使用 Supervisor 启动 monagent 进程。 +2. 启动 obagent。 ```bash # 将当前目录切换至 OBAgent 的安装目录 - cd /home/admin/obagent + cd /home/admin/obagent/bin # 启动 monagent 进程 - nohup ./bin/monagent -c conf/monagent.yaml >> ./log/monagent_stdout.log 2>&1 & + ./ob_agentctl start ``` - ```bash - # Supervisor 配置样例 - [program:monagent] - command=./bin/monagent -c conf/monagent.yaml - directory=/home/admin/obagent - autostart=true - autorestart=true - redirect_stderr=true - priority=10 - stdout_logfile=log/monagent_stdout.log - ``` - ### (可选)步骤2:部署 Prometheus > 说明:您需要安装 Prometheus。 @@ -88,10 +76,10 @@ groups: ## (可选)更新 KV 配置 -OBAgent 提供了更新配置的接口。您可以通过 HTTP 服务更新 KV 配置项: +您可以通过 OBAgent 的 黑屏运维工具 ob_agentctl 来更新 KV 配置项: ```bash # 您可以同时更新多个 KV 的配置项,写多组 key 和 value 即可。 -curl --user user:pass -H "Content-Type:application/json" -d '{"configs":[{"key":"monagent.pipeline.ob.status", "value":"active"}]}' -L 'http://ip:port/api/v1/module/config/update' +./ob_agentctl config -u monagent.pipeline.ob.status=active,monagent.host.ip=127.0.0.1 ``` diff --git a/docs/install-and-deploy/deploy-obagent-with-obd.md b/docs/install-and-deploy/deploy-obagent-with-obd.md index 38341cc2c2b40d2b4d730ae7db22649c3ab2bdf4..e2f1d864518d76bf34a266f37ca430d7d2c206ff 100644 --- a/docs/install-and-deploy/deploy-obagent-with-obd.md +++ b/docs/install-and-deploy/deploy-obagent-with-obd.md @@ -24,7 +24,8 @@ obagent: > **注意**:`servers` 字段必须与 oceanbase-ce 的 `servers` 字段一致。 -详细信息,请参考 [配置文件](https://github.com/oceanbase/obdeploy/blob/master/example/obagent/distributed-with-obproxy-and-obagent-example.yaml)。 +详细信息,请参考 [配置文件](https://github.com/oceanbase/obdeploy/blob/master/example/obagent/distributed-with- +obproxy-and-obagent-example.yaml)。 ## 单独部署 OBAgent diff --git a/docs/install-and-deploy/install-obagent.md b/docs/install-and-deploy/install-obagent.md index 0e6804027a7d60ba29d493d1f23e76205a22b8ac..a07870a6bb9a92aae668ab132196ea817d61c32c 100644 --- a/docs/install-and-deploy/install-obagent.md +++ b/docs/install-and-deploy/install-obagent.md @@ -4,7 +4,7 @@ ## 环境依赖 -构建 OBAgent 需要 Go 1.14 版本及以上。 +构建 OBAgent 需要 Go 1.19 版本及以上。 ## RPM 包 @@ -31,23 +31,46 @@ make build-release ## OBAgent 安装目录结构 -OBAgent 的安装目录包含三个子目录:`bin`、`conf` 和 `run`。OBAgent 的安装目录如下: +OBAgent 的安装目录包含以下子目录:`bin`、`conf`、`log` 和 `run`。OBAgent 的安装目录如下: ```bash # 目录结构示例 . ├── bin -│   └── monagent +│   ├── ob_monagent +│   ├── ob_mgragent +│   ├── ob_agentd +│   └── ob_agentctl ├── conf │   ├── config_properties -│   │   ├── monagent_basic_auth.yaml +│   │   ├── basic_auth.yaml +│   │   ├── common_meta.yaml +│   │   ├── log.yaml +│   │   ├── ob_logcleaner.yaml │   │   └── monagent_pipeline.yaml │   ├── module_config +│   │   ├── common_module.yaml +│   │   ├── log_module.yaml +│   │   ├── mgragent_logquerier_module.yaml +│   │   ├── mgragent_module.yaml │   │   ├── monagent_basic_auth.yaml -│   │   ├── monagent_config.yaml +│   │   ├── monitor_host_log.yaml +│   │   ├── monitor_mysql.yaml │   │   ├── monitor_node_host.yaml -│   │   └── monitor_ob.yaml +│   │   ├── monitor_ob.yaml +│   │   ├── monitor_ob_custom.yaml +│   │   ├── monitor_ob_log.yaml +│   │   ├── monitor_observer_log.yaml +│   │   └── ob_logcleaner_module.yaml +│   ├── scripts +│   │   └── obagent.service +│   ├── shell_templates +│   │   └── shell_template.yaml │   ├── monagent.yaml +│   ├── mgragent.yaml +│   ├── agentd.yaml +│   ├── agentctl.yaml +│   ├── obd_agent_mapper.yaml │   └── prometheus_config │   ├── prometheus.yaml │   └── rules @@ -56,4 +79,4 @@ OBAgent 的安装目录包含三个子目录:`bin`、`conf` 和 `run`。OBAgen └── run ``` -其中,`bin` 用来存放二进制文件。`conf` 用来存放程序启动配置、模块配置模板、KV 变量配置和 Prometheus 的配置模板。`run` 用来存放运行文件。更多关于配置文件的信息,参考 [monagent 配置文件](../config-reference/monagent-config.md)。 +其中,`bin` 用来存放二进制文件。`conf` 用来存放程序启动配置、模块配置模板、KV 变量配置和 Prometheus 的配置模板。`log` 用来存放 OBAgent 日志。 `run` 用来存放运行文件。更多关于配置文件的信息,参考 [monagent 配置文件](../config-reference/monagent-config.md)。 diff --git a/docs/obagent-metrics/exporter-metrics.md b/docs/obagent-metrics/exporter-metrics.md index 5c2df6e3a0b125230cb9ddbe5cefd249a192b3f6..459de8ca63cd92acd8cb4e7e87a7aaea478f095a 100644 --- a/docs/obagent-metrics/exporter-metrics.md +++ b/docs/obagent-metrics/exporter-metrics.md @@ -4,51 +4,76 @@ ## 主机指标 -| **指标名** | **Label** | **描述** | **类型** | -| --- | --- | --- | --- | -| node_cpu_seconds_total | cpu,mode,svr_ip | CPU 时间 | counter | -| node_disk_read_bytes_total | device,svr_ip | 磁盘读取字节数 | counter | -| node_disk_read_time_seconds_total | device,svr_ip | 磁盘读取消耗总时间 | counter | -| node_disk_reads_completed_total | device,svr_ip | 磁盘读取完成次数 | counter | -| node_disk_written_bytes_total | device,svr_ip | 磁盘写入字节数 | counter | -| node_disk_write_time_seconds_total | device,svr_ip | 磁盘写入消耗总时间 | counter | -| node_disk_writes_completed_total | device,svr_ip | 磁盘写入完成次数 | counter | +| **指标名** | **Label** | **描述** | **类型** | +| --- |--------------------------------| --- | --- | +| node_cpu_seconds_total | cpu,mode,svr_ip | CPU 时间 | counter | +| node_disk_read_bytes_total | device,svr_ip | 磁盘读取字节数 | counter | +| node_disk_read_time_seconds_total | device,svr_ip | 磁盘读取消耗总时间 | counter | +| node_disk_reads_completed_total | device,svr_ip | 磁盘读取完成次数 | counter | +| node_disk_written_bytes_total | device,svr_ip | 磁盘写入字节数 | counter | +| node_disk_write_time_seconds_total | device,svr_ip | 磁盘写入消耗总时间 | counter | +| node_disk_writes_completed_total | device,svr_ip | 磁盘写入完成次数 | counter | +| node_disk_io_time_weighted_seconds_total | device,svr_ip | 磁盘IO时间 | counter | | node_filesystem_avail_bytes | device,fstype,mountpoint,svr_ip | 文件系统可用大小 | gauge | -| node_filesystem_readonly | device,fstype,mountpoint,svr_ip | 文件系统是否只读 | gauge | | node_filesystem_size_bytes | device,fstype,mountpoint,svr_ip | 文件系统大小 | gauge | -| node_load1 | svr_ip | 1 分钟平均 load | gauge | -| node_load5 | svr_ip | 5 分钟平均 load | gauge | -| node_load15 | svr_ip | 15 分钟平均 load | gauge | -| node_memory_Buffers_bytes | svr_ip | 内存 buffer 大小 | gauge | -| node_memory_Cached_bytes | svr_ip | 内存 cache 大小 | gauge | -| node_memory_MemFree_bytes | svr_ip | 内存 free 大小 | gauge | -| node_memory_MemTotal_bytes | svr_ip | 内存总大小 | gauge | -| node_network_receive_bytes_total | device,svr_ip | 网络接受总字节数 | counter | -| node_network_transmit_bytes_total | device,svr_ip | 网络发送总字节数 | counter | -| node_ntp_offset_seconds | svr_ip | NTP 时钟偏移 | gauge | +| node_load1 | svr_ip | 1 分钟平均 load | gauge | +| node_load5 | svr_ip | 5 分钟平均 load | gauge | +| node_load15 | svr_ip | 15 分钟平均 load | gauge | +| node_memory_Buffers_bytes | svr_ip | 内存 buffer 大小 | gauge | +| node_memory_Cached_bytes | svr_ip | 内存 cache 大小 | gauge | +| node_memory_MemFree_bytes | svr_ip | 内存 free 大小 | gauge | +| node_memory_SReclaimable_bytes | svr_ip | 可回收slab内存的大小 | gauge | +| node_memory_MemTotal_bytes | svr_ip | 内存总大小 | gauge | +| node_network_receive_bytes_total | device,svr_ip | 网络接受总字节数 | counter | +| node_network_transmit_bytes_total | device,svr_ip | 网络发送总字节数 | counter | +| node_ntp_offset_seconds | svr_ip | NTP 时钟偏移 | gauge | +| cpu_count | svr_ip | cpu核数 | gauge | +| node_net_bandwidth_bps | device,svr_ip | 网卡速率 | gauge | +| io_util | device,svr_ip | IO负载 | gauge | +| io_await | device,svr_ip | IO耗时 | gauge | + ## OceanBase 数据库指标 -| **指标名** | **label** | **含义** | **类型** | -| --- | --- | --- | --- | -| ob_active_session_num | ob_cluster_id,ob_cluster_name,obzone,svr_ip,tenant_name | 活跃连接数 | gauge | -| ob_cache_size_bytes | ob_cluster_id,ob_cluster_name,obzone,svr_ip,tenant_name,cache_name | kvcache 大小 | gauge | -| ob_partition_num | ob_cluster_id,ob_cluster_name,obzone,svr_ip,tenant_name | 分区数 | gauge | -| ob_plan_cache_access_total | ob_cluster_id,ob_cluster_name,obzone,svr_ip,tenant_name | 执行计划访问次数 | counter | -| ob_plan_cache_hit_total | ob_cluster_id,ob_cluster_name,obzone,svr_ip,tenant_name | 执行计划命中次数 | counter | -| ob_plan_cache_memory_bytes | ob_cluster_id,ob_cluster_name,obzone,svr_ip,tenant_name | plancache大小 | gauge | -| ob_table_num | ob_cluster_id,ob_cluster_name,obzone,svr_ip,tenant_name | 表数量 | gauge | -| ob_waitevent_wait_seconds_total | ob_cluster_id,ob_cluster_name,obzone,svr_ip,tenant_name | 等待事件总等待时间 | counter | -| ob_waitevent_wait_total | ob_cluster_id,ob_cluster_name,obzone,svr_ip,tenant_name | 等待事件总等待次数 | counter | -| ob_disk_free_bytes | ob_cluster_id,ob_cluster_name,obzone,svr_ip | OceanBase 磁盘剩余大小 | gauge | -| ob_disk_total_bytes | ob_cluster_id,ob_cluster_name,obzone,svr_ip | OceanBase 磁盘总大小 | gauge | -| ob_memstore_active_bytes | ob_cluster_id,ob_cluster_name,obzone,svr_ip,tenant_name | 活跃 memstore 大小 | gauge | -| ob_memstore_freeze_times | ob_cluster_id,ob_cluster_name,obzone,svr_ip,tenant_name | memstore 冻结次数 | counter | -| ob_memstore_freeze_trigger_bytes | ob_cluster_id,ob_cluster_name,obzone,svr_ip,tenant_name | memstore 冻结阈值 | gauge | -| ob_memstore_total_bytes | ob_cluster_id,ob_cluster_name,obzone,svr_ip,tenant_name | memstore 总大小 | gauge | -| ob_server_resource_cpu | ob_cluster_id,ob_cluster_name,obzone,svr_ip | observer可用cpu数 | gauge | -| ob_server_resource_cpu_assigned | ob_cluster_id,ob_cluster_name,obzone,svr_ip | observer 已分配 CPU 数 | gauge | -| ob_server_resource_memory_bytes | ob_cluster_id,ob_cluster_name,obzone,svr_ip | observer 可用内存大小 | gauge | -| ob_server_resource_memory_assigned_bytes | ob_cluster_id,ob_cluster_name,obzone,svr_ip | observer 已分配内存大小 | gauge | -| ob_unit_num | ob_cluster_id,ob_cluster_name,obzone,svr_ip | observer unit 数量 | gauge | -| ob_sysstat | ob_cluster_id,ob_cluster_name,obzone,svr_ip,tenant_name,stat_id | ob内部统计项 | 不同 stat_id 不相同,参考对应部分解释 | +| **指标名** | **label** | **含义** | **类型** | +|--------------------------------------------|---------------------------------------------------------------------------------|--------------------| --- | +| ob_active_session_num | ob_cluster_id,ob_cluster_name,obzone,svr_ip,ob_tenant_id,tenant_name | 活跃连接数 | gauge | +| ob_all_session_num | ob_cluster_id,ob_cluster_name,obzone,svr_ip,ob_tenant_id,tenant_name | 总连接数 | gauge | +| ob_cache_size_bytes | ob_cluster_id,ob_cluster_name,obzone,svr_ip,ob_tenant_id,tenant_name,cache_name | kvcache 大小 | gauge | +| ob_server_num | ob_cluster_id,ob_cluster_name,obzone,svr_ip | observer数量 | gauge | +| ob_partition_num | ob_cluster_id,ob_cluster_name,obzone,svr_ip,ob_tenant_id,tenant_name | 分区数 | gauge | +| ob_plan_cache_access_total | ob_cluster_id,ob_cluster_name,obzone,svr_ip,ob_tenant_id,tenant_name | 执行计划访问次数 | counter | +| ob_plan_cache_hit_total | ob_cluster_id,ob_cluster_name,obzone,svr_ip,ob_tenant_id,tenant_name | 执行计划命中次数 | counter | +| ob_plan_cache_memory_bytes | ob_cluster_id,ob_cluster_name,obzone,svr_ip,ob_tenant_id,tenant_name | plancache大小 | gauge | +| ob_table_num | ob_cluster_id,ob_cluster_name,obzone,svr_ip,ob_tenant_id,tenant_name | 表数量 | gauge | +| ob_waitevent_wait_seconds_total | ob_cluster_id,ob_cluster_name,obzone,svr_ip,ob_tenant_id,tenant_name | 等待事件总等待时间 | counter | +| ob_waitevent_wait_total | ob_cluster_id,ob_cluster_name,obzone,svr_ip,ob_tenant_id,tenant_name | 等待事件总等待次数 | counter | +| ob_system_event_total_waits | ob_cluster_id,ob_cluster_name,obzone,svr_ip,ob_tenant_id,tenant_name,event_group | 系统事件总等待次数 | counter | +| ob_system_event_time_waited | ob_cluster_id,ob_cluster_name,obzone,svr_ip,ob_tenant_id,tenant_name,event_group | 系统事件总等待时间 | counter | +| ob_disk_free_bytes | ob_cluster_id,ob_cluster_name,obzone,svr_ip | OceanBase 磁盘剩余大小 | gauge | +| ob_disk_total_bytes | ob_cluster_id,ob_cluster_name,obzone,svr_ip | OceanBase 磁盘总大小 | gauge | +| ob_memstore_active_bytes | ob_cluster_id,ob_cluster_name,obzone,svr_ip,ob_tenant_id,tenant_name | 活跃 memstore 大小 | gauge | +| ob_memstore_freeze_times | ob_cluster_id,ob_cluster_name,obzone,svr_ip,ob_tenant_id,tenant_name | memstore 冻结次数 | counter | +| ob_memstore_freeze_trigger_bytes | ob_cluster_id,ob_cluster_name,obzone,svr_ip,ob_tenant_id,tenant_name | memstore 冻结阈值 | gauge | +| ob_memstore_total_bytes | ob_cluster_id,ob_cluster_name,obzone,svr_ip,ob_tenant_id,tenant_name | memstore 总大小 | gauge | +| ob_server_resource_cpu | ob_cluster_id,ob_cluster_name,obzone,svr_ip | observer可用cpu数 | gauge | +| ob_server_resource_cpu_assigned | ob_cluster_id,ob_cluster_name,obzone,svr_ip | observer 已分配 CPU 数 | gauge | +| ob_server_resource_memory_bytes | ob_cluster_id,ob_cluster_name,obzone,svr_ip | observer 可用内存大小 | gauge | +| ob_server_resource_memory_assigned_bytes | ob_cluster_id,ob_cluster_name,obzone,svr_ip | observer 已分配内存大小 | gauge | +| ob_server_resource_disk_bytes | ob_cluster_id,ob_cluster_name,obzone,svr_ip | observer可用磁盘大小 | gauge | +| ob_server_resource_cpu_assigned_percent | ob_cluster_id,ob_cluster_name,obzone,svr_ip | observer CPU使用率 | gauge | +| ob_server_resource_memory_assigned_percent | ob_cluster_id,ob_cluster_name,obzone,svr_ip | observer 内存使用率 | gauge | +| ob_tenant_resource_max_cpu | ob_cluster_id,ob_cluster_name,obzone,svr_ip,ob_tenant_id,tenant_name | 租户最大可用cpu数 | gauge | +| ob_tenant_resource_min_cpu | ob_cluster_id,ob_cluster_name,obzone,svr_ip,ob_tenant_id,tenant_name | 租户最小可用cpu数 | gauge | +| ob_tenant_resource_max_memory | ob_cluster_id,ob_cluster_name,obzone,svr_ip,ob_tenant_id,tenant_name | 租户最大可用内存大小 | gauge | +| ob_tenant_resource_min_memory | ob_cluster_id,ob_cluster_name,obzone,svr_ip,ob_tenant_id,tenant_name | 租户最小可用内存大小 | gauge | +| ob_tenant_assigned_cpu_total | ob_cluster_id,ob_cluster_name,obzone,svr_ip,ob_tenant_id,tenant_name | observer cpu总数 | gauge | +| ob_tenant_assigned_cpu_assigned | ob_cluster_id,ob_cluster_name,obzone,svr_ip,ob_tenant_id,tenant_name | observer已分配cpu数 | gauge | +| ob_tenant_assigned_mem_total | ob_cluster_id,ob_cluster_name,obzone,svr_ip,ob_tenant_id,tenant_name | observer内存总量 | gauge | +| ob_tenant_assigned_mem_assigned | ob_cluster_id,ob_cluster_name,obzone,svr_ip,ob_tenant_id,tenant_name | observer已分配内存量 | gauge | +| ob_tenant_disk_data_size | ob_cluster_id,ob_cluster_name,obzone,svr_ip,ob_tenant_id,tenant_name | observer数据盘使用量 | gauge | +| ob_tenant_disk_log_size | ob_cluster_id,ob_cluster_name,obzone,svr_ip,ob_tenant_id,tenant_name | observer日志盘使用量 | gauge | +| ob_disk_total_bytes | ob_cluster_id,ob_cluster_name,obzone,svr_ip | observer磁盘总量 | gauge | +| ob_disk_free_bytes | ob_cluster_id,ob_cluster_name,obzone,svr_ip | observer空闲磁盘量 | gauge | +| ob_unit_num | ob_cluster_id,ob_cluster_name,obzone,svr_ip | observer unit 数量 | gauge | +| ob_sysstat | ob_cluster_id,ob_cluster_name,obzone,svr_ip,tenant_name,stat_id | ob内部统计项 | 不同 stat_id 不相同,参考对应部分解释 | diff --git a/docs/obagent-metrics/sysstat.md b/docs/obagent-metrics/sysstat.md index c7e4fdc03285d6136b2239fb3868e398d8cbd956..a26485ee0f54bd041a4c10b0bc0194326073918e 100644 --- a/docs/obagent-metrics/sysstat.md +++ b/docs/obagent-metrics/sysstat.md @@ -2,58 +2,81 @@ 系统统计项(sysstat)的信息见下表: -| **stat_id** | **描述** | **类型** | -| --- | --- | --- | -| 10000 | 收到的 RPC 包数量 | counter | -| 10001 | 收到的 RPC 包大小 | counter | -| 10002 | 发送的 RPC 包数量 | counter | -| 10003 | 发送的 RPC 包大小 | counter | -| 10005 | RPC 网络延迟 | counter | -| 10006 | RPC 框架延迟 | counter | -| 20001 | 请求出队列次数 | counter | -| 20002 | 请求在队列中时间 | counter | -| 30000 | clog 同步时间 | counter | -| 30001 | clog 同步次数 | counter | -| 30002 | clog 提交次数 | counter | -| 30005 | 事务数 | counter | -| 30006 | 事务总时间 | counter | -| 40000 | select SQL 数 | counter | -| 40001 | select SQL执行时间 | counter | -| 40002 | insert SQL 数 | counter | -| 40003 | insert SQL 执行时间 | counter | -| 40004 | replace SQL 数 | counter | -| 40005 | replace SQL 执行时间 | counter | -| 40006 | update SQL 数 | counter | -| 40007 | update SQL 执行时间 | counter | -| 40008 | delete SQL 数 | counter | -| 40009 | delete SQL 执行时间 | counter | -| 40010 | 本地 SQL 执行次数 | counter | -| 40011 | 远程 SQL 执行次数 | counter | -| 40012 | 分布式 SQL 执行次数 | counter | -| 50000 | row cache 命中次数 | counter | -| 50001 | row cache 没有命中次数 | counter | -| 50008 | block cache 命中次数 | counter | -| 50009 | block cache 没有命中次数 | counter | -| 60000 | IO 读次数 | counter | -| 60001 | IO 读延时 | counter | -| 60002 | IO 读字节数 | counter | -| 60003 | IO 写次数 | counter | -| 60004 | IO 写延时 | counter | -| 60005 | IO 写字节数 | counter | -| 60019 | memstore 读锁成功次数 | counter | -| 60020 | memstore 读锁失败次数 | counter | -| 60021 | memstore 写锁成功次数 | counter | -| 60022 | memstore 写锁成功次数 | counter | -| 60023 | memstore 等写锁时间 | counter | -| 60024 | memstore 等读锁时间 | counter | -| 80040 | clog写次数 | counter | -| 80041 | clog写时间 | counter | -| 80057 | clog大小 | counter | -| 130000 | 活跃 memstore 大小 | gauge | -| 130001 | memstore 总大小 | gauge | -| 130002 | 触发 major freeze 阈值 | gauge | -| 130004 | memstore 大小限制 | gauge | -| 140002 | 最大可使用内存 | gauge | -| 140003 | 已使用内存 | gauge | -| 140005 | 最大可使用 CPU | gauge | -| 140006 | 已使用 CPU | gauge | +| **stat_id** | **描述** | **类型** | +|-------------|--------------------| --- | +| 10000 | 收到的 RPC 包数量 | counter | +| 10001 | 收到的 RPC 包大小 | counter | +| 10002 | 发送的 RPC 包数量 | counter | +| 10003 | 发送的 RPC 包大小 | counter | +| 10004 | 发送rpc失败的数量 | counter | +| 10005 | RPC 网络延迟 | counter | +| 10006 | RPC 框架延迟 | counter | +| 20000 | 请求入队列次数 | counter | +| 20001 | 请求出队列次数 | counter | +| 20002 | 请求在队列中时间 | counter | +| 30000 | clog 同步时间 | counter | +| 30001 | clog 同步次数 | counter | +| 30002 | clog 提交次数 | counter | +| 30005 | 事务数 | counter | +| 30006 | 事务总时间 | counter | +| 30007 | 事务提交次数 | counter | +| 30008 | 事务提交耗时 | counter | +| 30009 | 事务回滚次数 | counter | +| 30010 | 事务回滚耗时 | counter | +| 30011 | 事务超时次数 | counter | +| 30012 | 本地事务次数 | counter | +| 30013 | 分布式事务次数 | counter | +| 40000 | select SQL 数 | counter | +| 40001 | select SQL执行时间 | counter | +| 40002 | insert SQL 数 | counter | +| 40003 | insert SQL 执行时间 | counter | +| 40004 | replace SQL 数 | counter | +| 40005 | replace SQL 执行时间 | counter | +| 40006 | update SQL 数 | counter | +| 40007 | update SQL 执行时间 | counter | +| 40008 | delete SQL 数 | counter | +| 40009 | delete SQL 执行时间 | counter | +| 40010 | 本地 SQL 执行次数 | counter | +| 40011 | 远程 SQL 执行次数 | counter | +| 40012 | 分布式 SQL 执行次数 | counter | +| 40018 | 其他sql数 | counter | +| 40019 | 其他sql执行时间 | counter | +| 40030 | 打开的游标数 | counter | +| 50000 | row cache 命中次数 | counter | +| 50001 | row cache 没有命中次数 | counter | +| 50004 | bloomfilter缓存命中数 | counter | +| 50005 | bloomfilter缓存miss数 | counter | +| 50008 | block cache 命中次数 | counter | +| 50009 | block cache 没有命中次数 | counter | +| 50010 | location 缓存命中数 | counter | +| 50011 | location 缓存miss数 | counter | +| 50037 | clog缓存命中数 | counter | +| 50038 | clog缓存miss数 | counter | +| 60000 | IO 读次数 | counter | +| 60001 | IO 读延时 | counter | +| 60002 | IO 读字节数 | counter | +| 60003 | IO 写次数 | counter | +| 60004 | IO 写延时 | counter | +| 60005 | IO 写字节数 | counter | +| 60019 | memstore 读锁成功次数 | counter | +| 60020 | memstore 读锁失败次数 | counter | +| 60021 | memstore 写锁成功次数 | counter | +| 60022 | memstore 写锁成功次数 | counter | +| 60023 | memstore 等写锁时间 | counter | +| 60024 | memstore 等读锁时间 | counter | +| 60087 | 索引微块缓存命中 | counter | +| 80040 | clog写次数 | counter | +| 80041 | clog写时间 | counter | +| 80057 | clog大小 | counter | +| 120000 | location 缓存大小 | gauge | +| 120001 | clog 缓存大小 | gauge | +| 120008 | 行缓存大小 | gauge | +| 120009 | bloomfilter 缓存大小 | gauge | +| 130000 | 活跃 memstore 大小 | gauge | +| 130001 | memstore 总大小 | gauge | +| 130002 | 触发 major freeze 阈值 | gauge | +| 130004 | memstore 大小限制 | gauge | +| 140002 | 最大可使用内存 | gauge | +| 140003 | 已使用内存 | gauge | +| 140005 | 最大可使用 CPU | gauge | +| 140006 | 已使用 CPU | gauge | diff --git a/engine/config_manager_test.go b/engine/config_manager_test.go deleted file mode 100644 index fd1ff58993482a0c215bdd1c7551f35c42d4645a..0000000000000000000000000000000000000000 --- a/engine/config_manager_test.go +++ /dev/null @@ -1,271 +0,0 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - -package engine - -import ( - "context" - "encoding/json" - "reflect" - "testing" - - "github.com/oceanbase/obagent/config" -) - -func TestConfigManagerHandleEvent(t *testing.T) { - testPipelineModule := &config.PipelineModule{} - err := json.Unmarshal([]byte(testJSONModule), testPipelineModule) - if err != nil { - t.Errorf("test config manager set config json decode failed %s", err.Error()) - return - } - addEvent := &configEvent{ - eventType: addConfigEvent, - pipelineModule: testPipelineModule, - } - updateEvent := &configEvent{ - eventType: updateConfigEvent, - pipelineModule: testPipelineModule, - } - delEvent := &configEvent{ - eventType: deleteConfigEvent, - pipelineModule: testPipelineModule, - } - - tests := []struct { - name string - fields *ConfigManager - args *configEvent - }{ - {name: "testAdd", fields: GetConfigManager(), args: addEvent}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c := &ConfigManager{ - configMap: tt.fields.configMap, - eventChan: tt.fields.eventChan, - eventCallbackChan: tt.fields.eventCallbackChan, - } - - GetPipelineManager().eventCallbackChan <- &pipelineCallbackEvent{ - eventType: addPipelineEvent, - execStatus: pipelineEventExecSucceed, - description: "", - } - callback := &configCallbackEvent{} - err = c.handleAddEvent(tt.args, callback) - if err != nil { - t.Errorf("config managaer test handle add event failed %s", err.Error()) - } - - GetPipelineManager().eventCallbackChan <- &pipelineCallbackEvent{ - eventType: addPipelineEvent, - execStatus: pipelineEventExecFailed, - description: "", - } - callback = &configCallbackEvent{} - _ = c.handleAddEvent(tt.args, callback) - }) - } - - tests = []struct { - name string - fields *ConfigManager - args *configEvent - }{ - {name: "testUpdate", fields: GetConfigManager(), args: updateEvent}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c := &ConfigManager{ - configMap: tt.fields.configMap, - eventChan: tt.fields.eventChan, - eventCallbackChan: tt.fields.eventCallbackChan, - } - GetPipelineManager().eventCallbackChan <- &pipelineCallbackEvent{ - eventType: updatePipelineEvent, - execStatus: pipelineEventExecSucceed, - description: "", - } - callback := &configCallbackEvent{} - err = c.handleUpdateEvent(tt.args, callback) - if err != nil { - t.Errorf("config managaer test handle update event failed %s", err.Error()) - } - - GetPipelineManager().eventCallbackChan <- &pipelineCallbackEvent{ - eventType: updatePipelineEvent, - execStatus: pipelineEventExecFailed, - description: "", - } - callback = &configCallbackEvent{} - _ = c.handleUpdateEvent(tt.args, callback) - }) - } - - tests = []struct { - name string - fields *ConfigManager - args *configEvent - }{ - {name: "testDel", fields: GetConfigManager(), args: delEvent}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c := &ConfigManager{ - configMap: tt.fields.configMap, - eventChan: tt.fields.eventChan, - eventCallbackChan: tt.fields.eventCallbackChan, - } - GetPipelineManager().eventCallbackChan <- &pipelineCallbackEvent{ - eventType: deletePipelineEvent, - execStatus: pipelineEventExecSucceed, - description: "", - } - callback := &configCallbackEvent{} - err = c.handleDelEvent(tt.args, callback) - if err != nil { - t.Errorf("config managaer test handle update event failed %s", err.Error()) - } - - GetPipelineManager().eventCallbackChan <- &pipelineCallbackEvent{ - eventType: deletePipelineEvent, - execStatus: pipelineEventExecFailed, - description: "", - } - callback = &configCallbackEvent{} - _ = c.handleDelEvent(tt.args, callback) - }) - } -} - -func TestConfigManagerSchedule(t *testing.T) { - testPipelineModule := &config.PipelineModule{} - err := json.Unmarshal([]byte(testJSONModule), testPipelineModule) - if err != nil { - t.Errorf("test config manager set config json decode failed %s", err.Error()) - return - } - tests := []struct { - name string - fields *ConfigManager - }{ - {name: "test", fields: GetConfigManager()}, - } - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c := &ConfigManager{ - configMap: tt.fields.configMap, - eventChan: tt.fields.eventChan, - eventCallbackChan: tt.fields.eventCallbackChan, - } - c.Schedule(ctx) - event := &configEvent{ - eventType: addConfigEvent, - pipelineModule: testPipelineModule, - } - GetConfigManager().eventChan <- event - GetPipelineManager().eventCallbackChan <- &pipelineCallbackEvent{ - eventType: addPipelineEvent, - execStatus: pipelineEventExecSucceed, - description: "", - } - _ = <-GetConfigManager().eventCallbackChan - - GetConfigManager().eventChan <- event - GetPipelineManager().eventCallbackChan <- &pipelineCallbackEvent{ - eventType: addPipelineEvent, - execStatus: pipelineEventExecFailed, - description: "", - } - _ = <-GetConfigManager().eventCallbackChan - - event = &configEvent{ - eventType: deleteConfigEvent, - pipelineModule: testPipelineModule, - } - GetConfigManager().eventChan <- event - GetPipelineManager().eventCallbackChan <- &pipelineCallbackEvent{ - eventType: deletePipelineEvent, - execStatus: pipelineEventExecSucceed, - description: "", - } - _ = <-GetConfigManager().eventCallbackChan - - GetConfigManager().eventChan <- event - GetPipelineManager().eventCallbackChan <- &pipelineCallbackEvent{ - eventType: deletePipelineEvent, - execStatus: pipelineEventExecFailed, - description: "", - } - _ = <-GetConfigManager().eventCallbackChan - - event = &configEvent{ - eventType: updateConfigEvent, - pipelineModule: testPipelineModule, - } - GetConfigManager().eventChan <- event - GetPipelineManager().eventCallbackChan <- &pipelineCallbackEvent{ - eventType: updatePipelineEvent, - execStatus: pipelineEventExecSucceed, - description: "", - } - _ = <-GetConfigManager().eventCallbackChan - - GetConfigManager().eventChan <- event - GetPipelineManager().eventCallbackChan <- &pipelineCallbackEvent{ - eventType: updatePipelineEvent, - execStatus: pipelineEventExecFailed, - description: "", - } - _ = <-GetConfigManager().eventCallbackChan - }) - } -} - -func TestConfigManagerConfig(t *testing.T) { - testPipelineModule := &config.PipelineModule{} - err := json.Unmarshal([]byte(testJSONModule), testPipelineModule) - if err != nil { - t.Errorf("test config manager set config json decode failed %s", err.Error()) - return - } - t.Run("setConfigTest", func(t *testing.T) { - GetConfigManager().setConfig("test", testPipelineModule) - }) - - t.Run("getConfigTest", func(t *testing.T) { - GetConfigManager().getConfig("test") - }) - - t.Run("setConfigTest", func(t *testing.T) { - GetConfigManager().delConfig("test") - }) -} - -func TestGetConfigManager(t *testing.T) { - tests := []struct { - name string - want *ConfigManager - }{ - {name: "test", want: GetConfigManager()}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := GetConfigManager(); !reflect.DeepEqual(got, tt.want) { - t.Errorf("GetConfigManager() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/engine/converter.go b/engine/converter.go deleted file mode 100644 index f0ef24454521e2f120ed477ab37af747a01116e6..0000000000000000000000000000000000000000 --- a/engine/converter.go +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - -package engine - -import "github.com/oceanbase/obagent/config" - -func createInputInstance(pluginNode *config.PluginNode) *InputInstance { - input := &InputInstance{ - PluginName: pluginNode.Plugin, - Config: pluginNode.Config, - Input: nil, - } - return input -} - -func createProcessorInstance(pluginNode *config.PluginNode) *ProcessorInstance { - processor := &ProcessorInstance{ - PluginName: pluginNode.Plugin, - Config: pluginNode.Config, - Processor: nil, - } - return processor -} - -func createOutputInstance(pluginNode *config.PluginNode) *OutputInstance { - output := &OutputInstance{ - PluginName: pluginNode.Plugin, - Config: pluginNode.Config, - Output: nil, - } - return output -} - -func createExporterInstance(pluginNode *config.PluginNode) *ExporterInstance { - exporter := &ExporterInstance{ - PluginName: pluginNode.Plugin, - Config: pluginNode.Config, - Exporter: nil, - } - return exporter -} - -func createPipeline(pipelineStructure *config.PipelineStructure) *pipeline { - inputs := make([]*InputInstance, len(pipelineStructure.Inputs)) - processors := make([]*ProcessorInstance, len(pipelineStructure.Processors)) - for idx, inputPluginNode := range pipelineStructure.Inputs { - inputs[idx] = createInputInstance(inputPluginNode) - } - for idx, processorPluginNode := range pipelineStructure.Processors { - processors[idx] = createProcessorInstance(processorPluginNode) - } - var output *OutputInstance - if pipelineStructure.Output != nil { - output = createOutputInstance(pipelineStructure.Output) - } - var exporter *ExporterInstance - if pipelineStructure.Exporter != nil { - exporter = createExporterInstance(pipelineStructure.Exporter) - } - pipeline := &pipeline{ - InputInstances: inputs, - ProcessorInstances: processors, - OutputInstance: output, - ExporterInstance: exporter, - } - return pipeline -} - -func createPipelineInstance(pipelineNode *config.PipelineNode) *PipelineInstance { - pipeline := createPipeline(pipelineNode.Structure) - pipelineInstance := &PipelineInstance{ - name: pipelineNode.Name, - pipeline: pipeline, - config: pipelineNode.Config, - } - return pipelineInstance -} - -//CreatePipelineInstances create pipeline instances according to the pipeline module -func CreatePipelineInstances(pipelineModule *config.PipelineModule) []*PipelineInstance { - pipelines := make([]*PipelineInstance, len(pipelineModule.Pipelines)) - for idx, pipelineNode := range pipelineModule.Pipelines { - pipelineInstance := createPipelineInstance(pipelineNode) - pipelines[idx] = pipelineInstance - } - return pipelines -} diff --git a/engine/converter_test.go b/engine/converter_test.go deleted file mode 100644 index a78d2a7c26ef1ef6ff9d65ca150254442f21e41b..0000000000000000000000000000000000000000 --- a/engine/converter_test.go +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - -package engine - -import ( - "encoding/json" - "reflect" - "testing" - - "github.com/oceanbase/obagent/config" -) - -func TestCreateModule(t *testing.T) { - testPipelineModule := &config.PipelineModule{} - err := json.Unmarshal([]byte(testJSONModule), testPipelineModule) - if err != nil { - t.Errorf("test create json moudle failed %s", err.Error()) - return - } - testPipelineInstances := CreatePipelineInstances(testPipelineModule) - - type args struct { - pipelineModule *config.PipelineModule - } - tests := []struct { - name string - args args - want []*PipelineInstance - }{ - {name: "test", args: args{testPipelineModule}, want: testPipelineInstances}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := CreatePipelineInstances(tt.args.pipelineModule); !reflect.DeepEqual(got, tt.want) { - t.Errorf("CreatePipelineInstances() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/engine/monagent_server.go b/engine/monagent_server.go deleted file mode 100644 index 1f4bfce07a94320f9af255b97674ee8d9a872ea5..0000000000000000000000000000000000000000 --- a/engine/monagent_server.go +++ /dev/null @@ -1,130 +0,0 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - -package engine - -import ( - "context" - "net/http" - "sync" - "time" - - "github.com/gin-gonic/gin" - "github.com/pkg/errors" - log "github.com/sirupsen/logrus" - - "github.com/oceanbase/obagent/api/route" - "github.com/oceanbase/obagent/api/web" - "github.com/oceanbase/obagent/config" - "github.com/oceanbase/obagent/utils" -) - -var monitorAgentServer *MonitorAgentServer - -func GetMonitorAgentServer() *MonitorAgentServer { - return monitorAgentServer -} - -type MonitorAgentServer struct { - // original configs - Config *config.MonitorAgentConfig - // sever of monitor metrics, selfstat, monitor manager API - Server *web.HttpServer - // server of pprof - AdminServer *web.HttpServer - // server status map - serverStatusMap sync.Map -} - -// NewMonitorAgentServer init monagent server: init configs and logger, register routers -func NewMonitorAgentServer(conf *config.MonitorAgentConfig) *MonitorAgentServer { - monagentServer := &MonitorAgentServer{ - Config: conf, - Server: &web.HttpServer{ - Counter: new(web.Counter), - Router: gin.Default(), - BasicAuthorizer: new(web.BasicAuth), - Server: &http.Server{}, - Address: conf.Server.Address, - }, - AdminServer: &web.HttpServer{ - Counter: new(web.Counter), - Router: gin.Default(), - BasicAuthorizer: new(web.BasicAuth), - Server: &http.Server{}, - Address: conf.Server.AdminAddress, - }, - } - monitorAgentServer = monagentServer - return monitorAgentServer -} - -// Run start mongagent servers: admin server, monitor server -func (server *MonitorAgentServer) Run() error { - // check port available before start server - go func() { - defer server.serverStatusMap.Store("adminServer", false) - ctx, cancel := context.WithCancel(context.Background()) - server.AdminServer.Cancel = cancel - server.AdminServer.Run(ctx) - log.Info("start admin server") - }() - go func() { - defer server.serverStatusMap.Store("server", false) - ctx, cancel := context.WithCancel(context.Background()) - server.Server.Cancel = cancel - server.Server.Run(ctx) - log.Info("start server") - }() - - for { - adminServerStatus, _ := server.serverStatusMap.LoadOrStore("adminServer", true) - serverStatus, _ := server.serverStatusMap.LoadOrStore("server", true) - adminServerOk, convertAdminServerOk := utils.ConvertToBool(adminServerStatus) - serverOk, convertServerOk := utils.ConvertToBool(serverStatus) - if !(convertAdminServerOk && convertServerOk) { - return errors.New("check monagent server status failed") - } - if !(adminServerOk && serverOk) { - log.Infof("server status ok: %v", serverOk) - log.Infof("admin server status ok: %v", adminServerOk) - return errors.New("start monagent server failed") - } - time.Sleep(time.Second * 1) - } - return nil -} - -// registerRouter register routers such as adminServer router and moniter metrics router -func (server *MonitorAgentServer) RegisterRouter() error { - if err := server.registerAdminServerRouter(); err != nil { - return errors.Wrap(err, "monitor agent server register admin server router") - } - if err := server.registerServerRouter(); err != nil { - return errors.Wrap(err, "monitor agent server register metrics server router") - } - return nil -} - -// registerServerRouter routers for moniter metrics. -func (server *MonitorAgentServer) registerServerRouter() error { - server.Server.UseCounter() - route.InitMonagentRoutes(server.Server.Router) - return nil -} - -// registerAdminServerRouter routers for selfstat, pprof and so on. -func (server *MonitorAgentServer) registerAdminServerRouter() error { - server.AdminServer.UseCounter() - route.InitPprofRouter(server.AdminServer.Router) - return nil -} diff --git a/engine/monagent_server_test.go b/engine/monagent_server_test.go deleted file mode 100644 index 1ad7e3efd9667332e74c6a2d9a545606e46c6639..0000000000000000000000000000000000000000 --- a/engine/monagent_server_test.go +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - -package engine - -import ( - "context" - "testing" - - . "github.com/smartystreets/goconvey/convey" - - "github.com/oceanbase/obagent/config" -) - -func TestMonitorAgentServerShutdown(t *testing.T) { - server := NewMonitorAgentServer(&config.MonitorAgentConfig{ - Log: &config.LogConfig{Level: "debug"}, - Server: config.MonitorAgentHttpConfig{ - Address: ":62889", - AdminAddress: ":62886", - }, - }) - go server.Run() - - t.Run("shutdown without any request", func(t *testing.T) { - err := server.Server.Shutdown(context.Background()) - Convey("shutdown err", t, func() { - So(err, ShouldBeNil) - }) - - adminerr := server.AdminServer.Shutdown(context.Background()) - Convey("shutdown adminerr", t, func() { - So(adminerr, ShouldBeNil) - }) - }) - -} diff --git a/engine/pipeline.go b/engine/pipeline.go deleted file mode 100644 index 4454f3d5bd4b3fbd94359bfff74cfc830a3859cb..0000000000000000000000000000000000000000 --- a/engine/pipeline.go +++ /dev/null @@ -1,208 +0,0 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - -package engine - -import ( - "context" - "sync" - "time" - - "github.com/pkg/errors" - prom "github.com/prometheus/client_golang/prometheus" - log "github.com/sirupsen/logrus" - - "github.com/oceanbase/obagent/config" - "github.com/oceanbase/obagent/metric" - "github.com/oceanbase/obagent/stat" -) - -//PipelineInstance pipeline working instance -type PipelineInstance struct { - pipeline *pipeline - name string - config *config.PipelineConfig - clock *clock -} - -type clock struct { - context context.Context - cancel context.CancelFunc - ticker *time.Ticker -} - -//Init initialize the pipeline instance. -//Including the initialization operation of the input processor output exporter. -//If err is nil, indicates successful initialization. -func (p *PipelineInstance) Init() error { - err := p.pipeline.init() - if err != nil { - return errors.Wrap(err, "pipeline init") - } - return nil -} - -//Start pipeline instance start working, divided into push and pull modes. -//Push mode, start the clock to push. -//Pull mode, register route with route manager and add pipeline. -func (p *PipelineInstance) Start() { - switch p.config.ScheduleStrategy { - case config.Trigger: - p.registerRoute() - GetRouteManager().addPipelineFromPipelineGroup(p.config.ExposeUrl, p) - case config.Periodic: - p.startClock(p.config.Period) - } -} - -//Stop pipeline instance stop working. -//Push mode, stop the clock to push. -//Pull mode, delete pipeline to the config manager. -func (p *PipelineInstance) Stop() { - switch p.config.ScheduleStrategy { - case config.Trigger: - GetRouteManager().delPipelineFromPipelineGroup(p.config.ExposeUrl, p) - case config.Periodic: - p.clock.stopClock() - } -} - -func (p *PipelineInstance) registerRoute() { - GetRouteManager().registerHTTPRoute(p.config.ExposeUrl) -} - -func (p *PipelineInstance) parallelCompute() []metric.Metric { - var waitGroup sync.WaitGroup - var metricTotal []metric.Metric - var metricMutex sync.Mutex - for _, inputInstance := range p.pipeline.InputInstances { - waitGroup.Add(1) - go collectAndProcess(&waitGroup, &metricTotal, &metricMutex, inputInstance, p.pipeline.ProcessorInstances) - } - waitGroup.Wait() - return metricTotal -} - -func (p *PipelineInstance) startClock(duration time.Duration) { - ctx, cancel := context.WithCancel(context.Background()) - ticker := time.NewTicker(duration) - c := &clock{ - context: ctx, - cancel: cancel, - ticker: ticker, - } - p.clock = c - go func() { - for { - select { - case <-c.ticker.C: - p.pipelinePush() - case <-c.context.Done(): - log.Info("pipeline clock is stop") - return - } - } - }() -} - -func (c *clock) stopClock() { - c.ticker.Stop() - c.cancel() -} - -//pipelinePush perform a pipeline push. -// ┌────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ -// │ Input1 │------>│ Processor1 │------>│ Processor2 │------>│ Processor3 │---┐ -// └────────┘ └────────────┘ └────────────┘ └────────────┘ │ -// ┌────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ ┌────────┐ -// │ Input2 │------>│ Processor1 │------>│ Processor2 │------>│ Processor3 │---┼--->│ Output │ -// └────────┘ └────────────┘ └────────────┘ └────────────┘ │ └────────┘ -// ┌────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ -// │ Input3 │------>│ Processor1 │------>│ Processor2 │------>│ Processor3 │---┘ -// └────────┘ └────────────┘ └────────────┘ └────────────┘ -func (p *PipelineInstance) pipelinePush() { - tStart := time.Now() - metrics := p.parallelCompute() - if metrics == nil || len(metrics) == 0 { - log.Warnf("push pipeline parallel compute result metrics is nil") - return - } - err := p.pipeline.OutputInstance.Write(metrics) - if err != nil { - log.WithError(err).Error("pipeline output plugin write metric failed") - return - } - elapsedTimeSeconds := time.Now().Sub(tStart).Seconds() - stat.MonAgentPipelineExecuteTotal.With(prom.Labels{"name": p.name, "status": "Successful"}).Inc() - stat.MonAgentPipelineExecuteSecondsTotal.With(prom.Labels{"name": p.name, "status": "Successful"}).Add(elapsedTimeSeconds) -} - -func collectAndProcess(waitGroup *sync.WaitGroup, metricTotal *[]metric.Metric, metricMutex *sync.Mutex, inputInstance *InputInstance, processorInstance []*ProcessorInstance) { - defer waitGroup.Done() - - metrics, err := inputInstance.Collect() - if err != nil { - log.WithError(err).Error("input plugin collect failed") - return - } - for _, processorInstance := range processorInstance { - metrics, err = processorInstance.Process(metrics...) - if err != nil { - log.WithError(err).Error("process plugin process failed") - continue - } - } - - metricMutex.Lock() - *metricTotal = append(*metricTotal, metrics...) - metricMutex.Unlock() -} - -type pipeline struct { - InputInstances []*InputInstance - ProcessorInstances []*ProcessorInstance - OutputInstance *OutputInstance - ExporterInstance *ExporterInstance -} - -func (p *pipeline) init() error { - - for _, inputInstance := range p.InputInstances { - err := inputInstance.init(inputInstance.Config) - if err != nil { - return errors.Wrap(err, "input init") - } - } - - for _, processorInstance := range p.ProcessorInstances { - err := processorInstance.init(processorInstance.Config) - if err != nil { - return errors.Wrap(err, "processor init") - } - } - - if p.OutputInstance != nil { - err := p.OutputInstance.init(p.OutputInstance.Config) - if err != nil { - return errors.Wrap(err, "output init") - } - } - - if p.ExporterInstance != nil { - err := p.ExporterInstance.init(p.ExporterInstance.Config) - if err != nil { - return errors.Wrap(err, "exporter init") - } - } - - return nil -} diff --git a/engine/pipeline_manager.go b/engine/pipeline_manager.go deleted file mode 100644 index b17b344b816ca083992be5a0b8268eaa10e42eed..0000000000000000000000000000000000000000 --- a/engine/pipeline_manager.go +++ /dev/null @@ -1,241 +0,0 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - -package engine - -import ( - "context" - "fmt" - "sync" - - "github.com/pkg/errors" - log "github.com/sirupsen/logrus" -) - -//PipelineManager responsible for managing the pipeline corresponding to the module name -type PipelineManager struct { - pipelinesMap map[string][]*PipelineInstance - pipelineEventChan chan *pipelineEvent - eventCallbackChan chan *pipelineCallbackEvent -} - -type pipelineEventType string - -type pipelineEventExecStatusType string - -const ( - addPipelineEvent pipelineEventType = "add" - deletePipelineEvent pipelineEventType = "delete" - updatePipelineEvent pipelineEventType = "update" - - pipelineEventExecSucceed pipelineEventExecStatusType = "succeed" - pipelineEventExecFailed pipelineEventExecStatusType = "failed" -) - -type pipelineEvent struct { - eventType pipelineEventType - name string - pipelines []*PipelineInstance -} - -type pipelineCallbackEvent struct { - eventType pipelineEventType - execStatus pipelineEventExecStatusType - description string -} - -var pipelineMgr *PipelineManager -var pipelineManagerOnce sync.Once - -// GetPipelineManager get pipeline manager singleton -func GetPipelineManager() *PipelineManager { - pipelineManagerOnce.Do(func() { - pipelineMgr = &PipelineManager{ - pipelinesMap: make(map[string][]*PipelineInstance, 16), - pipelineEventChan: make(chan *pipelineEvent, 16), - eventCallbackChan: make(chan *pipelineCallbackEvent, 16), - } - }) - return pipelineMgr -} - -//handlePipelineEvent handle pipeline events and get callback results -func (p *PipelineManager) handlePipelineEvent(eventType pipelineEventType, name string, pipelines []*PipelineInstance) *pipelineCallbackEvent { - event := &pipelineEvent{ - eventType: eventType, - name: name, - pipelines: pipelines, - } - p.pipelineEventChan <- event - return <-p.eventCallbackChan -} - -//Schedule responsible for handling events -func (p *PipelineManager) Schedule(context context.Context) { - go p.schedule(context) -} - -func (p *PipelineManager) schedule(context context.Context) { - for { - select { - case event, ok := <-p.pipelineEventChan: - if !ok { - log.Info("pipeline manager event chan closed") - return - } - - callbackEvent := &pipelineCallbackEvent{ - eventType: event.eventType, - } - - switch event.eventType { - case addPipelineEvent: - - err := p.handleAddEvent(event) - if err != nil { - callbackEvent.execStatus = pipelineEventExecFailed - callbackEvent.description = fmt.Sprintf("moudle %s pipelines add failed", event.name) - log.WithError(err).Error("pipeline manager handle add event failed") - } else { - callbackEvent.execStatus = pipelineEventExecSucceed - callbackEvent.description = "" - } - - case deletePipelineEvent: - - err := p.handleDelEvent(event) - if err != nil { - callbackEvent.execStatus = pipelineEventExecFailed - callbackEvent.description = fmt.Sprintf("moudle %s pipelines delete failed", event.name) - log.WithError(err).Error("pipeline manager handle delete event failed") - } else { - callbackEvent.execStatus = pipelineEventExecSucceed - callbackEvent.description = "" - } - - case updatePipelineEvent: - - err := p.handleUpdateEvent(event) - if err != nil { - callbackEvent.execStatus = pipelineEventExecFailed - callbackEvent.description = fmt.Sprintf("moudle %s pipelines update failed", event.name) - log.WithError(err).Error("pipeline manager handle update event failed") - } else { - callbackEvent.execStatus = pipelineEventExecSucceed - callbackEvent.description = "" - } - - } - - p.eventCallbackChan <- callbackEvent - - case <-context.Done(): - log.Info("pipeline manager scheduler exit") - return - } - } -} - -var initPipelinesFunc = func(pipelines []*PipelineInstance) error { - for _, pipeline := range pipelines { - err := pipeline.Init() - if err != nil { - return errors.Wrapf(err, "create pipeline %s", pipeline.name) - } - } - return nil -} - -var startPipelinesFunc = func(pipelines []*PipelineInstance) { - for _, pipeline := range pipelines { - pipeline.Start() - } -} - -var stopPipelinesFunc = func(pipelines []*PipelineInstance) { - for _, pipeline := range pipelines { - pipeline.Stop() - } -} - -//handleAddEvent handle the add event and return. -//If error is nil, indicates that the addition was successful. -func (p *PipelineManager) handleAddEvent(event *pipelineEvent) error { - - _, exist := p.getPipelines(event.name) - if exist { - return errors.Errorf("get moudle %s pipleines is exist", event.name) - } - - err := initPipelinesFunc(event.pipelines) - if err != nil { - return errors.Wrapf(err, "create moudle %s pipelines", event.name) - } - - startPipelinesFunc(event.pipelines) - - p.setPipelines(event.name, event.pipelines) - - return nil -} - -//handleUpdateEvent handle the update event and return. -//If error is nil, indicates that the addition was successful. -func (p *PipelineManager) handleUpdateEvent(event *pipelineEvent) error { - - oldPipelines, exist := p.getPipelines(event.name) - if !exist { - return errors.Errorf("get moudle %s pipleines is not exist", event.name) - } - - err := initPipelinesFunc(event.pipelines) - if err != nil { - return errors.Wrapf(err, "create moudle %s pipelines failed", event.name) - } - - stopPipelinesFunc(oldPipelines) - - startPipelinesFunc(event.pipelines) - - p.setPipelines(event.name, event.pipelines) - - return nil -} - -//handleDelEvent handle the delete event and return. -//If error is nil, indicates that the addition was successful. -func (p *PipelineManager) handleDelEvent(event *pipelineEvent) error { - - oldPipelines, exist := p.getPipelines(event.name) - if !exist { - return errors.Errorf("moudle %s pipleines is not exist", event.name) - } - - stopPipelinesFunc(oldPipelines) - - p.delPipelines(event.name) - - return nil -} - -func (p *PipelineManager) getPipelines(name string) ([]*PipelineInstance, bool) { - pipelines, exist := p.pipelinesMap[name] - return pipelines, exist -} - -func (p *PipelineManager) setPipelines(name string, pipelines []*PipelineInstance) { - p.pipelinesMap[name] = pipelines -} - -func (p *PipelineManager) delPipelines(name string) { - delete(p.pipelinesMap, name) -} diff --git a/engine/pipeline_manager_test.go b/engine/pipeline_manager_test.go deleted file mode 100644 index d50e740ec2343d2f905c57b5539ebcea924c2f4c..0000000000000000000000000000000000000000 --- a/engine/pipeline_manager_test.go +++ /dev/null @@ -1,178 +0,0 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - -package engine - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "testing" - - "github.com/gin-gonic/gin" - - "github.com/oceanbase/obagent/api/web" - "github.com/oceanbase/obagent/config" -) - -func TestPipelineManagerSchedule(t *testing.T) { - testPipelineModule := &config.PipelineModule{} - err := json.Unmarshal([]byte(testJSONModule), testPipelineModule) - if err != nil { - t.Errorf("test config manager set config json decode failed %s", err.Error()) - return - } - tests := []struct { - name string - fields *PipelineManager - }{ - - {name: "test", fields: GetPipelineManager()}, - } - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - GetPipelineManager().Schedule(ctx) - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - p := &PipelineManager{ - pipelinesMap: tt.fields.pipelinesMap, - pipelineEventChan: tt.fields.pipelineEventChan, - eventCallbackChan: tt.fields.eventCallbackChan, - } - - _ = p.handlePipelineEvent(addPipelineEvent, "test", CreatePipelineInstances(testPipelineModule)) - - _ = p.handlePipelineEvent(updatePipelineEvent, "test", CreatePipelineInstances(testPipelineModule)) - - _ = p.handlePipelineEvent(deletePipelineEvent, "test", CreatePipelineInstances(testPipelineModule)) - - }) - } -} - -func TestPipelineManagerHandleEvent(t *testing.T) { - testPipelineModule := &config.PipelineModule{} - err := json.Unmarshal([]byte(testJSONModule), testPipelineModule) - if err != nil { - t.Errorf("test config manager set config json decode failed %s", err.Error()) - return - } - fmt.Println(testPipelineModule.Pipelines[1].Config) - pipelineInstances := CreatePipelineInstances(testPipelineModule) - addEvent := &pipelineEvent{ - eventType: addPipelineEvent, - name: "test", - pipelines: pipelineInstances, - } - updateEvent := &pipelineEvent{ - eventType: updatePipelineEvent, - name: "test", - pipelines: pipelineInstances, - } - delEvent := &pipelineEvent{ - eventType: deletePipelineEvent, - name: "test", - pipelines: pipelineInstances, - } - - tests := []struct { - name string - fields *PipelineManager - args *pipelineEvent - }{ - {name: "testAdd", fields: GetPipelineManager(), args: addEvent}, - } - - monitorAgentServer = &MonitorAgentServer{} - GetMonitorAgentServer().Server = &web.HttpServer{ - Router: gin.Default(), - Server: &http.Server{ - Addr: ":0", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - p := &PipelineManager{ - pipelinesMap: tt.fields.pipelinesMap, - pipelineEventChan: tt.fields.pipelineEventChan, - eventCallbackChan: tt.fields.eventCallbackChan, - } - - _ = p.handleAddEvent(tt.args) - }) - } - - tests = []struct { - name string - fields *PipelineManager - args *pipelineEvent - }{ - {name: "testUpdate", fields: GetPipelineManager(), args: updateEvent}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - p := &PipelineManager{ - pipelinesMap: tt.fields.pipelinesMap, - pipelineEventChan: tt.fields.pipelineEventChan, - eventCallbackChan: tt.fields.eventCallbackChan, - } - - _ = p.handleUpdateEvent(tt.args) - }) - } - - tests = []struct { - name string - fields *PipelineManager - args *pipelineEvent - }{ - {name: "testDelete", fields: GetPipelineManager(), args: delEvent}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - p := &PipelineManager{ - pipelinesMap: tt.fields.pipelinesMap, - pipelineEventChan: tt.fields.pipelineEventChan, - eventCallbackChan: tt.fields.eventCallbackChan, - } - - _ = p.handleDelEvent(tt.args) - }) - } -} - -func TestPipelineManagerPipelines(t *testing.T) { - testPipelineModule := &config.PipelineModule{} - err := json.Unmarshal([]byte(testJSONModule), testPipelineModule) - if err != nil { - t.Errorf("test config manager set config json decode failed %s", err.Error()) - return - } - pipelineInstances := CreatePipelineInstances(testPipelineModule) - t.Run("setConfigTest", func(t *testing.T) { - GetPipelineManager().setPipelines("test", pipelineInstances) - }) - - t.Run("getConfigTest", func(t *testing.T) { - GetPipelineManager().getPipelines("test") - }) - - t.Run("setConfigTest", func(t *testing.T) { - GetPipelineManager().delPipelines("test") - }) -} diff --git a/engine/pipeline_module_callback.go b/engine/pipeline_module_callback.go deleted file mode 100644 index 2cffecf1eec4a5d7360f0d0e144175d9b8877242..0000000000000000000000000000000000000000 --- a/engine/pipeline_module_callback.go +++ /dev/null @@ -1,130 +0,0 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - -package engine - -import ( - "github.com/pkg/errors" - log "github.com/sirupsen/logrus" - - "github.com/oceanbase/obagent/config" -) - -func addPipeline(pipelineModule *config.PipelineModule) error { - configManager := GetConfigManager() - event := &configEvent{ - eventType: addConfigEvent, - pipelineModule: pipelineModule, - } - - configManager.eventChan <- event - callbackEvent := <-configManager.eventCallbackChan - - var err error - log.Infof("add pipeline module result %s %s ", callbackEvent.execStatus, callbackEvent.description) - switch callbackEvent.execStatus { - case configEventExecSucceed: - err = nil - case configEventExecFailed: - err = errors.Errorf("add pipeline module failed description %s", callbackEvent.description) - } - return err -} - -func deletePipeline(pipelineModule *config.PipelineModule) error { - configManager := GetConfigManager() - event := &configEvent{ - eventType: deleteConfigEvent, - pipelineModule: pipelineModule, - } - log.Infof("update pipeline event %v", event) - configManager.eventChan <- event - callbackEvent := <-configManager.eventCallbackChan - - var err error - log.Infof("delete pipeline module result %s %s ", callbackEvent.execStatus, callbackEvent.description) - switch callbackEvent.execStatus { - case configEventExecSucceed: - err = nil - case configEventExecFailed: - err = errors.Errorf("delete pipeline module failed description %s", callbackEvent.description) - } - return err -} - -func updatePipeline(pipelineModule *config.PipelineModule) error { - configManager := GetConfigManager() - event := &configEvent{ - eventType: updateConfigEvent, - pipelineModule: pipelineModule, - } - configManager.eventChan <- event - callbackEvent := <-configManager.eventCallbackChan - - var err error - log.Infof("update pipeline module result %s %s ", callbackEvent.execStatus, callbackEvent.description) - switch callbackEvent.execStatus { - case configEventExecSucceed: - err = nil - case configEventExecFailed: - err = errors.Errorf("update pipeline module failed description %s", callbackEvent.description) - } - return err -} - -func updateOrAddPipeline(pipelineModule *config.PipelineModule) error { - err := updatePipeline(pipelineModule) - if err != nil { - log.WithError(err).Error("update or add pipeline module failed") - } else { - return err - } - err = addPipeline(pipelineModule) - if err != nil { - log.WithError(err).Error("update or add pipeline module failed") - } - return err -} - -func NotifyServerBasicAuth(basicConf config.BasicAuthConfig) error { - monagentServer := GetMonitorAgentServer() - monagentServer.Server.BasicAuthorizer.SetConf(basicConf) - monagentServer.Server.UseBasicAuth() - return nil -} - -func NotifyAdminServerBasicAuth(basicConf config.BasicAuthConfig) error { - monagentServer := GetMonitorAgentServer() - monagentServer.AdminServer.BasicAuthorizer.SetConf(basicConf) - monagentServer.AdminServer.UseBasicAuth() - return nil -} - -func InitPipelineModuleCallback(pipelineModule *config.PipelineModule) error { - var err error - if pipelineModule.Status == config.INACTIVE { - log.Warnf("pipeline module %s is inactive, just skip", pipelineModule.Name) - } else { - err = addPipeline(pipelineModule) - } - return errors.Wrap(err, "init pipeline module callback") -} - -func UpdatePipelineModuleCallback(pipelineModule *config.PipelineModule) error { - var err error - if pipelineModule.Status == config.INACTIVE { - err = deletePipeline(pipelineModule) - } else { - err = updateOrAddPipeline(pipelineModule) - } - return errors.Wrap(err, "update pipeline module callback") -} diff --git a/engine/pipeline_module_callback_test.go b/engine/pipeline_module_callback_test.go deleted file mode 100644 index 96b1844c6236f2d211285e72388ca9701aa1e458..0000000000000000000000000000000000000000 --- a/engine/pipeline_module_callback_test.go +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - -package engine - -import ( - "testing" - - "github.com/oceanbase/obagent/config" -) - -func TestAddPipeline(t *testing.T) { - - GetConfigManager().eventCallbackChan <- &configCallbackEvent{ - eventType: addConfigEvent, - execStatus: configEventExecSucceed, - description: "", - } - testPipelineModule := &config.PipelineModule{} - err := addPipeline(testPipelineModule) - if err != nil { - t.Errorf("test add pipeline failed %s", err.Error()) - } - - GetConfigManager().eventCallbackChan <- &configCallbackEvent{ - eventType: addConfigEvent, - execStatus: configEventExecFailed, - description: "", - } - _ = addPipeline(testPipelineModule) - -} - -func TestDeletePipeline(t *testing.T) { - - GetConfigManager().eventCallbackChan <- &configCallbackEvent{ - eventType: deleteConfigEvent, - execStatus: configEventExecSucceed, - description: "", - } - testPipelineModule := &config.PipelineModule{} - err := deletePipeline(testPipelineModule) - if err != nil { - t.Errorf("test delete pipeline failed %s", err.Error()) - } - - GetConfigManager().eventCallbackChan <- &configCallbackEvent{ - eventType: deleteConfigEvent, - execStatus: configEventExecFailed, - description: "", - } - _ = deletePipeline(testPipelineModule) - -} - -func TestUpdatePipeline(t *testing.T) { - - GetConfigManager().eventCallbackChan <- &configCallbackEvent{ - eventType: updateConfigEvent, - execStatus: configEventExecSucceed, - description: "", - } - testPipelineModule := &config.PipelineModule{} - err := updatePipeline(testPipelineModule) - if err != nil { - t.Errorf("test update pipeline failed %s", err.Error()) - } - - GetConfigManager().eventCallbackChan <- &configCallbackEvent{ - eventType: updateConfigEvent, - execStatus: configEventExecFailed, - description: "", - } - _ = updatePipeline(testPipelineModule) -} diff --git a/engine/pipeline_test.go b/engine/pipeline_test.go deleted file mode 100644 index 70d29637d6cb422c0c39677f311f1da1e3978841..0000000000000000000000000000000000000000 --- a/engine/pipeline_test.go +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - -package engine - -import ( - "encoding/json" - "testing" - - "github.com/oceanbase/obagent/config" -) - -func TestPipelineInstance(t *testing.T) { - testPipelineNode := &config.PipelineNode{} - err := json.Unmarshal([]byte(testPipelinePullJSON), testPipelineNode) - if err != nil { - t.Errorf("test pipeline instance failed %s", err.Error()) - } - pipelineInstance := createPipelineInstance(testPipelineNode) - err = pipelineInstance.Init() - if err != nil { - t.Errorf("test pipeline instance init failed %s", err.Error()) - } - pipelineInstance.Start() - pipelineInstance.pipelinePush() - pipelineInstance.Stop() - - testPipelineNode = &config.PipelineNode{} - err = json.Unmarshal([]byte(testPipelinePushJSON), testPipelineNode) - if err != nil { - t.Errorf("test pipeline instance failed %s", err.Error()) - } - pipelineInstance = createPipelineInstance(testPipelineNode) - err = pipelineInstance.Init() - if err != nil { - t.Errorf("test pipeline instance init failed %s", err.Error()) - } - pipelineInstance.Start() - pipelineInstance.pipelinePush() - pipelineInstance.Stop() -} - -var testPipelinePullJSON = ` -{ - "name": "pipeline1", - "config": { - "scheduleStrategy": "trigger", - "exposeUrl": "/metrics/test" - }, - "structure": { - "inputs": [{ - "plugin": "test", - "config": { - "timeout": 10, - "pluginConfig": null - } - }], - "processors": [{ - "plugin": "test", - "config": { - "timeout": 10, - "pluginConfig": null - } - }], - "output": { - "plugin": "test", - "config": { - "timeout": 10, - "pluginConfig": null - } - } - } -} -` -var testPipelinePushJSON = ` -{ - "name": "pipeline2", - "config": { - "scheduleStrategy": "periodic", - "period": 5 - }, - "structure": { - "inputs": [{ - "plugin": "test", - "config": { - "timeout": 10, - "pluginConfig": null - } - }], - "processors": [{ - "plugin": "test", - "config": { - "timeout": 10, - "pluginConfig": null - } - }], - "output": { - "plugin": "test", - "config": { - "timeout": 10, - "pluginConfig": null - } - } - } - } -` diff --git a/engine/plugin_instance.go b/engine/plugin_instance.go deleted file mode 100644 index 2bb084371fa4b6016f503d10947405e9f31b76bd..0000000000000000000000000000000000000000 --- a/engine/plugin_instance.go +++ /dev/null @@ -1,339 +0,0 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - -package engine - -import ( - "bytes" - "context" - "time" - - "github.com/pkg/errors" - prom "github.com/prometheus/client_golang/prometheus" - log "github.com/sirupsen/logrus" - - "github.com/oceanbase/obagent/config" - "github.com/oceanbase/obagent/metric" - "github.com/oceanbase/obagent/plugins" - "github.com/oceanbase/obagent/stat" -) - -//InputInstance input instance responsible for Collect work -type InputInstance struct { - PluginName string - Config *config.PluginConfig - Input plugins.Input -} - -//ProcessorInstance processor instance responsible for Process work -type ProcessorInstance struct { - PluginName string - Config *config.PluginConfig - Processor plugins.Processor -} - -//OutputInstance output instance responsible for Write work -type OutputInstance struct { - PluginName string - Config *config.PluginConfig - Output plugins.Output -} - -//ExporterInstance exporter instance responsible for Export work -type ExporterInstance struct { - PluginName string - Config *config.PluginConfig - Exporter plugins.Exporter -} - -func (i *InputInstance) init(config *config.PluginConfig) error { - inputPlugin, err := plugins.GetInputManager().GetPlugin(i.PluginName) - if err != nil { - return errors.Wrapf(err, "input manager get plugin") - } - i.Input = inputPlugin - err = inputPlugin.Init(config.PluginInnerConfig) - if err != nil { - return errors.Wrapf(err, "input plugin init") - } - return nil -} - -func (p *ProcessorInstance) init(config *config.PluginConfig) error { - processorPlugin, err := plugins.GetProcessorManager().GetPlugin(p.PluginName) - if err != nil { - return errors.Wrapf(err, "processor manager get plugin") - } - p.Processor = processorPlugin - err = processorPlugin.Init(config.PluginInnerConfig) - if err != nil { - return errors.Wrapf(err, "processor plugin init") - } - return nil -} - -func (o *OutputInstance) init(config *config.PluginConfig) error { - outputPlugin, err := plugins.GetOutputManager().GetPlugin(o.PluginName) - if err != nil { - return errors.Wrapf(err, "output manager get plugin") - } - o.Output = outputPlugin - err = outputPlugin.Init(config.PluginInnerConfig) - if err != nil { - return errors.Wrapf(err, "output plugin init") - } - return nil -} - -func (e *ExporterInstance) init(config *config.PluginConfig) error { - exporterPlugin, err := plugins.GetExporterManager().GetPlugin(e.PluginName) - if err != nil { - return errors.Wrapf(err, "exporter manager get plugin") - } - e.Exporter = exporterPlugin - err = exporterPlugin.Init(config.PluginInnerConfig) - if err != nil { - return errors.Wrapf(err, "exporter plugin init") - } - return nil -} - -type collectResult struct { - metrics []metric.Metric - err error -} - -//Collect return the collection results as metrics -func (i *InputInstance) Collect() ([]metric.Metric, error) { - - defer GoroutineProtection(log.WithField("pipeline", "input instance collect")) - - var metrics []metric.Metric - var err error - tStart := time.Now() - if i.Config.Timeout != 0 { - metrics, err = i.collectWithTimeout() - } else { - metrics, err = i.Input.Collect() - } - elapsedTimeSeconds := time.Now().Sub(tStart).Seconds() - if err != nil { - stat.MonAgentPluginExecuteTotal.With(prom.Labels{"name": i.PluginName, "status": "Error", "type": "Input"}).Inc() - stat.MonAgentPluginExecuteSecondsTotal.With(prom.Labels{"name": i.PluginName, "status": "Error", "type": "Input"}).Add(elapsedTimeSeconds) - } else { - stat.MonAgentPluginExecuteTotal.With(prom.Labels{"name": i.PluginName, "status": "Successful", "type": "Input"}).Inc() - stat.MonAgentPluginExecuteSecondsTotal.With(prom.Labels{"name": i.PluginName, "status": "Successful", "type": "Input"}).Add(elapsedTimeSeconds) - } - return metrics, err -} - -func (i *InputInstance) collectWithTimeout() ([]metric.Metric, error) { - var metrics []metric.Metric - var err error - - ctx, cancel := context.WithTimeout(context.Background(), i.Config.Timeout) - defer cancel() - - done := make(chan *collectResult, 1) - go func() { - - defer GoroutineProtection(log.WithField("pipeline", "input instance collect with timeout")) - - metrics, err := i.Input.Collect() - done <- &collectResult{ - metrics: metrics, - err: err, - } - }() - - select { - case r := <-done: - metrics = r.metrics - err = r.err - case <-ctx.Done(): - err = errors.Errorf("input plugin %s collect time out", i.PluginName) - } - - return metrics, err -} - -type processorResult struct { - metrics []metric.Metric - err error -} - -//Process metrics content and return results -func (p *ProcessorInstance) Process(metrics ...metric.Metric) ([]metric.Metric, error) { - - defer GoroutineProtection(log.WithField("pipeline", "processor instance process")) - - var err error - tStart := time.Now() - if p.Config.Timeout != 0 { - metrics, err = p.processWithTimeout(metrics...) - } else { - metrics, err = p.Processor.Process(metrics...) - } - elapsedTimeSeconds := time.Now().Sub(tStart).Seconds() - if err != nil { - stat.MonAgentPluginExecuteTotal.With(prom.Labels{"name": p.PluginName, "status": "Error", "type": "Processor"}).Inc() - stat.MonAgentPluginExecuteSecondsTotal.With(prom.Labels{"name": p.PluginName, "status": "Error", "type": "Processor"}).Add(elapsedTimeSeconds) - } else { - stat.MonAgentPluginExecuteTotal.With(prom.Labels{"name": p.PluginName, "status": "Successful", "type": "Processor"}).Inc() - stat.MonAgentPluginExecuteSecondsTotal.With(prom.Labels{"name": p.PluginName, "status": "Successful", "type": "Processor"}).Add(elapsedTimeSeconds) - } - return metrics, err -} - -func (p *ProcessorInstance) processWithTimeout(metrics ...metric.Metric) ([]metric.Metric, error) { - var err error - - ctx, cancel := context.WithTimeout(context.Background(), p.Config.Timeout) - defer cancel() - - done := make(chan *processorResult, 1) - go func() { - - defer GoroutineProtection(log.WithField("pipeline", "processor instance process with timeout")) - - metrics, err := p.Processor.Process(metrics...) - done <- &processorResult{ - metrics: metrics, - err: err, - } - }() - - select { - case r := <-done: - metrics = r.metrics - err = r.err - case <-ctx.Done(): - err = errors.Errorf("processor plugin %s process time out", p.PluginName) - } - - return metrics, err -} - -type outputResult struct { - err error -} - -//Write metrics to output target -func (o *OutputInstance) Write(metrics []metric.Metric) error { - - defer GoroutineProtection(log.WithField("pipeline", "output instance write")) - - var err error - tStart := time.Now() - if o.Config.Timeout != 0 { - err = o.writeWithTimeout(metrics...) - } else { - err = o.Output.Write(metrics) - } - elapsedTimeSeconds := time.Now().Sub(tStart).Seconds() - if err != nil { - stat.MonAgentPluginExecuteTotal.With(prom.Labels{"name": o.PluginName, "status": "Error", "type": "Output"}).Inc() - stat.MonAgentPluginExecuteSecondsTotal.With(prom.Labels{"name": o.PluginName, "status": "Error", "type": "Output"}).Add(elapsedTimeSeconds) - } else { - stat.MonAgentPluginExecuteTotal.With(prom.Labels{"name": o.PluginName, "status": "Successful", "type": "Output"}).Inc() - stat.MonAgentPluginExecuteSecondsTotal.With(prom.Labels{"name": o.PluginName, "status": "Successful", "type": "Output"}).Add(elapsedTimeSeconds) - stat.MonAgentPipelineReportMetricsTotal.With(prom.Labels{"name": o.PluginName}).Add(float64(len(metrics))) - } - return err -} - -func (o *OutputInstance) writeWithTimeout(metrics ...metric.Metric) error { - var err error - - ctx, cancel := context.WithTimeout(context.Background(), o.Config.Timeout) - defer cancel() - - done := make(chan *outputResult, 1) - go func() { - - defer GoroutineProtection(log.WithField("pipeline", "output instance write with timeout")) - - err := o.Output.Write(metrics) - done <- &outputResult{ - err: err, - } - }() - - select { - case r := <-done: - err = r.err - case <-ctx.Done(): - err = errors.Errorf("output plugin %s write time out", o.PluginName) - } - - return err -} - -type exporterResult struct { - buffer *bytes.Buffer - err error -} - -//Export metrics to buffers and return -func (e *ExporterInstance) Export(metrics []metric.Metric) (*bytes.Buffer, error) { - - defer GoroutineProtection(log.WithField("pipeline", "exporter instance export")) - - var buffer *bytes.Buffer - var err error - tStart := time.Now() - if e.Config.Timeout != 0 { - buffer, err = e.exportWithTimeout(metrics...) - } else { - buffer, err = e.Exporter.Export(metrics) - } - elapsedTimeSeconds := time.Now().Sub(tStart).Seconds() - if err != nil { - stat.MonAgentPluginExecuteTotal.With(prom.Labels{"name": e.PluginName, "status": "Error", "type": "Exporter"}).Inc() - stat.MonAgentPluginExecuteSecondsTotal.With(prom.Labels{"name": e.PluginName, "status": "Error", "type": "Exporter"}).Add(elapsedTimeSeconds) - } else { - stat.MonAgentPluginExecuteTotal.With(prom.Labels{"name": e.PluginName, "status": "Successful", "type": "Exporter"}).Inc() - stat.MonAgentPluginExecuteSecondsTotal.With(prom.Labels{"name": e.PluginName, "status": "Successful", "type": "Exporter"}).Add(elapsedTimeSeconds) - stat.MonAgentPipelineReportMetricsTotal.With(prom.Labels{"name": e.PluginName}).Add(float64(len(metrics))) - } - return buffer, err -} - -func (e *ExporterInstance) exportWithTimeout(metrics ...metric.Metric) (*bytes.Buffer, error) { - var buffer *bytes.Buffer - var err error - - ctx, cancel := context.WithTimeout(context.Background(), e.Config.Timeout) - defer cancel() - - done := make(chan *exporterResult, 1) - go func() { - - defer GoroutineProtection(log.WithField("pipeline", "exporter instance export with timeout")) - - buffer, err := e.Exporter.Export(metrics) - done <- &exporterResult{ - buffer: buffer, - err: err, - } - }() - - select { - case r := <-done: - buffer = r.buffer - err = r.err - case <-ctx.Done(): - err = errors.Errorf("exporter plugin %s export time out", e.PluginName) - } - - return buffer, err -} diff --git a/engine/plugin_instance_test.go b/engine/plugin_instance_test.go deleted file mode 100644 index 7918da9b42cf3996b13e020dcccc96dddca30775..0000000000000000000000000000000000000000 --- a/engine/plugin_instance_test.go +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - -package engine - -import ( - "testing" - - "github.com/oceanbase/obagent/config" - "github.com/oceanbase/obagent/metric" -) - -func TestInstance(_ *testing.T) { - testPluginConfig := &config.PluginConfig{Timeout: 0} - testInput := &InputInstance{ - PluginName: "test", - Config: testPluginConfig, - } - testProcessor := &ProcessorInstance{ - PluginName: "test", - Config: testPluginConfig, - } - testOutput := &OutputInstance{ - PluginName: "test", - Config: testPluginConfig, - } - testExporter := &ExporterInstance{ - PluginName: "test", - Config: testPluginConfig, - } - - _ = testInput.init(testPluginConfig) - _ = testProcessor.init(testPluginConfig) - _ = testOutput.init(testPluginConfig) - _ = testExporter.init(testPluginConfig) - - _, _ = testInput.Collect() - _, _ = testProcessor.Process([]metric.Metric{}...) - _ = testOutput.Write([]metric.Metric{}) - _, _ = testExporter.Export([]metric.Metric{}) - - testPluginConfig = &config.PluginConfig{Timeout: 1} - testInput = &InputInstance{ - PluginName: "test", - Config: testPluginConfig, - } - testProcessor = &ProcessorInstance{ - PluginName: "test", - Config: testPluginConfig, - } - testOutput = &OutputInstance{ - PluginName: "test", - Config: testPluginConfig, - } - testExporter = &ExporterInstance{ - PluginName: "test", - Config: testPluginConfig, - } - - _ = testInput.init(testPluginConfig) - _ = testProcessor.init(testPluginConfig) - _ = testOutput.init(testPluginConfig) - _ = testExporter.init(testPluginConfig) - - _, _ = testInput.Collect() - _, _ = testProcessor.Process([]metric.Metric{}...) - _ = testOutput.Write([]metric.Metric{}) - _, _ = testExporter.Export([]metric.Metric{}) -} diff --git a/engine/route_manager.go b/engine/route_manager.go deleted file mode 100644 index bb6814272d33b11dd15de3d1a0840cbc200368f0..0000000000000000000000000000000000000000 --- a/engine/route_manager.go +++ /dev/null @@ -1,215 +0,0 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - -package engine - -import ( - "bytes" - "container/list" - "net/http" - "sync" - "time" - - "github.com/pkg/errors" - prom "github.com/prometheus/client_golang/prometheus" - log "github.com/sirupsen/logrus" - - "github.com/oceanbase/obagent/api/route" - "github.com/oceanbase/obagent/stat" -) - -//RouteManager responsible for managing the pipeline corresponding to the url -type RouteManager struct { - routeMap map[string]*list.List - rwMutex sync.RWMutex -} - -var routeManager *RouteManager -var routeManagerOnce sync.Once - -//GetRouteManager get route manager singleton -func GetRouteManager() *RouteManager { - routeManagerOnce.Do(func() { - routeManager = &RouteManager{ - routeMap: make(map[string]*list.List, 16), - rwMutex: sync.RWMutex{}, - } - }) - return routeManager -} - -//getPipelineGroup get the pipeline group corresponding to the route -func (r *RouteManager) getPipelineGroup(route string) (*list.List, error) { - r.rwMutex.RLock() - defer r.rwMutex.RUnlock() - - l, exist := r.routeMap[route] - if !exist { - return nil, errors.New("route path is not exist") - } - copyList := list.New() - copyList.PushBackList(l) - return copyList, nil -} - -//addPipelineFromPipelineGroup add pipeline instance to the route -func (r *RouteManager) addPipelineFromPipelineGroup(route string, pipeline *PipelineInstance) { - r.rwMutex.Lock() - defer r.rwMutex.Unlock() - - _, exist := r.routeMap[route] - if !exist { - r.routeMap[route] = list.New() - } - r.routeMap[route].PushBack(pipeline) -} - -//delPipelineFromPipelineGroup delete pipeline instance to the route -func (r *RouteManager) delPipelineFromPipelineGroup(route string, pipeline *PipelineInstance) error { - r.rwMutex.Lock() - defer r.rwMutex.Unlock() - - var element *list.Element - l, exist := r.routeMap[route] - if !exist { - return errors.New("route path is not exist") - } - for e := l.Front(); e != nil; e = e.Next() { - if e.Value == pipeline { - element = e - break - } - } - - if element != nil { - l.Remove(element) - } else { - return errors.New("pipeline is not exist") - } - return nil -} - -//registerHTTPRoute register http route -func (r *RouteManager) registerHTTPRoute(exposeURL string) { - r.rwMutex.Lock() - defer r.rwMutex.Unlock() - if _, exist := r.routeMap[exposeURL]; !exist { - rt := &httpRoute{ - routePath: exposeURL, - } - - var pullHandlerFunction = func(h http.Handler) http.Handler { - return rt - } - - route.RegisterPipelineRoute(GetMonitorAgentServer().Server.Router, exposeURL, pullHandlerFunction) - r.routeMap[exposeURL] = list.New() - } -} - -type httpRoute struct { - routePath string -} - -func (rt *httpRoute) ServeHTTP(writer http.ResponseWriter, request *http.Request) { - rt.pull(writer, request) -} - -//pull mode, parallel pull involving multiple pipelines. -//After concurrent pull, the various pipelines are aggregated and written back. -func (rt *httpRoute) pull(writer http.ResponseWriter, _ *http.Request) { - routeManager := GetRouteManager() - pipelineGroup, err := routeManager.getPipelineGroup(rt.routePath) - if err != nil { - - log.WithError(err).Error("pull pipeline route manager get pipeline group failed") - - if _, err := writer.Write([]byte("The current path is not registered successfully.")); err != nil { - - log.WithError(err).Error("http response write failed") - - return - } - - return - } - - if pipelineGroup.Len() == 0 { - - log.Warn("pull pipeline route manager pipeline group len is zero") - - if _, err := writer.Write([]byte("The current path does not have an executable pipeline.")); err != nil { - - log.WithError(err).Error("http response write failed") - - return - } - - return - } - - var waitGroup sync.WaitGroup - buffer := bytes.NewBuffer(make([]byte, 0, 4096)) - var mutex sync.Mutex - for e := pipelineGroup.Front(); e != nil; e = e.Next() { - pipelineInstance := e.Value.(*PipelineInstance) - - waitGroup.Add(1) - go rt.pipelinePull(&waitGroup, pipelineInstance, buffer, &mutex) - - } - waitGroup.Wait() - - if _, err = buffer.WriteTo(writer); err != nil { - log.WithError(err).Error("failed to write http response from buffer") - } -} - -//pipelinePull perform a pipeline pull. -// ┌────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ -// │ Input1 │------>│ Processor1 │------>│ Processor2 │------>│ Processor3 │---┐ -// └────────┘ └────────────┘ └────────────┘ └────────────┘ │ -// ┌────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ ┌──────────┐ -// │ Input2 │------>│ Processor1 │------>│ Processor2 │------>│ Processor3 │---┼--->│ exporter │ -// └────────┘ └────────────┘ └────────────┘ └────────────┘ │ └──────────┘ -// ┌────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ -// │ Input3 │------>│ Processor1 │------>│ Processor2 │------>│ Processor3 │---┘ -// └────────┘ └────────────┘ └────────────┘ └────────────┘ -func (rt *httpRoute) pipelinePull(waitGroup *sync.WaitGroup, p *PipelineInstance, buffer *bytes.Buffer, mutex *sync.Mutex) { - defer waitGroup.Done() - - tStart := time.Now() - metrics := p.parallelCompute() - if metrics == nil || len(metrics) == 0 { - log.Warnf("pull pipeline parallel compute result metrics is nil") - return - } - - newBuffer, err := p.pipeline.ExporterInstance.Export(metrics) - if err != nil { - log.WithError(err).Errorf("pull pipeline exporter export metrics failed") - return - } - - mutex.Lock() - _, err = buffer.ReadFrom(newBuffer) - mutex.Unlock() - if err != nil { - log.WithError(err).Error("pull pipeline read into buffer failed", err.Error()) - return - } - - elapsedTimeSeconds := time.Now().Sub(tStart).Seconds() - stat.MonAgentPipelineExecuteTotal.With(prom.Labels{"name": p.name, "status": "Successful"}).Inc() - stat.MonAgentPipelineExecuteSecondsTotal.With(prom.Labels{"name": p.name, "status": "Successful"}).Add(elapsedTimeSeconds) - -} diff --git a/engine/route_manager_test.go b/engine/route_manager_test.go deleted file mode 100644 index 463e3c989941547b492f76d58c749f772eca21b9..0000000000000000000000000000000000000000 --- a/engine/route_manager_test.go +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - -package engine - -import ( - "encoding/json" - "net/http" - "net/http/httptest" - "reflect" - "testing" - - "gopkg.in/yaml.v3" - - "github.com/oceanbase/obagent/config" -) - -func TestRouteManagerPipelineFromPipelineGroup(t *testing.T) { - testPipelineInstance := &PipelineInstance{} - err := json.Unmarshal([]byte(testJSONModule), testPipelineInstance) - if err != nil { - t.Errorf("test config manager set config json decode failed %s", err.Error()) - return - } - - tests := []struct { - name string - fields *RouteManager - }{ - {name: "test", fields: GetRouteManager()}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - manager := tt.fields - - manager.addPipelineFromPipelineGroup("/test", testPipelineInstance) - - pipelineGroup, _ := manager.getPipelineGroup("/test") - pipelineInstance := pipelineGroup.Front().Value.(*PipelineInstance) - - manager.delPipelineFromPipelineGroup("/test", testPipelineInstance) - - if deepEqual := reflect.DeepEqual(pipelineInstance, testPipelineInstance); !deepEqual { - t.Errorf("route manager test deep equal pipeline instance failed %s", err.Error()) - } - - }) - } -} - -func TestRoutePull(t *testing.T) { - var testPipelineYaml = `name: pipeline -config: - scheduleStrategy: trigger - exposeUrl: /metrics/test -structure: - inputs: - - plugin: test - config: - timeout: 10s - pluginConfig: {} - processors: - - plugin: test - config: - timeout: 10s - pluginConfig: {} - exporter: - plugin: test - config: - timeout: 10s - pluginConfig: {}` - testPipelineNode := &config.PipelineNode{} - err := yaml.Unmarshal([]byte(testPipelineYaml), testPipelineNode) - if err != nil { - t.Errorf("test pipeline instance failed %s", err.Error()) - } - - pipelineInstance := createPipelineInstance(testPipelineNode) - err = pipelineInstance.pipeline.init() - if err != nil { - t.Errorf("test pipeline instance failed %s", err.Error()) - return - } - - GetRouteManager().addPipelineFromPipelineGroup("/test", pipelineInstance) - - rt := &httpRoute{ - routePath: "/test", - } - responseRecorder := httptest.NewRecorder() - r := &http.Request{} - rt.ServeHTTP(responseRecorder, r) -} diff --git a/engine/util_test.go b/engine/util_test.go deleted file mode 100644 index 3b356f8b9b1f4c961a85fbf36fc156a03dee0c4e..0000000000000000000000000000000000000000 --- a/engine/util_test.go +++ /dev/null @@ -1,227 +0,0 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - -package engine - -import ( - "bytes" - "time" - - "github.com/oceanbase/obagent/metric" - "github.com/oceanbase/obagent/plugins" -) - -const ( - Test metric.Type = "Test" -) - -type testInput struct { -} - -func (t *testInput) Init(_ map[string]interface{}) error { - return nil -} - -func (t *testInput) Close() error { - return nil -} - -func (t *testInput) SampleConfig() string { - return "this is test input" -} - -func (t *testInput) Description() string { - return "this is test input" -} - -func (t *testInput) Collect() ([]metric.Metric, error) { - return []metric.Metric{metric.NewMetric("test", map[string]interface{}{"test": "test"}, map[string]string{"test": "test"}, time.Time{}, Test)}, nil -} - -type testProcessor struct { -} - -func (t *testProcessor) Init(_ map[string]interface{}) error { - return nil -} - -func (t *testProcessor) Close() error { - return nil -} - -func (t *testProcessor) SampleConfig() string { - return "this is test processor" -} - -func (t *testProcessor) Description() string { - return "this is test processor" -} - -func (t *testProcessor) Process(metrics ...metric.Metric) ([]metric.Metric, error) { - return metrics, nil -} - -type testOutput struct { -} - -func (t *testOutput) Init(_ map[string]interface{}) error { - return nil -} - -func (t *testOutput) Close() error { - return nil -} - -func (t *testOutput) SampleConfig() string { - return "this is test output" -} - -func (t *testOutput) Description() string { - return "this is test output" -} - -func (t *testOutput) Write(_ []metric.Metric) error { - return nil -} - -type testExporter struct { -} - -func (t *testExporter) Init(_ map[string]interface{}) error { - return nil -} - -func (t *testExporter) Close() error { - return nil -} - -func (t *testExporter) SampleConfig() string { - return "this is test exporter" -} - -func (t *testExporter) Description() string { - return "this is test exporter" -} - -func (t *testExporter) Export(_ []metric.Metric) (*bytes.Buffer, error) { - return bytes.NewBuffer([]byte("this is test exporter")), nil -} - -func init() { - plugins.GetInputManager().Register("test", func() plugins.Input { - return &testInput{} - }) - plugins.GetProcessorManager().Register("test", func() plugins.Processor { - return &testProcessor{} - }) - plugins.GetOutputManager().Register("test", func() plugins.Output { - return &testOutput{} - }) - plugins.GetExporterManager().Register("test", func() plugins.Exporter { - return &testExporter{} - }) -} - -var testJSONModule = ` -{ - "module": "test", - "testInput": { - "plugin": "test", - "config": { - "timeout": 10, - "pluginConfig": null - } - }, - "testProcessor": { - "plugin": "test", - "config": { - "timeout": 10, - "pluginConfig": null - } - }, - "testOutput": { - "plugin": "test", - "config": { - "timeout": 10, - "pluginConfig": null - } - }, - "testExporter": { - "plugin": "test", - "config": { - "timeout": 10, - "pluginConfig": null - } - }, - "pipelines": [{ - "name": "pipeline1", - "config": { - "scheduleStrategy": "trigger", - "exposeUrl": "/metrics/test" - }, - "structure": { - "inputs": [{ - "plugin": "test", - "config": { - "timeout": 10, - "pluginConfig": null - } - }], - "processors": [{ - "plugin": "test", - "config": { - "timeout": 10, - "pluginConfig": null - } - }], - "exporter": { - "plugin": "test", - "config": { - "timeout": 10, - "pluginConfig": null - } - } - } - }, - { - "name": "pipeline2", - "config": { - "scheduleStrategy": "periodic", - "period": 5 - }, - "structure": { - "inputs": [{ - "plugin": "test", - "config": { - "timeout": 10, - "pluginConfig": null - } - }], - "processors": [{ - "plugin": "test", - "config": { - "timeout": 10, - "pluginConfig": null - } - }], - "output": { - "plugin": "test", - "config": { - "timeout": 10, - "pluginConfig": null - } - } - } - } - ] -} -` diff --git a/errors/error.go b/errors/error.go index 6fdf5d7ce476c77fc67d05262f5e50a525e952a8..469b26c0d8c3b55fa94fdf7bb40c3f84fed8d0aa 100644 --- a/errors/error.go +++ b/errors/error.go @@ -1,15 +1,3 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - package errors import ( @@ -18,27 +6,27 @@ import ( "golang.org/x/text/language" ) -// AgentError defines Agent specific errors. +// OcpAgentError defines OB-Agent specific errors. // It implements error interface. -type AgentError struct { +type OcpAgentError struct { ErrorCode ErrorCode // error code Args []interface{} // args for error message formatting } -func (e AgentError) Message(lang language.Tag) string { +func (e OcpAgentError) Message(lang language.Tag) string { return GetMessage(lang, e.ErrorCode, e.Args) } -func (e AgentError) DefaultMessage() string { +func (e OcpAgentError) DefaultMessage() string { return e.Message(defaultLanguage) } -func (e AgentError) Error() string { - return fmt.Sprintf("AgentError: code = %d, message = %s", e.ErrorCode.Code, e.DefaultMessage()) +func (e OcpAgentError) Error() string { + return fmt.Sprintf("OcpAgentError: code = %d, message = %s", e.ErrorCode.Code, e.DefaultMessage()) } -func Occur(errorCode ErrorCode, args ...interface{}) *AgentError { - return &AgentError{ +func Occur(errorCode ErrorCode, args ...interface{}) *OcpAgentError { + return &OcpAgentError{ ErrorCode: errorCode, Args: args, } diff --git a/errors/error_test.go b/errors/error_test.go index f82723c4782e9524e076fe3a61418d4728de1e5b..ecd486d4a5c27e3d58c3803363b86ffccc387da2 100644 --- a/errors/error_test.go +++ b/errors/error_test.go @@ -1,15 +1,3 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - package errors import ( diff --git a/errors/errorcode.go b/errors/errorcode.go index 869bae6dbeff745eba46c9536f68282f0f9722f6..1e107e3c4ef6c866872b4354411ce69ee5e3d36b 100644 --- a/errors/errorcode.go +++ b/errors/errorcode.go @@ -1,15 +1,3 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - package errors import "net/http" @@ -43,7 +31,65 @@ func NewErrorCode(code int, kind ErrorKind, key string) ErrorCode { } var ( + // general error codes, range: 1000 ~ 1999 ErrBadRequest = NewErrorCode(1000, badRequest, "err.bad.request") ErrIllegalArgument = NewErrorCode(1001, illegalArgument, "err.illegal.argument") ErrUnexpected = NewErrorCode(1002, unexpected, "err.unexpected") + + // shell execute error codes + ErrExecuteCommand = NewErrorCode(1500, unexpected, "err.execute.command") + + // file error codes - file, range: 2000 ~ 2099 + ErrDownloadFile = NewErrorCode(2000, unexpected, "err.download.file") + ErrInvalidChecksum = NewErrorCode(2001, unexpected, "err.invalid.checksum") + ErrWriteFile = NewErrorCode(2002, unexpected, "err.write.file") + ErrFindFile = NewErrorCode(2003, unexpected, "err.find.file") + ErrCheckFileExists = NewErrorCode(2004, unexpected, "err.check.file.exists") + + // file error codes - directory, range: 2100 ~ 2199 + ErrCreateDirectory = NewErrorCode(2100, unexpected, "err.create.directory") + ErrRemoveDirectory = NewErrorCode(2101, unexpected, "err.remove.directory") + ErrChownDirectory = NewErrorCode(2102, unexpected, "err.chown.directory") + + // file error codes - symlink, range: 2200 ~ 2299 + ErrCreateSymlink = NewErrorCode(2200, unexpected, "err.create.symlink") + ErrProcessCGroup = NewErrorCode(2201, unexpected, "err.process.cgroup") + + // task error codes, range: 2300 ~ 2399 + ErrTaskNotFound = NewErrorCode(2300, notFound, "err.task.not.found") + + // software package error codes, range: 3000 ~ 3999 + ErrQueryPackage = NewErrorCode(3000, unexpected, "err.query.package") + ErrInstallPackage = NewErrorCode(3001, unexpected, "err.install.package") + ErrUninstallPackage = NewErrorCode(3002, unexpected, "err.uninstall.package") + ErrExtractPackage = NewErrorCode(3003, unexpected, "err.extract.package") + + // system management error codes, range: 4000 ~ 4999 + ErrCheckProcessExists = NewErrorCode(4000, unexpected, "err.check.process.exists") + ErrGetProcessInfo = NewErrorCode(4001, unexpected, "err.get.process.info") + ErrStopProcess = NewErrorCode(4002, unexpected, "err.stop.process") + ErrProcessProcInfo = NewErrorCode(4003, unexpected, "err.get.process.proc") + + ErrGetDiskUsage = NewErrorCode(4100, unexpected, "err.system.disk.get.usage") + ErrBatchGetDiskInfos = NewErrorCode(4101, unexpected, "err.system.disk.batch.get.disk.infos") + + // ob operation error codes, range: 10000 ~ 10999 + ErrObInstallPreCheck = NewErrorCode(10000, unexpected, "err.ob.install.pre-check") + ErrObIoBench = NewErrorCode(10001, unexpected, "err.ob.io.bench") + ErrStartObServerProcess = NewErrorCode(10002, unexpected, "err.observer.start") + ErrCheckObServerAccessible = NewErrorCode(10003, unexpected, "err.check.observer.accessible") + ErrBootstrap = NewErrorCode(10004, unexpected, "err.ob.bootstrap") + ErrCleanObDataFiles = NewErrorCode(10005, unexpected, "err.clean.ob.data.files") + ErrCleanObAllFiles = NewErrorCode(10006, unexpected, "err.clean.ob.all.files") + ErrRunUpgradeScript = NewErrorCode(10007, unexpected, "err.run.upgrade.script") + + // agent admin error codes, range: 13000 ~ 13999 + ErrAgentdRunning = NewErrorCode(13000, unexpected, "err.agent.agentd.already.running") + ErrAgentdNotRunning = NewErrorCode(13001, unexpected, "err.agent.agentd.not.running") + ErrAgentdExitedQuickly = NewErrorCode(13002, unexpected, "err.agent.agentd.exited.quickly") + + // monagent pipeline manager error codes, range: 14000 ~ 14999 + ErrMonPipelineStart = NewErrorCode(14000, unexpected, "err.monagent.pipeline.already.start") + ErrMonPipelineStartFail = NewErrorCode(14001, unexpected, "err.monagent.pipeline.start.failed") + ErrRemoveMonPipeline = NewErrorCode(14002, unexpected, "err.monagent.remove.pipeline") ) diff --git a/errors/errorcode_test.go b/errors/errorcode_test.go index ac27bcca94bcb074e52839feede03d2f52753770..f6ec263b5869c406c2218c2a81ae8deb8e4bee10 100644 --- a/errors/errorcode_test.go +++ b/errors/errorcode_test.go @@ -1,15 +1,3 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - package errors import ( diff --git a/errors/errormsg.go b/errors/errormsg.go index 0ad68617802d6487a7a4548b010f9d64d24c7cd7..3506a65f319f6927271b910c2429c37b58c24172 100644 --- a/errors/errormsg.go +++ b/errors/errormsg.go @@ -1,15 +1,3 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - package errors import ( @@ -31,15 +19,14 @@ func init() { bundle = i18n.NewBundle(defaultLanguage) bundle.RegisterUnmarshalFunc("json", json.Unmarshal) loadBundleMessage(errorsI18nEnResourceFile) - // TODO add more message file here if more language support needed } func loadBundleMessage(assetName string) { - asset := bindata.MustAsset(assetName) + asset, _ := bindata.Asset(assetName) bundle.MustParseMessageFileBytes(asset, assetName) } -//GetMessage Get localized error message +// GetMessage Get localized error message func GetMessage(lang language.Tag, errorCode ErrorCode, args []interface{}) string { localizer := i18n.NewLocalizer(bundle, lang.String()) message, err := localizer.Localize(&i18n.LocalizeConfig{ diff --git a/errors/errormsg_test.go b/errors/errormsg_test.go new file mode 100644 index 0000000000000000000000000000000000000000..7b1869c09f1fbdbc6683cb121965fa7c6e83d321 --- /dev/null +++ b/errors/errormsg_test.go @@ -0,0 +1,14 @@ +package errors + +import ( + "testing" +) + +func TestErrorCode_HasMessage(t *testing.T) { + for _, e := range errorCodes { + message := GetMessage(defaultLanguage, e, []interface{}{}) + if message == e.key { + t.Errorf("ErrorCode %v(%v) has no i18n message defined", e.Code, e.key) + } + } +} diff --git a/errors/wrap.go b/errors/wrap.go index 85a3204af04f61a757d528e48573c89e698125da..741b561fd7bc2dcbf036733b745dd64f35bc4664 100644 --- a/errors/wrap.go +++ b/errors/wrap.go @@ -1,15 +1,3 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - package errors import "github.com/pkg/errors" diff --git a/etc/.config_secret.key b/etc/.config_secret.key index 225aaeeec16f6530f878147fd9099d4d8d4aee5a..3f16129862de00aba7fdf4ab58b8790ebd263eaa 100644 --- a/etc/.config_secret.key +++ b/etc/.config_secret.key @@ -1 +1 @@ -c2VuZCB5b3VyIHJlc3VtZSB0byB4aW4uc3VuaHhAYWxpcGF5LmNvbQo= +SGVsbG8gT2NlYW5CYXNlIQo= \ No newline at end of file diff --git a/etc/agentctl.yaml b/etc/agentctl.yaml new file mode 100644 index 0000000000000000000000000000000000000000..aa2d2fb7d9d2d0081ca70222bb8cb775ae326a3c --- /dev/null +++ b/etc/agentctl.yaml @@ -0,0 +1,23 @@ +log: + level: info + filename: ${obagent.home.path}/log/agentctl.log + maxsize: 30 + maxage: 30 + maxbackups: 10 + localtime: true + compress: true + +runDir: ${obagent.home.path}/run +confDir: ${obagent.home.path}/conf +logDir: ${obagent.home.path}/log +backupDir: ${obagent.home.path}/backup +pkgStoreDir: ${obagent.home.path}/pkg_store +taskStoreDir: ${obagent.home.path}/task_store +agentPkgName: obagent +pkgExt: rpm + +sdkConfig: + configPropertiesDir: ${obagent.home.path}/conf/config_properties + moduleConfigDir: ${obagent.home.path}/conf/module_config + cryptoPath: ${obagent.home.path}/conf/.config_secret.key + cryptoMethod: aes diff --git a/etc/agentd.yaml b/etc/agentd.yaml new file mode 100644 index 0000000000000000000000000000000000000000..da9a8a4739ebfa6e7915bd22a9e80b9f0d0d418d --- /dev/null +++ b/etc/agentd.yaml @@ -0,0 +1,29 @@ + +runDir: ${obagent.home.path}/run +logDir: ${obagent.home.path}/log +services: + ob_mgragent: + program: ${obagent.home.path}/bin/ob_mgragent + runDir: ${obagent.home.path}/run + #kill_wait: 0s + finalWait: 5s + minLiveTime: 3s + quickExitLimit: 3 +# limit: +# cpuQuota: 2.0 +# memoryQuota: 1024MB + stdout: ${obagent.home.path}/log/ob_mgragent.output.log + stderr: ${obagent.home.path}/log/ob_mgragent.error.log + + ob_monagent: + program: ${obagent.home.path}/bin/ob_monagent + runDir: ${obagent.home.path}/run + #kill_wait: 0s + finalWait: 5s + minLiveTime: 3s + quickExitLimit: 3 + limit: + cpuQuota: 2.0 + memoryQuota: 2048MB + stdout: ${obagent.home.path}/log/ob_monagent.output.log + stderr: ${obagent.home.path}/log/ob_monagent.error.log diff --git a/etc/config_properties/basic_auth.yaml b/etc/config_properties/basic_auth.yaml new file mode 100644 index 0000000000000000000000000000000000000000..bdd73ce48bffcabfcd897df4d72f4907e6669938 --- /dev/null +++ b/etc/config_properties/basic_auth.yaml @@ -0,0 +1,13 @@ +configVersion: "" +configs: + - key: agent.http.basic.auth.username + value: ocp_agent + valueType: string + - key: agent.http.basic.auth.password + value: + valueType: string + encrypted: true + - key: agent.http.basic.auth.metricAuthEnabled + value: true + valueType: bool + encrypted: false diff --git a/etc/config_properties/common_meta.yaml b/etc/config_properties/common_meta.yaml new file mode 100644 index 0000000000000000000000000000000000000000..5cb74fbc01d6b04570bc7cf7493645a9cffe40c2 --- /dev/null +++ b/etc/config_properties/common_meta.yaml @@ -0,0 +1,17 @@ +configVersion: "" +configs: + - key: config.version.maxbackups + value: 30 + valueType: int64 + - key: ocp.agent.http.socks.proxy.enabled + value: false + valueType: bool + - key: ocp.agent.http.socks.proxy.address + value: "" + valueType: string + - key: ocp.agent.manager.http.port + value: 62888 + valueType: int64 + - key: ocp.agent.monitor.http.port + value: 62889 + valueType: int64 \ No newline at end of file diff --git a/etc/config_properties/log.yaml b/etc/config_properties/log.yaml new file mode 100644 index 0000000000000000000000000000000000000000..6bc30a4c186764f50f05cde5e2b2b3775b95cf87 --- /dev/null +++ b/etc/config_properties/log.yaml @@ -0,0 +1,32 @@ +configVersion: "" +configs: + - key: monagent.log.level + value: info + valueType: string + - key: monagent.log.maxsize.mb + value: 200 + valueType: int64 + - key: monagent.log.maxage.days + value: 30 + valueType: int64 + - key: monagent.log.maxbackups + value: 15 + valueType: int64 + - key: monagent.log.compress + value: true + valueType: bool + - key: mgragent.log.level + value: info + valueType: string + - key: mgragent.log.maxsize.mb + value: 200 + valueType: int64 + - key: mgragent.log.maxage.days + value: 30 + valueType: int64 + - key: mgragent.log.maxbackups + value: 15 + valueType: int64 + - key: mgragent.log.compress + value: true + valueType: bool diff --git a/etc/config_properties/monagent_basic_auth.yaml b/etc/config_properties/monagent_basic_auth.yaml deleted file mode 100644 index f708ba2d0976088d08aefe2157c97b2a9b3641c5..0000000000000000000000000000000000000000 --- a/etc/config_properties/monagent_basic_auth.yaml +++ /dev/null @@ -1,16 +0,0 @@ -configVersion: "2021-08-20T07:52:28.5443+08:00" -configs: - - key: http.server.basic.auth.username - value: {http_basic_auth_user} - valueType: string - - key: http.server.basic.auth.password - value: {http_basic_auth_password} - valueType: string - encrypted: true - - key: http.admin.basic.auth.username - value: {pprof_basic_auth_user} - valueType: string - - key: http.admin.basic.auth.password - value: {pprof_basic_auth_password} - valueType: string - encrypted: true diff --git a/etc/config_properties/monagent_pipeline.yaml b/etc/config_properties/monagent_pipeline.yaml index 1e42b119c19a50437156e3fe33880406da2499c3..8aa7439afa49142f0b551a2fb8e7bcfc5ac7c7be 100644 --- a/etc/config_properties/monagent_pipeline.yaml +++ b/etc/config_properties/monagent_pipeline.yaml @@ -1,58 +1,111 @@ -configVersion: "2021-08-20T07:52:28.5443+08:00" +configVersion: "" configs: - - key: monagent.mysql.monitor.user - value: mysql_monitor_user - valueType: string - - key: monagent.mysql.monitor.password - value: mysql_monitor_password - valueType: string - encrypted: true - - key: monagent.mysql.sql.port - value: 3306 - valueType: int64 - - key: monagent.mysql.host - value: 127.0.0.1 - valueType: string - key: monagent.ob.monitor.user - value: {monitor_user} + value: ocp_monitor valueType: string - key: monagent.ob.monitor.password - value: {monitor_password} + value: valueType: string encrypted: true - key: monagent.ob.sql.port - value: {sql_port} + value: 2881 valueType: int64 - key: monagent.ob.rpc.port - value: {rpc_port} + value: 2882 valueType: int64 - - key: monagent.ob.install.path - value: {ob_install_path} - valueType: string - key: monagent.host.ip - value: {host_ip} + value: 127.0.0.1 valueType: string + - key: monagent.cluster.id + value: + valueType: int64 - key: monagent.ob.cluster.name - value: {cluster_name} + value: "" valueType: string - key: monagent.ob.cluster.id - value: {cluster_id} + value: 0 valueType: int64 - key: monagent.ob.zone.name - value: {zone_name} + value: "" valueType: string - key: monagent.pipeline.ob.status - value: {ob_monitor_status} + value: inactive + valueType: string + - key: monagent.pipeline.node.status + value: active + valueType: string + - key: ob.log.path + value: /data/log1 + valueType: string + - key: ob.data.path + value: /data/1 + valueType: string + - key: ob.install.path + value: /home/admin/oceanbase + valueType: string + - key: host.check.readonly.mountpoint + value: / + valueType: string + + - key: monagent.node.custom.interval + value: 1s + valueType: string + - key: monagent.node.ntp.server + value: 127.0.0.1 + valueType: string + - key: observer.coredump.path + value: /data/1 valueType: string - key: monagent.pipeline.ob.log.status - value: {ob_log_monitor_status} + value: inactive valueType: string - - key: monagent.pipeline.node.status - value: {host_monitor_status} + - key: monagent.pipeline.ob.trace.status + value: inactive + valueType: string + - key: es.client.addresses + value: "" + valueType: string + - key: es.client.auth.username + value: "" + valueType: string + - key: es.client.auth.password + value: + valueType: string + encrypted: true + + - key: observer.log.path + value: /home/admin/oceanbase/log + valueType: string + - key: agent.log.path + value: /home/admin/obagent/log + valueType: string + - key: os.log.path + value: /var/log + valueType: string + + - key: monagent.second.metric.cache.update.interval + value: 15s + valueType: string + + - key: monagent.mysql.monitor.user + value: mysql_monitor_user + valueType: string + - key: monagent.mysql.monitor.password + value: + valueType: string + encrypted: true + - key: monagent.mysql.sql.port + value: 3306 + valueType: int64 + - key: monagent.mysql.host + value: 127.0.0.1 valueType: string - key: monagent.alertmanager.address - value: {alertmanager_address} + value: valueType: string - - key: monagent.pipeline.mysql.status + - key: monagent.pipeline.ob.alertmanager.status value: inactive valueType: string + - key: monagent.pipeline.mysql.status + value: inactive + valueType: string \ No newline at end of file diff --git a/etc/config_properties/ob_logcleaner.yaml b/etc/config_properties/ob_logcleaner.yaml new file mode 100644 index 0000000000000000000000000000000000000000..d0ade978d41e53d25c181f6697994a23b203133f --- /dev/null +++ b/etc/config_properties/ob_logcleaner.yaml @@ -0,0 +1,32 @@ +configVersion: "" +configs: + - key: ob.logcleaner.enabled + value: false + valueType: bool + - key: ob.logcleaner.run.internal + value: 5m + valueType: string + - key: ob.logcleaner.ob_log.disk.threshold + value: 80 + valueType: int64 + - key: ob.logcleaner.ob_log.rule0.retention.days + value: 8 + valueType: int64 + - key: ob.logcleaner.ob_log.rule0.keep.percentage + value: 60 + valueType: int64 + - key: ob.logcleaner.ob_log.rule1.retention.days + value: 30 + valueType: int64 + - key: ob.logcleaner.ob_log.rule1.keep.percentage + value: 80 + valueType: int64 + - key: ob.logcleaner.core_log.disk.threshold + value: 80 + valueType: int64 + - key: ob.logcleaner.core_log.rule0.retention.days + value: 8 + valueType: int64 + - key: ob.logcleaner.core_log.rule0.keep.percentage + value: 60 + valueType: int64 diff --git a/etc/mgragent.yaml b/etc/mgragent.yaml new file mode 100644 index 0000000000000000000000000000000000000000..a51de1b3b3b5d0c3a4b95b169487d8c2d364cbdc --- /dev/null +++ b/etc/mgragent.yaml @@ -0,0 +1,12 @@ +install: + path: ${obagent.home.path} +server: + address: 0.0.0.0:${ocp.agent.manager.http.port} + runDir: ${obagent.home.path}/run +sdkConfig: + configPropertiesDir: ${obagent.home.path}/conf/config_properties + moduleConfigDir: ${obagent.home.path}/conf/module_config + cryptoPath: ${obagent.home.path}/conf/.config_secret.key + cryptoMethod: aes +shellf: + template: ${obagent.home.path}/conf/shell_templates/shell_template.yaml diff --git a/etc/module_config/common_module.yaml b/etc/module_config/common_module.yaml new file mode 100644 index 0000000000000000000000000000000000000000..218feb8dc4815183329dcb832c9791e202a1a479 --- /dev/null +++ b/etc/module_config/common_module.yaml @@ -0,0 +1,32 @@ +modules: + - + module: mgragent.config.meta + moduleType: config.meta + process: ob_mgragent + config: + maxbackups: ${config.version.maxbackups} + - + module: mgragent.proxy.config + moduleType: proxy.config + process: ob_mgragent + config: + proxyAddress: ${ocp.agent.http.socks.proxy.address} + proxyEnabled: ${ocp.agent.http.socks.proxy.enabled} + - + module: monagent.config.meta + moduleType: config.meta + process: ob_monagent + config: + maxbackups: ${config.version.maxbackups} + - + module: mgragent.stat.config + moduleType: stat.config + process: ob_mgragent + config: + host_ip: ${monagent.host.ip} + - + module: monagent.stat.config + moduleType: stat.config + process: ob_monagent + config: + host_ip: ${monagent.host.ip} diff --git a/etc/module_config/log_module.yaml b/etc/module_config/log_module.yaml new file mode 100644 index 0000000000000000000000000000000000000000..3cb91e50d87280f90de96fa0d29be4ffcbc0ebae --- /dev/null +++ b/etc/module_config/log_module.yaml @@ -0,0 +1,23 @@ +modules: + - + module: monagent.log.config + moduleType: monagent.log.config + process: ob_monagent + config: + level: ${monagent.log.level} + filename: ${obagent.home.path}/log/monagent.log + maxsize: ${monagent.log.maxsize.mb} + maxage: ${monagent.log.maxage.days} + maxbackups: ${monagent.log.maxbackups} + compress: ${monagent.log.compress} + - + module: mgragent.log.config + moduleType: mgragent.log.config + process: ob_mgragent + config: + level: ${mgragent.log.level} + filename: ${obagent.home.path}/log/mgragent.log + maxsize: ${mgragent.log.maxsize.mb} + maxage: ${mgragent.log.maxage.days} + maxbackups: ${mgragent.log.maxbackups} + compress: ${mgragent.log.compress} diff --git a/etc/module_config/mgragent_logquerier_module.yaml b/etc/module_config/mgragent_logquerier_module.yaml new file mode 100644 index 0000000000000000000000000000000000000000..bdfa5b656d58f37206dfb7732e2db61ba8e63a82 --- /dev/null +++ b/etc/module_config/mgragent_logquerier_module.yaml @@ -0,0 +1,119 @@ +modules: + - + module: mgragent.logquerier + moduleType: mgragent.logquerier + process: ob_mgragent + config: + queryTimeout: 1m + downloadTimeout: 3m + errCountLimit: 100 + logTypeQueryConfigs: + - logType: observer + isOverrideByPriority: true + logLevelAndFilePatterns: + - logLevel: ERROR + dir: ${ob.install.path}/log + filePatterns: + - observer.log.wf* + logParserCategory: ob_light + - logLevel: WARN + dir: ${ob.install.path}/log + filePatterns: + - observer.log.wf* + logParserCategory: ob_light + - logLevel: INFO + dir: ${ob.install.path}/log + filePatterns: + - observer.log* + logParserCategory: ob_light + - logLevel: DEBUG + dir: ${ob.install.path}/log + filePatterns: + - observer.log* + logParserCategory: ob_light + - logType: rootservice + isOverrideByPriority: true + logLevelAndFilePatterns: + - logLevel: ERROR + dir: ${ob.install.path}/log + filePatterns: + - rootservice.log.wf* + logParserCategory: ob_light + - logLevel: WARN + dir: ${ob.install.path}/log + filePatterns: + - rootservice.log.wf* + logParserCategory: ob_light + - logLevel: INFO + dir: ${ob.install.path}/log + filePatterns: + - rootservice.log* + logParserCategory: ob_light + - logLevel: DEBUG + dir: ${ob.install.path}/log + filePatterns: + - rootservice.log* + logParserCategory: ob_light + - logType: election + isOverrideByPriority: true + logLevelAndFilePatterns: + - logLevel: ERROR + dir: ${ob.install.path}/log + filePatterns: + - election.log.wf* + logParserCategory: ob_light + - logLevel: WARN + dir: ${ob.install.path}/log + filePatterns: + - election.log.wf* + logParserCategory: ob_light + - logLevel: INFO + dir: ${ob.install.path}/log + filePatterns: + - election.log* + logParserCategory: ob_light + - logLevel: DEBUG + dir: ${ob.install.path}/log + filePatterns: + - election.log* + logParserCategory: ob_light + - logType: mgragent + isOverrideByPriority: false + logLevelAndFilePatterns: + - logLevel: ALL + dir: ${obagent.home.path}/log + filePatterns: + - mgragent.log* + logParserCategory: agent_light + - logType: agentctl + isOverrideByPriority: false + logLevelAndFilePatterns: + - logLevel: ALL + dir: ${obagent.home.path}/log + filePatterns: + - agentctl.log* + logParserCategory: agent_light + - logType: monagent + isOverrideByPriority: false + logLevelAndFilePatterns: + - logLevel: ALL + dir: ${obagent.home.path}/log + filePatterns: + - monagent.log* + logParserCategory: agent_light + - logType: agentd + isOverrideByPriority: false + logLevelAndFilePatterns: + - logLevel: ALL + dir: ${obagent.home.path}/log + filePatterns: + - agentd.log* + logParserCategory: agent_light + - logType: host + isOverrideByPriority: false + logLevelAndFilePatterns: + - logLevel: ALL + dir: /var/log + filePatterns: + - messages* + logParserCategory: host_light diff --git a/etc/module_config/mgragent_module.yaml b/etc/module_config/mgragent_module.yaml new file mode 100644 index 0000000000000000000000000000000000000000..65f93b93bdd33c2e074bf6ee612da7f3d010fc9f --- /dev/null +++ b/etc/module_config/mgragent_module.yaml @@ -0,0 +1,21 @@ +modules: + - + module: mgragent.basic.auth + moduleType: mgragent.basic.auth + process: ob_mgragent + config: + auth: basic + metricAuthEnabled: ${agent.http.basic.auth.metricAuthEnabled} + username: ${agent.http.basic.auth.username} + password: ${agent.http.basic.auth.password} + - + module: module.config.notify + moduleType: module.config.notify + process: ob_agentctl + config: + - process: ob_mgragent + notifyAddress: api/v1/module/config/notify + - process: ob_monagent + notifyAddress: api/v1/module/config/notify + - process: ob_agentctl + local: true diff --git a/etc/module_config/monagent_basic_auth.yaml b/etc/module_config/monagent_basic_auth.yaml index 4636211ebd44b6696a356a46a8fce98a6d24caad..198c07be737901be59d9eca8ffbda82651b3be7f 100644 --- a/etc/module_config/monagent_basic_auth.yaml +++ b/etc/module_config/monagent_basic_auth.yaml @@ -2,18 +2,9 @@ modules: - module: monagent.server.basic.auth moduleType: monagent.server.basic.auth - process: monagent - disabled: {disable_http_basic_auth} + process: ob_monagent config: auth: basic - username: ${http.server.basic.auth.username} - password: ${http.server.basic.auth.password} - - - module: monagent.admin.basic.auth - moduleType: monagent.admin.basic.auth - process: monagent - disabled: {disable_pprof_basic_auth} - config: - auth: basic - username: ${http.admin.basic.auth.username} - password: ${http.admin.basic.auth.password} + metricAuthEnabled: ${agent.http.basic.auth.metricAuthEnabled} + username: ${agent.http.basic.auth.username} + password: ${agent.http.basic.auth.password} diff --git a/etc/module_config/monagent_config.yaml b/etc/module_config/monagent_config.yaml deleted file mode 100644 index 7eb0334833e4b89fde890fceffd1d9df9b91f3e9..0000000000000000000000000000000000000000 --- a/etc/module_config/monagent_config.yaml +++ /dev/null @@ -1,8 +0,0 @@ -modules: - - - module: module.config.notify - moduleType: module.config.notify - process: monagent - config: - - process: monagent - local: true diff --git a/etc/module_config/monitor_host_log.yaml b/etc/module_config/monitor_host_log.yaml new file mode 100755 index 0000000000000000000000000000000000000000..92e81be0076b193978acb571ab124976ebd11b57 --- /dev/null +++ b/etc/module_config/monitor_host_log.yaml @@ -0,0 +1,72 @@ +logTailerInput: &logTailerInput + plugin: logTailerInput + config: + timeout: 60s + pluginConfig: + tailConfigs: + - logDir: /var/log + logFileName: kern + processLogInterval: 500ms + logSourceType: kern + logAnalyzerType: host_light + recoveryConfig: + enabled: true + triggerThreshold: 10000 + +logTransformer: &logTransformer + plugin: logTransformer + config: + timeout: 10s + pluginConfig: + +retagProcessor: &retagProcessor + plugin: retagProcessor + config: + timeout: 10s + pluginConfig: + newTags: + ip: ${monagent.host.ip} + obClusterId: ${monagent.ob.cluster.id} + obClusterName: ${monagent.ob.cluster.name} + +esOutput: &esOutput + plugin: esOutput + config: + timeout: 10s + pluginConfig: + clientAddresses: ${es.client.addresses} + auth: + username: ${es.client.auth.username} + password: ${es.client.auth.password} + indexNamePattern: host_log_%Y%m%d + batchSizeInBytes: 1048576 + maxBatchWait: 1s + docMap: + timestamp: timestamp + timestampPrecision: 1us + name: file + tags: + app: app + fields: + tags: tags + content: content + +modules: + - module: monitor.host.log + moduleType: monagent.pipeline + process: ob_monagent + config: + name: host.log + status: ${monagent.pipeline.ob.log.status} + pipelines: + - name: host_log_to_es + config: + scheduleStrategy: bySource + structure: + inputs: + - <<: *logTailerInput + processors: + - <<: *retagProcessor + - <<: *logTransformer + output: + <<: *esOutput diff --git a/etc/module_config/monitor_mysql.yaml b/etc/module_config/monitor_mysql.yaml index bfd6fd1dc5f2a2452ff6865dbf3e145466695916..15c72137e706d422f6cf196787c3166acfa32db4 100644 --- a/etc/module_config/monitor_mysql.yaml +++ b/etc/module_config/monitor_mysql.yaml @@ -3,7 +3,7 @@ mysqldInput: &mysqldInput config: timeout: 10s pluginConfig: - dsn: ${monagent.mysql.monitor.user}:${monagent.mysql.monitor.password}@(${monagent.mysql.host}:${monagent.mysql.sql.port})/ + dsn: ${monagent.mysql.monitor.user}:${monagent.mysql.monitor.password}@(${monagent.mysql.host}:${monagent.mysql.sql.port})/ retagProcessor: &retagProcessor plugin: retagProcessor @@ -20,19 +20,19 @@ prometheusExporter: &prometheusExporter timout: 10s pluginConfig: formatType: fmtText + exposeUrl: /metrics/mysql modules: - module: monitor.mysql moduleType: monagent.pipeline - process: monagent + process: ob_monagent config: name: monitor.mysql status: ${monagent.pipeline.mysql.status} pipelines: - name: mysql_info config: - scheduleStrategy: trigger - exposeUrl: /metrics/mysql + scheduleStrategy: bySource structure: inputs: - <<: *mysqldInput diff --git a/etc/module_config/monitor_node_host.yaml b/etc/module_config/monitor_node_host.yaml index 38c617e1fa9db4b29453d86a89bc92cd9910a3b8..1933f23de5ea3c3186aa0cadd7f3232c1240bdd6 100644 --- a/etc/module_config/monitor_node_host.yaml +++ b/etc/module_config/monitor_node_host.yaml @@ -3,40 +3,105 @@ nodeInput: &nodeInput config: timeout: 10s pluginConfig: - metricFamilies: [ node_cpu_seconds_total, node_filesystem_avail_bytes, node_filesystem_size_bytes, node_filesystem_readonly, node_disk_reads_completed_total, node_disk_read_bytes_total, node_disk_read_time_seconds_total, node_disk_writes_completed_total, node_disk_written_bytes_total, node_disk_write_time_seconds_total, node_load1, node_load15, node_load5, node_memory_Buffers_bytes, node_memory_MemFree_bytes, node_memory_Cached_bytes, node_memory_MemTotal_bytes, node_network_receive_bytes_total, node_network_transmit_bytes_total, node_ntp_offset_seconds ] + collect_interval: ${monagent.second.metric.cache.update.interval} + collectors: [ cpu, diskstats, loadavg, meminfo, filesystem, netdev] + metricFamilies: [ node_cpu_seconds_total, node_filesystem_avail_bytes, node_filesystem_size_bytes, node_disk_reads_completed_total, node_disk_read_bytes_total, node_disk_read_time_seconds_total, node_disk_writes_completed_total, node_disk_written_bytes_total, node_disk_write_time_seconds_total, node_load1, node_load15, node_load5, node_memory_Buffers_bytes, node_memory_MemFree_bytes, node_memory_Cached_bytes, node_memory_MemTotal_bytes, node_network_receive_bytes_total, node_network_transmit_bytes_total, node_disk_io_time_weighted_seconds_total ] -retagProcessor: &retagProcessor - plugin: retagProcessor +customInput: &customInput + plugin: hostCustomInput config: - timout: 10s + timeout: 60s pluginConfig: - newTags: - app: HOST - svr_ip: ${monagent.host.ip} + timeout: 50s + collect_interval: ${monagent.second.metric.cache.update.interval} + +processInput: &processInput + plugin: processInput + config: + timeout: 20s + pluginConfig: + processNames: [ob_agentd, ntpd, chronyd] + collect_interval: ${monagent.second.metric.cache.update.interval} + +attrProcessor: &attrProcessor + plugin: attrProcessor + config: + timeout: 10s + pluginConfig: + operations: + - oper: copyTags + tags: + mountpoint: mount_point + dev: device + - oper: addTags + tags: + app: HOST + svr_ip: ${monagent.host.ip} + svr_port: ${monagent.ob.rpc.port} + obzone: ${monagent.ob.zone.name} + ob_cluster_id: ${monagent.ob.cluster.id} + ob_cluster_name: ${monagent.ob.cluster.name} + - oper: removeMetric + condition: + metric: node_filesystem_size_bytes + tags: + fstype: tmpfs + - oper: removeMetric + condition: + metric: node_filesystem_avail_bytes + tags: + fstype: tmpfs + +aggregateProcessor: &aggregateProcessor + plugin: aggregateProcessor + config: + timeout: 10s + pluginConfig: + rules: + - metric: node_cpu_seconds_total + tags: [ mode ] + isRetainNativeMetric: false + +mountLabelProcessor: &mountLabelProcessor + plugin: mountLabelProcessor + config: + timeout: 10s + pluginConfig: + labelTags: + installPath: ${ob.install.path} + dataDiskPath: ${ob.data.path} + logDiskPath: ${ob.log.path} + checkReadonly: ${host.check.readonly.mountpoint} + ob_status: ${monagent.pipeline.ob.status} prometheusExporter: &prometheusExporter plugin: prometheusExporter config: - timout: 10s + timeout: 10s pluginConfig: formatType: fmtText + withTimestamp: true + exposeUrl: /metrics/node/host modules: - module: monitor.node.host moduleType: monagent.pipeline - process: monagent + process: ob_monagent config: name: monitor.node.host status: ${monagent.pipeline.node.status} pipelines: - name: node_info config: - scheduleStrategy: trigger - exposeUrl: /metrics/node/host + scheduleStrategy: bySource structure: inputs: - <<: *nodeInput + - <<: *customInput + - <<: *processInput processors: - - <<: *retagProcessor + - <<: *aggregateProcessor + - <<: *attrProcessor + - <<: *mountLabelProcessor exporter: <<: *prometheusExporter diff --git a/etc/module_config/monitor_ob.yaml b/etc/module_config/monitor_ob.yaml index fd8bb33d2987d4fbce608b0dcae7c19641c72b5e..0294c85823c435ee5d14fbae48997b95d91ef4c3 100644 --- a/etc/module_config/monitor_ob.yaml +++ b/etc/module_config/monitor_ob.yaml @@ -3,7 +3,7 @@ obInputBasic: &obInputBasic config: timeout: 10s pluginConfig: - maintainCacheThreads: 4 + collect_interval: ${monagent.second.metric.cache.update.interval} connection: url: ${monagent.ob.monitor.user}:${monagent.ob.monitor.password}@tcp(127.0.0.1:${monagent.ob.sql.port})/oceanbase?interpolateParams=true maxIdle: 2 @@ -11,69 +11,100 @@ obInputBasic: &obInputBasic defaultConditionValues: ob_svr_ip: ${monagent.host.ip} ob_svr_port: ${monagent.ob.rpc.port} - ob_is_rootservice: false - ob_is_v4: false - ob_is_v3: false + ob_is_rootservice: true collectConfig: - - - sql: select ob_version() REGEXP '^4' as ob_is_v4 - name: ob_is_v4 - conditionValues: - ob_is_v4: ob_is_v4 - - sql: select ob_version() REGEXP '^3' as ob_is_v3 - name: ob_is_v3 - conditionValues: - ob_is_v3: ob_is_v3 - - name: ob_role + - sql: select /*+ MONITOR_AGENT READ_CONSISTENCY(WEAK) */ with_rootserver from __all_server where svr_ip = ? and svr_port = ? + name: ob_role sqlSlowThreshold: 100ms - metrics: - is_rootserver: with_rootserver + params: [ ob_svr_ip, ob_svr_port ] + minObVersion: ~ + maxObVersion: 4.0.0.0 conditionValues: ob_is_rootservice: with_rootserver - conditions: [ ob_is_v3 ] + enableCache: true + cacheExpire: 1m + - sql: select (case when with_rootserver='YES' then 1 else 0 end) as with_rootserver from DBA_OB_SERVERS where svr_ip = ? and svr_port = ? + name: ob_role + sqlSlowThreshold: 100ms params: [ ob_svr_ip, ob_svr_port ] - sql: select /*+ MONITOR_AGENT READ_CONSISTENCY(WEAK) */ with_rootserver from __all_server where svr_ip = ? and svr_port = ? + minObVersion: 4.0.0.0 + maxObVersion: ~ + conditionValues: + ob_is_rootservice: with_rootserver + enableCache: true + cacheExpire: 1m - - name: ob_role + - sql: select /*+ MONITOR_AGENT READ_CONSISTENCY(WEAK) */ tenant_id, cache_name, cache_size from __all_virtual_kvcache_info where svr_ip = ? and svr_port = ? + params: [ ob_svr_ip, ob_svr_port ] sqlSlowThreshold: 100ms + name: ob_cache + minObVersion: ~ + maxObVersion: 4.0.0.0 + tags: + ob_tenant_id: tenant_id + tenant_name: tenant_name + cache_name: cache_name metrics: - is_rootserver: with_rootserver - conditionValues: - ob_is_rootservice: with_rootserver - conditions: [ ob_is_v4 ] + size_bytes: cache_size + enableCache: true + cacheExpire: ${monagent.second.metric.cache.update.interval} + - sql: select /* MONITOR_AGENT */ tenant_id, cache_name, cache_size from GV$OB_KVCACHE where svr_ip = ? and svr_port = ? params: [ ob_svr_ip, ob_svr_port ] - sql: select (case when with_rootserver='YES' then 1 else 0 end) as with_rootserver from DBA_OB_SERVERS where svr_ip = ? and svr_port = ? - - - sql: select /*+ MONITOR_AGENT READ_CONSISTENCY(WEAK) */ tenant_name, cache_name, cache_size from __all_virtual_kvcache_info, __all_tenant where __all_tenant.tenant_id = __all_virtual_kvcache_info.tenant_id and svr_ip = ? and svr_port = ? - params: [ob_svr_ip, ob_svr_port] + sqlSlowThreshold: 100ms name: ob_cache + minObVersion: 4.0.0.0 + maxObVersion: ~ tags: + ob_tenant_id: tenant_id tenant_name: tenant_name cache_name: cache_name metrics: size_bytes: cache_size - - sql: select /*+ MONITOR_AGENT READ_CONSISTENCY(WEAK) */ tenant_name, tenant_id, stat_id, value from v$sysstat, __all_tenant where stat_id IN (10000, 10001, 10002, 10003, 10005, 10006, 140002, 140003, 140005, 140006, 60019, 60020, 60024, 80040, 80041, 130000, 130001, 130002, 130004, 20001, 20002, 30000, 30001, 30002, 30005, 30006, 40000, 40001, 40002, 40003, 40004, 40005, 40006, 40007, 40008, 40009, 40010, 40011, 40012, 50000, 50001, 50008, 50009, 60000, 60001, 60002, 60003, 60004, 60005, 60019, 60020, 60021, 60022, 60023, 60024, 80057) and (con_id > 1000 or con_id = 1) and __all_tenant.tenant_id = v$sysstat.con_id and class < 1000 + enableCache: true + cacheExpire: ${monagent.second.metric.cache.update.interval} + + - sql: select /*+ MONITOR_AGENT READ_CONSISTENCY(WEAK) */ con_id tenant_id, stat_id, value from v$sysstat where stat_id IN (10000, 10001, 10002, 10003, 10004, 10005, 10006, 140002, 140003, 140005, 140006, 40030, 80040, 80041, 130000, 130001, 130002, 130004, 20000, 20001, 20002, 30000, 30001, 30002, 30005, 30006, 30007, 30008, 30009, 30010, 30011, 30012, 30013, 30080, 30081, 40000, 40001, 40002, 40003, 40004, 40005, 40006, 40007, 40008, 40009, 40010, 40011, 40012, 40018, 40019, 40116, 40117, 40118, 50000, 50001, 50002, 50004, 50005, 50008, 50009, 50010, 50011, 50037, 50038, 60000, 60001, 60002, 60003, 60004, 60005, 60019, 60020, 60021, 60022, 60023, 60024, 80057, 120000, 120001, 120009, 120008) and (con_id > 1000 or con_id = 1) and class < 1000 name: ob_sysstat + sqlSlowThreshold: 100ms + minObVersion: ~ + maxObVersion: 4.0.0.0 tags: - tenant_name: tenant_name - tenant_id: tenant_id + ob_tenant_id: tenant_id stat_id: stat_id metrics: value: value - - sql: select /*+ MONITOR_AGENT READ_CONSISTENCY(WEAK) QUERY_TIMEOUT(100000000) */ tenant_name, 1 as role, case when cnt is null then 0 else cnt end as cnt from (select __all_tenant.tenant_name, t1.cnt from __all_tenant left join (select tenant_id, count(*) as cnt from __all_virtual_partition_info where svr_ip = ? and svr_port = ? group by tenant_id) t1 on __all_tenant.tenant_id = t1.tenant_id) t2 - params: [ob_svr_ip, ob_svr_port] - conditions: [ ob_is_v3 ] - name: ob_partition + enableCache: true + cacheExpire: ${monagent.second.metric.cache.update.interval} + - sql: select /* MONITOR_AGENT */ con_id tenant_id, stat_id, value from v$sysstat where stat_id IN (10000, 10001, 10002, 10003, 10004, 10005, 10006, 140002, 140003, 140005, 140006, 40030, 80040, 80041, 130000, 130001, 130002, 130004, 20000, 20001, 20002, 30000, 30001, 30002, 30005, 30006, 30007, 30008, 30009, 30010, 30011, 30012, 30013, 40000, 40001, 40002, 40003, 40004, 40005, 40006, 40007, 40008, 40009, 40010, 40011, 40012, 40018, 40019, 50000, 50001, 60087, 50004, 50005, 50008, 50009, 50010, 50011, 50037, 50038, 60000, 60001, 60002, 60003, 60004, 60005, 60019, 60020, 60021, 60022, 60023, 60024, 80057, 120000, 120001, 120009, 120008) and (con_id > 1000 or con_id = 1) and class < 1000 + name: ob_sysstat + sqlSlowThreshold: 100ms + minObVersion: 4.0.0.0 + maxObVersion: ~ tags: - tenant_name: tenant_name - role: role + ob_tenant_id: tenant_id + stat_id: stat_id metrics: - num: cnt + value: value enableCache: true - cacheExpire: 1h - cacheDataExpire: 2h + cacheExpire: ${monagent.second.metric.cache.update.interval} + - sql: select /*+ MONITOR_AGENT READ_CONSISTENCY(WEAK) */ group_concat(svr_ip SEPARATOR ',') as servers, status, count(1) as cnt from __all_server group by status name: ob_server + sqlSlowThreshold: 100ms + minObVersion: ~ + maxObVersion: 4.0.0.0 + tags: + server_ips: servers + status: status + metrics: + num: cnt + enableCache: true + cacheExpire: 60s + - sql: select /* MONITOR_AGENT */ group_concat(svr_ip SEPARATOR ',') as servers, status, count(1) as cnt from DBA_OB_SERVERS group by status + name: ob_server + sqlSlowThreshold: 100ms + minObVersion: 4.0.0.0 + maxObVersion: ~ tags: server_ips: servers status: status @@ -81,165 +112,203 @@ obInputBasic: &obInputBasic num: cnt enableCache: true cacheExpire: 60s - cacheDataExpire: 120s - - sql: select /*+ MONITOR_AGENT READ_CONSISTENCY(WEAK) QUERY_TIMEOUT(100000000) */ tenant_name, count(*) as cnt from gv$table group by tenant_id + + - sql: select /*+ MONITOR_AGENT READ_CONSISTENCY(WEAK) QUERY_TIMEOUT(100000000) */ tenant_name, tenant_id, count(*) as cnt from gv$table group by tenant_id name: ob_table - conditions: [ ob_is_v3, ob_is_rootservice ] + sqlSlowThreshold: 100ms + condition: ob_is_rootservice + minObVersion: ~ + maxObVersion: 4.0.0.0 tags: - tenant_name: tenant_name + ob_tenant_id: tenant_id metrics: num: cnt enableCache: true cacheExpire: 1h - cacheDataExpire: 2h - - sql: select /*+ MONITOR_AGENT READ_CONSISTENCY(WEAK) QUERY_TIMEOUT(100000000) */ tenant_name, count(*) as cnt from CDB_TABLES join __all_tenant on CDB_TABLES.con_id = __all_tenant.tenant_id group by con_id + - sql: select /*+ MONITOR_AGENT QUERY_TIMEOUT(100000000) */ con_id tenant_id, count(*) as cnt from CDB_TABLES group by con_id name: ob_table - conditions: [ ob_is_v4, ob_is_rootservice ] + sqlSlowThreshold: 100ms + condition: ob_is_rootservice + minObVersion: 4.0.0.0 + maxObVersion: ~ tags: - tenant_name: tenant_name + ob_tenant_id: tenant_id metrics: num: cnt enableCache: true cacheExpire: 1h - cacheDataExpire: 2h - - sql: select /*+ MONITOR_AGENT READ_CONSISTENCY(WEAK) */ case when cnt is null then 0 else cnt end as cnt, tenant_name from (select __all_tenant.tenant_name, cnt from __all_tenant left join (select count(*) as cnt, tenant as tenant_name from __all_virtual_processlist where svr_ip = ? and svr_port = ? group by tenant) t1 on __all_tenant.tenant_name = t1.tenant_name) t2 - params: [ob_svr_ip, ob_svr_port] + + - sql: select /*+ MONITOR_AGENT READ_CONSISTENCY(WEAK) */ case when cnt is null then 0 else cnt end as cnt, tenant_name, tenant_id from (select __all_tenant.tenant_name, __all_tenant.tenant_id, cnt from __all_tenant left join (select count(`state`='ACTIVE' OR NULL) as cnt, tenant as tenant_name from __all_virtual_processlist where svr_ip = ? and svr_port = ? group by tenant) t1 on __all_tenant.tenant_name = t1.tenant_name) t2 + params: [ ob_svr_ip, ob_svr_port ] + sqlSlowThreshold: 100ms name: ob_active_session + minObVersion: ~ + maxObVersion: 4.0.0.0 tags: + ob_tenant_id: tenant_id tenant_name: tenant_name metrics: num: cnt - - sql: select /*+ MONITOR_AGENT READ_CONSISTENCY(WEAK) */ zone, name, value, time_to_usec(now()) as current from __all_zone - name: ob_zone + enableCache: true + cacheExpire: ${monagent.second.metric.cache.update.interval} + - sql: select /* MONITOR_AGENT */ case when cnt is null then 0 else cnt end as cnt, tenant_name, tenant_id from (select DBA_OB_TENANTS.tenant_name, DBA_OB_TENANTS.tenant_id, cnt from DBA_OB_TENANTS left join (select count(`state`='ACTIVE' OR NULL) as cnt, tenant as tenant_name from GV$OB_PROCESSLIST where svr_ip = ? and svr_port = ? group by tenant) t1 on DBA_OB_TENANTS.tenant_name = t1.tenant_name where DBA_OB_TENANTS.tenant_type<>'META') t2 + params: [ ob_svr_ip, ob_svr_port ] + sqlSlowThreshold: 100ms + name: ob_active_session + minObVersion: 4.0.0.0 + maxObVersion: ~ tags: - zone: zone - name: name + ob_tenant_id: tenant_id + tenant_name: tenant_name metrics: - stat: value - current_timestamp: current - - sql: select /*+ MONITOR_AGENT READ_CONSISTENCY(WEAK) */ tenant_name, mem_used, access_count, hit_count from v$plan_cache_stat join __all_tenant on v$plan_cache_stat.tenant_id = __all_tenant.tenant_id group by tenant_name - name: ob_plan_cache - conditions: [ ob_is_v3 ] + num: cnt + enableCache: true + cacheExpire: ${monagent.second.metric.cache.update.interval} + + - sql: select /*+ MONITOR_AGENT READ_CONSISTENCY(WEAK) */ case when cnt is null then 0 else cnt end as cnt, tenant_name, tenant_id from (select __all_tenant.tenant_name, __all_tenant.tenant_id, cnt from __all_tenant left join (select count(1) as cnt, tenant as tenant_name from __all_virtual_processlist where svr_ip = ? and svr_port = ? group by tenant) t1 on __all_tenant.tenant_name = t1.tenant_name) t2 + params: [ ob_svr_ip, ob_svr_port ] + sqlSlowThreshold: 100ms + name: ob_all_session + minObVersion: ~ + maxObVersion: 4.0.0.0 + tags: + ob_tenant_id: tenant_id + tenant_name: tenant_name + metrics: + num: cnt + enableCache: true + cacheExpire: ${monagent.second.metric.cache.update.interval} + - sql: select /* MONITOR_AGENT */ case when cnt is null then 0 else cnt end as cnt, tenant_name, tenant_id from (select DBA_OB_TENANTS.tenant_name, DBA_OB_TENANTS.tenant_id, cnt from DBA_OB_TENANTS left join (select count(1) as cnt, tenant as tenant_name from GV$OB_PROCESSLIST where svr_ip = ? and svr_port = ? group by tenant) t1 on DBA_OB_TENANTS.tenant_name = t1.tenant_name where DBA_OB_TENANTS.tenant_type<>'META') t2 + params: [ ob_svr_ip, ob_svr_port ] + sqlSlowThreshold: 100ms + name: ob_all_session + minObVersion: 4.0.0.0 + maxObVersion: ~ tags: + ob_tenant_id: tenant_id tenant_name: tenant_name + metrics: + num: cnt + enableCache: true + cacheExpire: ${monagent.second.metric.cache.update.interval} + + - sql: select /*+ MONITOR_AGENT READ_CONSISTENCY(WEAK) */ tenant_id, mem_used, access_count, hit_count from v$plan_cache_stat + name: ob_plan_cache + sqlSlowThreshold: 100ms + minObVersion: ~ + maxObVersion: 4.0.0.0 + tags: + ob_tenant_id: tenant_id metrics: memory_bytes: mem_used access_total: access_count hit_total: hit_count - - sql: select /*+ MONITOR_AGENT READ_CONSISTENCY(WEAK) */ tenant_name, mem_used, access_count, hit_count from V$OB_PLAN_CACHE_STAT join __all_tenant on V$OB_PLAN_CACHE_STAT.tenant_id = __all_tenant.tenant_id group by tenant_name + enableCache: true + cacheExpire: ${monagent.second.metric.cache.update.interval} + - sql: select /* MONITOR_AGENT */ tenant_id, mem_used, access_count, hit_count from V$OB_PLAN_CACHE_STAT name: ob_plan_cache - condition: [ ob_is_v4 ] - + sqlSlowThreshold: 100ms + minObVersion: 4.0.0.0 + maxObVersion: ~ tags: - tenant_name: tenant_name + ob_tenant_id: tenant_id metrics: memory_bytes: mem_used access_total: access_count hit_total: hit_count - - sql: select /*+ MONITOR_AGENT READ_CONSISTENCY(WEAK) */ tenant_name, sum(total_waits) as total_waits, sum(time_waited_micro) / 1000000 as time_waited from v$system_event join __all_tenant on v$system_event.con_id = __all_tenant.tenant_id where v$system_event.wait_class <> 'IDLE' group by tenant_name + enableCache: true + cacheExpire: ${monagent.second.metric.cache.update.interval} + + - sql: select /*+ MONITOR_AGENT READ_CONSISTENCY(WEAK) */ con_id tenant_id, sum(total_waits) as total_waits, sum(time_waited_micro) / 1000000 as time_waited from v$system_event where v$system_event.wait_class <> 'IDLE' and (con_id > 1000 or con_id = 1) group by tenant_id name: ob_waitevent + sqlSlowThreshold: 100ms + minObVersion: ~ + maxObVersion: 4.0.0.0 tags: - tenant_name: tenant_name + ob_tenant_id: tenant_id metrics: wait_total: total_waits wait_seconds_total: time_waited - - sql: select /*+ MONITOR_AGENT READ_CONSISTENCY(WEAK) */ __all_tenant.tenant_name as tenant_name, cast(v_acc_response_time.response_time / 1000000 as float) as response_time_seconds, v_acc_response_time.count as bucket, case when v_acc_response_time.response_time = (select max(response_time) from __all_virtual_query_response_time) then v_acc_response_time.count else null end as count, case when v_acc_response_time.response_time = (select max(response_time) from __all_virtual_query_response_time) then cast(v_acc_response_time.sum / 1000000 as float) else null end as sum from (select b.tenant_id, b.response_time as response_time, sum(a.count) as count, sum(a.total) as sum from __all_virtual_query_response_time a, __all_virtual_query_response_time b where a.response_time <= b.response_time and a.svr_ip = b.svr_ip and a.svr_port = b.svr_port and b.svr_ip = ? and b.svr_port = ? group by b.tenant_id, b.response_time) v_acc_response_time, __all_tenant where v_acc_response_time.tenant_id = __all_tenant.tenant_id; - params: [ob_svr_ip, ob_svr_port] - name: ob_query_response_time_seconds + enableCache: true + cacheExpire: ${monagent.second.metric.cache.update.interval} + - sql: select /* MONITOR_AGENT */ con_id tenant_id, sum(total_waits) as total_waits, sum(time_waited_micro) / 1000000 as time_waited from v$system_event where v$system_event.wait_class <> 'IDLE' and (con_id > 1000 or con_id = 1) group by tenant_id + name: ob_waitevent + sqlSlowThreshold: 100ms + minObVersion: 4.0.0.0 + maxObVersion: ~ tags: - tenant_name: tenant_name - le: response_time_seconds + ob_tenant_id: tenant_id metrics: - bucket: bucket - count: count - sum: sum + wait_total: total_waits + wait_seconds_total: time_waited + enableCache: true + cacheExpire: ${monagent.second.metric.cache.update.interval} -obInputExtra: &obInputExtra - plugin: mysqlTableInput - config: - timeout: 10s - pluginConfig: - maintainCacheThreads: 4 - connection: - url: ${monagent.ob.monitor.user}:${monagent.ob.monitor.password}@tcp(127.0.0.1:${monagent.ob.sql.port})/oceanbase?interpolateParams=true - maxIdle: 2 - maxOpen: 32 - defaultConditionValues: - ob_svr_ip: ${monagent.host.ip} - ob_svr_port: ${monagent.ob.rpc.port} - ob_is_rootservice: false - ob_is_v4: false - ob_is_v3: false - collectConfig: - - sql: select ob_version() REGEXP '^4' as ob_is_v4 - name: ob_is_v4 - conditionValues: - ob_is_v4: ob_is_v4 - - sql: select ob_version() REGEXP '^3' as ob_is_v3 - name: ob_is_v3 - conditionValues: - ob_is_v3: ob_is_v3 - - name: ob_role + - sql: select /*+ MONITOR_AGENT READ_CONSISTENCY(WEAK) */ con_id tenant_id, case when event_id = 10000 then 'INTERNAL' when event_id = 13000 then 'SYNC_RPC' when event_id = 14003 then 'ROW_LOCK_WAIT' when (event_id >= 10001 and event_id <= 11006) or (event_id >= 11008 and event_id <= 11011) then 'IO' when event like 'latch:%' then 'LATCH' else 'OTHER' END event_group, sum(total_waits) as total_waits, sum(time_waited_micro / 1000000) as time_waited from v$system_event where v$system_event.wait_class <> 'IDLE' and (con_id > 1000 or con_id = 1) group by tenant_id, event_group + name: ob_system_event sqlSlowThreshold: 100ms + minObVersion: ~ + maxObVersion: 4.0.0.0 + tags: + ob_tenant_id: tenant_id + event_group: event_group metrics: - is_rootserver: with_rootserver - conditionValues: - ob_is_rootservice: with_rootserver - conditions: [ ob_is_v3 ] - params: [ ob_svr_ip, ob_svr_port ] - sql: select /*+ MONITOR_AGENT READ_CONSISTENCY(WEAK) */ with_rootserver from __all_server where svr_ip = ? and svr_port = ? - - - name: ob_role + total_waits: total_waits + time_waited: time_waited + enableCache: true + cacheExpire: ${monagent.second.metric.cache.update.interval} + - sql: select /* MONITOR_AGENT */ con_id tenant_id, case when event_id = 10000 then 'INTERNAL' when event_id = 13000 then 'SYNC_RPC' when event_id = 14003 then 'ROW_LOCK_WAIT' when (event_id >= 10001 and event_id <= 11006) or (event_id >= 11008 and event_id <= 11011) then 'IO' when event like 'latch:%' then 'LATCH' else 'OTHER' END event_group, sum(total_waits) as total_waits, sum(time_waited_micro / 1000000) as time_waited from v$system_event where v$system_event.wait_class <> 'IDLE' and (con_id > 1000 or con_id = 1) group by tenant_id, event_group + name: ob_system_event sqlSlowThreshold: 100ms + minObVersion: 4.0.0.0 + maxObVersion: ~ + tags: + ob_tenant_id: tenant_id + event_group: event_group metrics: - is_rootserver: with_rootserver - conditionValues: - ob_is_rootservice: with_rootserver - conditions: [ ob_is_v4 ] + total_waits: total_waits + time_waited: time_waited + enableCache: true + cacheExpire: ${monagent.second.metric.cache.update.interval} + + - sql: select /*+ MONITOR_AGENT READ_CONSISTENCY(WEAK) */ cpu_total, cpu_assigned, mem_total, mem_assigned,disk_total, cpu_assigned_percent, mem_assigned_percent from __all_virtual_server_stat where svr_ip = ? and svr_port = ? params: [ ob_svr_ip, ob_svr_port ] - sql: select (case when with_rootserver='YES' then 1 else 0 end) as with_rootserver from DBA_OB_SERVERS where svr_ip = ? and svr_port = ? - - sql: select /*+ MONITOR_AGENT READ_CONSISTENCY(WEAK) */ count(*) as cnt from v$unit - name: ob_unit - conditions: [ ob_is_v3 ] - metrics: - num: cnt - - sql: select /*+ MONITOR_AGENT */ count(*) as cnt from V$OB_UNITS - name: ob_unit - conditions: [ ob_is_v4 ] - metrics: - num: cnt - - sql: select /*+ MONITOR_AGENT READ_CONSISTENCY(WEAK) */ total_size, free_size from __all_virtual_disk_stat where svr_ip = ? and svr_port = ? - params: [ob_svr_ip, ob_svr_port] - name: ob_disk + sqlSlowThreshold: 100ms + name: ob_server_resource + minObVersion: ~ + maxObVersion: 2.0.0 metrics: - total_bytes: total_size - free_bytes: free_size + cpu: cpu_total + cpu_assigned: cpu_assigned + memory_bytes: mem_total + memory_assigned_bytes: mem_assigned + disk_bytes: disk_total + cpu_assigned_percent: cpu_assigned_percent + memory_assigned_percent: mem_assigned_percent enableCache: true - cacheExpire: 1h - cacheDataExpire: 2h - - sql: select /*+ MONITOR_AGENT READ_CONSISTENCY(WEAK) */ cpu_total,cpu_assigned,mem_total,mem_assigned,disk_total,disk_assigned,unit_num,migrating_unit_num,cpu_assigned_percent, mem_assigned_percent, disk_assigned_percent from __all_virtual_server_stat where svr_ip = ? and svr_port = ? - params: [ob_svr_ip, ob_svr_port] + cacheExpire: 60s + - sql: select /*+ MONITOR_AGENT READ_CONSISTENCY(WEAK) */ cpu_total,cpu_max_assigned as cpu_assigned,mem_total,mem_max_assigned as mem_assigned,disk_total, cpu_assigned_percent, mem_assigned_percent from __all_virtual_server_stat where svr_ip = ? and svr_port = ? + params: [ ob_svr_ip, ob_svr_port ] + sqlSlowThreshold: 100ms name: ob_server_resource - conditions: [ ob_is_v3 ] + minObVersion: 2.0.0 + maxObVersion: 4.0.0.0 metrics: cpu: cpu_total cpu_assigned: cpu_assigned memory_bytes: mem_total memory_assigned_bytes: mem_assigned disk_bytes: disk_total - disk_assigned_bytes: disk_assigned - unit_num: unit_num - migrating_unit_num: migrating_unit_num cpu_assigned_percent: cpu_assigned_percent memory_assigned_percent: mem_assigned_percent - disk_assigned_percent: disk_assigned_percent enableCache: true cacheExpire: 60s - cacheDataExpire: 120s - sql: select /* MONITOR_AGENT */ cpu_capacity_max as cpu_total,cpu_assigned_max as cpu_assigned,mem_capacity as mem_total,mem_assigned as mem_assigned,data_disk_capacity as disk_total, (cpu_assigned_max / cpu_capacity_max) as cpu_assigned_percent, (mem_assigned / mem_capacity) as mem_assigned_percent from GV$OB_SERVERS where svr_ip = ? and svr_port = ? - params: [ob_svr_ip, ob_svr_port] + params: [ ob_svr_ip, ob_svr_port ] + sqlSlowThreshold: 100ms name: ob_server_resource - conditions: [ ob_is_v4 ] + minObVersion: 4.0.0.0 + maxObVersion: ~ metrics: cpu: cpu_total cpu_assigned: cpu_assigned @@ -250,73 +319,277 @@ obInputExtra: &obInputExtra memory_assigned_percent: mem_assigned_percent enableCache: true cacheExpire: 60s - cacheDataExpire: 120s - - sql: select /*+ MONITOR_AGENT READ_CONSISTENCY(WEAK) */ count(1) as cnt from __all_virtual_trans_stat where part_trans_action > 2 and ctx_create_time < date_sub(now(), interval 600 second) and svr_ip = ? and svr_port = ? - params: [ob_svr_ip, ob_svr_port] - name: ob_trans - conditions: [ ob_is_rootservice ] + + - sql: SELECT /*+read_consistency(weak) */ COALESCE(tenant_id, -1) as tenant_id, tenant_name, SUM(max_cpu) AS max_cpu, SUM(min_cpu) AS min_cpu, SUM(max_memory) AS max_memory, SUM(min_memory) AS min_memory FROM v$unit GROUP BY tenant_id + sqlSlowThreshold: 100ms + name: ob_tenant_resource + minObVersion: ~ + maxObVersion: 4.0.0.0 + tags: + tenant_name: tenant_name + ob_tenant_id: tenant_id metrics: - expire_num: cnt - - sql: select /*+ MONITOR_AGENT READ_CONSISTENCY(WEAK) QUERY_TIMEOUT(100000000) */ count(*) as cnt from gv$table where table_type in (5) and index_status in (5, 6) + max_cpu: max_cpu + min_cpu: min_cpu + max_memory: max_memory + min_memory: min_memory + enableCache: true + cacheExpire: 60s + - sql: select coalesce(t1.tenant_id, -1) as tenant_id, tenant_name, sum(max_cpu) as max_cpu, sum(min_cpu) as min_cpu, sum(max_memory) as max_memory, sum(min_memory) as min_memory from (select t1.unit_id, t1.svr_ip, t1.svr_port, t2.tenant_id, t1.min_cpu, t1.max_cpu, t1.min_memory, t1.max_memory from (select unit_id, svr_ip, svr_port, sum(min_cpu) as min_cpu, sum(max_cpu) as max_cpu, sum(memory_size) as min_memory, sum(memory_size) as max_memory from v$ob_units group by unit_id ) t1 join dba_ob_units t2 on t1.unit_id = t2.unit_id) t1 join dba_ob_tenants t2 on t1.tenant_id = t2.tenant_id where tenant_type <>'meta' group by tenant_id + sqlSlowThreshold: 100ms + name: ob_tenant_resource + minObVersion: 4.0.0.0 + maxObVersion: ~ + tags: + tenant_name: tenant_name + ob_tenant_id: tenant_id + metrics: + max_cpu: max_cpu + min_cpu: min_cpu + max_memory: max_memory + min_memory: min_memory + enableCache: true + cacheExpire: 60s + + - sql: SELECT t2.tenant_id, t2.tenant_name, t1.cpu_total, t1.cpu_assigned, t1.mem_total, t1.mem_assigned FROM __all_virtual_server_stat t1 JOIN (SELECT tenant_id, tenant_name, svr_ip, svr_port FROM `gv$unit`) t2 ON t1.svr_ip=t2.svr_ip AND t1.svr_port=t2.svr_port WHERE t1.svr_ip = ? AND t1.svr_port = ? + params: [ ob_svr_ip, ob_svr_port ] + sqlSlowThreshold: 100ms + name: ob_tenant_assigned + minObVersion: ~ + maxObVersion: 2.0.0 + metrics: + cpu_total: cpu_total + cpu_assigned: cpu_assigned + mem_total: mem_total + mem_assigned: mem_assigned + tags: + tenant_name: tenant_name + ob_tenant_id: tenant_id + enableCache: true + cacheExpire: 60s + - sql: select /* MONITOR_AGENT */ t4.tenant_id, t4.tenant_name, cpu_capacity as cpu_total,cpu_assigned,mem_capacity as mem_total,mem_assigned as mem_assigned from GV$OB_SERVERS t1 JOIN (SELECT t2.tenant_id, t3.tenant_name, t2.svr_ip, t2.svr_port FROM gv$ob_units t2 left join DBA_OB_TENANTS t3 on t2.tenant_id=t3.tenant_id WHERE T3.tenant_type<>'META') t4 ON t1.svr_ip=t4.svr_ip AND t1.svr_port=t4.svr_port WHERE t1.svr_ip = ? AND t1.svr_port = ? + params: [ ob_svr_ip, ob_svr_port ] + sqlSlowThreshold: 100ms + name: ob_tenant_assigned + minObVersion: 4.0.0.0 + maxObVersion: ~ + metrics: + cpu_total: cpu_total + cpu_assigned: cpu_assigned + mem_total: mem_total + mem_assigned: mem_assigned + tags: + tenant_name: tenant_name + ob_tenant_id: tenant_id + enableCache: true + cacheExpire: 60s + + - sql: SELECT t2.tenant_id, t2.tenant_name, t1.total_size FROM __all_virtual_disk_stat t1 JOIN (SELECT tenant_id, tenant_name, svr_ip, svr_port FROM `gv$unit`) t2 ON t1.svr_ip=t2.svr_ip AND t1.svr_port=t2.svr_port WHERE t1.svr_ip = ? AND t1.svr_port = ? + params: [ ob_svr_ip, ob_svr_port ] + sqlSlowThreshold: 100ms + name: ob_tenant_disk + minObVersion: ~ + maxObVersion: 4.0.0.0 + tags: + tenant_name: tenant_name + ob_tenant_id: tenant_id + metrics: + total_size: total_size + enableCache: true + cacheExpire: 60s + - sql: select coalesce(t1.tenant_id, -1) as tenant_id, tenant_name, sum(data_disk_in_use) as data_disk_in_use, sum(log_disk_in_use) as log_disk_in_use from (select t1.unit_id, t1.svr_ip, t1.svr_port, t2.tenant_id, t1.data_disk_in_use, t1.log_disk_in_use from (select unit_id, svr_ip, svr_port, sum(data_disk_in_use) as data_disk_in_use, sum(log_disk_in_use) as log_disk_in_use from v$ob_units group by unit_id ) t1 join dba_ob_units t2 on t1.unit_id = t2.unit_id) t1 join dba_ob_tenants t2 on t1.tenant_id = t2.tenant_id where tenant_type <>'meta' group by tenant_id + sqlSlowThreshold: 100ms + name: ob_tenant_disk + minObVersion: 4.0.0.0 + maxObVersion: ~ + tags: + tenant_name: tenant_name + ob_tenant_id: tenant_id + metrics: + data_size: data_disk_in_use + log_size: log_disk_in_use + enableCache: true + cacheExpire: 60s + + - sql: select /*+ MONITOR_AGENT READ_CONSISTENCY(WEAK) */ total_size, free_size from __all_virtual_disk_stat where svr_ip = ? and svr_port = ? + params: [ ob_svr_ip, ob_svr_port ] + sqlSlowThreshold: 100ms + name: ob_disk + minObVersion: ~ + maxObVersion: 4.0.0.0 + metrics: + total_bytes: total_size + free_bytes: free_size + enableCache: true + cacheExpire: 60s + - sql: select /* MONITOR_AGENT */ data_disk_capacity as total_size, (data_disk_capacity - data_disk_in_use) as free_size from GV$OB_SERVERS where svr_ip = ? and svr_port = ? + params: [ ob_svr_ip, ob_svr_port ] + sqlSlowThreshold: 100ms + name: ob_disk + minObVersion: 4.0.0.0 + maxObVersion: ~ + metrics: + total_bytes: total_size + free_bytes: free_size + enableCache: true + cacheExpire: ${monagent.second.metric.cache.update.interval} + +obInputExtra: &obInputExtra + plugin: mysqlTableInput + config: + timeout: 10s + pluginConfig: + collect_interval: ${monagent.collector.ob.extra.interval} + connection: + url: ${monagent.ob.monitor.user}:${monagent.ob.monitor.password}@tcp(127.0.0.1:${monagent.ob.sql.port})/oceanbase?interpolateParams=true + maxIdle: 2 + maxOpen: 32 + defaultConditionValues: + ob_svr_ip: ${monagent.host.ip} + ob_svr_port: ${monagent.ob.rpc.port} + ob_is_rootservice: true + collectConfig: + - sql: select /*+ MONITOR_AGENT READ_CONSISTENCY(WEAK) */ with_rootserver from __all_server where svr_ip = ? and svr_port = ? + name: ob_role + sqlSlowThreshold: 100ms + params: [ ob_svr_ip, ob_svr_port ] + minObVersion: ~ + maxObVersion: 4.0.0.0 + conditionValues: + ob_is_rootservice: with_rootserver + enableCache: true + cacheExpire: 1m + - sql: select (case when with_rootserver='YES' then 1 else 0 end) as with_rootserver from DBA_OB_SERVERS where svr_ip = ? and svr_port = ? + name: ob_role + sqlSlowThreshold: 100ms + params: [ ob_svr_ip, ob_svr_port ] + minObVersion: 4.0.0.0 + maxObVersion: ~ + conditionValues: + ob_is_rootservice: with_rootserver + enableCache: true + cacheExpire: 1m + + - sql: select /*+ MONITOR_AGENT READ_CONSISTENCY(WEAK) */ count(*) as cnt from v$unit + name: ob_unit + sqlSlowThreshold: 100ms + minObVersion: ~ + maxObVersion: 4.0.0.0 + metrics: + num: cnt + enableCache: true + cacheExpire: 60s + - sql: select /* MONITOR_AGENT */ count(*) as cnt from V$OB_UNITS + name: ob_unit + sqlSlowThreshold: 100ms + minObVersion: 4.0.0.0 + maxObVersion: ~ + metrics: + num: cnt + enableCache: true + cacheExpire: 60s + + - sql: select /*+ MONITOR_AGENT READ_CONSISTENCY(WEAK) QUERY_TIMEOUT(100000000) */ count(*) as cnt from gv$table where table_type in (5) and index_status in (5, 6) name: ob_index - conditions: [ ob_is_v3, ob_is_rootservice] + sqlSlowThreshold: 100ms + condition: ob_is_rootservice + minObVersion: ~ + maxObVersion: 4.0.0.0 metrics: error_num: cnt - - sql: select /*+ MONITOR_AGENT READ_CONSISTENCY(WEAK) QUERY_TIMEOUT(100000000) */ count(*) as cnt from CDB_INDEXES where table_type in (5) and status in (5, 6) + enableCache: true + cacheExpire: 60s + - sql: select /*+ MONITOR_AGENT QUERY_TIMEOUT(100000000) */ count(*) as cnt from CDB_INDEXES where status in ('ERROR','UNUSABLE') name: ob_index - conditions: [ob_is_v4, ob_is_rootservice ] + sqlSlowThreshold: 100ms + condition: ob_is_rootservice + minObVersion: 4.0.0.0 + maxObVersion: ~ metrics: error_num: cnt - - sql: select /*+ MONITOR_AGENT READ_CONSISTENCY(WEAK) */ __all_tenant.tenant_name, active, total, freeze_trigger, freeze_cnt from gv$memstore, __all_tenant where gv$memstore.tenant_id = __all_tenant.tenant_id and ip = ? and port = ? - params: [ob_svr_ip, ob_svr_port] + enableCache: true + cacheExpire: 60s + + - sql: select /*+ MONITOR_AGENT READ_CONSISTENCY(WEAK) */ tenant_id, active, total, freeze_trigger, freeze_cnt from gv$memstore where ip = ? and port = ? + params: [ ob_svr_ip, ob_svr_port ] + sqlSlowThreshold: 100ms name: ob_memstore - conditions: [ ob_is_v3 ] + minObVersion: ~ + maxObVersion: 4.0.0.0 tags: - tenant_name: tenant_name + ob_tenant_id: tenant_id metrics: active_bytes: active total_bytes: total freeze_trigger_bytes: freeze_trigger freeze_times: freeze_cnt - - sql: select /* MONITOR_AGENT */ __all_tenant.tenant_name, active_span as active, memstore_used as total, freeze_trigger, freeze_cnt from GV$OB_MEMSTORE, __all_tenant where GV$OB_MEMSTORE.tenant_id = __all_tenant.tenant_id and svr_ip = ? and svr_port = ? - params: [ob_svr_ip, ob_svr_port] + enableCache: true + cacheExpire: 60s + - sql: select /* MONITOR_AGENT */ tenant_id, active_span as active, memstore_used as total, freeze_trigger, freeze_cnt from GV$OB_MEMSTORE where svr_ip = ? and svr_port = ? + params: [ ob_svr_ip, ob_svr_port ] + sqlSlowThreshold: 100ms name: ob_memstore - conditions: [ ob_is_v4 ] + minObVersion: 4.0.0.0 + maxObVersion: ~ tags: - tenant_name: tenant_name + ob_tenant_id: tenant_id metrics: active_bytes: active total_bytes: total freeze_trigger_bytes: freeze_trigger freeze_times: freeze_cnt - - sql: select /*+ MONITOR_AGENT READ_CONSISTENCY(WEAK) */ sum(hold) as hold, sum(used) as used from __all_virtual_memory_info where tenant_id = 500 and svr_ip = ? and svr_port = ? - params: [ob_svr_ip, ob_svr_port] + enableCache: true + cacheExpire: 60s + + - sql: select /*+ MONITOR_AGENT READ_CONSISTENCY(WEAK) */ sum(hold) as hold, sum(used) as used from __all_virtual_memory_info where tenant_id = 500 and svr_ip = ? and svr_port = ? and mod_name <> 'OB_KVSTORE_CACHE_MB' + params: [ ob_svr_ip, ob_svr_port ] + sqlSlowThreshold: 100ms name: ob_tenant500_memory + minObVersion: ~ + maxObVersion: 4.0.0.0 metrics: hold_bytes: hold used_bytes: used - - sql: select /*+ MONITOR_AGENT READ_CONSISTENCY(WEAK) */ tenant_name, __all_unit_config.name, max_cpu, min_cpu, max_memory, min_memory, max_iops, min_iops, max_disk_size, max_session_num from __all_tenant, __all_resource_pool, __all_unit_config, __all_unit where __all_tenant.tenant_id = __all_resource_pool.tenant_id and __all_resource_pool.unit_config_id = __all_unit_config.unit_config_id and __all_unit.resource_pool_id = __all_resource_pool.resource_pool_id and __all_unit.svr_ip = ? and __all_unit.svr_port = ? - params: [ob_svr_ip, ob_svr_port] + enableCache: true + cacheExpire: 60s + - sql: select /* MONITOR_AGENT */ sum(hold) as hold, sum(used) as used from GV$OB_MEMORY where tenant_id = 500 and svr_ip = ? and svr_port = ? and MOD_NAME <> 'KvstorCacheMb' + params: [ ob_svr_ip, ob_svr_port ] + sqlSlowThreshold: 100ms + name: ob_tenant500_memory + minObVersion: 4.0.0.0 + maxObVersion: ~ + metrics: + hold_bytes: hold + used_bytes: used + enableCache: true + cacheExpire: 60s + + - sql: select /*+ MONITOR_AGENT READ_CONSISTENCY(WEAK) */ tenant_id, __all_unit_config.name, max_cpu, min_cpu, max_memory, min_memory, max_iops, min_iops from __all_resource_pool, __all_unit_config, __all_unit where __all_resource_pool.unit_config_id = __all_unit_config.unit_config_id and __all_unit.resource_pool_id = __all_resource_pool.resource_pool_id and __all_unit.svr_ip = ? + params: [ ob_svr_ip ] + sqlSlowThreshold: 100ms name: ob_unit_config - conditions: [ ob_is_v3 ] + minObVersion: ~ + maxObVersion: 4.0.0.0 tags: - tenant_name: tenant_name + ob_tenant_id: tenant_id unit_config_name: name metrics: max_cpu: max_cpu min_cpu: min_cpu max_memory_bytes: max_memory - min_memory_bytes: min_memory max_iops: max_iops min_iops: min_iops - max_disk_size_bytes: max_disk_size max_session_num: max_session_num - - sql: select /* MONITOR_AGENT */ __all_tenant.tenant_name, DBA_OB_UNIT_CONFIGS.name, DBA_OB_UNIT_CONFIGS.max_cpu, DBA_OB_UNIT_CONFIGS.min_cpu, DBA_OB_UNIT_CONFIGS.memory_size as max_memory, DBA_OB_UNIT_CONFIGS.max_iops, DBA_OB_UNIT_CONFIGS.min_iops from __all_tenant, DBA_OB_RESOURCE_POOLS, DBA_OB_UNIT_CONFIGS, DBA_OB_UNITS where DBA_OB_RESOURCE_POOLS.tenant_id = __all_tenant.tenant_id and DBA_OB_RESOURCE_POOLS.unit_config_id = DBA_OB_UNIT_CONFIGS.unit_config_id and DBA_OB_UNITS.resource_pool_id = DBA_OB_RESOURCE_POOLS.resource_pool_id and DBA_OB_UNITS.svr_ip = ? and DBA_OB_UNITS.svr_port = ? - params: [ob_svr_ip, ob_svr_port] + enableCache: true + cacheExpire: 60s + - sql: select /* MONITOR_AGENT */ DBA_OB_RESOURCE_POOLS.tenant_id, DBA_OB_UNIT_CONFIGS.name, DBA_OB_UNIT_CONFIGS.max_cpu, DBA_OB_UNIT_CONFIGS.min_cpu, DBA_OB_UNIT_CONFIGS.memory_size as max_memory, DBA_OB_UNIT_CONFIGS.max_iops, DBA_OB_UNIT_CONFIGS.min_iops from DBA_OB_RESOURCE_POOLS, DBA_OB_UNIT_CONFIGS, DBA_OB_UNITS where DBA_OB_RESOURCE_POOLS.unit_config_id = DBA_OB_UNIT_CONFIGS.unit_config_id and DBA_OB_UNITS.resource_pool_id = DBA_OB_RESOURCE_POOLS.resource_pool_id and DBA_OB_UNITS.svr_ip = ? + params: [ob_svr_ip] + sqlSlowThreshold: 100ms name: ob_unit_config - conditions: [ ob_is_v4 ] + minObVersion: 4.0.0.0 + maxObVersion: ~ tags: - tenant_name: tenant_name + ob_tenant_id: tenant_id unit_config_name: name metrics: max_cpu: max_cpu @@ -327,68 +600,171 @@ obInputExtra: &obInputExtra min_iops: min_iops enableCache: true cacheExpire: 60s - - sql: select /*+ MONITOR_AGENT READ_CONSISTENCY(WEAK) */ tenant_name, tenant_id, `latch#` as latch_no, name, gets, misses, sleeps, immediate_gets, immediate_misses, spin_gets, wait_time / 1000000 as wait_time from v$latch, __all_tenant where __all_tenant.tenant_id = v$latch.con_id - name: ob_latch + + - sql: select /*+ MONITOR_AGENT READ_CONSISTENCY(WEAK) QUERY_TIMEOUT(100000000) */ tenant_id, 1 as role, case when cnt is null then 0 else cnt end as cnt from (select tenant_id, count(*) as cnt from __all_virtual_partition_info where svr_ip = ? and svr_port = ? group by tenant_id) + params: [ ob_svr_ip, ob_svr_port ] + sqlSlowThreshold: 100ms + name: ob_partition + minObVersion: ~ + maxObVersion: 4.0.0.0 tags: - tenant_name: tenant_name - tenant_id: tenant_id + ob_tenant_id: tenant_id + role: role + metrics: + num: cnt + enableCache: true + cacheExpire: 1h + + - sql: select /*+ MONITOR_AGENT READ_CONSISTENCY(WEAK) */ zone, name, value, time_to_usec(now()) as current from __all_zone + name: ob_zone + sqlSlowThreshold: 100ms + minObVersion: ~ + maxObVersion: 4.0.0.0 + tags: + zone: zone name: name - latch_no: latch_no - metrics: - get_total: gets - miss_total: misses - sleep_total: sleeps - immediate_get_total: immediate_gets - immediate_miss_total: immediate_misses - spin_get_total: spin_gets - wait_seconds_total: wait_time + condition: ob_is_rootservice + metrics: + stat: value + current_timestamp: current + enableCache: true + cacheExpire: 60s + + - sql: select /*+ MONITOR_AGENT READ_CONSISTENCY(WEAK) */ __all_tenant.tenant_name as tenant_name, cast(v_acc_response_time.response_time / 1000000 as float) as response_time_seconds, v_acc_response_time.count as bucket, case when v_acc_response_time.response_time = (select max(response_time) from __all_virtual_query_response_time) then v_acc_response_time.count else null end as count, case when v_acc_response_time.response_time = (select max(response_time) from __all_virtual_query_response_time) then cast(v_acc_response_time.sum / 1000000 as float) else null end as sum from (select b.tenant_id, b.response_time as response_time, sum(a.count) as count, sum(a.total) as sum from __all_virtual_query_response_time a, __all_virtual_query_response_time b where a.response_time <= b.response_time and a.svr_ip = b.svr_ip and a.svr_port = b.svr_port and b.svr_ip = ? and b.svr_port = ? group by b.tenant_id, b.response_time) v_acc_response_time, __all_tenant where v_acc_response_time.tenant_id = __all_tenant.tenant_id; + params: [ob_svr_ip, ob_svr_port] + name: ob_query_response_time_seconds + sqlSlowThreshold: 100ms + minObVersion: ~ + maxObVersion: 4.0.0.0 + tags: + tenant_name: tenant_name + le: response_time_seconds + metrics: + bucket: bucket + count: count + sum: sum + enableCache: true + cacheExpire: 60s -retagProcessor: &retagProcessor + - sql: select /*+ MONITOR_AGENT READ_CONSISTENCY(WEAK) */ count(1) as cnt from __all_virtual_trans_stat where part_trans_action > 2 and ctx_create_time < date_sub(now(), interval 600 second) and svr_ip = ? and svr_port = ? + params: [ob_svr_ip, ob_svr_port] + name: ob_trans + sqlSlowThreshold: 100ms + condition: ob_is_rootservice + minObVersion: ~ + maxObVersion: 4.0.0.0 + metrics: + expire_num: cnt + enableCache: true + cacheExpire: 60s + +jointableProcessor: &jointableProcessor + plugin: jointable + config: + timeout: 10s + pluginConfig: + connection: + url: ${monagent.ob.monitor.user}:${monagent.ob.monitor.password}@tcp(127.0.0.1:${monagent.ob.sql.port})/oceanbase?interpolateParams=true + maxIdle: 2 + maxOpen: 32 + joinTableConfigs: + - queryDataConfigs: + - querySql: SELECT tenant_id ob_tenant_id, tenant_name FROM __all_tenant + queryArgs: [ ] + minOBVersion: ~ + maxOBVersion: 4.0.0.0 + cacheExpire: 3m + conditions: + - metrics: [ "ob_cache", "ob_partition", "ob_table", "ob_plan_cache", "ob_waitevent", "ob_system_event", "ob_tenant_task", "ob_clog", "ob_memstore", "ob_compaction", "ob_unit_config", "ob_tenant", "ob_tenant_server", "ob_tenant_server_available" ] + tagNames: [ "ob_tenant_id" ] + removeNotMatchedTagValueMessage: true + - queryDataConfigs: + - querySql: SELECT tenant_id ob_tenant_id, tenant_name FROM DBA_OB_TENANTS WHERE tenant_type<>'META' + queryArgs: [ ] + minOBVersion: 4.0.0.0 + maxOBVersion: ~ + cacheExpire: 3m + conditions: + - metrics: [ "ob_cache", "ob_partition", "ob_table", "ob_plan_cache", "ob_waitevent", "ob_system_event", "ob_tenant_task", "ob_clog", "ob_memstore", "ob_compaction", "ob_unit_config" ] + tagNames: [ "ob_tenant_id" ] + removeNotMatchedTagValueMessage: true + - queryDataConfigs: + - querySql: SELECT tenant_id ob_tenant_id, tenant_name FROM __all_tenant + queryArgs: [ ] + minOBVersion: ~ + maxOBVersion: 4.0.0.0 + cacheExpire: 3m + conditions: + - metrics: [ "ob_sysstat" ] + tagNames: [ "ob_tenant_id" ] + removeNotMatchedTagValueMessage: false # contain internal tenant, do not remove + - queryDataConfigs: + - querySql: SELECT tenant_id ob_tenant_id, tenant_name FROM DBA_OB_TENANTS WHERE tenant_type<>'META' + queryArgs: [ ] + minOBVersion: 4.0.0.0 + maxOBVersion: ~ + cacheExpire: 3m + conditions: + - metrics: [ "ob_sysstat" ] + tagNames: [ "ob_tenant_id" ] + removeNotMatchedTagValueMessage: false # contain internal tenant, do not remove + +obRetagProcessor: &obRetagProcessor plugin: retagProcessor config: - timout: 10s + timeout: 10s pluginConfig: newTags: app: OB obzone: ${monagent.ob.zone.name} svr_ip: ${monagent.host.ip} - ob_cluster_id: ${monagent.ob.cluster.id} + svr_port: ${monagent.ob.rpc.port} ob_cluster_name: ${monagent.ob.cluster.name} + ob_cluster_id: ${monagent.ob.cluster.id} -prometheusExporter: &prometheusExporter +basicExporter: &basicExporter plugin: prometheusExporter config: - timout: 10s + timeout: 10s + pluginConfig: + formatType: fmtText + exposeUrl: /metrics/ob/basic + +extraExporter: &extraExporter + plugin: prometheusExporter + config: + timeout: 10s pluginConfig: formatType: fmtText + exposeUrl: /metrics/ob/extra modules: - module: monitor.ob moduleType: monagent.pipeline - process: monagent + process: ob_monagent config: name: monitor.ob status: ${monagent.pipeline.ob.status} pipelines: - name: ob_basic config: - scheduleStrategy: trigger - exposeUrl: /metrics/ob/basic + scheduleStrategy: bySource structure: inputs: - <<: *obInputBasic processors: - - <<: *retagProcessor + - <<: *obRetagProcessor + - <<: *jointableProcessor exporter: - <<: *prometheusExporter + <<: *basicExporter - name: ob_extra config: - scheduleStrategy: trigger - exposeUrl: /metrics/ob/extra + scheduleStrategy: bySource structure: inputs: - <<: *obInputExtra processors: - - <<: *retagProcessor + - <<: *obRetagProcessor + - <<: *jointableProcessor exporter: - <<: *prometheusExporter + <<: *extraExporter diff --git a/etc/module_config/monitor_ob_custom.yaml b/etc/module_config/monitor_ob_custom.yaml new file mode 100755 index 0000000000000000000000000000000000000000..0ea5d364d6bbbf6d0a92c99154e9ac1ea526b1a8 --- /dev/null +++ b/etc/module_config/monitor_ob_custom.yaml @@ -0,0 +1,44 @@ +dbConnectivityInput: &dbConnectivityInput + plugin: dbConnectivityInput + config: + timeout: 20s + pluginConfig: + timeout: 10s + targets: + oceanbase: ${monagent.ob.monitor.user}:${monagent.ob.monitor.password}@tcp(127.0.0.1:${monagent.ob.sql.port})/oceanbase?interpolateParams=true + collect_interval: ${monagent.second.metric.cache.update.interval} + +processInput: &processInput + plugin: processInput + config: + timeout: 20s + pluginConfig: + processNames: [observer] + collect_interval: ${monagent.second.metric.cache.update.interval} + +prometheusExporter: &prometheusExporter + plugin: prometheusExporter + config: + timeout: 10s + pluginConfig: + formatType: fmtText + exposeUrl: /metrics/node/ob + +modules: + - module: monitor.ob.custom + moduleType: monagent.pipeline + process: ob_monagent + config: + name: ob.custom + status: ${monagent.pipeline.ob.status} + pipelines: + - name: custom_info + config: + scheduleStrategy: bySource + structure: + inputs: + - <<: *dbConnectivityInput + - <<: *processInput + processors: + exporter: + <<: *prometheusExporter diff --git a/etc/module_config/monitor_ob_log.yaml b/etc/module_config/monitor_ob_log.yaml index 06aeafccf7e342efeec1927286e6d99103335beb..1fedb8c6768fbf0d879a67fff1371d1b24f5ccc3 100644 --- a/etc/module_config/monitor_ob_log.yaml +++ b/etc/module_config/monitor_ob_log.yaml @@ -8,15 +8,15 @@ errorLogInput: &errorLogInput logServiceConfig: rootservice: logConfig: - logDir: ${monagent.ob.install.path}/log + logDir: ${ob.install.path}/log logFileName: rootservice.log.wf election: logConfig: - logDir: ${monagent.ob.install.path}/log + logDir: ${ob.install.path}/log logFileName: election.log.wf observer: logConfig: - logDir: ${monagent.ob.install.path}/log + logDir: ${ob.install.path}/log logFileName: observer.log.wf @@ -43,12 +43,12 @@ alertmanagerOutput: &alertmanagerOutput retryTimes: 3 modules: - - module: monitor.ob.log + - module: monitor.ob.err.log moduleType: monagent.pipeline - process: monagent + process: ob_monagent config: - name: monitor.ob.log - status: ${monagent.pipeline.ob.log.status} + name: monitor.ob.err.log + status: ${monagent.pipeline.ob.alertmanager.status} pipelines: - name: node_info config: diff --git a/etc/module_config/monitor_observer_log.yaml b/etc/module_config/monitor_observer_log.yaml new file mode 100755 index 0000000000000000000000000000000000000000..1c506fb53b7946effeb80eda0e02dfdfa81b94a4 --- /dev/null +++ b/etc/module_config/monitor_observer_log.yaml @@ -0,0 +1,83 @@ +logTailerInput: &logTailerInput + plugin: logTailerInput + config: + timeout: 60s + pluginConfig: + tailConfigs: + - logDir: ${ob.install.path}/log + logFileName: observer.log + processLogInterval: 500ms + logSourceType: observer + logAnalyzerType: ob + - logDir: ${ob.install.path}/log + logFileName: election.log + processLogInterval: 500ms + logSourceType: election + logAnalyzerType: ob + - logDir: ${ob.install.path}/log + logFileName: rootservice.log + processLogInterval: 500ms + logSourceType: rootservice + logAnalyzerType: ob + recoveryConfig: + enabled: true + triggerThreshold: 10000 + +logTransformer: &logTransformer + plugin: logTransformer + config: + timeout: 10s + pluginConfig: + +retagProcessor: &retagProcessor + plugin: retagProcessor + config: + timeout: 10s + pluginConfig: + newTags: + ip: ${monagent.host.ip} + obClusterId: ${monagent.ob.cluster.id} + obClusterName: ${monagent.ob.cluster.name} + +esOutput: &esOutput + plugin: esOutput + config: + timeout: 10s + pluginConfig: + clientAddresses: ${es.client.addresses} + auth: + username: ${es.client.auth.username} + password: ${es.client.auth.password} + indexNamePattern: ocp_log_%Y%m%d + batchSizeInBytes: 1048576 + maxBatchWait: 1s + docMap: + timestamp: timestamp + timestampPrecision: 1us + name: file + tags: + level: level + app: app + fields: + content: content + tags: tags + +modules: + - module: monitor.ob.log + moduleType: monagent.pipeline + process: ob_monagent + config: + name: ob.log + status: ${monagent.pipeline.ob.log.status} + pipelines: + - name: ob_log_to_es + config: + scheduleStrategy: bySource + structure: + inputs: + - <<: *logTailerInput + processors: + - <<: *retagProcessor + - <<: *logTransformer + output: + <<: *esOutput diff --git a/etc/module_config/ob_logcleaner_module.yaml b/etc/module_config/ob_logcleaner_module.yaml new file mode 100644 index 0000000000000000000000000000000000000000..275b7be6ab18cabe9ad4b8db5abd02d871d6e234 --- /dev/null +++ b/etc/module_config/ob_logcleaner_module.yaml @@ -0,0 +1,28 @@ +modules: + - + module: ob.logcleaner + moduleType: ob.logcleaner + process: ob_mgragent + config: + runInterval: ${ob.logcleaner.run.internal} + enabled: ${ob.logcleaner.enabled} + cleanerConfig: + logCleaners: + - logName: ob_log + path: ${ob.install.path}/log + diskThreshold: ${ob.logcleaner.ob_log.disk.threshold} + rules: + - fileRegex: '([a-z]+.)?[a-z]+.log.[0-9]+' + retentionDays: ${ob.logcleaner.ob_log.rule0.retention.days} + keepPercentage: ${ob.logcleaner.ob_log.rule0.keep.percentage} + - fileRegex: '([a-z]+.)?[a-z]+.log.wf.[0-9]+' + retentionDays: ${ob.logcleaner.ob_log.rule1.retention.days} + keepPercentage: ${ob.logcleaner.ob_log.rule1.keep.percentage} + - logName: core_log + path: ${ob.install.path} + diskThreshold: ${ob.logcleaner.core_log.disk.threshold} + rules: + - fileRegex: 'core.[0-9]+' + retentionDays: ${ob.logcleaner.core_log.rule0.retention.days} + keepPercentage: ${ob.logcleaner.core_log.rule0.keep.percentage} + diff --git a/etc/monagent.yaml b/etc/monagent.yaml index 12f20b24094467cc4bdfd420a259c84763fea82a..8e3cf809a4a741c5df5956f83ecbdbdf99496d03 100644 --- a/etc/monagent.yaml +++ b/etc/monagent.yaml @@ -1,18 +1,8 @@ -log: - level: debug - filename: log/monagent.log - maxsize: 30 - maxage: 7 - maxbackups: 10 - localtime: true - compress: true - server: - address: "0.0.0.0:8088" - adminAddress: "0.0.0.0:8089" - runDir: run + address: 0.0.0.0:${ocp.agent.monitor.http.port} + runDir: ${obagent.home.path}/run -cryptoMethod: plain -cryptoPath: conf/.config_secret.key -modulePath: conf/module_config -propertiesPath: conf/config_properties +cryptoMethod: aes +cryptoPath: ${obagent.home.path}/conf/.config_secret.key +modulePath: ${obagent.home.path}/conf/module_config +propertiesPath: ${obagent.home.path}/conf/config_properties diff --git a/etc/obd_agent_mapper.yaml b/etc/obd_agent_mapper.yaml new file mode 100644 index 0000000000000000000000000000000000000000..4ad87efda85eb7c0a17890ff914884bf89d2c4a4 --- /dev/null +++ b/etc/obd_agent_mapper.yaml @@ -0,0 +1,31 @@ +config_mapper: + home_path: obagent.home.path + log_path: agent.log.path + http_basic_auth_user: agent.http.basic.auth.username + http_basic_auth_password: agent.http.basic.auth.password + mgragent_http_port: ocp.agent.manager.http.port + mgragent_log_level: mgragent.log.level + mgragent_log_max_size: mgragent.log.maxsize.mb + mgragent_log_max_days: mgragent.log.maxage.days + mgragent_log_max_backups: mgragent.log.maxbackups + mgragent_log_compress: mgragent.log.compress + monagent_http_port: ocp.agent.monitor.http.port + monagent_host_ip: monagent.host.ip + monitor_password: monagent.ob.monitor.password + sql_port: monagent.ob.sql.port + rpc_port: monagent.ob.rpc.port + cluster_name: monagent.ob.cluster.name + cluster_id: monagent.ob.cluster.id + zone_name: monagent.ob.zone.name + ob_log_path: ob.log.path + ob_data_path: ob.data.path + ob_install_path: ob.install.path + observer_log_path: observer.log.path + monagent_log_level: monagent.log.level + monagent_log_max_size: monagent.log.maxsize.mb + monagent_log_max_days: monagent.log.maxage.days + monagent_log_max_backups: monagent.log.maxbackups + monagent_log_compress: monagent.log.compress + ob_monitor_status: monagent.pipeline.ob.status + + alertmanager_address: monagent.alertmanager.address \ No newline at end of file diff --git a/etc/prometheus_config/prometheus.yaml b/etc/prometheus_config/prometheus.yaml index 7579db1412524d12cfc8491bc94f8e9ae3bebd1e..fc57d320f279e0ac4ed7b2598d4ead1038582e2c 100644 --- a/etc/prometheus_config/prometheus.yaml +++ b/etc/prometheus_config/prometheus.yaml @@ -10,8 +10,8 @@ scrape_configs: metrics_path: /metrics scheme: http static_configs: - - targets: - - 'localhost:9090' + - targets: + - 'localhost:9090' - job_name: node basic_auth: username: {http_basic_auth_user} @@ -20,7 +20,7 @@ scrape_configs: scheme: http static_configs: - targets: - - {target} + - {target} - job_name: ob_basic basic_auth: username: {http_basic_auth_user} @@ -29,7 +29,7 @@ scrape_configs: scheme: http static_configs: - targets: - - {target} + - {target} - job_name: ob_extra basic_auth: username: {http_basic_auth_user} @@ -38,7 +38,7 @@ scrape_configs: scheme: http static_configs: - targets: - - {target} + - {target} - job_name: agent basic_auth: username: {http_basic_auth_user} @@ -47,4 +47,4 @@ scrape_configs: scheme: http static_configs: - targets: - - {target} + - {target} diff --git a/etc/prometheus_config/rules/host_rules.yaml b/etc/prometheus_config/rules/host_rules.yaml index bf64ed0af3d9931e5fa14d65a075f9577de76fe1..401f5d9622549da54da3fef14a8e2104308f4c89 100644 --- a/etc/prometheus_config/rules/host_rules.yaml +++ b/etc/prometheus_config/rules/host_rules.yaml @@ -1,65 +1,65 @@ groups: - name: node-alert rules: - - alert: ob_host_disk_percent_over_threshold - expr: 100 * (1 - node_filesystem_avail_bytes / node_filesystem_size_bytes) > 97 - for: 1m - labels: - serverity: page - annotations: - summary: "{{ $labels.instance }} {{ $labels.mountpoint }} used percent over threshold " - description: "{{ $labels.instance }} {{ $labels.mountpoint }} used percent > {{ $value }}" + - alert: ob_host_disk_percent_over_threshold + expr: 100 * (1 - node_filesystem_avail_bytes / node_filesystem_size_bytes) > 97 + for: 1m + labels: + serverity: page + annotations: + summary: "{{ $labels.instance }} {{ $labels.mountpoint }} used percent over threshold " + description: "{{ $labels.instance }} {{ $labels.mountpoint }} used percent > {{ $value }}" - - alert: ob_host_cpu_percent - expr: 100 * (1 - sum(rate(node_cpu_seconds_total{mode="idle"}[60s])) by (instance) / sum(rate(node_cpu_seconds_total[60s])) by (instance)) > 100 - for: 1m - labels: - serverity: page - annotations: - summary: "{{ $labels.instance }} cpu used percent over threshold " - description: "{{ $labels.instance }} cpu_percent > {{ $value }}" + - alert: ob_host_cpu_percent + expr: 100 * (1 - sum(rate(node_cpu_seconds_total{mode="idle"}[60s])) by (instance) / sum(rate(node_cpu_seconds_total[60s])) by (instance)) > 100 + for: 1m + labels: + serverity: page + annotations: + summary: "{{ $labels.instance }} cpu used percent over threshold " + description: "{{ $labels.instance }} cpu_percent > {{ $value }}" - - alert: ob_host_net_recv_percent_over_threshold - expr: rate(node_network_receive_bytes_total[60s]) > 100000000 - for: 1m - labels: - serverity: page - annotations: - summary: "{{ $labels.instance }} net recv percent over threshold " - description: "{{ $labels.instance }} net_recv_percent > {{ $value }}" + - alert: ob_host_net_recv_percent_over_threshold + expr: rate(node_network_receive_bytes_total[60s]) > 100000000 + for: 1m + labels: + serverity: page + annotations: + summary: "{{ $labels.instance }} net recv percent over threshold " + description: "{{ $labels.instance }} net_recv_percent > {{ $value }}" - - alert: ob_host_net_send_percent_over_threshold - expr: rate(node_network_transmit_bytes_total[60s]) > 100000000 - for: 1m - labels: - serverity: page - annotations: - summary: "{{ $labels.instance }} net send percent over threshold " - description: "{{ $labels.instance }} net_send_percent > {{ $value }}" + - alert: ob_host_net_send_percent_over_threshold + expr: rate(node_network_transmit_bytes_total[60s]) > 100000000 + for: 1m + labels: + serverity: page + annotations: + summary: "{{ $labels.instance }} net send percent over threshold " + description: "{{ $labels.instance }} net_send_percent > {{ $value }}" - - alert: ob_host_load1_per_cpu_over_threshold - expr: node_load1 > 64 - for: 1m - labels: - serverity: page - annotations: - summary: "{{ $labels.instance }} load1 over threshold " - description: "{{ $labels.instance }} load1 > {{ $value }}" + - alert: ob_host_load1_per_cpu_over_threshold + expr: node_load1 > 64 + for: 1m + labels: + serverity: page + annotations: + summary: "{{ $labels.instance }} load1 over threshold " + description: "{{ $labels.instance }} load1 > {{ $value }}" - - alert: ob_host_mem_percent_over_threshold - expr: (1 - (node_memory_MemFree_bytes + node_memory_Cached_bytes + node_memory_Buffers_bytes) / node_memory_MemTotal_bytes) * 100 > 90 - for: 1m - labels: - serverity: page - annotations: - summary: "{{ $labels.instance }} memory used percent over threshold " - description: "{{ $labels.instance }} memory_used_percent > {{ $value }}" + - alert: ob_host_mem_percent_over_threshold + expr: (1 - (node_memory_MemFree_bytes + node_memory_Cached_bytes + node_memory_Buffers_bytes) / node_memory_MemTotal_bytes) * 100 > 90 + for: 1m + labels: + serverity: page + annotations: + summary: "{{ $labels.instance }} memory used percent over threshold " + description: "{{ $labels.instance }} memory_used_percent > {{ $value }}" - - alert: ob_host_disk_readonly - expr: node_filesystem_readonly > 0 - for: 1m - labels: - serverity: page - annotations: - summary: "{{ $labels.instance }} {{ $labels.mountpoint }} is readonly " - description: "{{ $labels.instance }} {{ $labels.mountpoint }} is readonly" + - alert: ob_host_disk_readonly + expr: node_filesystem_readonly > 0 + for: 1m + labels: + serverity: page + annotations: + summary: "{{ $labels.instance }} {{ $labels.mountpoint }} is readonly " + description: "{{ $labels.instance }} {{ $labels.mountpoint }} is readonly" diff --git a/etc/prometheus_config/rules/ob_rules.yaml b/etc/prometheus_config/rules/ob_rules.yaml index eb9ad3248202b16ca186f4be15c5a0ce4ef33659..ca727391780041b02d95e029e647b53bb942939c 100644 --- a/etc/prometheus_config/rules/ob_rules.yaml +++ b/etc/prometheus_config/rules/ob_rules.yaml @@ -1,101 +1,101 @@ groups: - name: ob-alert rules: - - alert: ob_host_connection_percent_over_threshold - expr: 100 * ob_active_session_num / 262144 > 80 - for: 1m - labels: - serverity: page - annotations: - summary: "{{ $labels.ob_cluster_name }} {{ $labels.obzone }} {{ $labels.svr_ip }} connection used percent over threshold " - description: "{{ $labels.ob_cluster_name }} {{ $labels.obzone }} {{ $labels.svr_ip }} connection_used_percent = {{ $value }}" + - alert: ob_host_connection_percent_over_threshold + expr: 100 * ob_active_session_num / 262144 > 80 + for: 1m + labels: + serverity: page + annotations: + summary: "{{ $labels.ob_cluster_name }} {{ $labels.obzone }} {{ $labels.svr_ip }} connection used percent over threshold " + description: "{{ $labels.ob_cluster_name }} {{ $labels.obzone }} {{ $labels.svr_ip }} connection_used_percent = {{ $value }}" - - alert: ob_cluster_frozen_version_delta_over_threshold - expr: max(ob_zone_stat{name="frozen_version"}) by (ob_cluster_name, ob_cluster_id) - min(ob_zone_stat{name="last_merged_version"}) by (ob_cluster_name, ob_cluster_id) > 1 - for: 1m - labels: - serverity: page - annotations: - summary: "{{ $labels.ob_cluster_name }} {{ $labels.ob_cluster_id }} too much frozen memstore not merged " - description: "{{ $labels.ob_cluster_name }} {{ $labels.ob_cluster_id }} no_merge_memstore_count = {{ $value }}" + - alert: ob_cluster_frozen_version_delta_over_threshold + expr: max(ob_zone_stat{name="frozen_version"}) by (ob_cluster_name, ob_cluster_id) - min(ob_zone_stat{name="last_merged_version"}) by (ob_cluster_name, ob_cluster_id) > 1 + for: 1m + labels: + serverity: page + annotations: + summary: "{{ $labels.ob_cluster_name }} {{ $labels.ob_cluster_id }} too much frozen memstore not merged " + description: "{{ $labels.ob_cluster_name }} {{ $labels.ob_cluster_id }} no_merge_memstore_count = {{ $value }}" - - alert: ob_cluster_exists_inactive_server - expr: max(ob_server_num{status="inactive"}) by (ob_cluster_name, ob_cluster_id) > 0 - for: 1m - labels: - serverity: page - annotations: - summary: "{{ $labels.ob_cluster_name }} {{ $labels.ob_cluster_id }} exists inactive observer " - description: "{{ $labels.ob_cluster_name }} {{ $labels.ob_cluster_id }} inactive observer count is {{ $value }}" + - alert: ob_cluster_exists_inactive_server + expr: max(ob_server_num{status="inactive"}) by (ob_cluster_name, ob_cluster_id) > 0 + for: 1m + labels: + serverity: page + annotations: + summary: "{{ $labels.ob_cluster_name }} {{ $labels.ob_cluster_id }} exists inactive observer " + description: "{{ $labels.ob_cluster_name }} {{ $labels.ob_cluster_id }} inactive observer count is {{ $value }}" - - alert: ob_cluster_exists_index_fail_table - expr: sum(ob_index_error_num) by (ob_cluster_name, ob_cluster_id) > 0 - for: 1m - labels: - serverity: page - annotations: - summary: "{{ $labels.ob_cluster_name }} {{ $labels.ob_cluster_id }} exists error index table" - description: "{{ $labels.ob_cluster_name }} {{ $labels.ob_cluster_id }} error index table count is {{ $value }}" + - alert: ob_cluster_exists_index_fail_table + expr: sum(ob_index_error_num) by (ob_cluster_name, ob_cluster_id) > 0 + for: 1m + labels: + serverity: page + annotations: + summary: "{{ $labels.ob_cluster_name }} {{ $labels.ob_cluster_id }} exists error index table" + description: "{{ $labels.ob_cluster_name }} {{ $labels.ob_cluster_id }} error index table count is {{ $value }}" - - alert: ob_cluster_merge_timeout - expr: max(ob_zone_stat{name="is_merge_timeout"}) by (ob_cluster_name, ob_cluster_id) == 1 - for: 1m - labels: - serverity: page - annotations: - summary: "{{ $labels.ob_cluster_name }} {{ $labels.ob_cluster_id }} merge time out" - description: "{{ $labels.ob_cluster_name }} {{ $labels.ob_cluster_id }} merge time out" + - alert: ob_cluster_merge_timeout + expr: max(ob_zone_stat{name="is_merge_timeout"}) by (ob_cluster_name, ob_cluster_id) == 1 + for: 1m + labels: + serverity: page + annotations: + summary: "{{ $labels.ob_cluster_name }} {{ $labels.ob_cluster_id }} merge time out" + description: "{{ $labels.ob_cluster_name }} {{ $labels.ob_cluster_id }} merge time out" - - alert: ob_cluster_merge_error - expr: max(ob_zone_stat{name="is_merge_error"}) by (ob_cluster_name, ob_cluster_id) == 1 - for: 1m - labels: - serverity: page - annotations: - summary: "{{ $labels.ob_cluster_name }} {{ $labels.ob_cluster_id }} merge error" - description: "{{ $labels.ob_cluster_name }} {{ $labels.ob_cluster_id }} merge error" + - alert: ob_cluster_merge_error + expr: max(ob_zone_stat{name="is_merge_error"}) by (ob_cluster_name, ob_cluster_id) == 1 + for: 1m + labels: + serverity: page + annotations: + summary: "{{ $labels.ob_cluster_name }} {{ $labels.ob_cluster_id }} merge error" + description: "{{ $labels.ob_cluster_name }} {{ $labels.ob_cluster_id }} merge error" - - alert: ob_host_partition_count_over_threshold - expr: ob_partition_num > 30000 - for: 1m - labels: - serverity: page - annotations: - summary: "{{ $labels.ob_cluster_name }} {{ $labels.obzone }} {{ $labels.svr_ip }} partition count over threshold " - description: "{{ $labels.ob_cluster_name }} {{ $labels.obzone }} {{ $labels.svr_ip }} partition_count = {{ $value }}" + - alert: ob_host_partition_count_over_threshold + expr: ob_partition_num > 30000 + for: 1m + labels: + serverity: page + annotations: + summary: "{{ $labels.ob_cluster_name }} {{ $labels.obzone }} {{ $labels.svr_ip }} partition count over threshold " + description: "{{ $labels.ob_cluster_name }} {{ $labels.obzone }} {{ $labels.svr_ip }} partition_count = {{ $value }}" - - alert: ob_server_sstable_percent_over_threshold - expr: 100 * (ob_disk_total_bytes - ob_disk_free_bytes) / ob_disk_total_bytes > 85 - for: 1m - labels: - serverity: page - annotations: - summary: "{{ $labels.ob_cluster_name }} {{ $labels.obzone }} {{ $labels.svr_ip }} sstable used percent over threshold " - description: "{{ $labels.ob_cluster_name }} {{ $labels.obzone }} {{ $labels.svr_ip }} sstable used percent = {{ $value }}" + - alert: ob_server_sstable_percent_over_threshold + expr: 100 * (ob_disk_total_bytes - ob_disk_free_bytes) / ob_disk_total_bytes > 85 + for: 1m + labels: + serverity: page + annotations: + summary: "{{ $labels.ob_cluster_name }} {{ $labels.obzone }} {{ $labels.svr_ip }} sstable used percent over threshold " + description: "{{ $labels.ob_cluster_name }} {{ $labels.obzone }} {{ $labels.svr_ip }} sstable used percent = {{ $value }}" - - alert: tenant_cpu_percent_over_threshold - expr: 100 * sum(ob_sysstat{stat_id="140006"}) by (svr_ip, tenant_name) / sum(ob_sysstat{stat_id="140005"}) by (svr_ip, tenant_name) > 90 - for: 1m - labels: - serverity: page - annotations: - summary: "{{ $labels.ob_cluster_name }} {{ $labels.obzone }} {{ $labels.svr_ip }} {{ $labels.tenant_name }} cpu used percent over threshold " - description: "{{ $labels.ob_cluster_name }} {{ $labels.obzone }} {{ $labels.svr_ip }} {{ $labels.tenant_name }} cpu used percent = {{ $value }}" + - alert: tenant_cpu_percent_over_threshold + expr: 100 * sum(ob_sysstat{stat_id="140006"}) by (svr_ip, tenant_name) / sum(ob_sysstat{stat_id="140005"}) by (svr_ip, tenant_name) > 90 + for: 1m + labels: + serverity: page + annotations: + summary: "{{ $labels.ob_cluster_name }} {{ $labels.obzone }} {{ $labels.svr_ip }} {{ $labels.tenant_name }} cpu used percent over threshold " + description: "{{ $labels.ob_cluster_name }} {{ $labels.obzone }} {{ $labels.svr_ip }} {{ $labels.tenant_name }} cpu used percent = {{ $value }}" - - alert: tenant_active_memstore_percent_over_threshold - expr: 100 * sum(ob_sysstat{stat_id="130000"}) by (svr_ip, tenant_name) / sum(ob_sysstat{stat_id="130002"}) by (svr_ip, tenant_name) > 110 - for: 1m - labels: - serverity: page - annotations: - summary: "{{ $labels.ob_cluster_name }} {{ $labels.obzone }} {{ $labels.svr_ip }} {{ $labels.tenant_name }} tenant active memstore used percent over threshold " - description: "{{ $labels.ob_cluster_name }} {{ $labels.obzone }} {{ $labels.svr_ip }} {{ $labels.tenant_name }} tenant active memstore used percent = {{ $value }}" + - alert: tenant_active_memstore_percent_over_threshold + expr: 100 * sum(ob_sysstat{stat_id="130000"}) by (svr_ip, tenant_name) / sum(ob_sysstat{stat_id="130002"}) by (svr_ip, tenant_name) > 110 + for: 1m + labels: + serverity: page + annotations: + summary: "{{ $labels.ob_cluster_name }} {{ $labels.obzone }} {{ $labels.svr_ip }} {{ $labels.tenant_name }} tenant active memstore used percent over threshold " + description: "{{ $labels.ob_cluster_name }} {{ $labels.obzone }} {{ $labels.svr_ip }} {{ $labels.tenant_name }} tenant active memstore used percent = {{ $value }}" - - alert: tenant_memstore_percent_over_threshold - expr: 100 * sum(ob_sysstat{stat_id="130001"}) by (svr_ip, tenant_name) / sum(ob_sysstat{stat_id="130004"}) by (svr_ip, tenant_name) > 85 - for: 1m - labels: - serverity: page - annotations: - summary: "{{ $labels.ob_cluster_name }} {{ $labels.obzone }} {{ $labels.svr_ip }} {{ $labels.tenant_name }} tenant memstore used percent over threshold " - description: "{{ $labels.ob_cluster_name }} {{ $labels.obzone }} {{ $labels.svr_ip }} {{ $labels.tenant_name }} tenant memstore used percent = {{ $value }}" + - alert: tenant_memstore_percent_over_threshold + expr: 100 * sum(ob_sysstat{stat_id="130001"}) by (svr_ip, tenant_name) / sum(ob_sysstat{stat_id="130004"}) by (svr_ip, tenant_name) > 85 + for: 1m + labels: + serverity: page + annotations: + summary: "{{ $labels.ob_cluster_name }} {{ $labels.obzone }} {{ $labels.svr_ip }} {{ $labels.tenant_name }} tenant memstore used percent over threshold " + description: "{{ $labels.ob_cluster_name }} {{ $labels.obzone }} {{ $labels.svr_ip }} {{ $labels.tenant_name }} tenant memstore used percent = {{ $value }}" diff --git a/etc/scripts/obagent.service b/etc/scripts/obagent.service new file mode 100644 index 0000000000000000000000000000000000000000..3a521ed16357c9846eb8792474696f625b45351c --- /dev/null +++ b/etc/scripts/obagent.service @@ -0,0 +1,10 @@ +[Unit] +Description=start obagent +[Service] +Type=forking +ExecStart=${obagent.home.path}/bin/ob_agentctl start +ExecStop=${obagent.home.path}/bin/ob_agentctl stop +ExecReload=${obagent.home.path}/bin/ob_agentctl restart +RemainAfterExit=yes +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/etc/shell_templates/shell_template.yaml b/etc/shell_templates/shell_template.yaml new file mode 100644 index 0000000000000000000000000000000000000000..09c52008d3e0ba65509e349356962cb6c7c8a861 --- /dev/null +++ b/etc/shell_templates/shell_template.yaml @@ -0,0 +1,84 @@ +commandGroups: + + - name: package.query + user: admin + timeout: 5s + commands: + - case: + os: debian + cmd: dpkg -l | awk '{print $2"-"$3"."$4}' | sed 's/amd64/x86_64/g;s/arm64/aarch64/g' | grep -e '^${PACKAGE_NAME}' + - default: + cmd: rpm -q ${PACKAGE_NAME} + params: + - name: PACKAGE_NAME + validate: PACKAGE_NAME + + - name: package.install + user: root + timeout: 10m + commands: + - case: + os: debian + arch: x86_64 + cmd: alien -k -i ${PACKAGE_FILE} + - case: + os: debian + arch: aarch64 + cmd: alien -k -i ${PACKAGE_FILE} --target=arm64 + - default: + cmd: rpm -Uvh --oldpackage ${PACKAGE_FILE} + params: + - name: PACKAGE_FILE + validate: PATH + + - name: package.install.relocate + user: root + timeout: 10m + commands: + # debian 系统暂不支持自定义安装路径,只能安装到原路径 + - case: + os: debian + arch: x86_64 + cmd: alien -k -i ${PACKAGE_FILE} + - case: + os: debian + arch: aarch64 + cmd: alien -k -i ${PACKAGE_FILE} --target=arm64 + - default: + cmd: rpm -Uvh --oldpackage --prefix=${INSTALL_PATH} ${PACKAGE_FILE} + params: + - name: INSTALL_PATH + validate: PATH + - name: PACKAGE_FILE + validate: PATH + + - name: package.install.downgrade + user: root + timeout: 10m + commands: + - case: + os: debian + arch: x86_64 + cmd: alien -k -i ${PACKAGE_FILE} + - case: + os: debian + arch: aarch64 + cmd: alien -k -i ${PACKAGE_FILE} --target=arm64 + - default: + cmd: rpm -Uvh --force ${PACKAGE_FILE} + params: + - name: PACKAGE_FILE + validate: PATH + + - name: package.uninstall + user: root + timeout: 3m + commands: + - case: + os: debian + cmd: echo ${PACKAGE_NAME} | sed -r 's/-[0-9.-]+.*//g' | xargs dpkg -P --force-depends + - default: + cmd: rpm -e --nodeps ${PACKAGE_NAME} + params: + - name: PACKAGE_NAME + validate: PACKAGE_NAME diff --git a/executor/agent/admin.go b/executor/agent/admin.go new file mode 100644 index 0000000000000000000000000000000000000000..d6610f8e3c34879fbba9f3d9974a9a645792eda3 --- /dev/null +++ b/executor/agent/admin.go @@ -0,0 +1,882 @@ +package agent + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" + "syscall" + "time" + + process2 "github.com/shirou/gopsutil/v3/process" + log "github.com/sirupsen/logrus" + + "github.com/oceanbase/obagent/agentd/api" + "github.com/oceanbase/obagent/lib/command" + "github.com/oceanbase/obagent/lib/file" + "github.com/oceanbase/obagent/lib/http" + "github.com/oceanbase/obagent/lib/path" + "github.com/oceanbase/obagent/lib/pkg" + "github.com/oceanbase/obagent/lib/process" +) + +type Admin struct { + conf AdminConf + taskStore command.StatusStore + downloadPkgFunc func(opCtx *OpCtx, param DownloadParam) error + installPkgFunc func(opCtx *OpCtx, pkgPath string) error + checkCurrentPkgFunc func(opCtx *OpCtx) error +} + +type AdminConf struct { + RunDir string + ConfDir string + LogDir string + BackupDir string + TempDir string + TaskStoreDir string + AgentPkgName string + PkgExt string + PkgStoreDir string + StartWaitSeconds int + StopWaitSeconds int + + AgentdPath string +} + +func DefaultAdminConf() AdminConf { + return AdminConf{ + ConfDir: path.ConfDir(), + RunDir: path.RunDir(), + LogDir: path.LogDir(), + BackupDir: path.BackupDir(), + TempDir: path.TempDir(), + TaskStoreDir: path.TaskStoreDir(), + AgentPkgName: filepath.Join(path.AgentDir(), "obagent"), + PkgExt: "rpm", + PkgStoreDir: path.PkgStoreDir(), + + StartWaitSeconds: 10, + StopWaitSeconds: 10, + AgentdPath: path.AgentdPath(), + } +} + +type OpCtx struct { + agentdPid int + mgrAgentPid int + monAgentPid int + + curPkgPath string + newPkgPath string + tmpPkgPath string + //tmpPkgInfo *pkg.PackageInfo + confBackupDir string + + rollbackErr error + + taskToken command.ExecutionToken + storedStatus *command.StoredStatus +} + +//todo: update result and progress with async task id + +type AdminLock struct { + Pid int `json:"pid"` + Exe string `json:"exe"` + //TaskToken string `json:"task_token"` + //Operation string `json:"operation"` +} + +func NewAdmin(conf AdminConf) *Admin { + log.Infof("NewAdmin with config %+v", conf) + ret := &Admin{ + conf: conf, + taskStore: command.NewFileTaskStore(conf.TaskStoreDir), + } + ret.checkCurrentPkgFunc = ret.checkCurrentPkg + ret.downloadPkgFunc = ret.downloadPkg + ret.installPkgFunc = ret.installPkg + return ret +} + +func (a *Admin) cleanDanglingLock() error { + log.Infof("process %d try cleaning dangling admin lock", os.Getpid()) + lockPath := filepath.Join(a.conf.RunDir, "admin.lock") + bytes, err := ioutil.ReadFile(lockPath) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return CleanDanglingAdminLockFailedErr.NewError(lockPath).WithCause(err) + } + lock := AdminLock{} + err = json.Unmarshal(bytes, &lock) + if err != nil { + log.WithError(err).Warnf("invalid lock file %s, will be removed", lockPath) + err = a.Unlock() + if err != nil { + return CleanDanglingAdminLockFailedErr.NewError(lockPath).WithCause(err) + } + } + + proc, err := process2.NewProcess(int32(lock.Pid)) + if err == nil { + exe, _ := proc.Exe() + if exe == lock.Exe { + log.Infof("lock file %s owner pid:%d, exe:%s still exists", lockPath, lock.Pid, lock.Exe) + return nil + } + } + err = a.Unlock() + if err != nil { + return CleanDanglingAdminLockFailedErr.NewError(lockPath).WithCause(err) + } + return nil +} + +func (a *Admin) Lock() error { + log.Infof("process %d fetching admin lock", os.Getpid()) + lockPath := filepath.Join(a.conf.RunDir, "admin.lock") + err := a.cleanDanglingLock() + if err != nil { + return err + } + f, err := os.OpenFile(lockPath, os.O_EXCL|os.O_CREATE|os.O_RDWR, 0644) + if err != nil { + log.Warnf("fetch admin lock '%s' failed: %v", lockPath, err) + return FetchAdminLockFailedErr.NewError(lockPath).WithCause(err) + } + defer f.Close() + + exe, _ := os.Executable() + content, err := json.Marshal(AdminLock{ + Pid: os.Getpid(), + Exe: exe, + }) + if err != nil { + return FetchAdminLockFailedErr.NewError(lockPath).WithCause(err) + } + _, err = f.Write(content) + if err != nil { + log.Warnf("write admin lock info failed: %v", err) + return FetchAdminLockFailedErr.NewError(lockPath).WithCause(err) + } + log.Infof("process %d got admin lock", os.Getpid()) + return nil +} + +func (a *Admin) Unlock() error { + lockPath := filepath.Join(a.conf.RunDir, "admin.lock") + err := os.Remove(lockPath) + if err == nil { + log.Infof("process %d release admin lock", os.Getpid()) + return nil + } + log.Errorf("process %d release admin lock '%s', got error: %v", os.Getpid(), lockPath, err) + return ReleaseAdminLockFailedErr.NewError(lockPath).WithCause(err) +} + +func (a *Admin) StartAgent() error { + err := a.Lock() + if err != nil { + return err + } + defer a.Unlock() + + opCtx := OpCtx{} + log.Info("starting agent") + err = a.startAgent(&opCtx) + if err == nil { + log.Info("start agent succeed") + } else { + log.Errorf("start agent failed: %v", err) + } + return err +} + +func (a *Admin) startAgent(opCtx *OpCtx) (err error) { + a.progressStart(opCtx, "startAgent") + defer a.progressEnd(opCtx, "startAgent", err) + if a.pidFileRunning(path.Agentd) { + log.Warn("agentd already running") + return nil // AgentdAlreadyRunningErr.NewError() + } + + agentdPath := a.conf.AgentdPath + confPath := filepath.Join(a.conf.ConfDir, "agentd.yaml") + agentdProc := process.NewProc(process.ProcessConfig{ + Program: agentdPath, + Args: []string{"-c", confPath}, + Stdout: filepath.Join(a.conf.LogDir, "agentd.out.log"), + Stderr: filepath.Join(a.conf.LogDir, "agentd.err.log"), + }) + log.Infof("starting agentd with config file '%s'", confPath) + err = agentdProc.Start(nil) + //agentdProc.Pid() + if err != nil { + log.Errorf("start agentd got error: %v", err) + return err + } + var status api.Status + for i := 0; i < a.conf.StartWaitSeconds; i++ { + time.Sleep(time.Second) + procState := agentdProc.State() + if !procState.Running { + log.Error("start agentd failed, agentd exited") + err = AgentdExitedQuicklyErr.NewError() + return + } + status, err = a.AgentStatus() + if err == nil { + if status.Ready { + log.Info("agentd is ready") + return nil + } + } + } + log.Error("wait for agent ready timeout") + err = WaitForReadyTimeoutErr.NewError() + return +} + +func (a *Admin) signalPid(pid int, signal syscall.Signal) error { + if pid == 0 { + return nil + } + p, err := process2.NewProcess(int32(pid)) + if err != nil { + log.Warnf("pid %d not exists", pid) + return err + } + log.Infof("sending signal %v to pid %d", signal, pid) + err = p.SendSignal(signal) + if err != nil { + log.Errorf("send signal %v to pid %d got error: %v", signal, pid, err) + } + return err +} + +func (a *Admin) pidRunning(pid int, program string) bool { + p, err := process2.NewProcess(int32(pid)) + if err != nil { + return false + } + name, err := p.Name() + if err != nil { + return false + } + if name != program { + return false + } + return true +} + +func (a *Admin) pidFileRunning(program string) bool { + pid, err := a.readPid(program) + if err != nil { + return false + } + return a.pidRunning(pid, program) +} + +func (a *Admin) StopAgent(taskToken TaskToken) (err error) { + log.Infof("StopAgent: %+v", taskToken) + err = a.Lock() + if err != nil { + return + } + + defer a.Unlock() + opCtx := a.taskOpCtx(taskToken.TaskToken) + err = a.stopAgent(&opCtx) + return +} + +func (a *Admin) stopAgent(opCtx *OpCtx) error { + pidPath := PidPath(a.conf.RunDir, path.Agentd) + if !fileExists(pidPath) { + log.Warnf("pid file '%s' not exists. agentd may not running", pidPath) + return nil + } + pid, err := ReadPid(pidPath) + if err != nil { + log.Errorf("can not find agentd via pid file '%s'", pidPath) + return err + } + return a.stopAgentPid(opCtx, pid) +} + +func (a *Admin) stopAgentPid(opCtx *OpCtx, pid int) (err error) { + a.progressStart(opCtx, "stopAgentPid", pid) + defer a.progressEnd(opCtx, "stopAgentPid", err) + + if !a.pidRunning(pid, path.Agentd) { + log.Warnf("agentd not running") + return nil + //return AgentdNotRunningErr.NewError() + } + err = a.signalPid(pid, syscall.SIGTERM) + if err != nil { + return err + } + for i := 0; i < a.conf.StopWaitSeconds; i++ { + time.Sleep(time.Second) + if !a.pidRunning(pid, path.Agentd) { + log.Infof("agentd with pid '%d' stopped", pid) + return nil + } + } + log.Errorf("wait for agentd with pid %d exit timeout", pid) + err = WaitForExitTimeoutErr.NewError() + return +} + +func (a *Admin) AgentStatus() (api.Status, error) { + log.Info("AgentStatus") + status := api.Status{} + cl, err := a.NewClient(path.Agentd) + if err != nil { + log.Errorf("failed create client of '%s': %v", path.Agentd, err) + return status, err + } + err = cl.Call("/api/v1/status", nil, &status) + if err != nil { + log.Errorf("failed to get agentd status via api: %v", err) + return status, err + } + log.Infof("check agentd status got: %+v", status) + return status, nil +} + +func (a *Admin) StartService(param StartStopServiceParam) error { + log.Infof("StartService %+v", param) + cl, err := a.NewClient(path.Agentd) + if err != nil { + log.Errorf("failed create client of '%s': %v", path.Agentd, err) + return err + } + return cl.Call("/api/v1/startService", param, nil) +} + +func (a *Admin) StopService(param StartStopServiceParam) error { + log.Infof("StopService %+v", param) + cl, err := a.NewClient(path.Agentd) + if err != nil { + log.Errorf("failed create client of '%s': %v", path.Agentd, err) + return err + } + return cl.Call("/api/v1/stopService", param, nil) +} + +func (a *Admin) ReinstallAgent(param ReinstallParam) (err error) { + err = a.Lock() + if err != nil { + return err + } + defer a.Unlock() + log.Infof("reinstall agent. param %+v", param) + opCtx := a.taskOpCtx(param.TaskToken.TaskToken) + defer func() { + a.saveResult(&opCtx, err) + }() + + err = a.checkCurrentPkgFunc(&opCtx) + if err != nil { + return err + } + if strings.HasPrefix(param.Source, "http:") || strings.HasPrefix(param.Source, "https:") { + err = a.downloadPkgFunc(&opCtx, param.DownloadParam) + if err != nil { + log.Errorf("download agent package source: '%s' failed: %v", opCtx.tmpPkgPath, err) + return err + } + defer os.Remove(opCtx.tmpPkgPath) + } else { + // todo check checksum, version + log.Infof("use local package file: '%s'", param.Source) + opCtx.tmpPkgPath = param.Source + } + err = a.backupConfig(&opCtx) + if err != nil { + return err + } + //defer func() { + // os.RemoveAll(opCtx.confBackupDir) + //}() + + err = a.installPkgFunc(&opCtx, opCtx.tmpPkgPath) + if err != nil { + log.Errorf("install agent package '%s' failed: %v", opCtx.tmpPkgPath, err) + return err + } + defer func() { + if err != nil { + err2 := a.restoreConfig(&opCtx) + if err2 != nil { + opCtx.rollbackErr = err2 + log.Errorf("restore config failed %v", err2) + } + } + }() + + defer func() { + if err != nil { + // log + err2 := a.installPkgFunc(&opCtx, opCtx.curPkgPath) + if err2 != nil { + opCtx.rollbackErr = err2 + log.Errorf("install prev package '%s' failed: %v", opCtx.curPkgPath, err2) + } + } + }() + err = a.updateConfig(&opCtx) + + if err != nil { + return err + } + + err = a.restartAgent(&opCtx) + if err != nil { + a.saveStartRollbackProgress(&opCtx) + return err + } + err = a.saveInstallPackage(&opCtx) + if err != nil { + log.Errorf("save installed temp failed, %v", err) + } + log.Infof("reinstall agent succeed") + return nil +} + +func (a *Admin) saveInstallPackage(opCtx *OpCtx) (err error) { + a.progressStart(opCtx, "saveInstallPackage") + defer a.progressEnd(opCtx, "saveInstallPackage", err) + + var info *pkg.PackageInfo + info, err = a.currentVersion(opCtx) + if err != nil { + return err + } + newPkgPath := filepath.Join(a.conf.PkgStoreDir, a.pkgFileName(info)) + err = os.Rename(opCtx.tmpPkgPath, newPkgPath) + if err != nil { + log.Errorf("move installed package from '%s' to '%s' failed, %v", opCtx.tmpPkgPath, newPkgPath, err) + return err + } + return nil +} + +func (a *Admin) backupPid(opCtx *OpCtx) (err error) { + a.progressStart(opCtx, "backupPid") + defer a.progressEnd(opCtx, "backupPid", err) + + agentdPid, err := BackupPid(a.conf.RunDir, path.Agentd) + if err != nil { + log.Errorf("backup pid for %s failed %v", path.Agentd, err) + return err + } + opCtx.agentdPid = agentdPid + + mgrAgentPid, err := BackupPid(a.conf.RunDir, path.MgrAgent) + if err != nil { + log.Errorf("backup pid for %s failed %v", path.MgrAgent, err) + return err + } + opCtx.mgrAgentPid = mgrAgentPid + + monAgentPid, err := BackupPid(a.conf.RunDir, path.MonAgent) + if err != nil { + log.Errorf("backup pid for %s failed %v", path.MonAgent, err) + return err + } + opCtx.monAgentPid = monAgentPid + return nil +} + +func (a *Admin) restorePid(opCtx *OpCtx) { + a.progressStart(opCtx, "restorePid") + defer a.progressEnd(opCtx, "restorePid", nil) + + _ = RestorePid(a.conf.RunDir, path.Agentd, opCtx.agentdPid) + _ = RestorePid(a.conf.RunDir, path.MgrAgent, opCtx.mgrAgentPid) + _ = RestorePid(a.conf.RunDir, path.MonAgent, opCtx.monAgentPid) +} + +func (a *Admin) confirmCanSignal(opCtx *OpCtx) (err error) { + a.progressStart(opCtx, "confirmCanSignal") + defer a.progressEnd(opCtx, "confirmCanSignal", err) + + err = a.signalPid(opCtx.agentdPid, 0) + if err != nil { + return err + } + err = a.signalPid(opCtx.mgrAgentPid, 0) + if err != nil { + return err + } + err = a.signalPid(opCtx.monAgentPid, 0) + if err != nil { + return err + } + return nil +} + +func (a *Admin) taskOpCtx(token string) OpCtx { + execToken := command.ExecutionTokenFromString(token) + storedStatus, err := a.taskStore.Load(execToken) + if err != nil { + log.WithField("task_token", token).WithError(err).Warn("load task status failed") + return OpCtx{} + } + return OpCtx{ + taskToken: execToken, + storedStatus: &storedStatus, + } +} + +func (a *Admin) RestartAgent(taskToken TaskToken) error { + log.Infof("RestartAgent: %+v", taskToken) + err := a.Lock() + if err != nil { + return err + } + defer a.Unlock() + opCtx := a.taskOpCtx(taskToken.TaskToken) + err = a.restartAgent(&opCtx) + a.saveResult(&opCtx, err) + return err +} + +type ProgressEntry struct { + Timestamp int64 `json:"timestamp"` + Name string `json:"name"` + Status string `json:"status"` + Message string `json:"message,omitempty"` +} + +func (a *Admin) updateProgress(opCtx *OpCtx, entry ProgressEntry) { + if opCtx.storedStatus == nil { + log.WithField("task_token", opCtx.taskToken).Warn("updateProgress: missing storedStatus") + return + } + if opCtx.storedStatus.Progress == nil { + opCtx.storedStatus.Progress = []ProgressEntry{} + } + progress, ok := opCtx.storedStatus.Progress.([]ProgressEntry) + if !ok { + log.WithField("task_token", opCtx.taskToken).Error("invalid storedStatus progress") + } + progress = append(progress, entry) + opCtx.storedStatus.Progress = progress + err := a.taskStore.StoreStatus(opCtx.taskToken, *opCtx.storedStatus) + if err != nil { + log.WithField("task_token", opCtx.taskToken).WithError(err).Error("store progress failed") + } +} + +func (a *Admin) progressStart(opCtx *OpCtx, name string, args ...interface{}) { + log.Infof("%s start: %+v", name, args) + a.updateProgress(opCtx, ProgressEntry{ + Name: name, + Timestamp: time.Now().UnixNano(), + Status: "start", + }) +} + +func (a *Admin) progressEnd(opCtx *OpCtx, name string, err error) { + entry := ProgressEntry{ + Name: name, + Timestamp: time.Now().UnixNano(), + Status: "ok", + } + if err != nil { + entry.Status = "fail" + entry.Message = err.Error() + log.WithError(err).Warnf("%s end with err", name) + } else { + log.Infof("%s end", name) + } + a.updateProgress(opCtx, entry) +} + +func (a *Admin) saveResult(opCtx *OpCtx, err error) { + if opCtx.storedStatus == nil { + log.WithField("task_token", opCtx.taskToken).Warn("saveResult: missing storedStatus") + return + } + if err != nil { + a.progressEnd(opCtx, "rollback", opCtx.rollbackErr) + } + opCtx.storedStatus.Finished = true + opCtx.storedStatus.Ok = err == nil + opCtx.storedStatus.Result = true + if err != nil { + opCtx.storedStatus.Err = err.Error() + } + err = a.taskStore.StoreStatus(opCtx.taskToken, *opCtx.storedStatus) + if err != nil { + log.WithField("task_token", opCtx.taskToken).WithError(err).Errorf("store task result failed") + } +} + +func (a *Admin) saveStartRollbackProgress(opCtx *OpCtx) { + a.progressStart(opCtx, "rollback") +} + +func (a *Admin) restartAgent(opCtx *OpCtx) (err error) { + a.progressStart(opCtx, "restartAgent") + defer a.progressEnd(opCtx, "restartAgent", err) + + err = a.backupPid(opCtx) + if err != nil { + return err + } + err = a.confirmCanSignal(opCtx) + + err = a.startAgent(opCtx) + if err != nil { + err2 := a.stopAgent(opCtx) + if err2 != nil { + log.WithError(err2).Errorf("stop failed new agentd got error") + } + a.restorePid(opCtx) + return err + } + err = a.stopAgentPid(opCtx, opCtx.agentdPid) + if err != nil { + // todo: force cleanup??? + return err + } + return nil +} + +func (a *Admin) checkCurrentPkg(opCtx *OpCtx) (err error) { + a.progressStart(opCtx, "checkCurrentPkg") + defer a.progressEnd(opCtx, "checkCurrentPkg", err) + + curVer, err := a.currentVersion(opCtx) + if err != nil { + return err + } + storePath := filepath.Join(a.conf.PkgStoreDir, a.pkgFileName(curVer)) + _, err = os.Stat(storePath) + if err != nil { + log.Errorf("current agent package file '%s' not exists", storePath) + } else { + opCtx.curPkgPath = storePath + log.Infof("found current agent package file '%s'", storePath) + } + return err +} + +func (a *Admin) pkgFileName(info *pkg.PackageInfo) string { + // obagent-3.2.0-.alios7.x86_64.rpm + return fmt.Sprintf("%s.%s", info.FullPackageName, a.conf.PkgExt) +} + +type DownloadParam struct { + Source string `json:"source"` + Checksum string `json:"checksum"` + Version string `json:"version"` +} + +type ReinstallParam struct { + TaskToken + DownloadParam +} + +type StartStopServiceParam struct { + TaskToken + api.StartStopAgentParam +} + +type TaskTokenParam interface { + SetTaskToken(token string) + GetTaskToken() string +} + +type TaskToken struct { + TaskToken string `json:"taskToken"` +} + +func (t *TaskToken) SetTaskToken(token string) { + t.TaskToken = token +} + +func (t *TaskToken) GetTaskToken() string { + return t.TaskToken +} + +func (a *Admin) downloadPkg(opCtx *OpCtx, param DownloadParam) (err error) { + a.progressStart(opCtx, "downloadPkg") + defer a.progressEnd(opCtx, "downloadPkg", err) + + pkgFileName := fmt.Sprintf("%s-%s.%s", a.conf.AgentPkgName, param.Version, a.conf.PkgExt) + tmpPath := filepath.Join(a.conf.TempDir, pkgFileName) + err = http.HttpImpl{}.DownloadFile(tmpPath, param.Source) + if err != nil { + log.Errorf("download package from source '%s', failed: %v", param.Source, err) + return + } + realChecksum, err := file.FileImpl{}.Sha1Checksum(tmpPath) + if err != nil { + log.Errorf("calculate checksum of temp package file '%s' failed: %v", tmpPath, err) + return + } + if param.Checksum != realChecksum { + log.Errorf("checksum not match. expected: '%s', real: '%s'", param.Checksum, realChecksum) + return ChecksumNotMatchErr.NewError() + } + log.Infof("package from source '%s', checksum='%s', version='%s' downloaded. tmp path: %s", param.Source, param.Checksum, param.Version, tmpPath) + //opCtx.tmpPkgInfo = info + opCtx.tmpPkgPath = tmpPath + return +} + +func (a *Admin) installPkg(opCtx *OpCtx, pkgPath string) (err error) { + a.progressStart(opCtx, "installPkg", pkgPath) + defer a.progressEnd(opCtx, "installPkg", err) + pkgImpl := pkg.PackageImpl{} + err = pkgImpl.DowngradePackage(pkgPath) // force install package + if err != nil { + log.Errorf("install package '%s' failed: %v", pkgPath, err) + return + } + return +} + +func (a *Admin) currentVersion(opCtx *OpCtx) (info *pkg.PackageInfo, err error) { + a.progressStart(opCtx, "currentVersion") + defer a.progressEnd(opCtx, "currentVersion", err) + + pkgImpl := pkg.PackageImpl{} + + info, err = pkgImpl.GetPackageInfo(a.conf.AgentPkgName) + if err != nil { + log.Warnf("query current installed agent package '%s' version failed: %v", a.conf.AgentPkgName, err) + return + } + log.Infof("current installed agent package '%s' version: %s", a.conf.AgentPkgName, info.Version) + return +} + +func (a *Admin) backupConfig(opCtx *OpCtx) (err error) { + a.progressStart(opCtx, "backupConfig") + defer a.progressEnd(opCtx, "backupConfig", err) + + confBackupDir := filepath.Join(a.conf.BackupDir, fmt.Sprintf("conf_%d", time.Now().UnixNano())) + err = copyFiles(a.conf.ConfDir, confBackupDir) + //err = os.Rename(a.conf.ConfDir, confBackupDir) + if err == nil { + opCtx.confBackupDir = confBackupDir + } + return +} + +func (a *Admin) restoreConfig(opCtx *OpCtx) (err error) { + a.progressStart(opCtx, "restoreConfig") + defer a.progressEnd(opCtx, "restoreConfig", err) + + if opCtx.confBackupDir == "" { + log.Warn("missing confBackupDir in OpCtx") + return + } + err = os.RemoveAll(a.conf.ConfDir) + if err != nil { + return + } + err = copyFiles(opCtx.confBackupDir, a.conf.ConfDir) + //err = os.Rename(opCtx.confBackupDir, a.conf.ConfDir) + return +} + +func (a *Admin) updateConfig(opCtx *OpCtx) (err error) { + log.Info("updating config") + a.progressStart(opCtx, "updateConfig") + defer a.progressEnd(opCtx, "updateConfig", err) + + propertiesDir := "config_properties" + backup := filepath.Join(opCtx.confBackupDir, propertiesDir) + target := filepath.Join(a.conf.ConfDir, propertiesDir) + err = copyFiles(backup, target) + return +} + +func (a *Admin) readPid(program string) (int, error) { + pidPath := PidPath(a.conf.RunDir, program) + return ReadPid(pidPath) +} + +func (a *Admin) NewClient(program string) (*http.ApiClient, error) { + pid, err := a.readPid(program) + if err != nil { + return nil, err + } + socketPath := SocketPath(a.conf.RunDir, program, pid) + cl := http.NewSocketApiClient(socketPath, time.Second*10) + return cl, nil +} + +func copyFiles(src, dest string) (err error) { + in, err := os.Open(src) + if err != nil { + return err + } + defer in.Close() + info, err := in.Stat() + if err != nil { + return err + } + if !info.IsDir() { + return copyFile(src, dest) + } + err = os.MkdirAll(dest, info.Mode()) + if err != nil { + return err + } + entries, err := in.Readdir(0) + if err != nil { + return err + } + for _, entry := range entries { + subSrc := filepath.Join(src, entry.Name()) + subDest := filepath.Join(dest, entry.Name()) + if entry.IsDir() { + err = copyFiles(subSrc, subDest) + if err != nil { + return err + } + } else { + err = copyFile(subSrc, subDest) + if err != nil { + return err + } + } + } + return nil +} + +func copyFile(src, dest string) (err error) { + in, err := os.Open(src) + if err != nil { + return err + } + defer in.Close() + out, err := os.OpenFile(dest, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + return err + } + defer out.Close() + if _, err = io.Copy(out, in); err != nil { + return err + } + err = out.Sync() + return +} + +func (a *Admin) Repair() { +} diff --git a/executor/agent/admin_test.go b/executor/agent/admin_test.go new file mode 100644 index 0000000000000000000000000000000000000000..5daf3e4bc1786a6fe65b89e0bd4841c7997a9f99 --- /dev/null +++ b/executor/agent/admin_test.go @@ -0,0 +1,301 @@ +package agent + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/shirou/gopsutil/v3/process" + + "github.com/oceanbase/obagent/lib/command" + "github.com/oceanbase/obagent/lib/path" + "github.com/oceanbase/obagent/tests/testutil" +) + +func writeConfig() { + agentdConfTpl := ` +runDir: %RUN_DIR% +logDir: %LOG_DIR% +services: + ob_mgragent: + program: %MOCK_AGENT% + args: [ ob_mgragent, %RUN_DIR%, 1, 1 ] + runDir: %RUN_DIR% + #killWait: 0s + finalWait: 2s + minLiveTime: 5s + quickExitLimit: 3 + stdout: %LOG_DIR%/mockmgr.output.log + stderr: %LOG_DIR%/mockmgr.error.log + + ob_monagent: + program: %MOCK_AGENT% + args: [ ob_monagent, %RUN_DIR%, 1, 1 ] + runDir: %RUN_DIR% + #killWait: 0s + finalWait: 2s + minLiveTime: 5s + quickExitLimit: 3 + stdout: %LOG_DIR%/mockmon.output.log + stderr: %LOG_DIR%/mockmon.error.log +` + r := strings.NewReplacer( + "%RUN_DIR%", testutil.RunDir, + "%LOG_DIR%", testutil.LogDir, + "%MOCK_AGENT%", testutil.MockAgentPath, + ) + agentdConfContent := r.Replace(agentdConfTpl) + fmt.Println(agentdConfContent) + _ = ioutil.WriteFile(filepath.Join(testutil.ConfDir, "agentd.yaml"), []byte(agentdConfContent), 0644) +} + +func writeFailConfig() { + agentdConfTpl := ` +runDir: %RUN_DIR% +logDir: %LOG_DIR% +services: + ob_mgragent: + program: %MOCK_AGENT% + args: [ ob_mgragent, %RUN_DIR%, -1, 1 ] + runDir: %RUN_DIR% + #killWait: 0s + finalWait: 2s + minLiveTime: 5s + quickExitLimit: 3 + stdout: %LOG_DIR%/mockmgr.output.log + stderr: %LOG_DIR%/mockmgr.error.log + + ob_monagent: + program: %MOCK_AGENT% + args: [ ob_monagent, %RUN_DIR%, 1, 1 ] + runDir: %RUN_DIR% + #killWait: 0s + finalWait: 2s + minLiveTime: 5s + quickExitLimit: 3 + stdout: %LOG_DIR%/mockmon.output.log + stderr: %LOG_DIR%/mockmon.error.log +` + r := strings.NewReplacer( + "%RUN_DIR%", testutil.RunDir, + "%LOG_DIR%", testutil.LogDir, + "%MOCK_AGENT%", testutil.MockAgentPath, + ) + agentdConfContent := r.Replace(agentdConfTpl) + fmt.Println(agentdConfContent) + _ = ioutil.WriteFile(filepath.Join(testutil.ConfDir, "agentd.yaml"), []byte(agentdConfContent), 0644) +} + +func TestMain(m *testing.M) { + err := testutil.BuildBins() + if err != nil { + os.Exit(2) + } + err = testutil.BuildMockAgent() + if err != nil { + os.Exit(2) + } + + ret := m.Run() + + testutil.KillAll() + testutil.DelTestFiles() + os.Exit(ret) +} + +var adminConf = AdminConf{ + RunDir: testutil.RunDir, + ConfDir: testutil.ConfDir, + LogDir: testutil.LogDir, + BackupDir: testutil.BackupDir, + TempDir: testutil.TempDir, + TaskStoreDir: testutil.TaskStoreDir, + AgentPkgName: "obagent", + PkgStoreDir: testutil.PkgStoreDir, + PkgExt: "rpm", + StartWaitSeconds: 3, + StopWaitSeconds: 3, + + AgentdPath: testutil.AgentdPath, +} + +func TestAdmin_StartStopAgent(t *testing.T) { + defer testutil.KillAll() + testutil.MakeDirs() + defer testutil.DelTestFiles() + writeConfig() + + admin := NewAdmin(adminConf) + err := admin.StartAgent() + if err != nil { + t.Error(err) + return + } + err = admin.StopAgent(TaskToken{TaskToken: command.GenerateTaskId()}) + if err != nil { + t.Error(err) + return + } +} + +func TestAdmin_StartAgentFail(t *testing.T) { + defer testutil.KillAll() + testutil.MakeDirs() + defer testutil.DelTestFiles() + writeFailConfig() + admin := NewAdmin(adminConf) + err := admin.StartAgent() + if err == nil { + t.Error("should timeout") + return + } + fmt.Println(err) +} + +func TestAdmin_RestartAgent(t *testing.T) { + defer testutil.KillAll() + testutil.MakeDirs() + defer testutil.DelTestFiles() + writeConfig() + + admin := NewAdmin(adminConf) + err := admin.StartAgent() + if err != nil { + t.Error(err) + return + } + + err = admin.RestartAgent(TaskToken{TaskToken: command.GenerateTaskId()}) + if err != nil { + t.Error(err) + return + } +} + +func TestAdmin_RestartAgentFail(t *testing.T) { + defer testutil.KillAll() + testutil.MakeDirs() + defer testutil.DelTestFiles() + writeConfig() + admin := NewAdmin(adminConf) + err := admin.StartAgent() + if err != nil { + t.Error(err) + return + } + writeFailConfig() + err = admin.RestartAgent(TaskToken{TaskToken: "test"}) + if err == nil { + t.Error("should timeout") + return + } + fmt.Println(err) + + ps, _ := process.Processes() + n := 0 + for _, p := range ps { + name, _ := p.Name() + exe, _ := p.Exe() + if name == path.Agentd && strings.HasPrefix(exe, testutil.ProjectRoot) { + n++ + } + } + if n != 1 { + t.Errorf("should only has one live agentd, but %d", n) + } +} + +func TestAdmin_BackupRestoreConfig(t *testing.T) { + testutil.MakeDirs() + writeConfig() + + admin := NewAdmin(adminConf) + + opCtx := OpCtx{} + + err := admin.backupConfig(&opCtx) + if err != nil { + t.Error(err) + } + if opCtx.confBackupDir == "" { + t.Error("confBackupDir empty") + } + fmt.Println(opCtx.confBackupDir) + if _, err = os.Stat(filepath.Join(opCtx.confBackupDir, "agentd.yaml")); err != nil { + t.Error("backup not copied files") + } + _ = os.MkdirAll(admin.conf.ConfDir, 0755) + err = admin.updateConfig(&opCtx) + if err != nil { + t.Error(err) + } + err = admin.restoreConfig(&opCtx) + if err != nil { + t.Error(err) + } +} + +func TestAdmin_BackupRestorePid(t *testing.T) { + defer testutil.KillAll() + testutil.MakeDirs() + defer testutil.DelTestFiles() + writeConfig() + + admin := NewAdmin(adminConf) + err := admin.StartAgent() + if err != nil { + t.Error(err) + return + } + opCtx := OpCtx{} + err = admin.backupPid(&opCtx) + if err != nil { + t.Error(err) + return + } + admin.restorePid(&opCtx) + _, err = admin.readPid(path.Agentd) + if err != nil { + t.Error(err) + return + } +} + +func TestAdmin_ReinstallAgent(t *testing.T) { + defer testutil.KillAll() + testutil.MakeDirs() + defer testutil.DelTestFiles() + writeConfig() + + admin := NewAdmin(adminConf) + admin.installPkgFunc = func(opCtx *OpCtx, pkgPath string) error { + //admin.restoreConfig(opCtx) + return nil + } + admin.checkCurrentPkgFunc = func(opCtx *OpCtx) error { + return nil + } + admin.downloadPkgFunc = func(opCtx *OpCtx, param DownloadParam) error { + return nil + } + err := admin.StartAgent() + if err != nil { + t.Error(err) + return + } + token := "test-token" + command.NewFileTaskStore(testutil.TaskStoreDir).CreateStatus(command.ExecutionTokenFromString(token), command.StoredStatus{ + ResponseType: command.TypeStructured, + }) + err = admin.ReinstallAgent(ReinstallParam{ + TaskToken: TaskToken{ + TaskToken: token, + }, + }) + if err != nil { + t.Error(err) + } +} diff --git a/executor/agent/agent.go b/executor/agent/agent.go new file mode 100644 index 0000000000000000000000000000000000000000..323500f41371ca8ab2a136b84624179d0bdf740a --- /dev/null +++ b/executor/agent/agent.go @@ -0,0 +1,75 @@ +package agent + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strconv" + "strings" +) + +func ReadPid(pidPath string) (int, error) { + content, err := ioutil.ReadFile(pidPath) + if err != nil { + return 0, ReadPidFailedErr.NewError(pidPath).WithCause(err) + } + ret, err := strconv.Atoi(strings.TrimSpace(string(content))) + if err != nil { + return 0, ReadPidFailedErr.NewError(pidPath).WithCause(err) + } + return ret, nil +} + +func BackupPid(runDir, program string) (int, error) { + pidPath := PidPath(runDir, program) + if !fileExists(pidPath) { + return 0, nil + } + + pid, err := ReadPid(pidPath) + if err != nil { + _ = os.Remove(pidPath) + return 0, nil + //return 0, BackupPidFailedErr.NewError(pidPath).WithCause(err) + } + backupPidPath := BackupPidPath(runDir, program, pid) + err = os.Rename(pidPath, backupPidPath) + if err != nil { + return 0, BackupPidFailedErr.NewError(pidPath).WithCause(err) + } + return pid, nil +} + +func RestorePid(runDir, program string, pid int) error { + if pid <= 0 { + return nil + } + backupPidPath := BackupPidPath(runDir, program, pid) + pidPath := PidPath(runDir, program) + err := os.Rename(backupPidPath, pidPath) + if err != nil { + return RestorePidFailedErr.NewError(pidPath).WithCause(err) + } + return nil +} + +func PidPath(runDir, program string) string { + return filepath.Join(runDir, program+".pid") +} + +func BackupPidPath(runDir, program string, pid int) string { + return filepath.Join(runDir, fmt.Sprintf("%s.%d.pid", program, pid)) +} + +func SocketPath(runDir, program string, pid int) string { + return filepath.Join(runDir, fmt.Sprintf("%s.%d.sock", program, pid)) +} + +func fileExists(path string) bool { + _, err := os.Stat(path) + if err == nil { + return true + } + return !os.IsNotExist(err) +} diff --git a/executor/agent/agentctl.go b/executor/agent/agentctl.go new file mode 100644 index 0000000000000000000000000000000000000000..16cd4f08fa6e86f06033fc38820fd896e12ab911 --- /dev/null +++ b/executor/agent/agentctl.go @@ -0,0 +1,73 @@ +package agent + +import ( + "os/exec" + + log "github.com/sirupsen/logrus" + + "github.com/oceanbase/obagent/lib/mask" + "github.com/oceanbase/obagent/lib/path" +) + +type AgentctlCmd struct { + agentctlPath string +} + +// AgentctlResponse the formatted content written to stdout or stderr +type AgentctlResponse struct { + Successful bool `json:"successful"` + Message interface{} `json:"message"` + Error string `json:"error"` +} + +func NewAgentctlCmd() *AgentctlCmd { + return &AgentctlCmd{ + agentctlPath: path.AgentctlPath(), + } +} + +func (c *AgentctlCmd) StopService(param StartStopServiceParam) error { + args := []string{"service", "stop", param.Service, "--task-token=" + param.TaskToken.TaskToken} + log.Infof("execute agentctl '%s' %v", c.agentctlPath, args) + result, err := exec.Command(c.agentctlPath, args...).CombinedOutput() + if err != nil { + return AgentctlStopServiceFailedErr.NewError(string(result)).WithCause(err) + } + return nil +} + +func (c *AgentctlCmd) StartService(param StartStopServiceParam) error { + args := []string{"service", "start", param.Service, "--task-token=" + param.TaskToken.TaskToken} + log.Infof("execute agentctl '%s' %v", c.agentctlPath, args) + result, err := exec.Command(c.agentctlPath, args...).CombinedOutput() + if err != nil { + return AgentctlStartServiceFailedErr.NewError(string(result)).WithCause(err) + } + return nil +} + +func (c *AgentctlCmd) Restart(token TaskToken) error { + args := []string{"restart", "--task-token=" + token.TaskToken} + log.Infof("execute agentctl '%s' %v", c.agentctlPath, args) + result, err := exec.Command(c.agentctlPath, args...).CombinedOutput() + if err != nil { + return AgentctlRestartFailedErr.NewError(string(result)).WithCause(err) + } + return nil +} + +func (c *AgentctlCmd) Reinstall(param ReinstallParam) error { + args := []string{ + "reinstall", + "--source=" + param.Source, + "--checksum=" + param.Checksum, + "--version=" + param.Version, + "--task-token=" + param.TaskToken.TaskToken, + } + log.Infof("execute agentctl '%s' %v", c.agentctlPath, mask.MaskSlice(args)) + result, err := exec.Command(c.agentctlPath, args...).CombinedOutput() + if err != nil { + return AgentctlReinstallFailedErr.NewError(string(result)).WithCause(err) + } + return nil +} diff --git a/executor/agent/agentctl_test.go b/executor/agent/agentctl_test.go new file mode 100644 index 0000000000000000000000000000000000000000..ae41310a3748cb69433c568365b2e8e75e998152 --- /dev/null +++ b/executor/agent/agentctl_test.go @@ -0,0 +1,46 @@ +package agent + +import ( + "strings" + "testing" + + "github.com/oceanbase/obagent/agentd/api" +) + +func TestAgentctlCmd(t *testing.T) { + agentctl := NewAgentctlCmd() + if !strings.HasSuffix(agentctl.agentctlPath, "/ob_agentctl") { + t.Error("bad agentctl path") + } + agentctl.agentctlPath = "echo" + err := agentctl.Restart(TaskToken{ + TaskToken: "test-token", + }) + if err != nil { + t.Error(err) + } + err = agentctl.Reinstall(ReinstallParam{}) + if err != nil { + t.Error(err) + } + + err = agentctl.StartService(StartStopServiceParam{ + TaskToken: TaskToken{"test-token"}, + StartStopAgentParam: api.StartStopAgentParam{ + Service: "ob_mgragent", + }, + }) + if err != nil { + t.Error(err) + } + + err = agentctl.StopService(StartStopServiceParam{ + TaskToken: TaskToken{"test-token"}, + StartStopAgentParam: api.StartStopAgentParam{ + Service: "ob_mgragent", + }, + }) + if err != nil { + t.Error(err) + } +} diff --git a/executor/agent/error.go b/executor/agent/error.go new file mode 100644 index 0000000000000000000000000000000000000000..1790d8d2995ce69e8b087a2ef2ab98eb929d5c6f --- /dev/null +++ b/executor/agent/error.go @@ -0,0 +1,24 @@ +package agent + +import "github.com/oceanbase/obagent/lib/errors" + +var ( + ReadPidFailedErr = errors.Internal.NewCode("agent", "read_pid_failed") + BackupPidFailedErr = errors.Internal.NewCode("agent", "backup_pid_failed") + RestorePidFailedErr = errors.Internal.NewCode("agent", "restore_pid_failed") + ExecuteAgentctlFailedErr = errors.Internal.NewCode("agent", "execute_agentctl_failed") + FetchAdminLockFailedErr = errors.Internal.NewCode("agent", "fetch_admin_lock_failed") + CleanDanglingAdminLockFailedErr = errors.Internal.NewCode("agent", "clean_dangling_admin_lock_failed") + ReleaseAdminLockFailedErr = errors.Internal.NewCode("agent", "release_admin_lock_failed") + ChecksumNotMatchErr = errors.FailedPrecondition.NewCode("agent", "checksum_not_match") + AgentdNotRunningErr = errors.FailedPrecondition.NewCode("agent", "agentd_not_running") + AgentdAlreadyRunningErr = errors.FailedPrecondition.NewCode("agent", "agentd_already_running") + WaitForReadyTimeoutErr = errors.DeadlineExceeded.NewCode("agent", "wait_for_ready_timeout") + WaitForExitTimeoutErr = errors.DeadlineExceeded.NewCode("agent", "wait_for_exit_timeout") + AgentdExitedQuicklyErr = errors.Internal.NewCode("agent", "agentd_exited_quickly") + + AgentctlStopServiceFailedErr = errors.Internal.NewCode("agentctl", "agentctl_stop_service_failed") + AgentctlStartServiceFailedErr = errors.Internal.NewCode("agentctl", "agentctl_start_service_failed") + AgentctlRestartFailedErr = errors.Internal.NewCode("agentctl", "agentctl_restart_failed") + AgentctlReinstallFailedErr = errors.Internal.NewCode("agentctl", "agentctl_reinstall_failed") +) diff --git a/executor/cleaner/cleaner.go b/executor/cleaner/cleaner.go new file mode 100644 index 0000000000000000000000000000000000000000..04d538af7cf238b568f6b07d631efef240ce3144 --- /dev/null +++ b/executor/cleaner/cleaner.go @@ -0,0 +1,255 @@ +package cleaner + +import ( + "context" + "os" + "sync/atomic" + "time" + + log "github.com/sirupsen/logrus" + + "github.com/oceanbase/obagent/config/mgragent" + "github.com/oceanbase/obagent/errors" +) + +var ( + obCleaner *ObCleaner +) + +// ObCleaner clean observer log +type ObCleaner struct { + *mgragent.ObCleanerConfig + isStop chan bool + // runCount Number of executions, there is a multiProcess race, requires atomic operations + runCount uint64 +} + +func InitOBCleanerConf(conf *mgragent.ObCleanerConfig) error { + return startOBCleaner(conf) +} + +func UpdateOBCleanerConf(conf *mgragent.ObCleanerConfig) error { + if obCleaner != nil && obCleaner.Enabled { + // stop current cleaner + obCleaner.Stop() + obCleaner = nil + } + + return startOBCleaner(conf) +} + +// start ob cleaner by conf +func startOBCleaner(conf *mgragent.ObCleanerConfig) error { + if obCleaner != nil { + return errors.Errorf("ob cleaner has already been initialized.") + } + + log.Infof("start ob cleaner") + + if !conf.Enabled { + log.Warnf("ob cleaner is disabled") + return nil + } + obCleaner = NewObCleaner(conf) + go obCleaner.Run(context.Background()) + + return nil +} + +func NewObCleaner(conf *mgragent.ObCleanerConfig) *ObCleaner { + if conf != nil && conf.RunInterval == 0 { + conf.RunInterval = 300 * time.Second + } + return &ObCleaner{ + ObCleanerConfig: conf, + isStop: make(chan bool), + runCount: 0, + } +} + +// DeleteFileByRetentionDays Delete the files whose modification date is earlier than the retention period +func (o *ObCleaner) DeleteFileByRetentionDays(ctx context.Context, dirToClean, fileRegex string, retentionDays uint64) error { + ctxLog := log.WithContext(ctx) + ctxLog.WithFields(log.Fields{ + "dirToClean": dirToClean, + "fileRegex": fileRegex, + "retentionDays": retentionDays, + }).Info("delete file by retentionDays") + + realPath, err := GetRealPath(ctx, dirToClean) + if err != nil { + ctxLog.WithError(err).Error("GetRealPath failed") + return err + } + + ctxLog.WithField("realPath", realPath).Info("get real path") + + retentionDaysDuration := time.Duration(retentionDays) * time.Hour * 24 + matchedFiles, err := FindFilesByRegexAndMTime(ctx, realPath, fileRegex, retentionDaysDuration) + if err != nil { + ctxLog.WithError(err).Error("FindFilesByRegexAndMTime failed") + return err + } + ctxLog.WithField("matchedFiles", matchedFiles).Info("get matched files") + + for _, fileInfo := range matchedFiles { + err := os.Remove(fileInfo.Path) + if err != nil { + ctxLog.WithField("filePath", fileInfo.Path).WithError(err).Error("remove file failed") + return err + } + ctxLog.WithField("filePath", fileInfo.Path).Info("delete file or dir") + } + + return nil +} + +// DeleteFileByKeepPercentage Delete files based on retention +func (o *ObCleaner) DeleteFileByKeepPercentage(ctx context.Context, dirToClean, fileRegex string, keepPercentage uint64) error { + ctxLog := log.WithContext(ctx) + ctxLog.WithFields(log.Fields{ + "dirToClean": dirToClean, + "fileRegex": fileRegex, + "keepPercentage": keepPercentage, + }).Info("delete by keepPercentage") + + matchedFiles, err := FindFilesAndSortByMTime(ctx, dirToClean, fileRegex) + if err != nil { + ctxLog.WithError(err).Error("FindFilesAndSortByMTime failed") + return err + } + ctxLog.WithField("matchedFiles", matchedFiles).Info("get matched(sorted) files") + if len(matchedFiles) == 0 { + return nil + } + var ( + totalSize int64 + deletedSize int64 + toBeDeletedSize int64 + ) + for _, file := range matchedFiles { + totalSize += file.Info.Size() + } + toBeDeletedSize = int64(float64(totalSize) * ((100.0 - float64(keepPercentage)) / 100.0)) + for _, file := range matchedFiles { + if deletedSize >= toBeDeletedSize { + ctxLog.WithFields(log.Fields{ + "toBeDeletedSize": toBeDeletedSize, + "deletedSize": deletedSize, + "totalSize": totalSize, + }).Info("delete files finished") + break + } + err := os.Remove(file.Path) + if err != nil { + ctxLog.WithField("filePath", file.Path).WithError(err).Error("remove file failed") + return err + } + + ctxLog.WithFields(log.Fields{ + "filePath": file.Path, + "fileSize": file.Info.Size(), + }).Info("delete file") + + deletedSize += file.Info.Size() + } + + return nil +} + +func (o *ObCleaner) CleanFilesByRules(ctx context.Context, lcr *mgragent.LogCleanerRules) error { + ctxLog := log.WithContext(ctx) + ctxLog.WithField("logCleanerRules", lcr).Infof("run CleanFilesByRules task") + + usage, err := GetDiskUsage(ctx, lcr.Path) + if err != nil { + ctxLog.WithError(err).Error("GetDiskUsage failed") + return err + } + ctxLog.Debugf("path %s usage %.2f before delete file by retention days", lcr.Path, usage) + + if usage <= float64(lcr.DiskThreshold) { + ctxLog.Debugf("path %s usage %.2f %% is less than threshold %.2f %% ", + lcr.Path, usage, float64(lcr.DiskThreshold)) + return nil + } + + for _, rule := range lcr.Rules { + err := o.DeleteFileByRetentionDays(ctx, lcr.Path, rule.FileRegex, rule.RetentionDays) + if err != nil { + ctxLog.WithFields(log.Fields{ + "path": lcr.Path, + "rule": rule, + }).WithError(err).Error("DeleteFileByRetentionDays failed") + return err + } + } + + usage, err = GetDiskUsage(ctx, lcr.Path) + if err != nil { + ctxLog.WithError(err).Error("GetDiskUsage failed") + return err + } + + if usage <= float64(lcr.DiskThreshold) { + ctxLog.Debugf("path %s usage %.2f %% is less than threshold %.2f %% after delete file by retention days", + lcr.Path, usage, float64(lcr.DiskThreshold)) + return nil + } + + ctxLog.Infof("path %s usage %.2f before delete file by keepPercentage", lcr.Path, usage) + for _, rule := range lcr.Rules { + err := o.DeleteFileByKeepPercentage(ctx, lcr.Path, rule.FileRegex, rule.KeepPercentage) + if err != nil { + ctxLog.WithFields(log.Fields{ + "path": lcr.Path, + "rule": rule, + }).WithError(err).Error("DeleteFileByKeepPercentage failed") + return err + } + } + return nil +} + +func (o *ObCleaner) Clean(ctx context.Context) error { + ctxLog := log.WithContext(ctx) + if o.CleanerConf == nil { + return nil + } + for _, logCleaner := range o.CleanerConf.LogCleaners { + err := o.CleanFilesByRules(ctx, logCleaner) + if err != nil { + ctxLog.WithError(err).Errorf("clean %s failed", logCleaner.LogName) + return err + } + } + + return nil +} + +func (o *ObCleaner) Run(ctx context.Context) { + ctxLog := log.WithContext(ctx) + ticker := time.NewTicker(o.RunInterval) + for { + atomic.AddUint64(&o.runCount, 1) + ctxLog.Infof("obCleaner run %d times", atomic.LoadUint64(&o.runCount)) + err := o.Clean(ctx) + if err != nil { + ctxLog.Error("run cleaning failed", err) + } + select { + case <-ticker.C: + case isStop := <-o.isStop: + if isStop { + ctxLog.Info("obCleaner finished") + return + } + } + } +} + +func (o *ObCleaner) Stop() { + log.Infof("stop ob cleaner") + atomic.StoreUint64(&o.runCount, 0) + o.isStop <- true +} diff --git a/executor/cleaner/cleaner_test.go b/executor/cleaner/cleaner_test.go new file mode 100644 index 0000000000000000000000000000000000000000..a52aad395a03cb2dfef3e7a7bc91f4961b369ea8 --- /dev/null +++ b/executor/cleaner/cleaner_test.go @@ -0,0 +1,384 @@ +package cleaner + +import ( + "context" + "os" + "path/filepath" + "testing" + "time" + + . "github.com/smartystreets/goconvey/convey" + + "github.com/oceanbase/obagent/config/mgragent" +) + +func TestObCleaner_DeleteFileByRetentionDays(t *testing.T) { + tmpDir, err := prepareTestDirTree("tmp1") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + err = os.Chdir(tmpDir) + if err != nil { + t.Fatal(err) + } + filesToBeCreated := []string{"a.b.log", "a.b.log.1", "b.log.2", "c.d.log.wf.1", "log.wf.1", "f.log.wf.2"} + + for _, filesToBeCreated := range filesToBeCreated { + _, err := os.OpenFile(filesToBeCreated, os.O_RDWR|os.O_CREATE, 0755) + if err != nil { + t.Fatal(err) + } + } + + now := time.Now() + err = os.Chtimes("b.log.2", now, now.Add(-12*time.Hour)) + if err != nil { + t.Fatal() + } + err = os.Chtimes("f.log.wf.2", now, now.AddDate(0, 0, -3)) + if err != nil { + t.Fatal() + } + err = os.Chtimes("a.b.log.1", now, now.AddDate(0, 0, -4)) + if err != nil { + t.Fatal() + } + + type args struct { + dirToClean string + fileReg string + retentionDays uint64 + } + tests := []struct { + description string + args args + wantErr bool + }{ + { + description: "删除修改时间在 1 天前并且文件名匹配 regex 的文件", + args: args{ + dirToClean: tmpDir, + fileReg: "([a-z]+.)?[a-z]+.log.[0-9]+", + retentionDays: 1, + }, + }, { + description: "删除修改时间在 2 天前并且文件名匹配 regex 的文件", + args: args{ + dirToClean: tmpDir, + fileReg: "([a-z]+.)?[a-z]+.log.wf.[0-9]+", + retentionDays: 2, + }, + }, { + description: "不匹配任何文件的情况", + args: args{ + dirToClean: tmpDir, + fileReg: "([a-z]+.)?[a-z]+.l.[0-9]+", + retentionDays: 2, + }, + }, { + description: "传入非法目录", + args: args{ + dirToClean: "empty", + fileReg: "([a-z]+.)?[a-z]+.l.[0-9]+", + retentionDays: 2, + }, + wantErr: true, + }, + } + obCleaner := NewObCleaner(nil) + for _, tt := range tests { + Convey(tt.description, t, func() { + err := obCleaner.DeleteFileByRetentionDays(context.Background(), tt.args.dirToClean, tt.args.fileReg, tt.args.retentionDays) + if !tt.wantErr { + So(err, ShouldBeNil) + + retentionDaysDuration := time.Duration(tt.args.retentionDays) * time.Hour * 24 + + matchedFiles, err := FindFilesByRegexAndMTime(context.Background(), tt.args.dirToClean, tt.args.fileReg, retentionDaysDuration) + So(err, ShouldBeNil) + So(len(matchedFiles), ShouldBeZeroValue) + } else { + So(err, ShouldNotBeNil) + } + }) + } +} + +func TestObCleaner_DeleteFileByKeepPercentage(t *testing.T) { + tmpDir, err := prepareTestDirTree("tmp1") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + err = os.Chdir(tmpDir) + if err != nil { + t.Fatal(err) + } + filesToBeCreated := []string{"a.b.log.1", "a.b.log.2", "b.log.2", "c.d.log.wf.1", "log.wf.1", "f.log.wf.2"} + + for _, filesToBeCreated := range filesToBeCreated { + _, err := os.OpenFile(filesToBeCreated, os.O_RDWR|os.O_CREATE, 0755) + if err != nil { + t.Fatal(err) + } + } + + err = os.Truncate("a.b.log.1", 1024) + if err != nil { + t.Fatal(err) + } + + err = os.Truncate("a.b.log.2", 1024) + if err != nil { + t.Fatal(err) + } + now := time.Now() + err = os.Chtimes("a.b.log.2", now, now.AddDate(0, 0, -2)) + if err != nil { + t.Fatal(err) + } + err = os.Truncate("b.log.2", 2048) + if err != nil { + t.Fatal(err) + } + + err = os.Chtimes("f.log.wf.2", now, now.AddDate(0, 0, -2)) + if err != nil { + t.Fatal(err) + } + + err = os.Truncate("c.d.log.wf.1", 2048) + if err != nil { + t.Fatal(err) + } + + type args struct { + dirToClean string + fileRegex string + keepPercentage uint64 + remainedFiles []string + } + tests := []struct { + description string + args args + wantErr bool + }{ + { + description: "删除超过保留比例的文件,(删除掉 a.b.log.2,其他匹配的保留)", + args: args{ + dirToClean: tmpDir, + fileRegex: "([a-z]+.)?[a-z]+.log.[0-9]+", + keepPercentage: 75, + remainedFiles: []string{"a.b.log.1", "b.log.2"}, + }, + }, + { + description: "保留全部匹配的文件", + args: args{ + dirToClean: tmpDir, + fileRegex: "([a-z]+.)?[a-z]+.log.wf.[0-9]+", + keepPercentage: 100, + remainedFiles: []string{"c.d.log.wf.1", "f.log.wf.2"}, + }, + }, + { + description: "删除全部匹配的文件", + args: args{ + dirToClean: tmpDir, + fileRegex: "([a-z]+.)?[a-z]+.log.wf.[0-9]+", + keepPercentage: 0.0, + remainedFiles: []string{}, + }, + }, + { + description: "不匹配任何文件", + args: args{ + dirToClean: tmpDir, + fileRegex: "([a-z]+.)?[a-z]+.l.w.[0-9]+", + keepPercentage: 50, + remainedFiles: []string{}, + }, + }, { + description: "非法目录", + args: args{ + dirToClean: "empty", + fileRegex: "([a-z]+.)?[a-z]+.l.w.[0-9]+", + keepPercentage: 50, + remainedFiles: []string{}, + }, + wantErr: true, + }, + } + obCleaner := NewObCleaner(nil) + for _, tt := range tests { + Convey(tt.description, t, func() { + err := obCleaner.DeleteFileByKeepPercentage(context.Background(), tt.args.dirToClean, tt.args.fileRegex, tt.args.keepPercentage) + if !tt.wantErr { + So(err, ShouldBeNil) + + matchedFiles, err := FindFilesByRegexAndMTime(context.Background(), tt.args.dirToClean, tt.args.fileRegex, 0) + So(err, ShouldBeNil) + So(len(matchedFiles), ShouldEqual, len(tt.args.remainedFiles)) + for i, matchedFile := range matchedFiles { + So(matchedFile.Path, ShouldEqual, filepath.Join(tt.args.dirToClean, tt.args.remainedFiles[i])) + } + } else { + So(err, ShouldNotBeNil) + } + }) + } +} + +func TestObCleaner_Clean(t *testing.T) { + tmpDir, err := prepareTestDirTree("tmp1") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + err = os.Chdir(tmpDir) + if err != nil { + t.Fatal(err) + } + filesToBeCreated := []string{"a.b.log.1", "a.b.log.2", "b.log.3", "c.d.log.wf.1", "log.wf.1", "f.log.wf.2"} + + for _, filesToBeCreated := range filesToBeCreated { + _, err := os.OpenFile(filesToBeCreated, os.O_RDWR|os.O_CREATE, 0755) + if err != nil { + t.Fatal(err) + } + } + + err = os.Truncate("a.b.log.1", 1024) + if err != nil { + t.Fatal(err) + } + + err = os.Truncate("a.b.log.2", 1024) + if err != nil { + t.Fatal(err) + } + + err = os.Truncate("b.log.3", 2048) + if err != nil { + t.Fatal(err) + } + + now := time.Now() + err = os.Chtimes("a.b.log.1", now, now.AddDate(0, 0, -4)) + if err != nil { + t.Fatal(err) + } + err = os.Chtimes("a.b.log.2", now, now.AddDate(0, 0, -2)) + if err != nil { + t.Fatal(err) + } + + err = os.Truncate("c.d.log.wf.1", 2048) + if err != nil { + t.Fatal(err) + } + + err = os.Truncate("f.log.wf.2", 1000) + if err != nil { + t.Fatal(err) + } + + cleanerConfig := &mgragent.CleanerConfig{ + LogCleaners: []*mgragent.LogCleanerRules{ + { + LogName: "ob_log", + Path: tmpDir, + DiskThreshold: 0, + Rules: []*mgragent.Rule{ + { + FileRegex: "([a-z]+.)?[a-z]+.log.[0-9]+", + RetentionDays: 3, + KeepPercentage: 70, + }, + { + FileRegex: "([a-z]+.)?[a-z]+.log.wf.[0-9]+", + RetentionDays: 1, + KeepPercentage: 80, + }, + }, + }, + }, + } + conf := &mgragent.ObCleanerConfig{ + RunInterval: 300, + Enabled: true, + CleanerConf: cleanerConfig, + } + + Convey("按照配置执行 clean,最终应该只剩 b.log.3, f.log.wf.2, log.wf.1 这几个文件", t, func() { + obCleaner := NewObCleaner(conf) + err = obCleaner.Clean(context.Background()) + So(err, ShouldBeNil) + + matchedFiles, err := FindFilesByRegexAndMTime(context.Background(), tmpDir, "", 0) + So(err, ShouldBeNil) + So(len(matchedFiles), ShouldEqual, 3) + remainedFiles := []string{"b.log.3", "f.log.wf.2", "log.wf.1"} + for i, matchedFile := range matchedFiles { + So(matchedFile.Path, ShouldEqual, filepath.Join(tmpDir, remainedFiles[i])) + } + }) +} + +func TestObCleaner_Run(t *testing.T) { + conf := &mgragent.ObCleanerConfig{ + RunInterval: time.Millisecond, + Enabled: true, + } + Convey("运行次数应该 >= 10", t, func() { + obCleaner := NewObCleaner(conf) + go func() { + obCleaner.Run(context.Background()) + }() + for { + if obCleaner.runCount >= 10 { + So(obCleaner.runCount, ShouldBeGreaterThanOrEqualTo, 10) + obCleaner.Stop() + break + } + } + }) + + Convey("init ob cleaner, then update config", t, func() { + conf := &mgragent.ObCleanerConfig{ + RunInterval: time.Millisecond, + CleanerConf: nil, + Enabled: true, + } + err := InitOBCleanerConf(conf) + So(err, ShouldBeNil) + + conf.RunInterval = time.Millisecond * 2 + err = UpdateOBCleanerConf(conf) + So(err, ShouldBeNil) + + obCleaner.Stop() + }) + + Convey("init-update ob cleaner config order is wrong", t, func() { + // update, before init + obCleaner = nil + conf := &mgragent.ObCleanerConfig{ + RunInterval: time.Millisecond, + CleanerConf: nil, + Enabled: true, + } + err := UpdateOBCleanerConf(conf) + So(err, ShouldBeNil) + + obCleaner = nil + // init, after init + err = InitOBCleanerConf(conf) + So(err, ShouldBeNil) + err = InitOBCleanerConf(conf) + So(err, ShouldNotBeNil) + + obCleaner.Stop() + }) +} diff --git a/executor/cleaner/cleaner_utils.go b/executor/cleaner/cleaner_utils.go new file mode 100644 index 0000000000000000000000000000000000000000..28f8ffd8391bcae394d76e5f2d0852ddabdc5f68 --- /dev/null +++ b/executor/cleaner/cleaner_utils.go @@ -0,0 +1,107 @@ +package cleaner + +import ( + "context" + "os" + "path/filepath" + "regexp" + "sort" + "strings" + "time" + + "github.com/oceanbase/obagent/lib/file" + "github.com/shirou/gopsutil/v3/disk" + log "github.com/sirupsen/logrus" + + "github.com/oceanbase/obagent/errors" +) + +var libFile file.File = file.FileImpl{} + +func GetRealPath(ctx context.Context, dir string) (string, error) { + dir = strings.TrimSpace(dir) + if dir == "" { + return "", errors.Occur(errors.ErrIllegalArgument) + } + absDir, err := filepath.Abs(dir) + if err != nil { + return "", errors.Occur(errors.ErrUnexpected, err) + } + realPath, err := filepath.EvalSymlinks(absDir) + if err != nil { + return "", errors.Occur(errors.ErrUnexpected, err) + } + return realPath, nil +} + +func GetDiskUsage(ctx context.Context, dir string) (float64, error) { + ctxLog := log.WithContext(ctx) + + realPath, err := GetRealPath(ctx, dir) + if err != nil { + return 0, errors.Occur(errors.ErrUnexpected, err) + } + + usageStat, err := disk.Usage(realPath) + if err != nil { + ctxLog.WithField("realPath", realPath).WithError(err).Error("disk.Usage failed") + return 0, errors.Occur(errors.ErrUnexpected, err) + } + + ctxLog.WithFields(log.Fields{ + "usageStat": usageStat, + "realPath": realPath, + }).Info("get usage") + + return usageStat.UsedPercent, nil +} + +func FindFilesAndSortByMTime(ctx context.Context, dir, fileRegex string) ([]fileInfoWithPath, error) { + realPath, err := GetRealPath(ctx, dir) + if err != nil { + return nil, err + } + matchedFiles, err := FindFilesByRegexAndMTime(ctx, realPath, fileRegex, 0) + if err != nil { + return nil, err + } + sort.Sort(ByMTime(matchedFiles)) + + return matchedFiles, nil +} + +func FindFilesByRegexAndMTime(ctx context.Context, dir, fileRegex string, mTime time.Duration) ([]fileInfoWithPath, error) { + matchedFiles, err := libFile.FindFilesByRegexAndMTime(ctx, dir, fileRegex, matchRegex, mTime, time.Now(), file.GetFileModTime) + if err != nil { + return nil, err + } + fileInfoWithPathArr := make([]fileInfoWithPath, len(matchedFiles)) + for i, matchedFile := range matchedFiles { + fileInfoWithPathArr[i] = fileInfoWithPath{ + Path: filepath.Join(dir, matchedFile.Name()), + Info: matchedFile, + } + } + return fileInfoWithPathArr, nil +} + +type fileInfoWithPath struct { + Path string + Info os.FileInfo +} + +func (f fileInfoWithPath) String() string { + return f.Info.Name() +} + +// ByMTime implements sort.Interface for []fileInfoWithPath based on ModTime(). +type ByMTime []fileInfoWithPath + +func (a ByMTime) Len() int { return len(a) } +func (a ByMTime) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a ByMTime) Less(i, j int) bool { return a[i].Info.ModTime().Before(a[j].Info.ModTime()) } + +func matchRegex(reg string, content string) (matched bool, err error) { + matched, err = regexp.MatchString(reg, content) + return +} diff --git a/executor/cleaner/cleaner_utils_test.go b/executor/cleaner/cleaner_utils_test.go new file mode 100644 index 0000000000000000000000000000000000000000..122676081af75a5f8b8812f3ce6de03a8e4fe542 --- /dev/null +++ b/executor/cleaner/cleaner_utils_test.go @@ -0,0 +1,331 @@ +package cleaner + +import ( + "context" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "testing" + "time" + + . "github.com/smartystreets/goconvey/convey" +) + +func prepareTestDirTree(tree string) (string, error) { + tmpDir, err := ioutil.TempDir("", "") + if err != nil { + return "", fmt.Errorf("error creating temp directory: %v\n", err) + } + + tmpDir, err = filepath.EvalSymlinks(tmpDir) + if err != nil { + return "", fmt.Errorf("error evaling temp directory: %v\n", err) + } + + err = os.MkdirAll(filepath.Join(tmpDir, tree), 0755) + if err != nil { + os.RemoveAll(tmpDir) + return "", err + } + + return filepath.Join(tmpDir, tree), nil +} + +func TestGetRealPath(t *testing.T) { + tmpDirA, err := prepareTestDirTree("a") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDirA) + + tmpDirB, err := prepareTestDirTree("b") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDirB) + tmpDirC := filepath.Join(tmpDirB, "c") + + err = os.Symlink(tmpDirA, tmpDirC) + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDirC) + + tests := []struct { + description string + symbolicLink string + realPath string + wantErr bool + }{ + { + description: "普通路径,没有软链的情况", + realPath: tmpDirA, + symbolicLink: tmpDirA, + }, + { + description: "路径为软链的情况", + symbolicLink: tmpDirC, + realPath: tmpDirA, + }, + { + description: "路径不存在", + symbolicLink: "empty", + realPath: "", + wantErr: true, + }, + } + for _, tt := range tests { + Convey(tt.description, t, func() { + symlinkRealPath, err := GetRealPath(context.Background(), tt.symbolicLink) + if !tt.wantErr { + So(err, ShouldBeNil) + So(symlinkRealPath, ShouldEqual, tt.realPath) + } else { + So(err, ShouldNotBeNil) + } + }) + } +} + +func TestGetDiskUsage(t *testing.T) { + tmpDir, err := prepareTestDirTree("tmp1") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + err = os.Chdir(tmpDir) + if err != nil { + t.Fatal(err) + } + + tmpFile, err := os.OpenFile("test.txt", os.O_RDWR|os.O_CREATE, 0755) + if err != nil { + t.Fatal(err) + } + _, err = tmpFile.WriteString("Test GetDiskUsage ~~~") + if err != nil { + t.Fatal(err) + } + + Convey("查询磁盘使用率", t, func() { + percentage, err := GetDiskUsage(context.Background(), tmpDir) + So(err, ShouldBeNil) + So(percentage, ShouldNotBeZeroValue) + }) + + Convey("查询不存在的路径", t, func() { + percentage, err := GetDiskUsage(context.Background(), "empty") + So(err, ShouldNotBeNil) + So(percentage, ShouldBeZeroValue) + }) +} + +func TestFindFilesByRegexAndMTime(t *testing.T) { + tmpDir, err := prepareTestDirTree("tmp1") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + err = os.Chdir(tmpDir) + if err != nil { + t.Fatal(err) + } + filesToBeCreated := []string{"a.b.log", "a.b.log.1", "b.log.2", "c.d.log.wf.1", "log.wf.1", "f.log.wf.2"} + + for _, filesToBeCreated := range filesToBeCreated { + _, err := os.OpenFile(filesToBeCreated, os.O_RDWR|os.O_CREATE, 0755) + if err != nil { + t.Fatal(err) + } + } + + now := time.Now() + err = os.Chtimes("a.b.log.1", now, now.AddDate(0, 0, -2)) + if err != nil { + t.Fatal() + } + + type args struct { + dir string + fileReg string + mTime int64 + } + tests := []struct { + description string + args args + matchedFiles []string + }{ + { + description: "查询 1 天内满足给定 regex 的文件", + args: args{ + dir: tmpDir, + fileReg: "([a-z]+.)?[a-z]+.log.[0-9]+", + mTime: -1, + }, + matchedFiles: []string{"b.log.2"}, + }, + { + description: "查询 1 天前满足给定 regex 的文件", + args: args{ + dir: tmpDir, + fileReg: "([a-z]+.)?[a-z]+.log.[0-9]+", + mTime: 1, + }, + matchedFiles: []string{"a.b.log.1"}, + }, + { + description: "查询 1 天内满足给定 regex 的文件", + args: args{ + dir: tmpDir, + fileReg: "([a-z]+.)?[a-z]+.log.wf.[0-9]+", + mTime: -1, + }, + matchedFiles: []string{"c.d.log.wf.1", "f.log.wf.2"}, + }, + } + for _, tt := range tests { + Convey(tt.description, t, func() { + mTimeDuration := time.Duration(tt.args.mTime) * time.Hour * 24 + matchedFiles, err := FindFilesByRegexAndMTime(context.Background(), tt.args.dir, tt.args.fileReg, mTimeDuration) + So(err, ShouldBeNil) + for i := range matchedFiles { + fmt.Println(matchedFiles[i].Path) + So(matchedFiles[i].Path, ShouldEqual, filepath.Join(tmpDir, tt.matchedFiles[i])) + } + }) + } +} + +func TestFindFilesAndSortByMTime(t *testing.T) { + tmpDir, err := prepareTestDirTree("tmp1") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + err = os.Chdir(tmpDir) + if err != nil { + t.Fatal(err) + } + filesToBeCreated := []string{"a.b.log", "a.b.log.1", "b.log.2", "c.d.log.wf.1", "log.wf.1", "f.log.wf.2"} + + for _, fileToBeCreated := range filesToBeCreated { + _, err := os.OpenFile(fileToBeCreated, os.O_RDWR|os.O_CREATE, 0755) + if err != nil { + t.Fatal(err) + } + } + + now := time.Now() + err = os.Chtimes("f.log.wf.2", now, now.AddDate(0, 0, -2)) + if err != nil { + t.Fatal() + } + err = os.Chtimes("a.b.log.1", now, now.AddDate(0, 0, -2)) + if err != nil { + t.Fatal() + } + type args struct { + dir string + fileReg string + } + tests := []struct { + description string + args args + matchedFiles []string + }{ + { + description: "查询所有满足给定 regex 的文件,且返回的文件按照 MTime 排序", + args: args{ + dir: tmpDir, + fileReg: "([a-z]+.)?[a-z]+.log.[0-9]+", + }, + matchedFiles: []string{"a.b.log.1", "b.log.2"}, + }, { + description: "查询所有满足给定 regex 的文件,且返回的文件按照 MTime 排序", + args: args{ + dir: tmpDir, + fileReg: "([a-z]+.)?[a-z]+.log.wf.[0-9]+", + }, + matchedFiles: []string{"f.log.wf.2", "c.d.log.wf.1"}, + }, { + description: "fileRegex 参数为空字符串,默认匹配所有", + args: args{ + dir: tmpDir, + fileReg: "", + }, + matchedFiles: []string{"a.b.log.1", "f.log.wf.2", "a.b.log", "b.log.2", "c.d.log.wf.1", "log.wf.1"}, + }, + } + for _, tt := range tests { + Convey(tt.description, t, func() { + matchedFiles, err := FindFilesAndSortByMTime(context.Background(), tt.args.dir, tt.args.fileReg) + So(err, ShouldBeNil) + for i := range matchedFiles { + So(matchedFiles[i].Path, ShouldEqual, filepath.Join(tmpDir, tt.matchedFiles[i])) + } + }) + } +} + +func Test_matchRegex(t *testing.T) { + type args struct { + reg string + content string + } + tests := []struct { + name string + args args + wantMatched bool + wantErr bool + }{ + { + name: "normal observer.log", + args: args{ + reg: "([a-z]+.)?[a-z]+.log.[0-9]+", + content: "observer.log.20210824131916", + }, + wantMatched: true, + wantErr: false, + }, + { + name: "abnormal observer.log", + args: args{ + reg: "([a-z]+.)?[a-z]+.log.[0-9]+", + content: "observer.log", + }, + wantMatched: false, + wantErr: false, + }, + { + name: "normal observer.log.wf", + args: args{ + reg: "([a-z]+.)?[a-z]+.log.wf.[0-9]+", + content: "observer.log.wf.20210823083142", + }, + wantMatched: true, + wantErr: false, + }, + { + name: "abnormal observer.log.wf", + args: args{ + reg: "([a-z]+.)?[a-z]+.log.wf.[0-9]+", + content: "observer.log.wf", + }, + wantMatched: false, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotMatched, err := matchRegex(tt.args.reg, tt.args.content) + if (err != nil) != tt.wantErr { + t.Errorf("matchRegex() error = %v, wantErr %v", err, tt.wantErr) + return + } + if gotMatched != tt.wantMatched { + t.Errorf("matchRegex() = %v, want %v", gotMatched, tt.wantMatched) + } + }) + } +} diff --git a/executor/disk/disk.go b/executor/disk/disk.go new file mode 100644 index 0000000000000000000000000000000000000000..31d4e28978f435624a85847728fc2e3600e8f899 --- /dev/null +++ b/executor/disk/disk.go @@ -0,0 +1,45 @@ +package disk + +import ( + "context" + + log "github.com/sirupsen/logrus" + + "github.com/oceanbase/obagent/errors" + "github.com/oceanbase/obagent/lib/system" +) + +var libDisk system.Disk = system.DiskImpl{} + +type GetDiskUsageParam struct { + Path string `json:"path" binding:"required"` +} + +type GetFsTypeParam struct { + Path string `json:"path" binding:"required"` +} + +func GetDiskUsage(ctx context.Context, param GetDiskUsageParam) (*system.DiskUsage, *errors.OcpAgentError) { + path := param.Path + ctxlog := log.WithContext(ctx).WithField("path", path) + + usage, err := libDisk.GetDiskUsage(path) + if err != nil { + ctxlog.WithError(err).Error("get disk usage failed") + return nil, errors.Occur(errors.ErrGetDiskUsage, path, err) + } + + ctxlog.Infof("get disk usage done, usage=%#v\n", usage) + return usage, nil +} + +func BatchGetDiskInfos(ctx context.Context) ([]*system.DiskInfo, *errors.OcpAgentError) { + diskInfos, err := libDisk.BatchGetDiskInfos() + if err != nil { + log.WithError(err).Error("get disk infos failed") + return nil, errors.Occur(errors.ErrBatchGetDiskInfos, err) + } + + log.Infof("get file system type done, diskInfos=%#v\n", diskInfos) + return diskInfos, nil +} diff --git a/executor/disk/disk_test.go b/executor/disk/disk_test.go new file mode 100644 index 0000000000000000000000000000000000000000..08efe05e2382f9ab0c8d2d3cf703b4b5c4f24e75 --- /dev/null +++ b/executor/disk/disk_test.go @@ -0,0 +1,29 @@ +package disk + +import ( + "context" + "testing" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + + "github.com/oceanbase/obagent/lib/system" + "github.com/oceanbase/obagent/tests/mock" +) + +func TestGetDiskUsage(t *testing.T) { + ctl := gomock.NewController(t) + defer ctl.Finish() + + mockDisk := mock.NewMockDisk(ctl) + libDisk = mockDisk + + path := "/data/1" + t.Run("get disk usage", func(t *testing.T) { + mockDisk.EXPECT().GetDiskUsage(path).Return(&system.DiskUsage{}, nil) + param := GetDiskUsageParam{Path: path} + usage, err := GetDiskUsage(context.Background(), param) + assert.Nil(t, err) + assert.NotNil(t, usage) + }) +} diff --git a/executor/file/file.go b/executor/file/file.go new file mode 100644 index 0000000000000000000000000000000000000000..91325b01187c85ed3f5931e44e875160884961e5 --- /dev/null +++ b/executor/file/file.go @@ -0,0 +1,285 @@ +package file + +import ( + "context" + "fmt" + "path/filepath" + "strings" + "time" + + log "github.com/sirupsen/logrus" + + "github.com/oceanbase/obagent/errors" + "github.com/oceanbase/obagent/executor/agent" + "github.com/oceanbase/obagent/lib/file" + "github.com/oceanbase/obagent/lib/http" + "github.com/oceanbase/obagent/lib/shell" +) + +var libFile file.File = file.FileImpl{} +var libShell shell.Shell = shell.ShellImpl{} +var libHttp http.Http = http.HttpImpl{} + +type BasicFileInfo struct { + Path string `json:"path"` +} + +type DownloadFileParam struct { + agent.TaskToken + Source DownloadFileSource `json:"source"` // download file source + Target string `json:"target"` // target file relative path + ValidateChecksum bool `json:"validateChecksum"` // whether validate checksum + Checksum string `json:"checksum"` // expected SHA-1 checksum of file +} + +type DownloadFileSource struct { + Type DownloadFileSourceType `json:"type"` // source type, OCP_URL or LOCAL_FILE + Url string `json:"url"` // download http url, only valid when type is OCP_URL + Path string `json:"path"` // local file path, only valid when type is LOCAL_FILE +} + +type DownloadFileSourceType string + +const ( + DownloadFileFromLocalFile DownloadFileSourceType = "LOCAL_FILE" +) + +type GetFileExistsParam struct { + FilePath string `json:"filePath"` // path of file +} + +type ExistsResult struct { + Exists bool `json:"exists"` // if path exists +} + +type RemoveFilesParam struct { + PathList []string `json:"pathList"` +} + +type GetRealStaticPathParam struct { + SymbolicLink string `json:"symbolicLink"` // symbolic link path +} + +type RealStaticPath struct { + Path string `json:"path"` // real absolute path +} + +type GetDirectoryUsedParam struct { + agent.TaskToken + Path string `json:"path"` // directory path + TimeoutMillis int64 `json:"timeoutMillis"` // shell command timeout +} + +type DirectoryUsed struct { + DirectoryUsedBytes int64 `json:"directoryUsedBytes"` // directory used bytes +} + +func DownloadFile(ctx context.Context, param DownloadFileParam) (*BasicFileInfo, *errors.OcpAgentError) { + targetPath := NewPathFromRelPath(param.Target) + ctxLog := log.WithContext(ctx).WithFields(log.Fields{ + "source": fmt.Sprintf("%#v", param.Source), + "target": targetPath, + "validateChecksum": param.ValidateChecksum, + "expectedChecksum": param.Checksum, + }) + ctxLog.Info("download file") + fileInfo := &BasicFileInfo{ + Path: param.Target, + } + if exists, err := libFile.FileExists(targetPath.FullPath()); err == nil && exists { + if sha1sum, err := libFile.Sha1Checksum(targetPath.FullPath()); err == nil && sha1sum == param.Checksum { + ctxLog.Info("download file skipped, file with same checksum already exists") + return fileInfo, nil + } + } + + err := libFile.CreateDirectoryForUser(filepath.Dir(targetPath.FullPath()), file.AdminUser, file.AdminGroup) + if err != nil { + return nil, errors.Occur(errors.ErrUnexpected, err) + } + if param.Source.Type == DownloadFileFromLocalFile { + ctxLog.Info("download file from local file") + err = libFile.CopyFile(param.Source.Path, targetPath.FullPath(), 0777) + if err != nil { + ctxLog.WithError(err).Errorf("download file from local file failed") + return nil, errors.Occur(errors.ErrDownloadFile, param.Source.Path, err) + } + } else { + ctxLog.Info("download file from ocp url") + err = libHttp.DownloadFile(targetPath.FullPath(), param.Source.Url) + if err != nil { + ctxLog.WithError(err).Error("download file from ocp url failed") + return nil, errors.Occur(errors.ErrDownloadFile, param.Source.Url, err) + } + } + + if param.ValidateChecksum { + checksum, err := libFile.Sha1Checksum(targetPath.FullPath()) + if err != nil { + return nil, errors.Occur(errors.ErrUnexpected, err) + } + if checksum != param.Checksum { + ctxLog.WithFields(log.Fields{ + "actualChecksum": checksum, + }).Error("download file validate checksum failed, invalid checksum") + return nil, errors.Occur(errors.ErrInvalidChecksum) + } + } else { + ctxLog.Info("download file validate checksum skipped") + } + ctxLog.Info("download file done") + return fileInfo, nil +} + +func IsFileExists(ctx context.Context, param GetFileExistsParam) (*ExistsResult, *errors.OcpAgentError) { + if !checkFilePath(param.FilePath) { + return nil, errors.Occur(errors.ErrIllegalArgument) + } + + targetPath := NewPathFromRelPath(param.FilePath) + ctxLog := log.WithContext(ctx).WithFields(log.Fields{ + "FilePath": param.FilePath, + }) + + ctxLog.Info("test path exists") + exists, err := libFile.FileExists(targetPath.FullPath()) + if err != nil { + return nil, errors.Occur(errors.ErrUnexpected, err) + } + return &ExistsResult{ + Exists: exists, + }, nil +} + +func RemoveFiles(ctx context.Context, param RemoveFilesParam) *errors.OcpAgentError { + ctxLog := log.WithContext(ctx).WithFields(log.Fields{ + "PathList": param.PathList, + }) + ctxLog.Info("remove files") + for _, path := range param.PathList { + targetPath := NewPathFromRelPath(path).FullPath() + var err error = nil + if libFile.IsDir(targetPath) { + err = libFile.RemoveDirectory(targetPath) + } else if libFile.IsFile(targetPath) { + err = libFile.RemoveFileIfExists(targetPath) + } + + if err != nil { + ctxLog.WithError(err).Errorf("remove file:'%s' failed.", path) + return errors.Occur(errors.ErrUnexpected, err) + } + } + ctxLog.Info("remove files done") + return nil +} + +func GetRealStaticPath(ctx context.Context, param GetRealStaticPathParam) (*RealStaticPath, *errors.OcpAgentError) { + if !checkFilePath(param.SymbolicLink) { + return nil, errors.Occur(errors.ErrIllegalArgument) + } + ctxLog := log.WithContext(ctx).WithFields(log.Fields{ + "SymbolicLink": param.SymbolicLink, + }) + ctxLog.Info("get real path by symbolic link") + realPath, err := libFile.GetRealPathBySymbolicLink(param.SymbolicLink) + if err != nil { + ctxLog.WithError(err).Errorf("get real path:'%s' failed.", param.SymbolicLink) + return nil, errors.Occur(errors.ErrUnexpected, err) + } + return &RealStaticPath{ + Path: realPath, + }, nil +} + +type PermissionType string + +const ( + accessRead PermissionType = "ACCESS_READ" + accessWrite PermissionType = "ACCESS_WRITE" + accessExecute PermissionType = "ACCESS_EXECUTE" +) + +var filePermissionShellValue = map[PermissionType]string{ + accessRead: "r", + accessWrite: "w", + accessExecute: "x", +} + +type CheckDirectoryPermissionParm struct { + Directory string `json:"directory"` // directory to check permission + User string `json:"user"` // host user to check directory permissions, e.g. admin + Permission PermissionType `json:"permission"` // expected permission of storage directory +} + +type CheckDirectoryPermissionResult string + +const ( + checkFailed CheckDirectoryPermissionResult = "CHECK_FAILED" + directoryNotExists CheckDirectoryPermissionResult = "DIRECTORY_NOT_EXISTS" + hasPermission CheckDirectoryPermissionResult = "HAS_PERMISSION" + noPermission CheckDirectoryPermissionResult = "NO_PERMISSION" +) + +const checkDirectoryPermissionCommand = "if [ -\"%s\" \"%s\" ]; then echo 0; else echo 1; fi" +const hasPermissionOutput = "0" + +func CheckDirectoryPermission(ctx context.Context, param CheckDirectoryPermissionParm) (CheckDirectoryPermissionResult, *errors.OcpAgentError) { + ctxlog := log.WithContext(ctx).WithFields(log.Fields{ + "directory": param.Directory, + "user": param.User, + "permission": param.Permission, + }) + + exists, err := libFile.FileExists(param.Directory) + if err != nil { + ctxlog.WithError(err).Info("check directory permission failed, cannot check directory exists") + return checkFailed, nil + } + if !exists { + ctxlog.Info("check directory permission done, directory not exists") + return directoryNotExists, nil + } + if !libFile.IsDir(param.Directory) { + ctxlog.Info("check directory permission done, path is not directory") + return directoryNotExists, nil + } + + cmd := fmt.Sprintf(checkDirectoryPermissionCommand, filePermissionShellValue[param.Permission], param.Directory) + executeResult, err := libShell.NewCommand(cmd).WithContext(ctx).WithUser(param.User).WithOutputType(shell.StdOutput).Execute() + if err != nil { + ctxlog.WithError(err).Info("check directory permission failed, cannot check directory permission") + return checkFailed, nil + } + if strings.TrimSpace(executeResult.Output) == hasPermissionOutput { + ctxlog.Info("check directory permission done, directory has permission") + return hasPermission, nil + } else { + ctxlog.Info("check directory permission done, directory has no permission") + return noPermission, nil + } +} + +func GetDirectoryUsed(ctx context.Context, param GetDirectoryUsedParam) (*DirectoryUsed, *errors.OcpAgentError) { + ctxLog := log.WithContext(ctx).WithField("Path", param.Path) + ctxLog.Info("get directory used bytes") + result, err := libFile.GetDirectoryUsedBytes(param.Path, time.Duration(param.TimeoutMillis)*time.Millisecond) + if err != nil { + ctxLog.WithError(err).Errorf("get directory '%s' used failed", param.Path) + return nil, errors.Occur(errors.ErrUnexpected, err) + } + return &DirectoryUsed{ + DirectoryUsedBytes: result, + }, nil +} + +func checkFilePath(filePath string) bool { + absPath, err := filepath.Abs(filePath) + if err != nil { + return false + } + if absPath != filePath { + return false + } + return true +} diff --git a/executor/file/file_test.go b/executor/file/file_test.go new file mode 100644 index 0000000000000000000000000000000000000000..7e3ecbfcbde108c27dd4ecc11c2b856b34369ca7 --- /dev/null +++ b/executor/file/file_test.go @@ -0,0 +1,470 @@ +package file + +import ( + "context" + "testing" + + "github.com/golang/mock/gomock" + . "github.com/smartystreets/goconvey/convey" + + "github.com/oceanbase/obagent/errors" + "github.com/oceanbase/obagent/lib/shell" + "github.com/oceanbase/obagent/tests/mock" +) + +func TestDownloadFileFromUrl(t *testing.T) { + ctl := gomock.NewController(t) + defer ctl.Finish() + + mockHttp := mock.NewMockHttp(ctl) + mockFile := mock.NewMockFile(ctl) + libHttp = mockHttp + libFile = mockFile + + type args struct { + url string + target string + expectedChecksum string + actualChecksum string + } + type want struct { + success bool + } + tests := []struct { + name string + args args + want want + }{ + { + name: "same checksum", + args: args{ + url: "http://127.0.0.1:8080/a.rpm", + target: "rpms/a.rpm", + expectedChecksum: "2439573625385400f2a669657a7db6ae7515d371", + actualChecksum: "2439573625385400f2a669657a7db6ae7515d371", + }, + want: want{ + success: true, + }, + }, + { + name: "different checksum", + args: args{ + url: "http://127.0.0.1:8080/b.rpm", + target: "rpms/b.rpm", + expectedChecksum: "2439573625385400f2a669657a7db6ae7515d371", + actualChecksum: "b85e079d153ccb06cc01db22b074dba0e0ec0e26", + }, + want: want{ + success: false, + }, + }, + } + + for _, tt := range tests { + Convey(tt.name, t, func() { + fullPath := NewPathFromRelPath(tt.args.target).FullPath() + mockFile.EXPECT().FileExists(gomock.Any()).Return(false, nil) + mockFile.EXPECT().CreateDirectoryForUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) + mockHttp.EXPECT().DownloadFile(fullPath, tt.args.url).Return(nil) + mockFile.EXPECT().Sha1Checksum(fullPath).Return(tt.args.actualChecksum, nil) + + _, err := DownloadFile(context.Background(), DownloadFileParam{ + Source: DownloadFileSource{ + Url: tt.args.url, + }, + Target: tt.args.target, + ValidateChecksum: true, + Checksum: tt.args.expectedChecksum, + }) + if tt.want.success { + So(err, ShouldBeNil) + } else { + So(err, ShouldNotBeNil) + } + }) + } +} + +func TestDownloadFileFromLocalFile(t *testing.T) { + ctl := gomock.NewController(t) + defer ctl.Finish() + + mockHttp := mock.NewMockHttp(ctl) + mockFile := mock.NewMockFile(ctl) + libHttp = mockHttp + libFile = mockFile + + type args struct { + path string + target string + } + type want struct { + } + + tests := []struct { + name string + args args + want want + }{ + { + name: "from local file", + args: args{ + path: "/home/admin/ocp_agent/lib", + target: "rpms/a.rpm", + }, + want: want{}, + }, + } + + for _, tt := range tests { + Convey(tt.name, t, func() { + mockFile.EXPECT().FileExists(gomock.Any()).Return(false, nil) + mockFile.EXPECT().CreateDirectoryForUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) + mockFile.EXPECT().CopyFile(tt.args.path, gomock.Any(), gomock.Any()) + + _, err := DownloadFile(context.Background(), DownloadFileParam{ + Source: DownloadFileSource{ + Type: DownloadFileFromLocalFile, + Path: tt.args.path, + }, + Target: tt.args.target, + ValidateChecksum: false, + }) + So(err, ShouldBeNil) + }) + } +} + +func TestIsFileExists(t *testing.T) { + ctl := gomock.NewController(t) + defer ctl.Finish() + + mockFile := mock.NewMockFile(ctl) + libFile = mockFile + + type args struct { + path string + exists bool + } + type want struct { + exists bool + } + tests := []struct { + name string + args args + want want + }{ + { + name: "file exists", + args: args{ + path: "rpms/a.rpm", + exists: true, + }, + want: want{ + exists: true, + }, + }, + { + name: "file not exists", + args: args{ + path: "rpms/a.rpm", + exists: true, + }, + want: want{ + exists: true, + }, + }, + } + + for _, tt := range tests { + Convey(tt.name, t, func() { + mockFile.EXPECT().FileExists(gomock.Any()).Return(tt.args.exists, nil) + result, err := IsFileExists(context.Background(), GetFileExistsParam{ + FilePath: tt.args.path, + }) + So(err, ShouldBeNil) + So(result.Exists, ShouldEqual, tt.want.exists) + }) + } +} + +func TestRemoveFiles(t *testing.T) { + ctl := gomock.NewController(t) + defer ctl.Finish() + + mockFile := mock.NewMockFile(ctl) + libFile = mockFile + + type args struct { + path string + isDir bool + } + type want struct { + } + tests := []struct { + name string + args args + want want + }{ + { + name: "remove directory", + args: args{ + path: "rpms/a.rpm", + isDir: true, + }, + want: want{}, + }, + { + name: "remove file", + args: args{ + path: "rpms", + isDir: true, + }, + want: want{}, + }, + } + + for _, tt := range tests { + Convey(tt.name, t, func() { + mockFile.EXPECT().IsDir(gomock.Any()).Return(tt.args.isDir) + if tt.args.isDir { + mockFile.EXPECT().RemoveDirectory(gomock.Any()).Return(nil) + } else { + mockFile.EXPECT().RemoveFileIfExists(gomock.Any()).Return(nil) + } + err := RemoveFiles(context.Background(), RemoveFilesParam{ + PathList: []string{tt.args.path}, + }) + So(err, ShouldBeNil) + }) + } +} + +func TestGetRealStaticPath(t *testing.T) { + ctl := gomock.NewController(t) + defer ctl.Finish() + + mockFile := mock.NewMockFile(ctl) + libFile = mockFile + + type args struct { + symLinkPath string + realStaticPath string + } + type want struct { + realStaticPath string + } + tests := []struct { + name string + args args + want want + }{ + { + name: "get real static path", + args: args{ + symLinkPath: "/home/admin/oceanbase/store", + realStaticPath: "/data/1/cluster1", + }, + want: want{ + realStaticPath: "/data/1/cluster1", + }, + }, + } + + for _, tt := range tests { + Convey(tt.name, t, func() { + mockFile.EXPECT().GetRealPathBySymbolicLink(tt.args.symLinkPath).Return(tt.args.realStaticPath, nil) + result, err := GetRealStaticPath(context.Background(), GetRealStaticPathParam{ + SymbolicLink: tt.args.symLinkPath, + }) + So(err, ShouldBeNil) + So(result.Path, ShouldEqual, tt.want.realStaticPath) + }) + } +} + +func TestCheckDirectoryPermission(t *testing.T) { + ctl := gomock.NewController(t) + defer ctl.Finish() + + mockFile := mock.NewMockFile(ctl) + mockShell := mock.NewMockShell(ctl) + mockCommand := mock.NewMockCommand(ctl) + libFile = mockFile + libShell = mockShell + + const ( + checkDirectoryExistsError = iota + directoryNotExist + pathIsNotDirectory + checkDirectoryPermissionError + directoryHasPermission + directoryHasNoPermission + ) + + type args struct { + scenario int + } + type want struct { + checkResult CheckDirectoryPermissionResult + } + tests := []struct { + name string + args args + want want + }{ + { + name: "cannot check directory exists", + args: args{ + scenario: checkDirectoryExistsError, + }, + want: want{ + checkResult: checkFailed, + }, + }, + { + name: "directory not exists", + args: args{ + scenario: directoryNotExist, + }, + want: want{ + checkResult: directoryNotExists, + }, + }, + { + name: "path is not directory", + args: args{ + scenario: pathIsNotDirectory, + }, + want: want{ + checkResult: directoryNotExists, + }, + }, + { + name: "cannot check directory permission", + args: args{ + scenario: checkDirectoryPermissionError, + }, + want: want{ + checkResult: checkFailed, + }, + }, + { + name: "directory has permission", + args: args{ + scenario: directoryHasPermission, + }, + want: want{ + checkResult: hasPermission, + }, + }, + { + name: "directory has no permission", + args: args{ + scenario: directoryHasNoPermission, + }, + want: want{ + checkResult: noPermission, + }, + }, + } + + ctx := context.Background() + param := CheckDirectoryPermissionParm{ + Directory: "/data/1", + User: "admin", + Permission: accessWrite, + } + + for _, tt := range tests { + Convey(tt.name, t, func() { + switch tt.args.scenario { + case checkDirectoryExistsError: + mockFile.EXPECT().FileExists(gomock.Any()).Return(false, errors.New("some error")) + case directoryNotExist: + mockFile.EXPECT().FileExists(gomock.Any()).Return(false, nil) + case pathIsNotDirectory: + mockFile.EXPECT().FileExists(gomock.Any()).Return(true, nil) + mockFile.EXPECT().IsDir(gomock.Any()).Return(false) + case checkDirectoryPermissionError: + mockFile.EXPECT().FileExists(gomock.Any()).Return(true, nil) + mockFile.EXPECT().IsDir(gomock.Any()).Return(true) + mockShell.EXPECT().NewCommand(gomock.Any()).Return(mockCommand) + mockCommand.EXPECT().WithContext(gomock.Any()).AnyTimes().Return(mockCommand) + mockCommand.EXPECT().WithUser(gomock.Any()).AnyTimes().Return(mockCommand) + mockCommand.EXPECT().WithOutputType(gomock.Any()).AnyTimes().Return(mockCommand) + mockCommand.EXPECT().Execute().Return(failedExecuteResult(), failedExecuteResult().AsError()) + case directoryHasPermission: + mockFile.EXPECT().FileExists(gomock.Any()).Return(true, nil) + mockFile.EXPECT().IsDir(gomock.Any()).Return(true) + mockShell.EXPECT().NewCommand(gomock.Any()).Return(mockCommand) + mockCommand.EXPECT().WithContext(gomock.Any()).AnyTimes().Return(mockCommand) + mockCommand.EXPECT().WithUser(gomock.Any()).AnyTimes().Return(mockCommand) + mockCommand.EXPECT().Execute().Return(successfulExecuteResult("0\n"), nil) + case directoryHasNoPermission: + mockFile.EXPECT().FileExists(gomock.Any()).Return(true, nil) + mockFile.EXPECT().IsDir(gomock.Any()).Return(true) + mockShell.EXPECT().NewCommand(gomock.Any()).Return(mockCommand) + mockCommand.EXPECT().WithContext(gomock.Any()).AnyTimes().Return(mockCommand) + mockCommand.EXPECT().WithUser(gomock.Any()).AnyTimes().Return(mockCommand) + mockCommand.EXPECT().Execute().Return(successfulExecuteResult("1\n"), nil) + } + + result, err := CheckDirectoryPermission(ctx, param) + So(err, ShouldBeNil) + So(result, ShouldEqual, tt.want.checkResult) + }) + } +} + +func TestGetDirectoryUsed(t *testing.T) { + ctl := gomock.NewController(t) + defer ctl.Finish() + + mockFile := mock.NewMockFile(ctl) + libFile = mockFile + + type args struct { + path string + usedBytes int64 + } + type want struct { + usedBytes int64 + } + tests := []struct { + name string + args args + want want + }{ + { + name: "get directory used bytes", + args: args{ + path: "/data/1", + usedBytes: 584930031, + }, + want: want{ + usedBytes: 584930031, + }, + }, + } + + for _, tt := range tests { + Convey(tt.name, t, func() { + mockFile.EXPECT().GetDirectoryUsedBytes(gomock.Any(), gomock.Any()).Return(tt.args.usedBytes, nil) + result, err := GetDirectoryUsed(context.Background(), GetDirectoryUsedParam{ + Path: tt.args.path, + }) + So(err, ShouldBeNil) + So(result.DirectoryUsedBytes, ShouldEqual, tt.want.usedBytes) + }) + } +} + +func successfulExecuteResult(output string) *shell.ExecuteResult { + return &shell.ExecuteResult{ExitCode: 0, Output: output} +} + +func failedExecuteResult() *shell.ExecuteResult { + return &shell.ExecuteResult{ExitCode: 1} +} diff --git a/executor/file/filepath.go b/executor/file/filepath.go new file mode 100644 index 0000000000000000000000000000000000000000..02cb297d450ad39b73e0099db9c5dfce753df2e6 --- /dev/null +++ b/executor/file/filepath.go @@ -0,0 +1,48 @@ +// OB-Agent provides basic file storage can caching. +// For example, you can download a rpm file in one HTTP request, and install it in another. +// OB-Agent stores all files under /tmp directory. +// Files are temporarily stored in this directory and will be cleaned by host periodically. +// OB-Agent do not expose absolute path of files to user. +// Instead, user can only take relative paths under /tmp directory. +// For example, when user refer to file path rpms/a.rpm,the real file path is /tmp/rpms/a.rpm. +// This prevents destruction of host files by user. + +package file + +import ( + "path/filepath" + "strings" +) + +const defaultBasePath = "/tmp" + +type Path struct { + BasePath string // file base path + RelPath string // file relative path +} + +func (p *Path) FullPath() string { + if strings.HasPrefix(p.RelPath, defaultBasePath) { + return p.RelPath + } + return filepath.Join(p.BasePath, p.RelPath) +} + +func (p *Path) String() string { + return p.FullPath() +} + +func (p *Path) FileName() string { + return filepath.Base(p.RelPath) +} + +func (p *Path) Join(elem string) *Path { + return NewPathFromRelPath(filepath.Join(p.RelPath, elem)) +} + +func NewPathFromRelPath(relPath string) *Path { + return &Path{ + BasePath: defaultBasePath, + RelPath: relPath, + } +} diff --git a/executor/log_query/log_querier.go b/executor/log_query/log_querier.go new file mode 100644 index 0000000000000000000000000000000000000000..e389cb8fe3c8e5e014f1778a1c0c23e34ad50ca6 --- /dev/null +++ b/executor/log_query/log_querier.go @@ -0,0 +1,452 @@ +package log_query + +import ( + "bufio" + "context" + "github.com/oceanbase/obagent/monitor/plugins/common" + "io" + "os" + "path/filepath" + "sort" + "strings" + "sync" + "time" + + log "github.com/sirupsen/logrus" + + "github.com/oceanbase/obagent/config/mgragent" + "github.com/oceanbase/obagent/errors" + "github.com/oceanbase/obagent/lib/file" + "github.com/oceanbase/obagent/lib/log_analyzer" + "github.com/oceanbase/obagent/monitor/message" +) + +var libFile file.File = file.FileImpl{} + +var GlobalLogQuerier *LogQuerier + +const maxScanBufferSize = 128 * 1024 + +const minPosGap = 512 * 1024 + +type LogQuerier struct { + conf *mgragent.LogQueryConfig + mutex sync.Mutex + minPosGap int64 +} + +func NewLogQuerier(conf *mgragent.LogQueryConfig) *LogQuerier { + return &LogQuerier{ + conf: conf, + mutex: sync.Mutex{}, + minPosGap: minPosGap, + } +} + +func InitLogQuerierConf(conf *mgragent.LogQueryConfig) error { + if GlobalLogQuerier == nil { + GlobalLogQuerier = NewLogQuerier(conf) + } + return nil +} + +func UpdateLogQuerierConf(conf *mgragent.LogQueryConfig) error { + if GlobalLogQuerier == nil { + return errors.New("GlobalLogQuerier is nil") + } + return GlobalLogQuerier.UpdateConf(conf) +} + +func (l *LogQuerier) UpdateConf(conf *mgragent.LogQueryConfig) error { + l.mutex.Lock() + defer l.mutex.Unlock() + l.conf = conf + return nil +} + +func (l *LogQuerier) GetConf() mgragent.LogQueryConfig { + l.mutex.Lock() + defer l.mutex.Unlock() + return *l.conf +} + +func (l *LogQuerier) Query(ctx context.Context, logQuery *LogQuery) (*Position, error) { + defer close(logQuery.logEntryChan) + ctxLog := log.WithContext(ctx).WithField("params", *logQuery.queryLogParams) + + matchedFiles, err := l.getMatchedFiles(ctx, logQuery) + defer func() { + for _, matchedFile := range matchedFiles { + matchedFile.FileDesc.Close() + } + }() + if err != nil { + ctxLog.WithError(err).Error("getMatchedFiles failed") + return nil, err + } + ctxLog.WithField("matchedFiles:", matchedFiles).Info("queryLog get matchedFiles") + var lastPos *Position + + for _, matchedFile := range matchedFiles { + select { + case <-ctx.Done(): + ctxLog.Info("timeout exceed") + return lastPos, nil + default: + } + fileInfo := &FileInfo{ + FileName: matchedFile.FileInfo.Name(), + FileId: matchedFile.FileId, + FileOffset: matchedFile.FileOffset, + } + lastPos, err = l.queryLogByLine(ctx, fileInfo, matchedFile.FileDesc, logQuery, matchedFile.LogAnalyzer) + if err != nil { + ctxLog.WithError(err).Error("queryLogByLine failed") + return lastPos, err + } + if logQuery.IsExceedLimit() { + return lastPos, nil + } + } + return lastPos, nil +} + +func (l *LogQuerier) queryLogByLine( + ctx context.Context, + fileInfo *FileInfo, + reader io.Reader, + logQuery *LogQuery, + logAnalyzer log_analyzer.LogAnalyzer, +) (*Position, error) { + ctxLog := log.WithContext(ctx).WithFields(log.Fields{ + "fileName": fileInfo.FileName, + }) + fileOffset := fileInfo.FileOffset + fdScanner := bufio.NewScanner(reader) + fdScanner.Split(file.ScanLines) + fdScanner.Buffer(make([]byte, bufio.MaxScanTokenSize), maxScanBufferSize) + errCount := 0 + + prevLogEntry := LogEntry{ + FileId: fileInfo.FileId, + FileOffset: fileOffset, + } + for fdScanner.Scan() { + select { + case <-ctx.Done(): + ctxLog.Info("timeout exceed") + return &Position{ + FileId: prevLogEntry.FileId, + FileOffset: prevLogEntry.FileOffset, + }, nil + default: + } + lineBytes := fdScanner.Bytes() + fileOffset += int64(len(lineBytes)) + if len(lineBytes) == 0 { + continue + } + if errCount > l.GetConf().ErrCountLimit { + ctxLog.Info("exceed error count limit") + break + } + logLineInfo, isNewLine := logAnalyzer.ParseLine(string(lineBytes)) + + isMatchedByLogAt, isSkip, err1 := l.isMatchByLogAt(ctx, logLineInfo, logQuery) + if err1 != nil { + errCount++ + ctxLog.WithError(err1).Error("check isMatchByLogAt failed") + continue + } + + if isSkip { + break + } + + isMatchedByKeyword, err1 := l.isMatchByKeyword(ctx, lineBytes, logQuery) + if err1 != nil { + errCount++ + ctxLog.WithError(err1).Error("check isMatchByKeyword failed") + continue + } + + curLineBytes := make([]byte, len(lineBytes)) + copy(curLineBytes, lineBytes) + if isNewLine { + if prevLogEntry.isMatched { + logQuery.SendLogEntry(prevLogEntry) + if logQuery.IsExceedLimit() { + return &Position{ + FileId: prevLogEntry.FileId, + FileOffset: prevLogEntry.FileOffset, + }, nil + } + } + + logLevel, _ := logLineInfo.GetTag(common.Level) + isMatchedByLogLevel, err1 := l.isMatchByLogLevel(ctx, logLevel, logQuery) + if err1 != nil { + errCount++ + ctxLog.WithError(err1).Error("check isMatchByLogLevel failed") + continue + } + isMatchedByLogAtAndLogLevel := isMatchedByLogAt && isMatchedByLogLevel + prevLogEntry = LogEntry{ + LogAt: logLineInfo.GetTime(), + LogLine: curLineBytes, + LogLevel: logLevel, + FileName: fileInfo.FileName, + FileId: fileInfo.FileId, + FileOffset: fileOffset, + isMatchedByLogAtAndLogLevel: isMatchedByLogAtAndLogLevel, + isMatched: isMatchedByLogAtAndLogLevel && isMatchedByKeyword, + } + } else { + if isMatchedByKeyword { + prevLogEntry.isMatched = prevLogEntry.isMatchedByLogAtAndLogLevel + } + prevLogEntry.LogLine = append(prevLogEntry.LogLine, curLineBytes...) + prevLogEntry.FileOffset = fileOffset + } + } + if prevLogEntry.isMatched { + logQuery.SendLogEntry(prevLogEntry) + } + lastPos := &Position{ + FileId: prevLogEntry.FileId, + FileOffset: prevLogEntry.FileOffset, + } + if err := fdScanner.Err(); err != nil { + ctxLog.WithError(err).Error("failed to scan") + return lastPos, err + } + return lastPos, nil +} + +func (l *LogQuerier) isMatchByLogAt( + ctx context.Context, + logLineInfo *message.Message, + logQuery *LogQuery, +) (isMatched, isSkip bool, err error) { + if logQuery.queryLogParams == nil { + return false, false, nil + } + logAt := logLineInfo.GetTime() + + if logAt.Before(logQuery.queryLogParams.StartTime) { + return false, false, nil + } + if logAt.After(logQuery.queryLogParams.EndTime) { + return false, true, nil + } + return true, false, nil +} + +func (l *LogQuerier) isMatchByLogLevel( + ctx context.Context, + logLevel string, + logQuery *LogQuery, +) (isMatched bool, err error) { + if logQuery.queryLogParams == nil { + return false, nil + } + + if logLevel == "" { + return true, nil + } + + if logLevel != "" && !isInArray(logLevel, logQuery.queryLogParams.LogLevel) { + return false, nil + } + + return true, nil +} + +func (l *LogQuerier) isMatchByKeyword( + ctx context.Context, + logLineBytes []byte, + logQuery *LogQuery, +) (isMatched bool, err error) { + if logQuery.queryLogParams == nil { + return false, nil + } + + isMatchedKeyword := true + if len(logQuery.keywordRegexps) != 0 { + for _, keywordRegexp := range logQuery.keywordRegexps { + if !keywordRegexp.Match(logLineBytes) { + isMatchedKeyword = false + break + } + } + } else { + for _, keyword := range logQuery.keywords { + if !strings.Contains(string(logLineBytes), keyword) { + isMatchedKeyword = false + break + } + } + } + + isMatchedExcludeKeyword := false + if len(logQuery.excludeKeywordRegexps) != 0 { + for _, excludeKeywordRegexp := range logQuery.excludeKeywordRegexps { + if excludeKeywordRegexp.Match(logLineBytes) { + isMatchedExcludeKeyword = true + break + } + } + } else { + for _, excludeKeyword := range logQuery.excludeKeywords { + if strings.Contains(string(logLineBytes), excludeKeyword) { + isMatchedExcludeKeyword = true + break + } + } + } + + return isMatchedKeyword && !isMatchedExcludeKeyword, nil +} + +// getMatchedFiles The files to be queried are matched according to the conditions +func (l *LogQuerier) getMatchedFiles(ctx context.Context, logQuery *LogQuery) ([]FileDetailInfo, error) { + params := logQuery.queryLogParams + ctxLog := log.WithContext(ctx).WithField("params", *params) + filePatterns := getFilePattern(params.LogType, params.LogLevel, &logQuery.conf) + var matchedFiles []FileDetailInfo + for _, filePattern := range filePatterns { + logAnalyzer := log_analyzer.GetLogAnalyzer(filePattern.LogAnalyzerCategory, params.LogType) + if logAnalyzer == nil { + ctxLog.Error("get LogInfoAnalyzer failed") + continue + } + foundFiles, err := libFile.FindFilesByRegexAndTimeSpan(ctx, file.FindFilesParam{ + Dir: filePattern.Dir, + FileRegexps: filePattern.LogFilePatterns, + MatchRegex: matchString, + StartTime: params.StartTime, + EndTime: params.EndTime, + GetFileTime: logAnalyzer.GetFileEndTime, + MatchMTime: matchMTime, + }) + if err != nil { + ctxLog.WithError(err).Error("FindFilesByRegexAndTimeSpan failed") + continue + } + for _, foundFile := range foundFiles { + fileTime, err1 := logAnalyzer.GetFileEndTime(foundFile) + if err1 != nil { + ctxLog.WithError(err1).Error("GetTimeFromFileName failed") + continue + } + matchedFileName := filepath.Join(filePattern.Dir, foundFile.Name()) + ctxLog.WithField("matchedFileName", matchedFileName).Info("open matched file") + fd, err1 := os.Open(matchedFileName) + if err1 != nil { + ctxLog.WithError(err1).Error("open matchedFile failed") + continue + } + fileStatInfo, err1 := libFile.GetFileStatInfo(ctx, fd) + if err1 != nil { + ctxLog.WithError(err1).Error("GetFileStatInfo failed") + continue + } + matchedFiles = append(matchedFiles, FileDetailInfo{ + LogAnalyzer: logAnalyzer, + Dir: filePattern.Dir, + FileInfo: foundFile, + FileTime: fileTime, + FileDesc: fd, + FileId: fileStatInfo.FileId(), + }) + } + } + sort.Sort(ByFileTime(matchedFiles)) + + newPos := 0 + if params.LastQueryFileId != 0 { + for i := 0; i < len(matchedFiles); i++ { + if matchedFiles[i].FileId == params.LastQueryFileId { + newPos = i + break + } + } + } + matchedFiles = matchedFiles[newPos:] + if len(matchedFiles) != 0 { + var offset int64 + if params.LastQueryFileOffset != 0 { + offset = params.LastQueryFileOffset + } else { + newOffset, err := l.locateStartPosition(ctx, *matchedFiles[0].FileDesc, matchedFiles[0].LogAnalyzer, logQuery.queryLogParams.StartTime) + if err != nil { + ctxLog.WithError(err).Error("locateStartPosition failed") + return nil, err + } + offset = newOffset + } + _, err := matchedFiles[0].FileDesc.Seek(offset, 0) + if err != nil { + ctxLog.WithError(err).Error("seek failed") + return nil, err + } + matchedFiles[0].FileOffset = offset + } + return matchedFiles, nil +} + +func (l *LogQuerier) getNextLineLogAt( + reader io.Reader, + logAnalyzer log_analyzer.LogAnalyzer, +) (*time.Time, error) { + fdScanner := bufio.NewScanner(reader) + maxCount := 100 + for i := 0; i < maxCount; i++ { + fdScanner.Scan() + lineBytes := fdScanner.Bytes() + logLineInfo, isNewLine := logAnalyzer.ParseLine(string(lineBytes)) + if isNewLine { + logAt := logLineInfo.GetTime() + return &logAt, nil + } + } + return nil, nil +} + +func (l *LogQuerier) locateStartPosition( + ctx context.Context, + fd os.File, + logAnalyzer log_analyzer.LogAnalyzer, + at time.Time, +) (offset int64, err error) { + ctxLog := log.WithContext(ctx) + startPos := int64(0) + endPos, err := fd.Seek(0, 2) + if err != nil { + ctxLog.WithError(err).Error("seek failed") + return 0, err + } + + for startPos < endPos && endPos-startPos > l.minPosGap { + midPos := startPos + (endPos-startPos)>>1 + _, err1 := fd.Seek(midPos, 0) + if err1 != nil { + ctxLog.WithError(err).Error("seek midPos failed") + return 0, err1 + } + logAt, err1 := l.getNextLineLogAt(&fd, logAnalyzer) + if err1 != nil { + ctxLog.WithError(err).Error("getNextLineLogAt failed") + return 0, err1 + } + if logAt == nil { + return 0, nil + } + if logAt.After(at) || logAt.Equal(at) { + endPos = midPos + } else if logAt.Before(at) { + startPos = midPos + 1 + } + } + return startPos, nil +} diff --git a/executor/log_query/log_querier_test.go b/executor/log_query/log_querier_test.go new file mode 100644 index 0000000000000000000000000000000000000000..7ab4300f7416bb994fac4560ddf8c537c1638764 --- /dev/null +++ b/executor/log_query/log_querier_test.go @@ -0,0 +1,242 @@ +package log_query + +import ( + "context" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "regexp" + "strings" + "testing" + "time" + + . "github.com/smartystreets/goconvey/convey" + "github.com/stretchr/testify/assert" + + "github.com/oceanbase/obagent/config/mgragent" + "github.com/oceanbase/obagent/lib/log_analyzer" +) + +func TestLogQuerier_queryLogByLine(t *testing.T) { + fileInfo := &FileInfo{ + FileName: "test.log", + FileId: 1, + FileOffset: 0, + } + + now := time.Now() + observerLogAnalyzer := log_analyzer.NewObLogLightAnalyzer(fileInfo.FileName) + logQuery := &LogQuery{ + excludeKeywordRegexps: make([]*regexp.Regexp, 0), + queryLogParams: &QueryLogRequest{ + StartTime: now, + EndTime: now.AddDate(0, 0, 1), + LogType: "observer", + Keyword: make([]string, 0), + ExcludeKeyword: make([]string, 0), + LogLevel: []string{"INFO"}, + ReqId: "1", + LastQueryFileId: 0, + LastQueryFileOffset: 0, + Limit: 20, + }, + logEntryChan: make(chan LogEntry, 1), + count: 0, + } + + logQuerier := NewLogQuerier(&mgragent.LogQueryConfig{ErrCountLimit: 100}) + logLine := `[2021-12-06 10:03:00.086260] INFO [SERVER] ob_query_retry_ctrl.cpp:557 test line 1 +[2021-12-06 10:03:00.086270] WARN [SERVER] response_result (ob_sync_plan_driver.cpp:74) [1931][1750] test line 2 +[2021-12-06 10:03:00.086298] WARN [SERVER] response_result (ob_sync_plan_driver.cpp:81) [1931][1750] test line 3 +[2021-12-06 10:03:00.086317] INFO [SERVER] obmp_base.cpp:1230 [1931][1750][YB420BA64D8A-0005D10D459FD4EF] test line 4` + strReader := strings.NewReader(logLine) + + type args struct { + fileInfo *FileInfo + logQuery *LogQuery + strReader io.Reader + } + tests := []struct { + name string + args args + want *Position + }{ + { + name: "根据 logQuery 查询匹配的日志", + args: args{ + fileInfo: fileInfo, + logQuery: logQuery, + strReader: strReader, + }, + want: &Position{ + FileId: 1, + FileOffset: int64(len(logLine)), + }, + }, + } + + for _, tt := range tests { + Convey(tt.name, t, func() { + lastPos, err := logQuerier.queryLogByLine(context.Background(), fileInfo, strReader, logQuery, observerLogAnalyzer) + + So(err, ShouldBeNil) + So(lastPos.FileId, ShouldEqual, tt.want.FileId) + So(lastPos.FileOffset, ShouldEqual, tt.want.FileOffset) + }) + } +} + +func prepareTestDirTree(tree string) (string, error) { + tmpDir, err := ioutil.TempDir("", "") + if err != nil { + return "", fmt.Errorf("error creating temp directory: %v\n", err) + } + + tmpDir, err = filepath.EvalSymlinks(tmpDir) + if err != nil { + return "", fmt.Errorf("error evaling temp directory: %v\n", err) + } + + err = os.MkdirAll(filepath.Join(tmpDir, tree), 0755) + if err != nil { + os.RemoveAll(tmpDir) + return "", err + } + + return filepath.Join(tmpDir, tree), nil +} + +func TestLogQuerier_getMatchedFiles(t *testing.T) { + tmpDir, err := ioutil.TempDir("", "") + if err != nil { + t.Fatal(err) + } + now := time.Date(2022, 3, 31, 13, 33, 3, 3, time.Local) + logFileName := "observer.log.20220331005827" + os.Create(filepath.Join(tmpDir, logFileName)) + + defer os.RemoveAll(tmpDir) + logQuerier := &LogQuerier{} + logQuery := &LogQuery{ + conf: mgragent.LogQueryConfig{ + ErrCountLimit: 10, + QueryTimeout: 1000, + DownloadTimeout: 1000, + LogTypeQueryConfigs: []mgragent.LogTypeQueryConfig{ + { + LogType: "observer", + IsOverrideByPriority: true, + LogLevelAndFilePatterns: []mgragent.LogLevelAndFilePattern{ + { + LogLevel: "ERROR", + Dir: tmpDir, + FilePatterns: []string{"observer.log.wf*"}, + LogParserCategory: "ob_light", + }, { + LogLevel: "WARN", + Dir: tmpDir, + FilePatterns: []string{"observer.log.wf*"}, + LogParserCategory: "ob_light", + }, { + LogLevel: "INFO", + Dir: tmpDir, + FilePatterns: []string{"observer.log*"}, + LogParserCategory: "ob_light", + }, { + LogLevel: "DEBUG", + Dir: tmpDir, + FilePatterns: []string{"observer.log*"}, + LogParserCategory: "ob_light", + }, + }, + }, { + LogType: "election", + IsOverrideByPriority: true, + LogLevelAndFilePatterns: []mgragent.LogLevelAndFilePattern{ + { + LogLevel: "ERROR", + Dir: tmpDir, + FilePatterns: []string{"election.log.wf*"}, + LogParserCategory: "ob_light", + }, { + LogLevel: "WARN", + Dir: tmpDir, + FilePatterns: []string{"election.log.wf*"}, + LogParserCategory: "ob_light", + }, { + LogLevel: "INFO", + Dir: tmpDir, + FilePatterns: []string{"election.log*"}, + LogParserCategory: "ob_light", + }, { + LogLevel: "DEBUG", + Dir: tmpDir, + FilePatterns: []string{"election.log*"}, + LogParserCategory: "ob_light", + }, + }, + }, + }, + }, + queryLogParams: &QueryLogRequest{ + StartTime: now.AddDate(0, 0, -1), + EndTime: now.AddDate(0, 0, 1), + LogType: "observer", + Keyword: []string{"test"}, + LogLevel: []string{"INFO"}, + LastQueryFileId: 0, + LastQueryFileOffset: 0, + Limit: 10, + }, + } + fileDetailInfos, err := logQuerier.getMatchedFiles(context.Background(), logQuery) + assert.NoError(t, err) + assert.Equal(t, 1, len(fileDetailInfos)) + if len(fileDetailInfos) == 1 { + fileDetailInfo1 := fileDetailInfos[0] + assert.Equal(t, tmpDir, fileDetailInfo1.Dir) + assert.Equal(t, logFileName, fileDetailInfo1.FileInfo.Name()) + assert.NotNil(t, fileDetailInfo1.LogAnalyzer) + } +} + +func TestLogQuerier_locateStartPosition(t *testing.T) { + tmpDir, err := ioutil.TempDir("", "") + if err != nil { + t.Fatal(err) + } + logFileName := "observer.log" + file, err := os.OpenFile(filepath.Join(tmpDir, logFileName), os.O_CREATE|os.O_RDWR, 0755) + if err != nil { + t.Fatal(err) + } + file.WriteString(`[2022-03-31 17:52:30.796493] INFO [CLOG] ob_log_callback_engine.cpp:84 [1667][0][Y0-0000000000000000] [lt=17] [dc=0] callback queue task number(clog=-7, ret=0) +[2022-03-31 17:52:40.796642] INFO [CLOG] ob_log_flush_task.cpp:150 [1700][1078][YB420BA64D8A-0005DB42F83CA516] [lt=18] [dc=0] clog flush cb cost time(flush_cb_cnt=61, flush_cb_cost_time=2115, avg time=34) +[2022-03-31 17:52:41.800244] INFO [SHARE] ob_bg_thread_monitor.cpp:323 [2263][2043][Y0-0000000000000000] [lt=18] [dc=0] current monitor number(seq_=0) +[2022-03-31 17:52:42.821926] INFO [COMMON] ob_kvcache_store.cpp:811 [1389][464][Y0-0000000000000000] [lt=15] [dc=0] Wash compute wash size(sys_total_wash_size=-99128180736, global_cache_size=9640219456, tenant_max_wash_size=0, tenant_min_wash_size=0, tenant_ids_=[1, 500, 1001, 1002], sys_cache_reserve_size=419430400, tg=time guard 'compute_tenant_wash_size' cost too much time, used=160, time_dist: 112,4,1,12,3,0) +[2022-03-31 17:52:43.822301] INFO [COMMON] ob_kvcache_store.cpp:318 [1389][464][Y0-0000000000000000] [lt=39] [dc=0] Wash time detail, (refresh_score_time=843, compute_wash_size_time=179, wash_sort_time=332, wash_time=2) +[2022-03-31 17:52:44.823868] INFO [STORAGE] ob_partition_loop_worker.cpp:404 [2126][1922][Y0-0000000000000000] [lt=19] [dc=0] gene checkpoint(pkey={tid:1099511627961, partition_id:2, part_cnt:0}, state=6, last_checkpoint=1648720420756550, cur_checkpoint=1648720420756550, last_max_trans_version=1648455281795330, max_trans_version=1648455281795330) +[2022-03-31 17:52:49.835267] INFO [SERVER] ob_inner_sql_connection.cpp:1336 [2055][1788][Y0-0000000000000000] [lt=18] [dc=0] execute write sql(ret=0, tenant_id=1, affected_rows=1, sql=" update __all_weak_read_service set min_version=1648720420707406, max_version=1648720420707406 where level_id = 0 and level_value = '' and min_version = 1648720420657154 and max_version = 1648720420657154 ") +[2022-03-31 17:52:51.844175] INFO [COMMON] ob_kvcache_store.cpp:811 [1389][464][Y0-0000000000000000] [lt=13] [dc=0] Wash compute wash size(sys_total_wash_size=-99128180736, global_cache_size=9640219456, tenant_max_wash_size=0, tenant_min_wash_size=0, tenant_ids_=[1, 500, 1001, 1002], sys_cache_reserve_size=419430400, tg=time guard 'compute_tenant_wash_size' cost too much time, used=148, time_dist: 112,2,1,10,1,1) +[2022-03-31 17:52:56.844544] INFO [COMMON] ob_kvcache_store.cpp:318 [1389][464][Y0-0000000000000000] [lt=37] [dc=0] Wash time detail, (refresh_score_time=768, compute_wash_size_time=170, wash_sort_time=328, wash_time=2) +[2022-03-31 17:52:59.877324] INFO [STORAGE] ob_freeze_info_snapshot_mgr.cpp:982 [1644][970][Y0-0000000000000000] [lt=21] [dc=0] start reload freeze info and snapshots(is_remote_=true) +[2022-03-31 17:53:01.895471] INFO [SERVER] ob_inner_sql_connection.cpp:1336 [2055][1788][Y0-0000000000000000] [lt=22] [dc=0] execute write sql(ret=0, tenant_id=1, affected_rows=1, sql=" update __all_weak_read_service set min_version=1648720420757606, max_version=1648720420757606 where level_id = 0 and level_value = '' and min_version = 1648720420707406 and max_version = 1648720420707406 ") +[2022-03-31 17:53:10.927528] INFO [STORAGE] ob_partition_loop_worker.cpp:374 [2126][1922][Y0-0000000000000000] [lt=29] [dc=0] write checkpoint success(pkey={tid:1100611139454249, partition_id:0, part_cnt:0}, cur_checkpoint=1648720420811993) +[2022-03-31 17:53:12.939917] INFO [CLOG.EXTLOG] ob_external_fetcher.cpp:276 [1609][900][Y0-0000000000000000] [lt=23] [dc=0] [FETCH_LOG_STREAM] Wash Stream: wash expired stream success(count=0, retired_arr=[]) +[2022-03-31 17:53:14.947401] INFO [LIB] ob_json.cpp:278 [1294][274][Y0-0000000000000000] [lt=16] [dc=0] invalid token type, maybe it is valid empty json type(cur_token_.type=93, ret=-5006) +[2022-03-31 17:53:16.947443] INFO ob_config.cpp:956 [1294][274][Y0-0000000000000000] [lt=20] [dc=0] succ to format_option_str(src="ASYNC NET_TIMEOUT = 30000000", dest="ASYNC NET_TIMEOUT = 30000000") +[2022-03-31 17:53:20.948222] INFO [LIB] ob_json.cpp:278 [1294][274][Y0-0000000000000000] [lt=11] [dc=0] invalid token type, maybe it is valid empty json type(cur_token_.type=93, ret=-5006) +[2022-03-31 17:53:31.948249] INFO ob_config.cpp:956 [1294][274][Y0-0000000000000000] [lt=15] [dc=0] succ to format_option_str(src="ASYNC NET_TIMEOUT = 30000000", dest="ASYNC NET_TIMEOUT = 30000000") +[2022-03-31 17:53:40.948339] INFO [SERVER] ob_remote_server_provider.cpp:208 [1294][274][Y0-0000000000000000] [lt=8] [dc=0] [remote_server_provider] refresh server list(ret=0, ret="OB_SUCCESS", all_server_count=0)`) + + defer os.RemoveAll(tmpDir) + logQuerier := &LogQuerier{minPosGap: 0} + queryTime := time.Date(2022, 3, 31, 17, 53, 16, 0, time.Local) + offset, err := logQuerier.locateStartPosition(context.Background(), *file, log_analyzer.GetLogAnalyzer(log_analyzer.TypeObLight, logFileName), queryTime) + if err != nil { + t.Fatal(err) + } + assert.NotZero(t, offset) +} diff --git a/executor/log_query/log_query.go b/executor/log_query/log_query.go new file mode 100644 index 0000000000000000000000000000000000000000..343d2ccb48d559ec80ea1c0ebf4591526256914a --- /dev/null +++ b/executor/log_query/log_query.go @@ -0,0 +1,98 @@ +package log_query + +import ( + "regexp" + "sync/atomic" + + "github.com/oceanbase/obagent/config/mgragent" + "github.com/oceanbase/obagent/errors" +) + +// LogQuery single log query +type LogQuery struct { + conf mgragent.LogQueryConfig + keywordRegexps []*regexp.Regexp + keywords []string + excludeKeywordRegexps []*regexp.Regexp + excludeKeywords []string + queryLogParams *QueryLogRequest + logEntryChan chan LogEntry + count int64 +} + +func NewLogQuery(conf mgragent.LogQueryConfig, queryLogParams *QueryLogRequest, logEntryChan chan LogEntry) (*LogQuery, error) { + if !queryLogParams.validate() { + err := errors.New("invalid parameters") + return nil, err + } + + var ( + keywordRegexps []*regexp.Regexp + excludeKeywordRegexps []*regexp.Regexp + err error + ) + if queryLogParams.KeywordType == regex { + keywordRegexps, err = genRegexps(queryLogParams.Keyword) + if err != nil { + return nil, err + } + } + + if queryLogParams.ExcludeKeywordType == regex { + excludeKeywordRegexps, err = genRegexps(queryLogParams.ExcludeKeyword) + if err != nil { + return nil, err + } + } + + return &LogQuery{ + conf: conf, + keywords: queryLogParams.Keyword, + keywordRegexps: keywordRegexps, + excludeKeywords: queryLogParams.ExcludeKeyword, + excludeKeywordRegexps: excludeKeywordRegexps, + queryLogParams: queryLogParams, + logEntryChan: logEntryChan, + count: 0, + }, nil +} + +func genRegexps(regexpStrs []string) ([]*regexp.Regexp, error) { + keywordRegexps := make([]*regexp.Regexp, 0) + for _, keyword := range regexpStrs { + if len(keyword) == 0 { + continue + } + keywordRegexp, err := regexp.Compile(keyword) + if err != nil { + return nil, err + } + keywordRegexps = append(keywordRegexps, keywordRegexp) + } + return keywordRegexps, nil +} + +func (l *LogQuery) IncCount() { + atomic.AddInt64(&l.count, 1) +} + +func (l *LogQuery) GetCount() int64 { + return atomic.LoadInt64(&l.count) +} + +func (l *LogQuery) GetLimit() int64 { + if l.queryLogParams == nil { + return 0 + } + return l.queryLogParams.Limit +} + +func (l *LogQuery) SendLogEntry(logEntry LogEntry) { + l.logEntryChan <- logEntry + l.IncCount() +} + +func (l *LogQuery) IsExceedLimit() bool { + limit := l.GetLimit() + return limit != 0 && l.GetCount() >= limit +} diff --git a/executor/log_query/log_query_model.go b/executor/log_query/log_query_model.go new file mode 100644 index 0000000000000000000000000000000000000000..900dfcd3a855bfd105dda7ec5f19ecf96263bf9e --- /dev/null +++ b/executor/log_query/log_query_model.go @@ -0,0 +1,77 @@ +package log_query + +import ( + "os" + "time" + + "github.com/oceanbase/obagent/lib/log_analyzer" +) + +type ConditionType string + +const ( + text ConditionType = "TEXT" + regex ConditionType = "REGEX" +) + +type QueryLogRequest struct { + StartTime time.Time `json:"startTime"` + EndTime time.Time `json:"endTime"` + LogType string `json:"logType"` + Keyword []string `json:"keyword"` + KeywordType ConditionType `json:"keywordType"` + ExcludeKeyword []string `json:"excludeKeyword"` + ExcludeKeywordType ConditionType `json:"excludeKeywordType"` + LogLevel []string `json:"logLevel"` + ReqId string `json:"reqId"` + LastQueryFileId uint64 `json:"lastQueryFileId"` + LastQueryFileOffset int64 `json:"lastQueryFileOffset"` + Limit int64 `json:"limit"` +} + +type DirAndFilePattern struct { + LogAnalyzerCategory string + Dir string + LogFilePatterns []string +} + +type FileDetailInfo struct { + LogAnalyzer log_analyzer.LogAnalyzer + Dir string + FileInfo os.FileInfo + FileTime time.Time + FileDesc *os.File + FileId uint64 + FileOffset int64 +} + +type FileInfo struct { + FileName string + FileId uint64 + FileOffset int64 +} + +type LogEntry struct { + LogAt time.Time `json:"logAt"` + LogLine []byte `json:"logLine"` + LogLevel string `json:"logLevel"` + FileName string `json:"fileName"` + FileId uint64 `json:"fileId"` + FileOffset int64 `json:"fileOffset"` + isMatchedByLogAtAndLogLevel bool + isMatched bool +} + +type Position struct { + FileId uint64 `json:"fileId"` + FileOffset int64 `json:"fileOffset"` +} + +func (q *QueryLogRequest) validate() bool { + if q.StartTime.IsZero() || + q.EndTime.IsZero() || + q.LogType == "" { + return false + } + return true +} diff --git a/executor/log_query/log_query_utils.go b/executor/log_query/log_query_utils.go new file mode 100644 index 0000000000000000000000000000000000000000..89cf4dd1170b2c22097ac6d25257470fd1533b90 --- /dev/null +++ b/executor/log_query/log_query_utils.go @@ -0,0 +1,97 @@ +package log_query + +import ( + "path/filepath" + "strings" + "time" + + "github.com/oceanbase/obagent/config/mgragent" +) + +const allLevel string = "ALL" + +func getFilePattern(logType string, logLevels []string, logQueryConf *mgragent.LogQueryConfig) (filePatterns []DirAndFilePattern) { + var logTypeQueryConfig mgragent.LogTypeQueryConfig + for _, typeQueryConfig := range logQueryConf.LogTypeQueryConfigs { + if typeQueryConfig.LogType == logType { + logTypeQueryConfig = typeQueryConfig + break + } + } + matchedLogFilePatterns := make(map[string]DirAndFilePattern) + for _, levelAndFilePattern := range logTypeQueryConfig.LogLevelAndFilePatterns { + if !isInArray(levelAndFilePattern.LogLevel, logLevels) && + levelAndFilePattern.LogLevel != allLevel { + continue + } + if !logTypeQueryConfig.IsOverrideByPriority { + matchedLogFilePatterns[buildFilePatternStr(levelAndFilePattern.FilePatterns)] = DirAndFilePattern{ + Dir: levelAndFilePattern.Dir, + LogFilePatterns: levelAndFilePattern.FilePatterns, + LogAnalyzerCategory: levelAndFilePattern.LogParserCategory, + } + } else { + filePatterns = []DirAndFilePattern{ + { + Dir: levelAndFilePattern.Dir, + LogFilePatterns: levelAndFilePattern.FilePatterns, + LogAnalyzerCategory: levelAndFilePattern.LogParserCategory, + }, + } + } + } + if len(matchedLogFilePatterns) != 0 { + for _, filePattern := range matchedLogFilePatterns { + filePatterns = append(filePatterns, filePattern) + } + } + + return +} + +func isInArray(str string, arr []string) bool { + for _, val := range arr { + if str == val { + return true + } + } + return false +} + +func matchString(regs []string, content string) (matched bool, err error) { + for _, reg := range regs { + matchContent, err1 := filepath.Match(reg, content) + if err1 != nil { + return false, err1 + } + if matchContent { + return true, nil + } + } + return +} + +// ByFileTime implements sort.Interface for []FileDetailInfo based on FileTime. +type ByFileTime []FileDetailInfo + +func (a ByFileTime) Len() int { return len(a) } +func (a ByFileTime) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a ByFileTime) Less(i, j int) bool { return a[i].FileTime.Before(a[j].FileTime) } + +func matchMTime(mTime, startTime, endTime time.Time) (matched bool, err error) { + return mTime.After(startTime), nil +} + +func dropCRLF(data []byte) []byte { + if len(data) > 0 && data[len(data)-1] == '\n' { + data = data[0 : len(data)-1] + if len(data) > 0 && data[len(data)-1] == '\r' { + data = data[0 : len(data)-1] + } + } + return data +} + +func buildFilePatternStr(filePattern []string) string { + return strings.Join(filePattern, ",") +} diff --git a/executor/pkg/package.go b/executor/pkg/package.go new file mode 100644 index 0000000000000000000000000000000000000000..153c39afca1c2d6363226c05f66f1c8669f98295 --- /dev/null +++ b/executor/pkg/package.go @@ -0,0 +1,192 @@ +package pkg + +import ( + "context" + "fmt" + + log "github.com/sirupsen/logrus" + + "github.com/oceanbase/obagent/errors" + "github.com/oceanbase/obagent/executor/agent" + "github.com/oceanbase/obagent/executor/file" + libfile "github.com/oceanbase/obagent/lib/file" + "github.com/oceanbase/obagent/lib/pkg" + "github.com/oceanbase/obagent/lib/shell" +) + +var libShell shell.Shell = shell.ShellImpl{} +var libFile libfile.File = libfile.FileImpl{} +var libPackage pkg.Package = pkg.PackageImpl{} + +type GetPackageInfoParam struct { + Name string `json:"name" binding:"required"` // package name, e.g. `oceanbase` or `oceanbase-2.2.77-20210522122736.el7.x86_64` +} + +type GetPackageInfoResult struct { + PackageInfoList []*pkg.PackageInfo `json:"packageInfoList"` +} + +type InstallPackageParam struct { + agent.TaskToken + Name string `json:"name" binding:"required"` // package name, e.g. `oceanbase` or `oceanbase-2.2.77-20210522122736.el7.x86_64` + File string `json:"file" binding:"required"` // rpm file relative path, e.g. `rpms/oceanbase-2.2.77-20210522122736.el7.x86_64.rpm` + InstallPath *string `json:"installPath"` // custom install path, e.g. custom `/home/admin/oceanbase` to `/home/a/oceanbase` +} + +type UninstallPackageParam struct { + Name string `json:"name" binding:"required"` // package name, e.g. `oceanbase` or `oceanbase-2.2.77-20210522122736.el7.x86_64` +} + +type InstallPackageResult struct { + Changed bool `json:"changed"` // whether the install action is performed + PackageInfo *pkg.PackageInfo `json:"packageInfo"` +} + +type UninstallPackageResult struct { + Changed bool `json:"changed"` // whether the uninstall action is performed +} + +type InstalledPackageNamesResult struct { + Names []string `json:"names"` // every installed package that share the same name +} + +type ExtractPackageParam struct { + agent.TaskToken + PackageFile string `json:"packageFile" binding:"required"` // rpm file relative path, e.g. `rpms/oceanbase-2.2.77-20210522122736.el7.x86_64.rpm` + TargetPath string `json:"targetPath" binding:"required"` // target path to store extracted files, e.g. `rpms/extract` + ExtractAll bool `json:"extractAll"` // whether to extract all files in rpm, true for all files, false for single file + FileInPackage string `json:"fileInPackage" binding:"required_if=ExtractAll false"` // extract file path in rpm, only valid when ExtractAll is false, e.g. `/home/admin/oceanbase/etc/upgrade_pre.py` +} + +type ExtractPackageResult struct { + ExtractAll bool `json:"extractAll"` // whether extracted all files in rpm, true for all files, false for single file + BasePath string `json:"basePath"` // base path of all extracted files, e.g. `rpms/extract/oceanbase-xxx` + FilePath *string `json:"filePath,omitempty"` // path of the extracted single file, only valid when ExtractAll is false, e.g. `rpms/extract/oceanbase-xxx/home/admin/oceanbase/etc/upgrade_pre.py` +} + +func GetPackageInfo(ctx context.Context, param GetPackageInfoParam) (*GetPackageInfoResult, *errors.OcpAgentError) { + packageName := param.Name + packageInfos, err := libPackage.FindPackageInfo(packageName) + if err != nil { + return nil, errors.Occur(errors.ErrQueryPackage, err) + } + log.WithContext(ctx).WithFields(log.Fields{ + "packageName": packageName, + "packageInfos": fmt.Sprintf("%#v", packageInfos), + }).Info("get package info done") + return &GetPackageInfoResult{PackageInfoList: packageInfos}, nil +} + +func InstallPackage(ctx context.Context, param InstallPackageParam) (*InstallPackageResult, *errors.OcpAgentError) { + packageName := param.Name + rpmPath := file.NewPathFromRelPath(param.File) + ctxlog := log.WithContext(ctx).WithFields(log.Fields{ + "packageName": packageName, + "rpmFile": rpmPath, + "installPath": param.InstallPath, + }) + + if exists, err := libPackage.PackageInstalled(packageName); err == nil && exists { + result := &InstallPackageResult{ + Changed: false, + } + if packageInfo, err := libPackage.GetPackageInfo(packageName); err == nil { + result.PackageInfo = packageInfo + } + ctxlog.WithFields(log.Fields{ + "changed": result.Changed, + "packageInfo": result.PackageInfo, + }).Info("install package skipped, package already installed") + return result, nil + } + + var err error + if param.InstallPath != nil { + err = libPackage.InstallPackageToCustomPath(rpmPath.FullPath(), *param.InstallPath) + } else { + err = libPackage.InstallPackage(rpmPath.FullPath()) + } + if err != nil { + return nil, errors.Occur(errors.ErrInstallPackage, err) + } + + result := &InstallPackageResult{ + Changed: true, + } + if packageInfo, err := libPackage.GetPackageInfo(packageName); err == nil { + result.PackageInfo = packageInfo + } + ctxlog.WithFields(log.Fields{ + "changed": result.Changed, + "packageInfo": result.PackageInfo, + }).Info("install package done") + return result, nil +} + +func UninstallPackage(ctx context.Context, param UninstallPackageParam) (*UninstallPackageResult, *errors.OcpAgentError) { + packageName := param.Name + ctxlog := log.WithContext(ctx).WithField("packageName", packageName) + if exists, err := libPackage.PackageInstalled(packageName); err == nil && !exists { + ctxlog.Info("uninstall package skipped, package not exists") + return &UninstallPackageResult{ + Changed: false, + }, nil + } + err := libPackage.UninstallPackage(packageName) + if err != nil { + return nil, errors.Occur(errors.ErrUninstallPackage, err) + } + ctxlog.Info("uninstall package done") + return &UninstallPackageResult{ + Changed: true, + }, nil +} + +func ExtractPackage(ctx context.Context, param ExtractPackageParam) (*ExtractPackageResult, *errors.OcpAgentError) { + extractAll := param.ExtractAll + rpmPath := file.NewPathFromRelPath(param.PackageFile) + targetPath := file.NewPathFromRelPath(param.TargetPath) + extractBasePath := targetPath.Join(rpmPath.FileName()) + fileInRpmPath := param.FileInPackage + ctxlog := log.WithContext(ctx).WithFields(log.Fields{ + "extractBasePath": extractBasePath, + "extractAll": extractAll, + "rpmPath": rpmPath, + "fileInRpmPath": fileInRpmPath, + }) + ctxlog.Info("extract package start") + + _ = libFile.RemoveDirectory(extractBasePath.FullPath()) + err := libFile.CreateDirectoryForUser(extractBasePath.FullPath(), libfile.AdminUser, libfile.AdminGroup) + if err != nil { + ctxlog.WithError(err).Error("extract package failed, cannot create directory") + return nil, errors.Occur(errors.ErrCreateDirectory, extractBasePath, err) + } + + if extractAll { + err = libPackage.ExtractPackageAllFiles(extractBasePath.FullPath(), rpmPath.FullPath()) + } else { + err = libPackage.ExtractPackageSingleFile(extractBasePath.FullPath(), rpmPath.FullPath(), fileInRpmPath) + } + + if err != nil { + return nil, errors.Occur(errors.ErrExtractPackage, err) + } + + ctxlog.Info("extract package done") + var result *ExtractPackageResult + if extractAll { + result = &ExtractPackageResult{ + ExtractAll: extractAll, + BasePath: extractBasePath.RelPath, + } + } else { + extractFilePath := extractBasePath.Join(fileInRpmPath).RelPath + result = &ExtractPackageResult{ + ExtractAll: extractAll, + BasePath: extractBasePath.RelPath, + FilePath: &extractFilePath, + } + } + return result, nil +} diff --git a/executor/pkg/package_test.go b/executor/pkg/package_test.go new file mode 100644 index 0000000000000000000000000000000000000000..c9cf26121b53ab739827017d452832388a4b055e --- /dev/null +++ b/executor/pkg/package_test.go @@ -0,0 +1,242 @@ +package pkg + +import ( + "context" + "fmt" + "testing" + + "github.com/golang/mock/gomock" + . "github.com/smartystreets/goconvey/convey" + + "github.com/oceanbase/obagent/lib/pkg" + "github.com/oceanbase/obagent/lib/shell" + "github.com/oceanbase/obagent/tests/mock" + "github.com/oceanbase/obagent/tests/mock2" +) + +func TestGetPackageInfo(t *testing.T) { + ctl := gomock.NewController(t) + defer ctl.Finish() + + mockPackage := mock2.NewMockPackage(ctl) + libPackage = mockPackage + + type args struct { + name string + version string + buildNumber string + os string + architecture string + } + tests := []struct { + name string + args args + }{ + { + name: "get oceanbase package info", + args: args{ + name: "oceanbase", + version: "2.2.77", + buildNumber: "20210522122736", + os: "el7", + architecture: "x86_64", + }, + }, + } + + for _, tt := range tests { + Convey(tt.name, t, func() { + mockPackage.EXPECT().FindPackageInfo(gomock.Any()).Return([]*pkg.PackageInfo{{ + Name: tt.args.name, + Version: tt.args.version, + BuildNumber: tt.args.buildNumber, + Os: tt.args.os, + Architecture: tt.args.architecture, + }}, nil) + packageInfos, err := GetPackageInfo(context.Background(), GetPackageInfoParam{Name: tt.args.name}) + So(err, ShouldBeNil) + So(packageInfos.PackageInfoList, ShouldNotBeEmpty) + packageInfo := packageInfos.PackageInfoList[0] + So(packageInfo.Name, ShouldEqual, tt.args.name) + So(packageInfo.Version, ShouldEqual, tt.args.version) + So(packageInfo.BuildNumber, ShouldEqual, tt.args.buildNumber) + So(packageInfo.Os, ShouldEqual, tt.args.os) + So(packageInfo.Architecture, ShouldEqual, tt.args.architecture) + }) + } +} + +func TestInstallPackage(t *testing.T) { + ctl := gomock.NewController(t) + defer ctl.Finish() + + mockPackage := mock2.NewMockPackage(ctl) + libPackage = mockPackage + + type args struct { + name string + alreadyExists bool + customPath bool + } + type want struct { + changed bool + } + tests := []struct { + name string + args args + want want + }{ + { + name: "install package skipped", + args: args{ + name: "oceanbase", + alreadyExists: true, + }, + want: want{changed: false}, + }, + { + name: "install package done", + args: args{name: "test", alreadyExists: false}, + want: want{changed: true}, + }, + } + + for _, tt := range tests { + Convey(tt.name, t, func() { + mockPackage.EXPECT().PackageInstalled(gomock.Any()).Return(tt.args.alreadyExists, nil) + if !tt.args.alreadyExists { + mockPackage.EXPECT().InstallPackage(gomock.Any()).Return(nil) + } + mockPackage.EXPECT().GetPackageInfo(gomock.Any()).Return(&pkg.PackageInfo{}, nil) + + installPackageResult, err := InstallPackage(context.Background(), InstallPackageParam{Name: tt.args.name}) + So(err, ShouldBeNil) + So(installPackageResult.Changed, ShouldEqual, tt.want.changed) + }) + } +} + +func TestUninstallPackage(t *testing.T) { + ctl := gomock.NewController(t) + defer ctl.Finish() + + mockPackage := mock2.NewMockPackage(ctl) + libPackage = mockPackage + + type args struct { + name string + exists bool + } + type want struct { + changed bool + } + tests := []struct { + name string + args args + want want + }{ + { + name: "uninstall package skipped", + args: args{name: "oceanbase", exists: false}, + want: want{changed: false}, + }, + { + name: "uninstall package done", + args: args{name: "test", exists: true}, + want: want{changed: true}, + }, + } + + for _, tt := range tests { + Convey(tt.name, t, func() { + mockPackage.EXPECT().PackageInstalled(gomock.Any()).Return(tt.args.exists, nil) + if tt.args.exists { + mockPackage.EXPECT().UninstallPackage(gomock.Any()).Return(nil) + } + + uninstallPackageResult, err := UninstallPackage(context.Background(), UninstallPackageParam{Name: tt.args.name}) + So(err, ShouldBeNil) + So(uninstallPackageResult.Changed, ShouldEqual, tt.want.changed) + }) + } +} + +func TestExtractPackage(t *testing.T) { + ctl := gomock.NewController(t) + defer ctl.Finish() + + mockFile := mock.NewMockFile(ctl) + libFile = mockFile + mockPackage := mock2.NewMockPackage(ctl) + libPackage = mockPackage + + type args struct { + extractAll bool + rpmFile string + targetPath string + fileInRpm string + } + type want struct { + changed bool + } + tests := []struct { + name string + args args + want want + }{ + { + name: "extract all files", + args: args{ + extractAll: true, + rpmFile: "rpms/oceanbase-2.2.77-20210522122736.el7.x86_64.rpm", + targetPath: "rpms/extract", + }, + want: want{changed: false}, + }, + { + name: "extract single file", + args: args{ + extractAll: false, + rpmFile: "rpms/oceanbase-2.2.77-20210522122736.el7.x86_64.rpm", + targetPath: "rpms/extract", + fileInRpm: "/home/admin/oceanbase/etc/upgrade_pre.py", + }, + want: want{changed: true}, + }, + } + + for _, tt := range tests { + Convey(tt.name, t, func() { + mockFile.EXPECT().RemoveDirectory(gomock.Any()).Return(nil) + mockFile.EXPECT().CreateDirectoryForUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) + if tt.args.extractAll { + mockPackage.EXPECT().ExtractPackageAllFiles(gomock.Any(), gomock.Any()).Return(nil) + } else { + mockPackage.EXPECT().ExtractPackageSingleFile(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) + } + + var param ExtractPackageParam + if tt.args.extractAll { + param = ExtractPackageParam{ + ExtractAll: true, + PackageFile: tt.args.rpmFile, + TargetPath: tt.args.targetPath, + } + } else { + param = ExtractPackageParam{ + ExtractAll: false, + PackageFile: tt.args.rpmFile, + TargetPath: tt.args.targetPath, + FileInPackage: tt.args.fileInRpm, + } + } + extractPackageResult, err := ExtractPackage(context.Background(), param) + So(err, ShouldBeNil) + fmt.Printf("%#v\n", extractPackageResult) + }) + } +} + +func successfulExecuteResult() *shell.ExecuteResult { + return &shell.ExecuteResult{ExitCode: 0} +} diff --git a/executor/process/process.go b/executor/process/process.go new file mode 100644 index 0000000000000000000000000000000000000000..ed48675e487e891f93fc10c0c8e1b791f46951dc --- /dev/null +++ b/executor/process/process.go @@ -0,0 +1,154 @@ +package process + +import ( + "context" + + log "github.com/sirupsen/logrus" + + "github.com/oceanbase/obagent/errors" + "github.com/oceanbase/obagent/lib/shell" + "github.com/oceanbase/obagent/lib/system" +) + +var libShell shell.Shell = shell.ShellImpl{} + +type FindProcessType string + +const ( + byName FindProcessType = "BY_NAME" + byKeyword FindProcessType = "BY_KEYWORD" + byPid FindProcessType = "BY_PID" +) + +type FindProcessParam struct { + FindType FindProcessType `json:"findType" binding:"required,oneof='BY_NAME' 'BY_KEYWORD'"` // find process by name or keyword + Name string `json:"name" binding:"required_if=FindType BY_NAME"` // process name + Keyword string `json:"keyword" binding:"required_if=FindType BY_KEYWORD"` // process keyword +} + +type CheckProcessExistsParam struct { + Name string `json:"name" binding:"required"` // process name +} + +type GetProcessProcInfoParam struct { + FindType FindProcessType `json:"findType" binding:"required,oneof='BY_NAME' 'BY_PID'"` + Pid int32 `json:"pid" binding:"required_if=FindType BY_PID"` // pid + Name string `json:"name" binding:"required_if=FindType BY_NAME"` // process name + Type string `json:"type" binding:"required"` // proc type + User string `json:"user" binding:"required"` // user +} + +type GetProcessInfoParam struct { + Name string `json:"processName" binding:"required"` // process name +} + +type StopProcessParam struct { + Process FindProcessParam `json:"process" binding:"required"` // process name or process keyword + Force bool `json:"force"` // whether to force stop process +} + +type GetProcessInfoResult struct { + ProcessInfoList []*system.ProcessInfo `json:"processInfoList"` +} + +type GetProcessProcInfoResult struct { + ProcInfoList []string `json:"procInfoList"` +} + +var libProcess system.Process = system.ProcessImpl{} + +func ProcessExists(ctx context.Context, param CheckProcessExistsParam) (bool, *errors.OcpAgentError) { + name := param.Name + ctxlog := log.WithContext(ctx).WithField("name", name) + + exists, err := libProcess.ProcessExists(name) + if err != nil { + return false, errors.Occur(errors.ErrCheckProcessExists, name, err) + } + ctxlog.WithField("exists", exists).Info("check process exists done") + return exists, nil +} + +func GetProcessInfo(ctx context.Context, param GetProcessInfoParam) (*GetProcessInfoResult, *errors.OcpAgentError) { + name := param.Name + ctxlog := log.WithContext(ctx).WithField("name", name) + + processInfoList, err := libProcess.FindProcessInfoByName(name) + if err != nil { + return nil, errors.Occur(errors.ErrGetProcessInfo, name, err) + } + ctxlog.WithField("processes", processInfoList).Info("get process info done") + return &GetProcessInfoResult{ProcessInfoList: processInfoList}, nil +} + +func StopProcess(ctx context.Context, param StopProcessParam) *errors.OcpAgentError { + force := param.Force + + if param.Process.FindType == byName { + name := param.Process.Name + if force { + err := libProcess.KillProcessByName(name) + if err != nil { + return errors.Occur(errors.ErrStopProcess, name, err) + } + } else { + err := libProcess.TerminateProcessByName(name) + if err != nil { + return errors.Occur(errors.ErrStopProcess, name, err) + } + } + } else if param.Process.FindType == byKeyword { + keyword := param.Process.Keyword + if force { + err := libProcess.KillProcessByKeyword(keyword) + if err != nil { + return errors.Occur(errors.ErrStopProcess, keyword, err) + } + } else { + err := libProcess.TerminateProcessByKeyword(keyword) + if err != nil { + return errors.Occur(errors.ErrStopProcess, keyword, err) + } + } + } + log.WithContext(ctx).WithFields(log.Fields{ + "findType": param.Process.FindType, + "name": param.Process.Name, + "keyword": param.Process.Keyword, + "force": force, + }).Info("stop process done") + return nil +} + +// GetProcessProcInfo ls -l /proc/{pid}/{infoType} +// even root user can not list this info of others' in container, so user must be specified +func GetProcessProcInfo(ctx context.Context, param GetProcessProcInfoParam) (*GetProcessProcInfoResult, *errors.OcpAgentError) { + user := param.User + infoType := param.Type + var lines []string + var err error + var processIdentifier string + if param.FindType == byPid { + pid := param.Pid + processIdentifier = string(pid) + log.WithContext(ctx).WithFields(log.Fields{ + "pid": pid, + "user": user, + "infoType": infoType, + }).Info("list process proc info") + lines, err = libProcess.GetProcessProcInfoByPid(pid, infoType, user) + } else if param.FindType == byName { + name := param.Name + processIdentifier = name + log.WithContext(ctx).WithFields(log.Fields{ + "name": name, + "user": user, + "infoType": infoType, + }).Info("list process proc info") + lines, err = libProcess.GetProcessProcInfoByName(name, infoType, user) + } + if err != nil { + return nil, errors.Occur(errors.ErrProcessProcInfo, infoType, processIdentifier, user, err) + } + return &GetProcessProcInfoResult{ProcInfoList: lines}, nil +} diff --git a/executor/process/process_test.go b/executor/process/process_test.go new file mode 100644 index 0000000000000000000000000000000000000000..5181f2149825a37b3b1ac9d1f41c6dd1dc03d975 --- /dev/null +++ b/executor/process/process_test.go @@ -0,0 +1,129 @@ +package process + +import ( + "context" + "testing" + + "github.com/golang/mock/gomock" + . "github.com/smartystreets/goconvey/convey" + + "github.com/oceanbase/obagent/lib/system" + "github.com/oceanbase/obagent/tests/mock" +) + +func TestProcessExists(t *testing.T) { + ctl := gomock.NewController(t) + defer ctl.Finish() + + mockProcess := mock.NewMockProcess(ctl) + libProcess = mockProcess + + type args struct { + name string + exists bool + } + type want struct { + exists bool + } + tests := []struct { + name string + args args + want want + }{ + { + name: "oceanbase process exists", + args: args{name: "oceanbase", exists: true}, + want: want{exists: true}, + }, + { + name: "test process not exists", + args: args{name: "test", exists: false}, + want: want{exists: false}, + }, + } + for _, tt := range tests { + Convey(tt.name, t, func() { + mockProcess.EXPECT().ProcessExists(tt.args.name).Return(tt.args.exists, nil) + + exists, err := ProcessExists(context.Background(), CheckProcessExistsParam{Name: tt.args.name}) + So(err, ShouldBeNil) + So(exists, ShouldEqual, tt.want.exists) + }) + } +} + +func TestGetProcessInfo(t *testing.T) { + ctl := gomock.NewController(t) + defer ctl.Finish() + + mockProcess := mock.NewMockProcess(ctl) + libProcess = mockProcess + + Convey("get process info", t, func() { + processName := "oceanbase" + + mockProcess.EXPECT().FindProcessInfoByName(processName).Return([]*system.ProcessInfo{ + {Pid: 1001, Name: processName}, + {Pid: 1002, Name: processName}, + }, nil) + + processInfoResult, err := GetProcessInfo(context.Background(), GetProcessInfoParam{Name: processName}) + So(err, ShouldBeNil) + processInfos := processInfoResult.ProcessInfoList + So(processInfos, ShouldHaveLength, 2) + So(processInfos[0].Name, ShouldEqual, processName) + So(processInfos[1].Name, ShouldEqual, processName) + }) +} + +func TestStopProcess(t *testing.T) { + ctl := gomock.NewController(t) + defer ctl.Finish() + + mockProcess := mock.NewMockProcess(ctl) + libProcess = mockProcess + + type args struct { + name string + force bool + } + type want struct { + force bool + } + tests := []struct { + name string + args args + want want + }{ + { + name: "no force stop", + args: args{name: "oceanbase", force: false}, + want: want{force: false}, + }, + { + name: "force stop", + args: args{name: "oceanbase", force: true}, + want: want{force: true}, + }, + } + for _, tt := range tests { + Convey(tt.name, t, func() { + param := StopProcessParam{ + Process: FindProcessParam{ + FindType: byName, + Name: tt.args.name, + }, + Force: tt.args.force, + } + + if tt.want.force { + mockProcess.EXPECT().KillProcessByName(tt.args.name).Return(nil) + } else { + mockProcess.EXPECT().TerminateProcessByName(tt.args.name).Return(nil) + } + + err := StopProcess(context.Background(), param) + So(err, ShouldBeNil) + }) + } +} diff --git a/executor/system/system.go b/executor/system/system.go new file mode 100644 index 0000000000000000000000000000000000000000..e024f3a6f7d0cd1740c2d6df0f867b82cb1efdfa --- /dev/null +++ b/executor/system/system.go @@ -0,0 +1,18 @@ +package system + +import ( + "context" + + "github.com/oceanbase/obagent/errors" + "github.com/oceanbase/obagent/lib/system" +) + +var libSystem system.System = system.SystemImpl{} + +func GetHostInfo(ctx context.Context) (*system.HostInfo, *errors.OcpAgentError) { + info, err := libSystem.GetHostInfo() + if err != nil { + return nil, errors.Occur(errors.ErrUnexpected, err) + } + return info, nil +} diff --git a/go.mod b/go.mod index 6b6648965522e02f6f21cf2a72271079bffd2156..343d708bdb6bb4d8e38cc28002a5c98ebe32b67a 100644 --- a/go.mod +++ b/go.mod @@ -1,46 +1,133 @@ module github.com/oceanbase/obagent -go 1.14 +go 1.19 require ( - github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 // indirect + github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 + github.com/aliyun/aliyun-log-go-sdk v0.1.27 + github.com/bluele/gcache v0.0.2 + github.com/cenkalti/backoff v2.2.1+incompatible + github.com/containerd/cgroups v1.0.1 + github.com/didi/gendry v1.7.0 github.com/dolthub/go-mysql-server v0.10.0 - github.com/fsnotify/fsnotify v1.4.9 // indirect + github.com/dustin/go-humanize v1.0.0 + github.com/felixge/fgprof v0.9.1 github.com/gin-contrib/pprof v1.3.0 github.com/gin-gonic/gin v1.7.7 github.com/go-kit/kit v0.10.0 - github.com/go-kit/log v0.1.0 + github.com/go-kit/log v0.2.0 + github.com/go-playground/validator/v10 v10.4.1 github.com/go-sql-driver/mysql v1.6.0 - github.com/golang/protobuf v1.5.2 // indirect + github.com/golang/mock v1.6.0 + github.com/golang/protobuf v1.5.2 + github.com/google/uuid v1.3.0 github.com/gwatts/gin-adapter v0.0.0-20170508204228-c44433c485ad + github.com/huandu/go-assert v1.1.5 github.com/huandu/go-clone v1.3.0 - github.com/kr/text v0.2.0 // indirect + github.com/jmoiron/sqlx v1.3.4 + github.com/json-iterator/go v1.1.12 + github.com/lestrrat-go/strftime v1.0.1 github.com/mattn/go-isatty v0.0.12 - github.com/mitchellh/mapstructure v1.4.1 // indirect + github.com/moby/sys/mount v0.3.2 + github.com/moby/sys/mountinfo v0.6.1 github.com/nicksnyder/go-i18n/v2 v2.1.2 - github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect - github.com/opentracing/opentracing-go v1.2.0 // indirect - github.com/pelletier/go-toml v1.9.3 // indirect + github.com/opencontainers/runtime-spec v1.0.2 + github.com/opensearch-project/opensearch-go v1.1.0 github.com/pkg/errors v0.9.1 - github.com/prometheus/client_golang v1.11.0 + github.com/prometheus/client_golang v1.12.1 github.com/prometheus/client_model v0.2.0 - github.com/prometheus/common v0.30.0 - github.com/prometheus/mysqld_exporter v0.13.0 + github.com/prometheus/common v0.32.1 + github.com/prometheus/mysqld_exporter v0.14.0 github.com/prometheus/node_exporter v1.2.2 + github.com/shirou/gopsutil/v3 v3.21.6 github.com/sirupsen/logrus v1.8.1 - github.com/smartystreets/goconvey v1.6.4 - github.com/spf13/afero v1.5.1 // indirect + github.com/smartystreets/goconvey v1.7.2 + github.com/spf13/afero v1.5.1 github.com/spf13/cast v1.3.0 github.com/spf13/cobra v0.0.3 github.com/spf13/viper v1.3.2 github.com/stretchr/testify v1.7.0 - go.uber.org/atomic v1.9.0 // indirect - golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985 // indirect - golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect - golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c + golang.org/x/net v0.0.0-20211216030914-fe4d6282115f + golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 golang.org/x/text v0.3.6 + google.golang.org/protobuf v1.26.0 gopkg.in/alecthomas/kingpin.v2 v2.2.6 - gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 + gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.0 ) + +require ( + github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // indirect + github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect + github.com/beevik/ntp v0.3.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash v1.1.0 // indirect + github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf // indirect + github.com/coreos/go-systemd/v22 v22.1.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/docker/go-units v0.4.0 // indirect + github.com/dolthub/vitess v0.0.0-20210530214338-7755381e6501 // indirect + github.com/ema/qdisc v0.0.0-20200603082823-62d0308e3e00 // indirect + github.com/fsnotify/fsnotify v1.4.9 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-logfmt/logfmt v0.5.1 // indirect + github.com/go-ole/go-ole v1.2.4 // indirect + github.com/go-playground/locales v0.13.0 // indirect + github.com/go-playground/universal-translator v0.17.0 // indirect + github.com/godbus/dbus v0.0.0-20190402143921-271e53dc4968 // indirect + github.com/godbus/dbus/v5 v5.0.3 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect + github.com/google/go-cmp v0.5.6 // indirect + github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99 // indirect + github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 // indirect + github.com/hashicorp/golang-lru v0.5.3 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/hodgesds/perf-utils v0.2.5 // indirect + github.com/illumos/go-kstat v0.0.0-20210513183136-173c9b0a9973 // indirect + github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/josharian/native v0.0.0-20200817173448-b6b71def0850 // indirect + github.com/jsimonetti/rtnetlink v0.0.0-20210713125558-2bfdf1dbdbd6 // indirect + github.com/jtolds/gls v4.20.0+incompatible // indirect + github.com/kr/text v0.2.0 // indirect + github.com/leodido/go-urn v1.2.0 // indirect + github.com/lufia/iostat v1.1.0 // indirect + github.com/magiconair/properties v1.8.0 // indirect + github.com/mattn/go-xmlrpc v0.0.3 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect + github.com/mdlayher/genetlink v1.0.0 // indirect + github.com/mdlayher/netlink v1.4.1 // indirect + github.com/mdlayher/socket v0.0.0-20210307095302-262dc9984e00 // indirect + github.com/mdlayher/wifi v0.0.0-20200527114002-84f0b9457fdd // indirect + github.com/mitchellh/hashstructure v1.0.0 // indirect + github.com/mitchellh/mapstructure v1.1.2 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect + github.com/oliveagle/jsonpath v0.0.0-20180606110733-2e52cf6e6852 // indirect + github.com/opentracing/opentracing-go v1.1.0 // indirect + github.com/pelletier/go-toml v1.9.3 // indirect + github.com/pierrec/lz4 v2.6.0+incompatible // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/procfs v0.7.3 // indirect + github.com/safchain/ethtool v0.0.0-20201023143004-874930cb3ce0 // indirect + github.com/shopspring/decimal v0.0.0-20191130220710-360f2bc03045 // indirect + github.com/smartystreets/assertions v1.2.0 // indirect + github.com/soundcloud/go-runit v0.0.0-20150630195641-06ad41a06c4a // indirect + github.com/spf13/jwalterweatherman v1.0.0 // indirect + github.com/spf13/pflag v1.0.3 // indirect + github.com/src-d/go-oniguruma v1.1.0 // indirect + github.com/tklauser/go-sysconf v0.3.6 // indirect + github.com/tklauser/numcpus v0.2.2 // indirect + github.com/ugorji/go/codec v1.1.7 // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/multierr v1.6.0 // indirect + golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e // indirect + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect + google.golang.org/genproto v0.0.0-20200825200019-8632dd797987 // indirect + google.golang.org/grpc v1.31.0 // indirect + gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect + gopkg.in/src-d/go-errors.v1 v1.0.0 // indirect +) diff --git a/go.sum b/go.sum index ff5d2e7f487f2bdab4b650bd7540c1f2bb6ed66f..96ce62d52b027372b6ea44d0c29d703d29f93486 100644 --- a/go.sum +++ b/go.sum @@ -34,6 +34,7 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7 github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/DATA-DOG/go-sqlmock v1.4.0/go.mod h1:3TucWNLPFOLcHhha1CPp7Kis1UG2h/AqGROPyOeZzsM= github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= @@ -41,6 +42,8 @@ github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk= +github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= @@ -50,8 +53,10 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 h1:AUNCr9CiJuwrRYS3XieqF+Z9B9gNxo/eANAJCF2eiN4= -github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= +github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc= +github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= +github.com/aliyun/aliyun-log-go-sdk v0.1.27 h1:fXtaOAcdR3DsqN9GZkfJue8B2Dba0TV+8Ahwq4o+y5g= +github.com/aliyun/aliyun-log-go-sdk v0.1.27/go.mod h1:aBG0R+MWRTgvlIODQkz+a3/RM9bQYKsmSbKdbIx4vpc= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= @@ -61,6 +66,7 @@ github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go v1.42.27/go.mod h1:OGr6lGMAKGlG9CVrYnWYDKIyb829c6EVBRjxqjmPepc= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= github.com/beevik/ntp v0.3.0 h1:xzVrPrE4ziasFXgBVBZJDP0Wg/KpMwk2KHJ4Ba8GrDw= github.com/beevik/ntp v0.3.0/go.mod h1:hIHWr+l3+/clUnF44zdK+CWW7fO8dR5cIylAQ76NRpg= @@ -69,32 +75,44 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw= +github.com/bluele/gcache v0.0.2/go.mod h1:m15KV+ECjptwSPxKhOhQoAFQVtUFjTVkc3H8o0t/fp0= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= +github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/cilium/ebpf v0.4.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= github.com/cilium/ebpf v0.5.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= github.com/cilium/ebpf v0.6.2 h1:iHsfF/t4aW4heW2YKfeHrVPGdtYTL4C4KocpM8KTSnI= github.com/cilium/ebpf v0.6.2/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58 h1:F1EaeKL/ta07PY/k9Os/UFtwERei2/XzGemhpGnBKNg= +github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= +github.com/containerd/cgroups v1.0.1 h1:iJnMvco9XGvKUvNQkv88bE4uJXxRQH18efbKo9w5vHQ= +github.com/containerd/cgroups v1.0.1/go.mod h1:0SJrPIenamHDcZhEcJMNBB85rHcUsw4f25ZfBiPYRkU= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU= github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd/v22 v22.1.0 h1:kq/SbG2BCKLkDKkjQf5OWwKWUKj1lgs3lFI4PxnR5lg= +github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -102,12 +120,18 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs 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/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/didi/gendry v1.7.0 h1:dFR6+TVCnbjvLfNiGN53xInG/C5HqG7u0gfnkF5J/Vo= +github.com/didi/gendry v1.7.0/go.mod h1:cSLuShZ1Zbs1S05RIOLNQv616aBaOQ1BDrXJP9A3J+M= +github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dolthub/go-mysql-server v0.10.0 h1:T9qNuDnWxITz/4Syn0hm6L+lO1DYwD3pFBHyuVx7qO4= github.com/dolthub/go-mysql-server v0.10.0/go.mod h1:nY+4DzVnlryGHlnvkeDjplQH3SLBSymHs0woPHrBIfw= github.com/dolthub/sqllogictest/go v0.0.0-20201107003712-816f3ae12d81/go.mod h1:siLfyv2c92W1eN/R4QqG/+RjjX5W2+gCTRjZxBjI3TY= github.com/dolthub/vitess v0.0.0-20210530214338-7755381e6501 h1:QO+maZZoP4PUwS5Clk/lo5AvZ8J5jHevbC/tTAfLe70= github.com/dolthub/vitess v0.0.0-20210530214338-7755381e6501/go.mod h1:hUE8oSk2H5JZnvtlLBhJPYC8WZCA5AoSntdLTcBvdBM= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= @@ -122,8 +146,11 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7 github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239 h1:Ghm4eQYC0nEPnSJdVkTrXpu9KtoVCSo1hg7mtI7G9KU= github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239/go.mod h1:Gdwt2ce0yfBxPvZrHkprdPPTTS3N5rwmLE8T22KBXlw= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/felixge/fgprof v0.9.1 h1:E6FUJ2Mlv043ipLOCFqo8+cHo9MhQ203E2cdEK/isEs= +github.com/felixge/fgprof v0.9.1/go.mod h1:7/HK6JFtFaARhIljgP2IV8rJLIoHDoOYoUphsnGvqxE= github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= +github.com/frankban/quicktest v1.10.2/go.mod h1:K+q6oSqb0W0Ininfk863uOk1lMy69l/P6txr3mVT54s= github.com/frankban/quicktest v1.11.3 h1:8sXhOn0uLys67V8EsXLc6eszDs8VXWxL3iRvebPhedY= github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= @@ -144,12 +171,16 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2 github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.10.0 h1:dXFJfIHVvUcpSgDOV+Ne6t7jXri8Tfv2uOLHUZ2XNuo= github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= -github.com/go-kit/log v0.1.0 h1:DGJh0Sm43HbOeYDNnVZFl8BvcYVvjD5bqYJvp0REbwQ= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-kit/log v0.2.0 h1:7i2K3eKTos3Vc0enKCfnVcgHh2olr/MyfboYq7cAcFw= +github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logfmt/logfmt v0.5.0 h1:TrB8swr/68K7m9CcGut2g3UOihhbcbiMAYiuTXdEih4= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA= +github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI= +github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM= github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= @@ -161,16 +192,22 @@ github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7a github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/godbus/dbus v0.0.0-20190402143921-271e53dc4968 h1:s+PDl6lozQ+dEUtUtQnO7+A2iPG3sK1pI4liU+jxn90= github.com/godbus/dbus v0.0.0-20190402143921-271e53dc4968/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= +github.com/godbus/dbus/v5 v5.0.3 h1:ZqHaoEF7TBzh4jzPmqVhE/5A1z9of6orkAe5uHoAeME= +github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -184,6 +221,8 @@ github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -225,11 +264,14 @@ github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200615235658-03e1cf38a040/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99 h1:Ak8CrdlwwXwAZxzS66vgPt4U8yUZX7JwLvVR58FN5jM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= @@ -284,6 +326,10 @@ github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869 h1:IPJ3dvxmJ4uczJe5YQdrYB16oTJlGSC/OyZDqUk9xX4= github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869/go.mod h1:cJ6Cj7dQo+O6GJNiMx+Pa94qKj+TG8ONdKHgMNIyyag= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/jmoiron/sqlx v1.3.4 h1:wv+0IJZfL5z0uZoUjlpKgHkgaFSYD+r9CfrXjEXsO7w= +github.com/jmoiron/sqlx v1.3.4/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/josharian/native v0.0.0-20200817173448-b6b71def0850 h1:uhL5Gw7BINiiPAo24A2sxkcDI0Jt/sqp1v5xQCniEFA= github.com/josharian/native v0.0.0-20200817173448-b6b71def0850/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= @@ -304,8 +350,9 @@ github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/u github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= @@ -313,6 +360,8 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -332,6 +381,8 @@ github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc h1:RKf14vYWi2t github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is= github.com/lestrrat-go/strftime v1.0.1 h1:o7qz5pmLzPDLyGW4lG6JvTKPUfTFXwe+vOamIYWtnVU= github.com/lestrrat-go/strftime v1.0.1/go.mod h1:E1nN3pCbtMSu1yjSVeyuRFVm/U0xoR76fd03sz+Qz4g= +github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= github.com/lufia/iostat v1.1.0 h1:Z1wa4Hhxwi8uSKfgRsFc5RLtt3SuFPIOgkiPGkUtHDY= @@ -345,6 +396,8 @@ github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNx github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= +github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-xmlrpc v0.0.3 h1:Y6WEMLEsqs3RviBrAa1/7qmbGB7DVD3brZIbqMbQdGY= github.com/mattn/go-xmlrpc v0.0.3/go.mod h1:mqc2dz7tP5x5BKlCahN/n+hs7OSZKJkS9JsHNBRlrxA= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= @@ -378,15 +431,19 @@ github.com/mitchellh/hashstructure v1.0.0 h1:ZkRJX1CyOoTkar7p/mLS5TZU4nJ1Rn/F8u9 github.com/mitchellh/hashstructure v1.0.0/go.mod h1:QjSHrPWS+BGUVBYkbTZWEnOh3G1DutKwClXU/ABz6AQ= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= -github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/moby/sys/mount v0.3.2 h1:uq/CiGDZPvr+c85RYHtKIUORFbmavBUyWH3E1NEyjqI= +github.com/moby/sys/mount v0.3.2/go.mod h1:iN27Ec0LtJ0Mx/++rE6t6mTdbbEEZd+oKfAHP1y6vHs= +github.com/moby/sys/mountinfo v0.6.1 h1:+H/KnGEAGRpTrEAqNVQ2AM3SiwMgJUt/TXj+Z8cmCIc= +github.com/moby/sys/mountinfo v0.6.1/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= @@ -409,12 +466,15 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/opencontainers/runtime-spec v1.0.2 h1:UfAcuLBJB9Coz72x1hgl8O5RVzTdNiaglX6v2DM6FI0= +github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opensearch-project/opensearch-go v1.1.0 h1:eG5sh3843bbU1itPRjA9QXbxcg8LaZ+DjEzQH9aLN3M= +github.com/opensearch-project/opensearch-go v1.1.0/go.mod h1:+6/XHCuTH+fwsMJikZEWsucZ4eZMma3zNSeLrTtVGbo= github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= -github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= -github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= @@ -428,6 +488,8 @@ github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCko github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pierrec/lz4 v2.6.0+incompatible h1:Ix9yFKn1nSPBLFl/yZknTp8TU5G4Ps0JDmguYK6iH1A= +github.com/pierrec/lz4 v2.6.0+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -443,9 +505,9 @@ github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.10.0/go.mod h1:WJM3cc3yu7XKBKa/I8WeZm+V3eltZnBwfENSU7mdogU= -github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.12.1 h1:ZiaPsmm9uiBeaSMRznKsCDNtPCS0T3JVDGF+06gjBzk= +github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -457,17 +519,14 @@ github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8 github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.15.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s= -github.com/prometheus/common v0.18.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s= -github.com/prometheus/common v0.24.0/go.mod h1:H6QK/N6XVT42whUeIdI3dp36w49c+/iMDk7UAI2qm7Q= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/common v0.29.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= -github.com/prometheus/common v0.30.0 h1:JEkYlQnpzrzQFxi6gnukFPdQ+ac82oRhzMcIduJu/Ug= -github.com/prometheus/common v0.30.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= -github.com/prometheus/exporter-toolkit v0.5.1/go.mod h1:OCkM4805mmisBhLmVFw858QYi3v0wKdY6/UxrT0pZVg= +github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4= +github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= github.com/prometheus/exporter-toolkit v0.6.0/go.mod h1:ZUBIj498ePooX9t/2xtDjeQYwvRpiPP2lh5u4iblj2g= -github.com/prometheus/mysqld_exporter v0.13.0 h1:eU2cGRb0eAqAiOZaqWcAoIfIizVt6MZGIYUmKmQqQZ0= -github.com/prometheus/mysqld_exporter v0.13.0/go.mod h1:ZsOL7ddSifE8uyWVmJ1Yq7oX8uZFrCUASWHVD/ybY+A= +github.com/prometheus/exporter-toolkit v0.7.1/go.mod h1:ZUBIj498ePooX9t/2xtDjeQYwvRpiPP2lh5u4iblj2g= +github.com/prometheus/mysqld_exporter v0.14.0 h1:+mWuDBfd7WHX3PETrj3lISh3YEBdyjprPadCwOwmhAw= +github.com/prometheus/mysqld_exporter v0.14.0/go.mod h1:VgQzHnGyvOD/WyLOv/ozQuBAb4/TEcPhqnr/WzwM+no= github.com/prometheus/node_exporter v1.2.2 h1:ryGA5q75gD2IFnR7rOEXA8sB5Wd1FV+v+J//Jou6bck= github.com/prometheus/node_exporter v1.2.2/go.mod h1:S/l8/X/V4kdhdG4fPMTgIrUwyYohUWZVHETm+X4yxsI= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= @@ -476,8 +535,9 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.7.2 h1:zE6zJjRS9S916ptrZ326OU0++1XRwHgxkvCFflxx6Fo= github.com/prometheus/procfs v0.7.2/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= +github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= @@ -488,9 +548,9 @@ github.com/safchain/ethtool v0.0.0-20201023143004-874930cb3ce0/go.mod h1:Z0q5wiB github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= github.com/sanity-io/litter v1.2.0 h1:DGJO0bxH/+C2EukzOSBmAlxmkhVMGqzvcx/rvySYw9M= github.com/sanity-io/litter v1.2.0/go.mod h1:JF6pZUFgu2Q0sBZ+HSV35P8TVPI1TTzEwyu9FXAw2W4= -github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= -github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shirou/gopsutil/v3 v3.21.6 h1:vU7jrp1Ic/2sHB7w6UNs7MIkn7ebVtTb5D9j45o9VYE= +github.com/shirou/gopsutil/v3 v3.21.6/go.mod h1:JfVbDpIBLVzT8oKbvMg9P3wEIMDDpVn+LwHTKj0ST88= github.com/shopspring/decimal v0.0.0-20191130220710-360f2bc03045 h1:8CnFGhoe92Izugjok8nZEGYCNovJwdRFYwrEiLtG6ZQ= github.com/shopspring/decimal v0.0.0-20191130220710-360f2bc03045/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= @@ -499,12 +559,15 @@ github.com/siebenmann/go-kstat v0.0.0-20210513183136-173c9b0a9973/go.mod h1:G81a github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= +github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs= +github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg= +github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/soundcloud/go-runit v0.0.0-20150630195641-06ad41a06c4a h1:os5OBNhwOwybXZMNLqT96XqtjdTtwRFw2w08uluvNeI= @@ -536,10 +599,15 @@ github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRci github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/tebeka/strftime v0.1.4 h1:e0FKSyxthD1Xk4cIixFPoyfD33u2SbjNngOaaC3ePoU= github.com/tebeka/strftime v0.1.4/go.mod h1:7wJm3dZlpr4l/oVK0t1HYIc4rMzQ2XJlOMIUJUJH6XQ= +github.com/tklauser/go-sysconf v0.3.6 h1:oc1sJWvKkmvIxhDHeKWvZS4f6AW+YcoguSfRF2/Hmo4= +github.com/tklauser/go-sysconf v0.3.6/go.mod h1:MkWzOF4RMCshBAMXuhXJs64Rte09mITnppBXY/rYEFI= +github.com/tklauser/numcpus v0.2.2 h1:oyhllyrScuYI6g+h/zUvNXNp1wy7x8qQy3t/piefldA= +github.com/tklauser/numcpus v0.2.2/go.mod h1:x3qojaO3uyYt0i56EW/VUYs7uBvdl2fkfZFu0T9wgjM= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= @@ -548,11 +616,14 @@ github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= @@ -564,9 +635,8 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= -go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= @@ -584,7 +654,6 @@ golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e h1:gsTQYXdTw2Gq7RBsWvlQ91b+aEQ6bXFUngBGuR8sPpI= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -617,6 +686,7 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -661,9 +731,10 @@ golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985 h1:4CSI6oo7cOjJKajidEljs9h+uP0rRZBPPPhcCbj5mw8= -golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211216030914-fe4d6282115f h1:hEYJvxw1lSnWIl8X9ofsYMklzaDs90JI2az5YMd4fPM= +golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -678,6 +749,7 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -739,14 +811,17 @@ golang.org/x/sys v0.0.0-20210123111255-9b0068b26619/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210216163648-f7da38b97c65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210316164454-77fc1eacc6aa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 h1:XfKQ4OlFl8okEOr5UvAqFRVj8pY/4yfcXrddB8qAbU0= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -762,6 +837,7 @@ golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= @@ -803,9 +879,12 @@ golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -911,7 +990,7 @@ gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qS gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= -gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= diff --git a/lib/command/command.go b/lib/command/command.go new file mode 100644 index 0000000000000000000000000000000000000000..f5390c28ef20155a0c977a1740500795e39fbeb9 --- /dev/null +++ b/lib/command/command.go @@ -0,0 +1,44 @@ +package command + +import "context" + +// Command Abstract command interface, +// represents an executable object which accept a parameter and return a result or an error. +// Execution result may be get asynchronously. +type Command interface { + //Execute Execute the command with ExecutionContext + Execute(*ExecutionContext) error + + //DefaultParam returns the default parameter of the command. + //default parameter may be used to deserialize input parameter by a command handler from HTTP request. + DefaultParam() interface{} + + //ResponseType Returns response DataType of the command + ResponseType() DataType +} + +// DataType an enum type of data type of command result +type DataType string + +const ( + //TypeStructured Structured result, may be struct, map, etc. + //Structured data Will be serialize when storing result or deserialize when loading result. + TypeStructured DataType = "structured" + + //TypeText plain text result, may be a string. + //Text data won't be serialize or deserialize when storing or loading result. + TypeText DataType = "text" + + //TypeBinary binary result, may be a []byte. + //Binary data won't be serialize or deserialize when storing or loading result. + TypeBinary DataType = "binary" +) + +func Execute(command Command, param interface{}) (interface{}, error) { + execCtx := NewInputExecutionContext(context.Background(), param) + err := command.Execute(execCtx) + if err != nil { + return nil, err + } + return execCtx.Output().Get() +} diff --git a/lib/command/context.go b/lib/command/context.go new file mode 100644 index 0000000000000000000000000000000000000000..af0acb52bd5c94c009ff0a4afaffc53d78e616d3 --- /dev/null +++ b/lib/command/context.go @@ -0,0 +1,48 @@ +package command + +import "context" + +// ExecutionContext Execution Context for a Command to be executed with. +// Contains Input, Output, and can be canceled by a context.CancelFunc +type ExecutionContext struct { + input *Input + output *Output + cancel context.CancelFunc + // logger +} + +// NewExecutionContext Creates a new ExecutionContext with an Input +func NewExecutionContext(input *Input) *ExecutionContext { + ret := &ExecutionContext{ + input: input, + output: newOutput(), + cancel: nil, + } + ret.input.ctx, ret.cancel = context.WithCancel(ret.input.ctx) + return ret +} + +// NewInputExecutionContext Create a new ExecutionContext with context.Context and a param +// A shortcut function of NewExecutionContext(NewInput(ctx, param)) +func NewInputExecutionContext(ctx context.Context, param interface{}) *ExecutionContext { + return NewExecutionContext(NewInput(ctx, param)) +} + +// Input Returns Input of the ExecutionContext +func (execCtx *ExecutionContext) Input() *Input { + return execCtx.input +} + +// Output Returns Output of the ExecutionContext +func (execCtx *ExecutionContext) Output() *Output { + return execCtx.output +} + +// Cancel Cancel execution of the ExecutionContext. +// Caller will get a context.Canceled error when getting result. +func (execCtx *ExecutionContext) Cancel() { + err := execCtx.Output().FinishErr(context.Canceled) // use custom canceled error? + if err == nil { + execCtx.cancel() + } +} diff --git a/lib/command/error.go b/lib/command/error.go new file mode 100644 index 0000000000000000000000000000000000000000..c979ee4355d25ece78479d47492f67d31470982a --- /dev/null +++ b/lib/command/error.go @@ -0,0 +1,9 @@ +package command + +import "errors" + +var TimeoutErr = errors.New("command: wait timeout") +var AlreadyFinishedErr = errors.New("command: command already finished") +var ExecutionNotFoundErr = errors.New("command: execution not found") +var ExecutionAlreadyExistsErr = errors.New("command: execution already exists") +var BadTaskFunc = errors.New("command: bad task function") diff --git a/lib/command/input.go b/lib/command/input.go new file mode 100644 index 0000000000000000000000000000000000000000..2cddaa982e7ce52c4e95e45a4b093228a332742a --- /dev/null +++ b/lib/command/input.go @@ -0,0 +1,45 @@ +package command + +import "context" + +// Annotation type alias for annotation data +// May contains some extra data out of input parameter like trace id, request id. +type Annotation map[string]interface{} + +// Input command execution input. +// Used to pack context.Context, parameter and annotation into a single object passed to Command. +type Input struct { + ctx context.Context + annotation Annotation + param interface{} +} + +// NewInput Creates a new Input with context.Context and parameter +func NewInput(ctx context.Context, param interface{}) *Input { + return &Input{ctx: ctx, annotation: make(Annotation), param: param} +} + +// Context Returns the context.Context of an Input +func (input *Input) Context() context.Context { + return input.ctx +} + +// Annotation Returns the Annotation of an Input +func (input *Input) Annotation() Annotation { + return input.annotation +} + +// WithAnnotation Adds an Annotation key-value into an Input +func (input *Input) WithAnnotation(key string, value interface{}) *Input { + input.annotation[key] = value + return input +} + +// Param Returns the parameter of an Input +func (input *Input) Param() interface{} { + return input.param +} + +func (input *Input) WithRequestTaskToken(requestId string) *Input { + return input.WithAnnotation(RequestTaskTokenKey, requestId) +} diff --git a/lib/command/input_test.go b/lib/command/input_test.go new file mode 100644 index 0000000000000000000000000000000000000000..47eebc7ab8d1b49529f55df894ca7b8d1f1c4a4a --- /dev/null +++ b/lib/command/input_test.go @@ -0,0 +1,14 @@ +package command + +import ( + "context" + "testing" +) + +func TestInput(t *testing.T) { + ctx := context.Background() + input := NewInput(ctx, "arg") + if input.Context() != ctx || input.Param() != "arg" || input.Annotation() == nil { + t.Error("input init wrong") + } +} diff --git a/lib/command/output.go b/lib/command/output.go new file mode 100644 index 0000000000000000000000000000000000000000..28fc05a6a21fec5bcdd0b2e4d789c9a584c83a53 --- /dev/null +++ b/lib/command/output.go @@ -0,0 +1,142 @@ +package command + +import ( + "sync" + "sync/atomic" + "time" +) + +// Output Command execution output, used to handle command result, error, notify command finish event... +type Output struct { + lock sync.Mutex + result interface{} + err error + ok bool + finishChan chan struct{} + finished bool + progress atomic.Value +} + +// OutputStatus A snapshot of execution Output +type OutputStatus struct { + //Finished The execution is finished or not + Finished bool + //Ok The execution is finished successfully or not + Ok bool + //Result Success result + Result interface{} + //Err Fail error + Err string + //Progress progress data + Progress interface{} +} + +func newOutput() *Output { + return &Output{ + result: nil, + err: nil, + finishChan: make(chan struct{}), + finished: false, + } +} + +// FinishOk mark the command execution succeed with a result +// Returns AlreadyFinishedErr when execution was already finished +func (output *Output) FinishOk(result interface{}) error { + output.lock.Lock() + defer output.lock.Unlock() + if output.finished { + return AlreadyFinishedErr + } + output.finished = true + output.ok = true + output.result = result + close(output.finishChan) + return nil +} + +// FinishErr mark the command execution failed with an error +// Returns AlreadyFinishedErr when execution was already finished +func (output *Output) FinishErr(err error) error { + output.lock.Lock() + defer output.lock.Unlock() + if output.finished { + return AlreadyFinishedErr + } + output.finished = true + output.ok = false + output.err = err + close(output.finishChan) + return nil +} + +// Finished return the command execution is finished or not +func (output *Output) Finished() bool { + output.lock.Lock() + ret := output.finished + output.lock.Unlock() + return ret +} + +// UpdateProgress Update command execution progress. +func (output *Output) UpdateProgress(progress interface{}) { + output.progress.Store(progress) +} + +// Progress Get current command execution progress. +func (output *Output) Progress() interface{} { + return output.progress.Load() +} + +// Get Get command execution result or error synchronously. +func (output *Output) Get() (interface{}, error) { + <-output.finishChan + + output.lock.Lock() + defer output.lock.Unlock() + return output.result, output.err +} + +// GetWithTimeout Get command execution result or error synchronously with a timeout. +// Will return a timeout error when the execution not return a result after the timeout duration. +func (output *Output) GetWithTimeout(timeout time.Duration) (interface{}, error) { + ch := output.finishChan + select { + case <-ch: + return output.result, output.err + case <-time.After(timeout): + return nil, TimeoutErr + } +} + +// Ok Return whether the command execution is finished and with a success result +func (output *Output) Ok() bool { + output.lock.Lock() + ret := output.ok + output.lock.Unlock() + return ret +} + +// Done Return a chan to wait the command execution finished. +// When execution finished, the chan will be closed. +func (output *Output) Done() <-chan struct{} { + return output.finishChan +} + +// Status Return a snapshot of the execution Output. See OutputStatus +func (output *Output) Status() OutputStatus { + output.lock.Lock() + errStr := "" + if output.err != nil { + errStr = output.err.Error() + } + ret := OutputStatus{ + Finished: output.finished, + Ok: output.ok, + Result: output.result, + Err: errStr, + Progress: output.Progress(), + } + output.lock.Unlock() + return ret +} diff --git a/lib/command/output_test.go b/lib/command/output_test.go new file mode 100644 index 0000000000000000000000000000000000000000..8eea9807932e97c97f0252b7ab309fa1ec591faf --- /dev/null +++ b/lib/command/output_test.go @@ -0,0 +1,161 @@ +package command + +import ( + "errors" + "testing" + "time" +) + +func TestOutput_Init(t *testing.T) { + out := newOutput() + if out.Finished() == true { + t.Error("initial output MUST NOT be finished") + } +} + +func TestOutput_FinishOk(t *testing.T) { + out := newOutput() + + err := out.FinishOk("result") + if err != nil { + t.Error("finish failed") + } + if !out.Finished() { + t.Error("should be finished") + } + if !out.Ok() { + t.Error("should be ok") + } + + result, err := out.Get() + if err != nil { + t.Error("get result should success") + } + if result != "result" { + t.Error("result wrong") + } +} + +func TestOutput_FinishErr(t *testing.T) { + out := newOutput() + + err := out.FinishErr(errors.New("errors")) + if err != nil { + t.Error("finish failed") + } + if !out.Finished() { + t.Error("should be finished") + } + if out.Ok() { + t.Error("should not be ok") + } + + _, err = out.Get() + if err == nil { + t.Error("get should return errors") + return + } + if err.Error() != "errors" { + t.Error("errors wrong") + } +} + +func TestOutput_Wait(t *testing.T) { + out := newOutput() + go func() { + time.Sleep(time.Millisecond * 200) + out.FinishOk("ok") + }() + result, err := out.Get() + if err != nil || result != "ok" { + t.Error("bad result") + } +} + +func TestOutput_WaitTimeout(t *testing.T) { + out := newOutput() + go func() { + time.Sleep(time.Millisecond * 100) + out.FinishOk("ok") + }() + result, err := out.GetWithTimeout(time.Millisecond * 200) + if err != nil || result != "ok" { + t.Error("bad result") + } +} + +func TestOutput_WaitTimeout2(t *testing.T) { + out := newOutput() + go func() { + time.Sleep(time.Second) + out.FinishOk("ok") + }() + _, err := out.GetWithTimeout(time.Millisecond * 100) + if err == nil || err != TimeoutErr { + t.Error("should be timeout") + } +} + +func TestOutput_Chan(t *testing.T) { + out := newOutput() + go func() { + time.Sleep(time.Millisecond * 200) + out.FinishOk("ok") + }() + select { + case <-out.Done(): + case <-time.After(time.Second): + t.Error("select wait chan timeout") + return + } + if !out.Finished() { + t.Error("should be finished") + } +} + +func TestOutput_Progress(t *testing.T) { + out := newOutput() + out.UpdateProgress(1) + if out.Progress() != 1 { + t.Error("progress should be 1") + } + go func() { + out.UpdateProgress(2) + time.Sleep(time.Millisecond * 200) + out.FinishOk("ok") + }() + status := out.Status() + if status.Finished == true || status.Progress == nil { + t.Error("bad status in progress") + } + _, _ = out.Get() + if out.Progress() != 2 { + t.Error("progress should be 2") + } + status = out.Status() + if status.Finished == false || status.Progress != 2 || !status.Ok || status.Result != "ok" { + t.Error("bad status after done") + } +} + +func TestOutput_DoubleFinish(t *testing.T) { + out := newOutput() + out.FinishOk("ok") + err := out.FinishErr(errors.New("errors")) + if err == nil { + t.Error("double finish should failed") + } + if !out.Ok() { + t.Error("double finish should not change status") + } + + out2 := newOutput() + out2.FinishErr(errors.New("errors")) + err = out2.FinishOk("ok") + if err == nil { + t.Error("double finish should failed") + } + if out2.Ok() { + t.Error("double finish should not change status") + } +} diff --git a/lib/command/task.go b/lib/command/task.go new file mode 100644 index 0000000000000000000000000000000000000000..84b152e2c3077e4f8f3cda6e24343f4b1105f598 --- /dev/null +++ b/lib/command/task.go @@ -0,0 +1,376 @@ +package command + +import ( + "context" + "errors" + "fmt" + "reflect" + "regexp" + "sync" + "time" + + "github.com/google/uuid" +) + +// ProgressFunc function to update execution progress. See Output.UpdateProgress +type ProgressFunc func(interface{}) + +// TaskFunc A function represents a Task. To simplify Command +type TaskFunc func(*Input, ProgressFunc) (interface{}, error) + +// CancelFunc function to be executed when cmd canceled +type CancelFunc func(*ExecutionContext) + +const RequestTaskTokenKey = "reqTaskToken" + +var taskIdPattern = regexp.MustCompile("^[0-9a-zA-Z_-]{8,64}$") + +// Task a Command implement that turns synchronize function call to a Command. +type Task struct { + fn TaskFunc + cancel CancelFunc + defaultParam interface{} + dataType DataType +} + +// NewTask create a new Task +func NewTask(fn TaskFunc, defaultParam interface{}, dataType DataType) *Task { + return &Task{fn: fn, defaultParam: defaultParam, dataType: dataType} +} + +func (t *Task) DefaultParam() interface{} { + return t.defaultParam +} + +func (t *Task) Execute(execContext *ExecutionContext) error { + go func() { + defer func() { + e := recover() + if e != nil { + var err error + var ok bool + if err, ok = e.(error); ok { + _ = execContext.Output().FinishErr(err) + } else { + err = fmt.Errorf("panic: %+v. task: %+v, input: %+v", e, t, execContext.input) + _ = execContext.Output().FinishErr(err) + } + } + }() + + result, err := t.fn(execContext.Input(), execContext.Output().UpdateProgress) + if err == nil { + _ = execContext.Output().FinishOk(result) + } else { + _ = execContext.Output().FinishErr(err) + } + }() + go func() { + <-execContext.Output().Done() + if execContext.Output().err == context.Canceled { + <-execContext.input.ctx.Done() + if execContext.input.ctx.Err() == context.Canceled && t.cancel != nil { + t.cancel(execContext) + } + } + }() + return nil +} + +func (t *Task) ResponseType() DataType { + return t.dataType +} + +// OnCancel sets a callback that will be called when execution canceled. +func (t *Task) OnCancel(cancel CancelFunc) { + t.cancel = cancel +} + +// Execution a running command execution. +// A command can be executed many times. Execution will only lives while a command executing, +// and will be destroyed after execution finished. +type Execution struct { + cmd Command + ctx *ExecutionContext + token ExecutionToken + startAt time.Time + endAt time.Time +} + +// Command returns the cmd of the execution +func (e Execution) Command() Command { + return e.cmd +} + +// ExecutionContext returns the cmd of the execution +func (e Execution) ExecutionContext() *ExecutionContext { + return e.ctx +} + +// Executor runs Command s and maintains Command s' status +// Executor can run Command background and return a ExecutionToken +type Executor struct { + executions sync.Map + id int64 + nowFunc func() int64 + store StatusStore +} + +// NewExecutor create a new Executor with a StatusStore +func NewExecutor(store StatusStore) *Executor { + ret := &Executor{ + nowFunc: func() int64 { + return time.Now().UnixNano() + }, + store: store, + } + ret.id = ret.nowFunc() + return ret +} + +// ExecutionToken A token returned to caller of Executor.Execute. +// Caller can use it lately to query execution result. +type ExecutionToken struct { + id string +} + +// ExecutionTokenFromString convert a string to a ExecutionToken +func ExecutionTokenFromString(s string) ExecutionToken { + return ExecutionToken{ + id: s, + } +} + +// String convert a ExecutionToken to a string +func (t ExecutionToken) String() string { + return t.id +} + +func GenerateTaskId() string { + return uuid.New().String() +} + +// Execute Executes A Command with an Input. +// Returns an ExecutionToken A token returned to caller. +// Caller can use it lately to query execution result. +// When a request id provided via input Annotation "taskReqId", +// Executor will use the request id as the ExecutionToken. +// Execute call with same request id will be executed only once and return the previous execution's ExecutionToken. +// That means, when using request id, the request id MUST be unique all over the time. +// Execution result will be stored persistently and can be queried after process restart. +func (ex *Executor) Execute(cmd Command, input *Input) (ExecutionToken, error) { + var id string + id, fromReq := ex.getTaskId(input) + + if fromReq { + // check request id exists or not + // return prev token when exists + token := ExecutionTokenFromString(id) + if _, ok := ex.GetResult(token); ok { + return token, nil + } + } + execContext := NewExecutionContext(input) + ret := ExecutionToken{id: id} + execution := &Execution{cmd: cmd, ctx: execContext, token: ret} + err := ex.store.Create(ret, execution) + if err != nil { + if err == ExecutionAlreadyExistsErr { + return ret, nil + } + return ExecutionToken{}, err + } + err = cmd.Execute(execContext) + if err != nil { + return ExecutionToken{}, err + } + execution.startAt = time.Now() + ex.executions.Store(id, execution) + + err = ex.store.Store(ret, execution) + if err != nil { + return ExecutionToken{}, err + } + go func(token ExecutionToken, execution *Execution) { + <-execution.ExecutionContext().Output().Done() + execution.endAt = time.Now() + err1 := ex.store.Store(token, execution) + if err1 != nil { + // + } + ex.executions.Delete(id) + }(ret, execution) + return ret, nil +} + +func (ex *Executor) Detach(token ExecutionToken) { + ex.executions.Delete(token.id) +} + +// getTaskId get request id from input if exists, or generates a new token locally. +func (ex *Executor) getTaskId(input *Input) (string, bool) { + if reqIdVar, ok := input.Annotation()[RequestTaskTokenKey]; ok { + if reqId, ok := reqIdVar.(string); ok { + if taskIdPattern.MatchString(reqId) { + return reqId, true + } + } + } + return GenerateTaskId(), false +} + +// GetResult Gets execution result. It will return immediately with the OutputStatus and a +// bool means the execution specified by the token exists or not. +func (ex *Executor) GetResult(token ExecutionToken) (OutputStatus, bool) { + execCtx, ok := ex.getExecContext(token) + if ok { + return execCtx.Output().Status(), true + } + status, err := ex.store.Load(token) + if err != nil { + return OutputStatus{}, false + } + return OutputStatus{ + Finished: status.Finished, + Ok: status.Ok, + Result: status.Result, + Err: status.Err, + Progress: status.Progress, + }, true +} + +// Cancel cancels the execution specified by the token +// If execution has finished or not exists, will return ExecutionNotFoundErr +func (ex *Executor) Cancel(token ExecutionToken) error { + h, ok := ex.GetExecution(token) + if !ok { + return ExecutionNotFoundErr + } + h.ctx.Cancel() + return nil +} + +// GetExecution Gets the execution object. It will return immediately with the Execution and a +// bool means the execution is still running or not. +func (ex *Executor) GetExecution(token ExecutionToken) (*Execution, bool) { + loaded, ok := ex.executions.Load(token.id) + if ok { + h := loaded.(*Execution) + return h, true + } + return nil, false +} + +// WaitResult Wait for execution result synchronously. Returns the +// OutputStatus after execution finished when execution exists. +// If the execution not exists, it will returns with a false second return value immediately. +func (ex *Executor) WaitResult(token ExecutionToken) (OutputStatus, bool) { + if e, ok := ex.GetExecution(token); ok { + _, _ = e.ExecutionContext().Output().Get() + } + return ex.GetResult(token) +} + +func (ex *Executor) getExecContext(token ExecutionToken) (*ExecutionContext, bool) { + h, ok := ex.GetExecution(token) + if !ok { + return nil, false + } + return h.ctx, true +} + +// AllExecutions returns all running executions +func (ex *Executor) AllExecutions() []*Execution { + var ret []*Execution + ex.executions.Range(func(k interface{}, v interface{}) bool { + ret = append(ret, v.(*Execution)) + return true + }) + return ret +} + +// WrapFunc converts a function into a Task +// form of function can be one of: +// +// func() RetType +// func() (RetType, error) +// func() error +// func(arg SomeType) RetType +// func(arg SomeType) (RetType, error) +// func(arg SomeType) error +// func(ctx context.Context, arg SomeType) RetType +// func(ctx context.Context, arg SomeType) (RetType, error) +// func(ctx context.Context, arg SomeType) error +// func(ctx context.Context) RetType +// func(ctx context.Context) (RetType, error) +// func(ctx context.Context) error +func WrapFunc(fn interface{}) *Task { + fnValue := reflect.ValueOf(fn) + fnType := fnValue.Type() + if fnType.Kind() != reflect.Func { + panic("not a valid function") + } + if fnType.NumIn() > 2 && fnType.NumOut() > 2 { + panic("not a valid function") + } + var tCtx = context.Background() + var tErr = errors.New("") + errType := reflect.TypeOf(&tErr).Elem() + ctxType := reflect.TypeOf(&tCtx).Elem() + + ctxOffset := -1 + argOffset := -1 + retOffset := -1 + errOffset := -1 + var defaultParam interface{} = nil + if fnType.NumIn() == 1 { + if fnType.In(0).Implements(ctxType) { + ctxOffset = 0 + } else { + argOffset = 0 + } + } else if fnType.NumIn() == 2 { + ctxOffset = 0 + argOffset = 1 + } + if argOffset >= 0 { + defaultParam = reflect.New(fnType.In(argOffset)).Elem().Interface() + } + if fnType.NumOut() == 1 { + out0 := fnType.Out(0) + if out0.Implements(errType) { + errOffset = 0 + } else { + retOffset = 0 + } + } else if fnType.NumOut() == 2 { + retOffset = 0 + errOffset = 1 + } + + return &Task{fn: func(input *Input, progressFunc ProgressFunc) (interface{}, error) { + //recover() + var retValues []reflect.Value + inValues := make([]reflect.Value, fnType.NumIn()) + if ctxOffset >= 0 { + inValues[ctxOffset] = reflect.ValueOf(input.Context()) + } + if argOffset >= 0 { + inValues[argOffset] = reflect.ValueOf(input.Param()) + } + retValues = fnValue.Call(inValues) + if retValues == nil { + return nil, BadTaskFunc + } + var ret interface{} = nil + var err error = nil + if retOffset >= 0 { + ret = retValues[retOffset].Interface() + } + if errOffset >= 0 { + errValue := retValues[errOffset] + reflect.ValueOf(&err).Elem().Set(errValue) + } + return ret, err + }, defaultParam: defaultParam, dataType: TypeStructured} +} diff --git a/lib/command/task_store.go b/lib/command/task_store.go new file mode 100644 index 0000000000000000000000000000000000000000..37f9ea561bd72b92dc346937614c5c94a4afb0ef --- /dev/null +++ b/lib/command/task_store.go @@ -0,0 +1,236 @@ +package command + +import ( + "bufio" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" + "time" +) + +// StatusStore Abstract storage for execution status +type StatusStore interface { + //Create create a new execution store, and save execution status + //if the token already exists, it will return an ExecutionAlreadyExistsErr + Create(token ExecutionToken, execution *Execution) error + //Store save or update execution status of a token. + Store(token ExecutionToken, execution *Execution) error + //Load load execution status by the token + Load(token ExecutionToken) (StoredStatus, error) + //Delete delete execution data by the token + Delete(token ExecutionToken) error + // CreateStatus create a new execution store, and directly save status + //if the token already exists, it will return an ExecutionAlreadyExistsErr + CreateStatus(token ExecutionToken, status StoredStatus) error + //StoreStatus directly save or update status of a token. + StoreStatus(token ExecutionToken, status StoredStatus) error +} + +// FileTaskStore StatusStore implement that save data in local file system. +// It uses json to serialize/deserialize structured data +type FileTaskStore struct { + dir string + expire time.Duration +} + +// NewFileTaskStore create a new FileTaskStore with a fs dir to store data in. +func NewFileTaskStore(dir string) *FileTaskStore { + return &FileTaskStore{ + dir: dir, + } +} + +// FilePrefix all files stored by FileTaskStore will have a name starts with it. +const FilePrefix = "ocp_agent_task_" + +// StoredStatus struct used to serialize/deserialize execution status +type StoredStatus struct { + ResponseType DataType + Param interface{} + Annotation map[string]interface{} + Finished bool + Ok bool + Result interface{} + Err string + Progress interface{} + StartAt int64 + EndAt int64 +} + +// Path returns file path of the token +func (fts *FileTaskStore) Path(token ExecutionToken) string { + return filepath.Join(fts.dir, FilePrefix+token.String()) +} + +func (fts *FileTaskStore) Create(token ExecutionToken, execution *Execution) error { + status := executionToStoredStatus(execution) + return fts.store(token, status, true) +} + +func (fts *FileTaskStore) Store(token ExecutionToken, execution *Execution) error { + status := executionToStoredStatus(execution) + return fts.store(token, status, false) +} + +func (fts *FileTaskStore) CreateStatus(token ExecutionToken, status StoredStatus) error { + return fts.store(token, status, true) +} + +func (fts *FileTaskStore) StoreStatus(token ExecutionToken, status StoredStatus) error { + return fts.store(token, status, false) +} + +func (fts *FileTaskStore) store(token ExecutionToken, status StoredStatus, create bool) error { + filePath := fts.Path(token) + var flag int + if create { + flag = os.O_CREATE | os.O_RDWR | os.O_EXCL + } else { + flag = os.O_CREATE | os.O_RDWR | os.O_TRUNC + } + f, err := os.OpenFile(filePath, flag, 0644) + if err != nil { + if os.IsExist(err) { + return ExecutionAlreadyExistsErr + } + return fmt.Errorf("open task store file failed %v", err) + } + defer f.Close() + + err = fts.storeToWriter(status, f) + if err != nil { + return fmt.Errorf("write task store data failed %v", err) + } + err = f.Sync() + if err != nil { + return fmt.Errorf("sync task store data failed %v", err) + } + + return nil +} + +func executionToStoredStatus(execution *Execution) StoredStatus { + execCtx := execution.ExecutionContext() + input := execCtx.Input() + status := execCtx.Output().Status() + return StoredStatus{ + ResponseType: execution.Command().ResponseType(), + Param: input.Param(), + Annotation: input.Annotation(), + Finished: status.Finished, + Ok: status.Ok, + Result: status.Result, + Err: status.Err, + Progress: status.Progress, + StartAt: execution.startAt.UnixNano(), + EndAt: execution.endAt.UnixNano(), + } +} + +func (fts *FileTaskStore) storeToWriter(status StoredStatus, f io.Writer) error { + //header := executionToStoredStatus(execution) + isStructured := status.ResponseType == TypeStructured + result := status.Result + if !isStructured { + status.Result = nil + } + headerJson, err := json.Marshal(status) + if err != nil { + return err + } + _, err = f.Write(headerJson) + if err != nil { + return err + } + _, err = f.Write([]byte{'\n'}) + if !isStructured { + if err != nil { + return err + } + switch result.(type) { + case []byte: + _, err = f.Write(result.([]byte)) + case string: + _, err = f.Write([]byte(result.(string))) + } + } + return err +} + +func (fts *FileTaskStore) Load(token ExecutionToken) (StoredStatus, error) { + filePath := fts.Path(token) + f, err := os.Open(filePath) + if err != nil { + return StoredStatus{}, err + } + defer f.Close() + //f.Stat() check size + return fts.loadFromReader(f) +} + +func (fts *FileTaskStore) loadFromReader(f io.Reader) (StoredStatus, error) { + reader := bufio.NewReader(f) + + headerBytes, err := reader.ReadBytes('\n') + if err != nil { + return StoredStatus{}, err + } + header := StoredStatus{} + err = json.Unmarshal(headerBytes, &header) + if err != nil { + return StoredStatus{}, err + } + + if header.ResponseType != TypeStructured { + body, err := ioutil.ReadAll(reader) + if err != nil { + return StoredStatus{}, err + } + if header.ResponseType == TypeBinary { + header.Result = body + } else if header.ResponseType == TypeText { + header.Result = string(body) + } + } + return header, nil +} + +func (fts *FileTaskStore) Delete(token ExecutionToken) error { + filePath := fts.Path(token) + return os.Remove(filePath) +} + +// Cleanup removes all stored files which mtime before expire duration ago. +func (fts *FileTaskStore) Cleanup(expire time.Duration) { + d, err := os.Open(fts.dir) + if err != nil { + return + } + defer d.Close() + names, err := d.Readdirnames(0) + if err != nil { + return + } + now := time.Now() + for _, name := range names { + if !strings.HasPrefix(name, FilePrefix) { + continue + } + p := filepath.Join(fts.dir, name) + + info, err := os.Stat(p) + if err != nil { + continue + } + if info.IsDir() { + continue + } + if now.Sub(info.ModTime()) > expire { + _ = os.Remove(p) + } + } +} diff --git a/lib/command/task_store_test.go b/lib/command/task_store_test.go new file mode 100644 index 0000000000000000000000000000000000000000..9d2806bfec5d48b74bad11144d42193e97276b29 --- /dev/null +++ b/lib/command/task_store_test.go @@ -0,0 +1,181 @@ +package command + +import ( + "fmt" + "os" + "strconv" + "testing" + "time" +) + +func TestCreate(t *testing.T) { + store := NewFileTaskStore(os.TempDir()) + id := strconv.FormatInt(time.Now().UnixNano(), 10) + token := ExecutionToken{id} + execution := structuredExecution() + err := store.Create(token, execution) + if err != nil { + t.Error("first create should success") + } + err = store.Create(token, execution) + if err == nil { + t.Error("second create should fail") + } +} + +func TestStoreLoadStructured(t *testing.T) { + store := NewFileTaskStore(os.TempDir()) + token := ExecutionToken{"123"} + execution := structuredExecution() + err := store.Store(token, execution) + if err != nil { + t.Error("store failed", err) + return + } + status, err := store.Load(token) + if err != nil { + t.Error("load failed", err) + } + fmt.Printf("%+v\n", status) + if status.ResponseType != TypeStructured { + t.Error("bad ResponseType") + } + if status.Param == nil || status.Annotation == nil || status.Finished == false || status.Ok == false || status.Result == nil { + t.Error("bad status") + } + _ = store.Delete(token) +} + +func structuredExecution() *Execution { + input := &Input{ + annotation: map[string]interface{}{ + "command": "testCommand", + }, + param: map[string]interface{}{ + "arg0": 123, + "arg1": "xxx", + }, + } + output := &Output{ + result: map[string]interface{}{ + "a": 1, + "b": "2", + "c": []int{3, 3, 3}, + }, + err: nil, + finished: true, + ok: true, + } + ctx := &ExecutionContext{ + input: input, + output: output, + } + execution := &Execution{ + cmd: &Task{ + dataType: TypeStructured, + }, + ctx: ctx, + } + return execution +} + +func TestStoreLoadText(t *testing.T) { + store := NewFileTaskStore(os.TempDir()) + token := ExecutionToken{"123"} + fmt.Println(store.Path(token)) + input := &Input{ + annotation: map[string]interface{}{ + "command": "testCommand", + }, + param: map[string]interface{}{ + "arg0": 123, + "arg1": "xxx", + }, + } + output := &Output{ + result: "result text", + err: nil, + finished: true, + ok: true, + } + ctx := &ExecutionContext{ + input: input, + output: output, + } + execution := &Execution{ + cmd: &Task{ + dataType: TypeText, + }, + ctx: ctx, + } + err := store.Store(token, execution) + if err != nil { + t.Error("store failed", err) + return + } + status, err := store.Load(token) + if err != nil { + t.Error("load failed", err) + } + fmt.Printf("%+v\n", status) + if status.ResponseType != TypeText { + t.Error("bad ResponseType") + } + if status.Param == nil || status.Annotation == nil || status.Finished == false || status.Ok == false || status.Result == nil { + t.Error("bad status") + } + _ = store.Delete(token) +} + +func TestStoreLoadBinary(t *testing.T) { + store := NewFileTaskStore(os.TempDir()) + token := ExecutionToken{"123"} + fmt.Println(store.Path(token)) + input := &Input{ + annotation: map[string]interface{}{ + "command": "testCommand", + }, + param: map[string]interface{}{ + "arg0": 123, + "arg1": "xxx", + }, + } + output := &Output{ + result: []byte("result binary"), + err: nil, + finished: true, + ok: true, + } + ctx := &ExecutionContext{ + input: input, + output: output, + } + execution := &Execution{ + cmd: &Task{ + dataType: TypeBinary, + }, + ctx: ctx, + } + err := store.Store(token, execution) + if err != nil { + t.Error("store failed", err) + return + } + status, err := store.Load(token) + if err != nil { + t.Error("load failed", err) + } + fmt.Printf("%+v\n", status) + if status.ResponseType != TypeBinary { + t.Error("bad ResponseType") + } + if status.Param == nil || status.Annotation == nil || status.Finished == false || status.Ok == false || status.Result == nil { + t.Error("bad status") + } + _ = store.Delete(token) +} + +func TestCleanup(t *testing.T) { + store := NewFileTaskStore(os.TempDir()) + store.Cleanup(time.Minute) +} diff --git a/lib/command/task_test.go b/lib/command/task_test.go new file mode 100644 index 0000000000000000000000000000000000000000..48ab599b2ce98ed711c30a8ee6b3c9ebd50816eb --- /dev/null +++ b/lib/command/task_test.go @@ -0,0 +1,254 @@ +package command + +import ( + "context" + "errors" + "fmt" + "os" + "sync/atomic" + "testing" + "time" + + "github.com/google/uuid" +) + +var testStore = NewFileTaskStore(os.TempDir()) + +func TestTask_Init(t *testing.T) { + cmd := NewTask(func(*Input, ProgressFunc) (interface{}, error) { + return "", nil + }, "", TypeText) + if cmd.ResponseType() != TypeText || cmd.fn == nil { + t.Error("init wrong") + } +} + +func TestTask_Execute(t *testing.T) { + cmd := NewTask(func(input *Input, progressFunc ProgressFunc) (interface{}, error) { + progressFunc(1) + return "hello " + input.Param().(string), nil + }, "", TypeText) + execCtx := NewInputExecutionContext(context.Background(), "world") + _ = cmd.Execute(execCtx) + result, err := execCtx.Output().Get() + if err != nil || result != "hello world" { + t.Error("bad result") + } + p := execCtx.Output().Progress() + if p != 1 { + t.Error("bad progress") + } +} + +func TestTask_ExecutorInit(t *testing.T) { + executor := NewExecutor(testStore) + if executor.id == 0 || executor.nowFunc == nil { + t.Error("init wrong") + } +} + +func TestTask_Executor(t *testing.T) { + cmd := NewTask(func(input *Input, progressFunc ProgressFunc) (interface{}, error) { + progressFunc(1) + time.Sleep(time.Second) + return "hello " + input.Param().(string), nil + }, "", TypeText) + executor := NewExecutor(testStore) + token, _ := executor.Execute(cmd, NewInput(context.Background(), "world")) + if token.id == "" { + t.Error("bad token") + } + time.Sleep(time.Millisecond * 100) + result, ok := executor.GetResult(token) + if !ok { + t.Error("find result failed") + return + } + if result.Progress != 1 { + t.Error("can not get progress") + } + e, _ := executor.GetExecution(token) + if e.Command() == nil || e.ExecutionContext() == nil { + t.Error("bad execution") + } + execCtx, _ := executor.getExecContext(token) + <-execCtx.Output().Done() + time.Sleep(time.Millisecond * 100) // waiting for clearing execution in memory + // load from storage + result, ok = executor.GetResult(token) + if !ok { + t.Error("find result failed") + return + } + if result.Finished != true || result.Result != "hello world" { + t.Error("bad result") + } +} + +func TestTask_ExecutorReqId(t *testing.T) { + var n int32 = 0 + cmd := NewTask(func(input *Input, progressFunc ProgressFunc) (interface{}, error) { + atomic.AddInt32(&n, 1) + return "hello " + input.Param().(string), nil + }, "", TypeText) + executor := NewExecutor(testStore) + reqId := uuid.New().String() + token, _ := executor.Execute(cmd, NewInput(context.Background(), "world"). + WithRequestTaskToken(reqId)) + if token.id != reqId { + t.Error("bad token from reqId") + } + executor.WaitResult(token) + if n != 1 { + t.Error("should executed") + } + + // duplicated reqId + token, _ = executor.Execute(cmd, NewInput(context.Background(), "world"). + WithRequestTaskToken(reqId)) + if token.id != reqId { + t.Error("bad token from reqId") + } + executor.WaitResult(token) + if n != 1 { + t.Error("should executed only once!") + } + + // new executor, from store + executor = NewExecutor(testStore) + token, _ = executor.Execute(cmd, NewInput(context.Background(), "world"). + WithRequestTaskToken(reqId)) + if token.id != reqId { + t.Error("bad token from reqId") + } + executor.WaitResult(token) + if n != 1 { + t.Error("should executed only once!") + } +} + +func TestTask_ExecutorMissing(t *testing.T) { + executor := NewExecutor(testStore) + _, ok := executor.GetResult(ExecutionToken{"1"}) + if ok { + t.Error("should errors") + } + err := executor.Cancel(ExecutionToken{"1"}) + if err == nil { + t.Error("should errors") + } +} + +func TestTask_Cancel(t *testing.T) { + cmd := NewTask(func(input *Input, progressFunc ProgressFunc) (interface{}, error) { + select { + case <-input.Context().Done(): + return nil, input.Context().Err() + case <-time.After(time.Second): + } + return "hello " + input.Param().(string), nil + }, "", TypeText) + + canceled := make(chan struct{}) + cmd.OnCancel(func(execCtx *ExecutionContext) { + fmt.Println("!!!!!!!!") + close(canceled) + }) + execCtx := NewInputExecutionContext(context.Background(), "world") + _ = cmd.Execute(execCtx) + execCtx.Cancel() + _, err := execCtx.Output().Get() + if err != context.Canceled { + t.Error("should be canceled") + } + select { + case <-canceled: + case <-time.After(time.Second * 5): + t.Error("cmd cancel func not called") + } +} + +func TestTask_ExecutorCancel(t *testing.T) { + executor := NewExecutor(testStore) + cmd := NewTask(func(input *Input, progressFunc ProgressFunc) (interface{}, error) { + select { + case <-input.Context().Done(): + return nil, input.Context().Err() + case <-time.After(time.Second): + } + return "hello " + input.Param().(string), nil + }, "", TypeText) + token, _ := executor.Execute(cmd, NewInput(context.Background(), "world")) + _ = executor.Cancel(token) + execCtx, ok := executor.getExecContext(token) + if !ok { + t.Error("execCtx not found") + return + } + _, err := execCtx.Output().Get() + if err != context.Canceled { + t.Error("should be canceled") + } +} + +func TestTask_ExecutorTasks(t *testing.T) { + executor := NewExecutor(testStore) + cmd := NewTask(func(input *Input, progressFunc ProgressFunc) (interface{}, error) { + select { + case <-input.Context().Done(): + return nil, input.Context().Err() + case <-time.After(time.Millisecond * 100): + } + return "hello " + input.Param().(string), nil + }, "", TypeText) + if len(executor.AllExecutions()) != 0 { + t.Error("executor should be empty") + } + _, _ = executor.Execute(cmd, NewInput(context.Background(), "1")) + _, _ = executor.Execute(cmd, NewInput(context.Background(), "2")) + if len(executor.AllExecutions()) != 2 { + t.Error("executor should have 2 executions") + } +} + +func TestTask_Wrap(t *testing.T) { + task1 := WrapFunc(func(s string) (string, error) { + return s + s, nil + }) + ret, err := Execute(task1, "a") + if err != nil || ret != "aa" { + t.Error("failed") + } + + task2 := WrapFunc(func() (string, error) { + return "hello", nil + }) + ret, err = Execute(task2, nil) + if err != nil || ret != "hello" { + t.Error("failed") + } + + task3 := WrapFunc(func(ctx context.Context) (string, error) { + return "hello", nil + }) + ret, err = Execute(task3, nil) + if err != nil || ret != "hello" { + t.Error("failed") + } + + task4 := WrapFunc(func(ctx context.Context, s string) string { + return s + }) + ret, err = Execute(task4, "test") + if err != nil || ret != "test" { + t.Error("failed") + } + + task5 := WrapFunc(func(ctx context.Context) error { + return errors.New("err") + }) + ret, err = Execute(task5, nil) + if err == nil || err.Error() != "err" || ret != nil { + t.Error("failed") + } +} diff --git a/lib/crypto/aes.go b/lib/crypto/aes.go index 605aefbbc201f2539a50122e07db4de504459f36..4596738cada3576769238e335993528223892d71 100644 --- a/lib/crypto/aes.go +++ b/lib/crypto/aes.go @@ -1,15 +1,3 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - package crypto import ( @@ -59,7 +47,7 @@ func PKCS7UnPadding(origData []byte) []byte { func AesCBCEncrypt(rawData, key []byte) ([]byte, error) { block, err := aes.NewCipher(key) if err != nil { - panic(err) + return nil, err } blockSize := block.BlockSize() @@ -67,7 +55,7 @@ func AesCBCEncrypt(rawData, key []byte) ([]byte, error) { cipherText := make([]byte, blockSize+len(rawData)) iv := cipherText[:blockSize] if _, err := io.ReadFull(rand.Reader, iv); err != nil { - panic(err) + return nil, err } mode := cipher.NewCBCEncrypter(block, iv) @@ -79,19 +67,19 @@ func AesCBCEncrypt(rawData, key []byte) ([]byte, error) { func AesCBCDecrypt(encryptData, key []byte) ([]byte, error) { block, err := aes.NewCipher(key) if err != nil { - panic(err) + return nil, err } blockSize := block.BlockSize() if len(encryptData) < blockSize { - panic("ciphertext too short") + return nil, errors.New("ciphertext too short") } iv := encryptData[:blockSize] encryptData = encryptData[blockSize:] if len(encryptData)%blockSize != 0 { - panic("ciphertext is not a multiple of the block size") + return nil, errors.New("ciphertext is not a multiple of the block size") } mode := cipher.NewCBCDecrypter(block, iv) diff --git a/lib/crypto/aes_test.go b/lib/crypto/aes_test.go index 9e0dfb935b85c1dae8dd8f20f289040356e32c0c..aba9bad891ea7b32e93719a2021f1554ab305e80 100644 --- a/lib/crypto/aes_test.go +++ b/lib/crypto/aes_test.go @@ -1,15 +1,3 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - package crypto import ( diff --git a/lib/crypto/crypto.go b/lib/crypto/crypto.go index 6fe458533c9a3ac8d5b373e3b8f27688c24265f9..88d302616c296f28067f7ac570efa67509f2ff8b 100644 --- a/lib/crypto/crypto.go +++ b/lib/crypto/crypto.go @@ -1,15 +1,3 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - package crypto type CryptoMethod string diff --git a/lib/crypto/plain.go b/lib/crypto/plain.go index 29f37cc91cd639e7ac79ec7c527ef62ae5b0a486..c942f0de25945295b6faf0eb7d43eabd7fc3a80d 100644 --- a/lib/crypto/plain.go +++ b/lib/crypto/plain.go @@ -1,15 +1,3 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - package crypto type PlainCrypto struct{} diff --git a/lib/errors/error.go b/lib/errors/error.go new file mode 100644 index 0000000000000000000000000000000000000000..db60b9b26b9f639c38f14e40316e0e7f75fe0d0c --- /dev/null +++ b/lib/errors/error.go @@ -0,0 +1,112 @@ +package errors + +import ( + "fmt" + "path" + "runtime" + "strconv" + "strings" +) + +type Error struct { + code Code + values []interface{} + cause error + stack string +} + +type FormatFunc func(err *Error) string + +var defaultFormatFunc = func(err *Error) string { + code := err.code + return fmt.Sprintf("Module=%s, kind=%s, code=%s; ", + code.Module, code.Kind.Name, code.Name) + code.Format(err.values...) +} + +var GlobalFormatFunc = defaultFormatFunc + +func (err *Error) Error() string { + ret := defaultFormatFunc(err) + if err.stack != "" { + ret += "\n" + string(err.stack) + } + if err.cause != nil { + ret += "\ncause:\n" + if causeErr, ok := err.cause.(*Error); ok { + ret += causeErr.Error() + } else { + ret += err.cause.Error() + } + } + return ret +} + +func (err *Error) Kind() Kind { + return err.code.Kind +} + +func (err *Error) CodeName() string { + return err.code.Name +} + +func (err *Error) Module() string { + return err.code.Module +} + +func (err *Error) Code() Code { + return err.code +} + +func (err *Error) HttpCode() int { + return err.code.Kind.HttpCode +} + +func (err *Error) Message() string { + return GlobalFormatFunc(err) +} + +func (err *Error) WithCause(cause error) *Error { + err.cause = cause + return err +} + +func (err *Error) WithStack() *Error { + lines := make([]string, 0, 8) + lines = append(lines, "stack:\n") + i := 1 + for { + pc, file, line, ok := runtime.Caller(i) + if !ok { + break + } + fn := runtime.FuncForPC(pc) + fnName := "" + if fn != nil { + fnName = fn.Name() + } + + lines = append(lines, fnName+" ("+path.Base(file)+":"+strconv.Itoa(line)+")\n") + i++ + } + err.stack = strings.Join(lines, "") + return err +} + +func (err *Error) Values() []interface{} { + return err.values +} + +func (err *Error) Cause() error { + return err.cause +} + +func (err *Error) Stack() string { + return err.stack +} + +func HttpCode(err error, def int) int { + if e, ok := err.(*Error); ok { + return e.HttpCode() + } + return def +} diff --git a/lib/errors/error_test.go b/lib/errors/error_test.go new file mode 100644 index 0000000000000000000000000000000000000000..9125df2055f515395f38aa6089662c3545138cbc --- /dev/null +++ b/lib/errors/error_test.go @@ -0,0 +1,28 @@ +package errors + +import ( + "fmt" + "testing" +) + +func TestError(t *testing.T) { + err := InvalidArgument.NewCode("test", "bad_input").NewError("111"). + WithStack() + + fmt.Println(err.Message()) + err2 := Internal.NewCode("test", "internal").NewError().WithCause(err) + fmt.Println(err2.Error()) + + if HttpCode(err, 200) != 400 { + t.Error("bad http code") + } + if err2.Cause() == nil { + t.Error("should have cause") + } + if err.Stack() == "" { + t.Error("should have stack") + } + if KindByName("UNKNOWN") != Unknown { + t.Error("KindByName wrong") + } +} diff --git a/lib/errors/kind.go b/lib/errors/kind.go new file mode 100644 index 0000000000000000000000000000000000000000..b0c5af496fa650adec59969908b2f33b5dae1b5a --- /dev/null +++ b/lib/errors/kind.go @@ -0,0 +1,99 @@ +package errors + +import "fmt" + +// Kind A wide catalog of error code. A Kind implies general error cause and handling suggestion. +type Kind struct { + Id int + Name string + HttpCode int +} + +// Code A concrete type of error. Such as "file not exists" +type Code struct { + Kind Kind + Module string + Name string + MessageTemplate string +} + +var ( + Ok = Kind{Id: 0, Name: "OK", HttpCode: 200} + + Cancelled = Kind{Id: 50, Name: "CANCELLED", HttpCode: 499} + InvalidArgument = Kind{Id: 51, Name: "INVALID_ARGUMENT", HttpCode: 400} + NotFound = Kind{Id: 52, Name: "NOT_FOUND", HttpCode: 404} + AlreadyExists = Kind{Id: 53, Name: "ALREADY_EXISTS", HttpCode: 409} + PermissionDenied = Kind{Id: 54, Name: "PERMISSION_DENIED", HttpCode: 403} + Unauthenticated = Kind{Id: 55, Name: "UNAUTHENTICATED", HttpCode: 401} + ResourceExhausted = Kind{Id: 56, Name: "RESOURCE_EXHAUSTED", HttpCode: 429} + FailedPrecondition = Kind{Id: 57, Name: "FAILED_PRECONDITION", HttpCode: 400} + OutOfRange = Kind{Id: 58, Name: "OUT_OF_RANGE", HttpCode: 400} + Aborted = Kind{Id: 59, Name: "ABORTED", HttpCode: 409} + + Unknown = Kind{Id: 10, Name: "UNKNOWN", HttpCode: 500} + DeadlineExceeded = Kind{Id: 11, Name: "DEADLINE_EXCEEDED", HttpCode: 504} + Internal = Kind{Id: 12, Name: "INTERNAL", HttpCode: 500} + Unavailable = Kind{Id: 13, Name: "UNAVAILABLE", HttpCode: 503} + DataLoss = Kind{Id: 14, Name: "DATA_LOSS", HttpCode: 500} + + byName = map[string]Kind{ + Ok.Name: Ok, + Cancelled.Name: Cancelled, + InvalidArgument.Name: InvalidArgument, + NotFound.Name: NotFound, + AlreadyExists.Name: AlreadyExists, + PermissionDenied.Name: PermissionDenied, + Unauthenticated.Name: Unauthenticated, + ResourceExhausted.Name: ResourceExhausted, + FailedPrecondition.Name: FailedPrecondition, + OutOfRange.Name: OutOfRange, + Aborted.Name: Aborted, + Unknown.Name: Unknown, + DeadlineExceeded.Name: DeadlineExceeded, + Internal.Name: Internal, + Unavailable.Name: Unavailable, + DataLoss.Name: DataLoss, + } +) + +func KindByName(name string) Kind { + return byName[name] +} + +func (k Kind) NewCode(module, code string) Code { + return Code{ + Kind: k, + Module: module, + Name: code, + MessageTemplate: "", + } +} + +func (c Code) NewError(values ...interface{}) *Error { + return &Error{ + code: c, + values: values, + cause: nil, + stack: "", + } +} + +func (c Code) Equals(o Code) bool { + return c.Module == o.Module && c.Name == o.Name && c.Kind.Id == o.Kind.Id +} + +func (c Code) Format(values ...interface{}) string { + if len(values) == 0 { + return c.MessageTemplate + } + if c.MessageTemplate == "" { + return fmt.Sprintf("%+v", values) + } + return fmt.Sprintf(c.MessageTemplate, values...) +} + +func (c Code) WithMessageTemplate(tpl string) Code { + c.MessageTemplate = tpl + return c +} diff --git a/lib/file/custom_scanline.go b/lib/file/custom_scanline.go new file mode 100644 index 0000000000000000000000000000000000000000..5fe245a4ca47288090628daf08f4a696f62149cc --- /dev/null +++ b/lib/file/custom_scanline.go @@ -0,0 +1,20 @@ +package file + +import "bytes" + +// Adaptation of bufio.ScanLines that returns end-of-line carriage returns and line feeds. +func ScanLines(data []byte, atEOF bool) (advance int, token []byte, err error) { + if atEOF && len(data) == 0 { + return 0, nil, nil + } + if i := bytes.IndexByte(data, '\n'); i >= 0 { + // We have a full newline-terminated line. + return i + 1, data[0 : i+1], nil + } + // If we're at EOF, we have a final, non-terminated line. Return it. + if atEOF { + return len(data), data, nil + } + // Request more data. + return 0, nil, nil +} diff --git a/lib/file/directory.go b/lib/file/directory.go new file mode 100644 index 0000000000000000000000000000000000000000..6782e560697314963b9dafb1853098ce55db3ec3 --- /dev/null +++ b/lib/file/directory.go @@ -0,0 +1,134 @@ +package file + +import ( + "io/fs" + "os" + "os/user" + "path/filepath" + "strconv" + + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + "github.com/spf13/afero" +) + +const defaultDirectoryPerm = 0755 + +func (f FileImpl) CreateDirectory(path string) error { + err := os.MkdirAll(path, defaultDirectoryPerm) + if err != nil { + return errors.Errorf("failed to create directory %s: %s", path, err) + } + return nil +} + +func (f FileImpl) CreateDirectoryForUser(path string, userName string, groupName string) error { + err := f.CreateDirectory(path) + if err != nil { + return errors.Wrap(err, "create directory for user") + } + err = f.ChownDirectory(path, userName, groupName, true) + if err != nil { + return errors.Wrap(err, "create directory for user") + } + return nil +} + +func (f FileImpl) RemoveDirectory(path string) error { + err := Fs.RemoveAll(path) + if err != nil { + return errors.Errorf("failed to remove directory %s: %s", path, err) + } + return nil +} + +func (f FileImpl) ChownDirectory(path string, userName string, groupName string, recursive bool) error { + var err error + uid, err := lookForUid(userName) + if err != nil { + return errors.Errorf("failed to chown directory, invalid user name %s: %s", userName, err) + } + gid, err := lookForGid(groupName) + if err != nil { + return errors.Errorf("failed to chown directory, invalid group name %s: %s", groupName, err) + } + if recursive { + err = afero.Walk(Fs, path, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + return os.Lchown(path, uid, gid) + }) + } else { + err = os.Lchown(path, uid, gid) + } + if err != nil { + log.WithFields(log.Fields{ + "path": path, + "userName": userName, + "groupName": groupName, + "recursive": recursive, + }).WithError(err).Error("chown directory failed") + return errors.Errorf("failed to chown directory %s: %s", path, err) + } + log.WithFields(log.Fields{ + "path": path, + "userName": userName, + "groupName": groupName, + "recursive": recursive, + }).Info("chown directory done") + return nil +} + +func lookForUid(userName string) (int, error) { + u, err := user.Lookup(userName) + if err != nil { + return 0, err + } + uid, err := strconv.Atoi(u.Uid) + if err != nil { + return 0, err + } + return uid, nil +} + +func lookForGid(groupName string) (int, error) { + g, err := user.LookupGroup(groupName) + if err != nil { + return 0, err + } + gid, err := strconv.Atoi(g.Gid) + if err != nil { + return 0, err + } + return gid, nil +} + +func (f FileImpl) ListFiles(basePath string, flag int) ([]string, error) { + var result []string + err := filepath.Walk(basePath, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + if flag&DirType != 0 { + result = append(result, path) + } + } else { + if info.Mode()&fs.ModeSymlink != 0 { + if flag&LinkType != 0 { + result = append(result, path) + } + } else { + if flag&FileType != 0 { + result = append(result, path) + } + } + } + return nil + }) + if err != nil { + return nil, errors.Errorf("failed to list files under %s: %s", basePath, err) + } + return result, nil +} diff --git a/lib/file/file.go b/lib/file/file.go new file mode 100644 index 0000000000000000000000000000000000000000..337ef7ae707929c89484ff30ab99b82370012830 --- /dev/null +++ b/lib/file/file.go @@ -0,0 +1,458 @@ +package file + +import ( + "context" + "crypto/sha1" + "encoding/hex" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/moby/sys/mountinfo" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + + "github.com/oceanbase/obagent/lib/shell" +) + +const ( + AdminUser = "admin" + AdminGroup = "admin" +) + +const ( + FileType = 0b1 + DirType = 0b10 + LinkType = 0b100 +) + +const computeDirectoryUsedCommandTpl = "du -sk %v |awk '{print $1}'" + +var libShell shell.Shell = shell.ShellImpl{} + +type File interface { + FileExists(path string) (bool, error) + + FileExistsEqualsSha1sum(path string, sha1sum string) (bool, error) + + Sha1Checksum(path string) (string, error) + + ReadFile(path string) (string, error) + + SaveFile(path string, content string, mode os.FileMode) error + + CopyFile(sourcePath string, targetPath string, mode os.FileMode) error + + RemoveFileIfExists(path string) error + + CreateDirectory(path string) error + + CreateDirectoryForUser(path string, userName string, groupName string) error + + RemoveDirectory(path string) error + + ChownDirectory(path string, userName string, groupName string, recursive bool) error + + ListFiles(basePath string, flag int) ([]string, error) + + CreateSymbolicLink(sourcePath string, targetPath string) error + + SymbolicLinkExists(linkPath string) (bool, error) + + GetRealPathBySymbolicLink(symbolicLink string) (string, error) + + IsDir(path string) bool + + IsDirEmpty(path string) (bool, error) + + IsFile(path string) bool + + GetDirectoryUsedBytes(path string, timeout time.Duration) (int64, error) + + FindFilesByRegexAndTimeSpan(ctx context.Context, findFilesParam FindFilesParam) ([]os.FileInfo, error) + + FindFilesByRegexAndMTime(ctx context.Context, dir string, fileRegex string, matchFunc MatchRegexFunc, + mTime time.Duration, now time.Time, getFileTime GetFileTimeFunc) ([]os.FileInfo, error) + + GetFileStatInfo(ctx context.Context, f *os.File) (*FileInfoEx, error) + + Mount(ctx context.Context, source, target, mType, options string) error + + Unmount(ctx context.Context, path string) error + + IsMountPoint(ctx context.Context, fileName string) (bool, error) + + GetMountInfos(ctx context.Context, filter mountinfo.FilterFunc) ([]*mountinfo.Info, error) + + GetAllParentDirectories(ctx context.Context, path string) []string +} + +type FileImpl struct { +} + +func (f FileImpl) FileExists(path string) (bool, error) { + return fileExists(path) +} + +func fileExists(path string) (bool, error) { + _, err := Fs.Stat(path) + if err == nil { + return true, nil + } else if os.IsNotExist(err) { + return false, nil + } else { + return false, errors.Errorf("failed to check file exists: %s", err) + } +} + +func (f FileImpl) Sha1Checksum(path string) (string, error) { + file, err := Fs.Open(path) + if err != nil { + return "", errors.Errorf("failed to compute sha1 for %s, cannot open file: %s", path, err) + } + defer file.Close() + + hasher := sha1.New() + _, err = io.Copy(hasher, file) + if err != nil { + return "", errors.Errorf("failed to compute sha1 for %s, cannot read file: %s", path, err) + } + + value := hex.EncodeToString(hasher.Sum(nil)) + log.WithFields(log.Fields{ + "file": path, + "checksum": value, + }).Info("compute sha1 done") + return value, nil +} + +func (f FileImpl) ReadFile(path string) (string, error) { + data, err := ioutil.ReadFile(path) + if err != nil { + return "", errors.Errorf("failed to read file %s: %s", path, err) + } + return string(data), nil +} + +func (f FileImpl) SaveFile(path string, content string, mode os.FileMode) error { + data := []byte(content) + err := ioutil.WriteFile(path, data, mode) + if err != nil { + return errors.Errorf("failed to save file %s: %s", path, err) + } + return nil +} + +func (f FileImpl) CopyFile(sourcePath string, targetPath string, mode os.FileMode) error { + data, err := ioutil.ReadFile(sourcePath) + if err != nil { + return errors.Errorf("failed to copy file, cannot open file %s: %s", sourcePath, err) + } + + err = ioutil.WriteFile(targetPath, data, mode) + if err != nil { + return errors.Wrapf(err, "failed to copy file, cannot write to file %s: %s", targetPath, err) + } + return nil +} + +func (f FileImpl) RemoveFileIfExists(path string) error { + if exists, err := f.FileExists(path); err == nil && !exists { + log.WithField("file", path).Info("file not exists, skip remove") + return nil + } + err := Fs.Remove(path) + if err != nil { + return errors.Errorf("failed to remove file %s: %s", path, err) + } + return nil +} + +func (f FileImpl) FileExistsEqualsSha1sum(path string, expectSha1sum string) (bool, error) { + exists, err := f.FileExists(path) + if err != nil { + return false, errors.Wrap(err, "check file exists with sha1") + } + if !exists { + return false, nil + } + sha1sum, err := f.Sha1Checksum(path) + if err != nil { + return false, errors.Wrap(err, "check file exists with sha1") + } + return sha1sum == expectSha1sum, nil +} + +func (f FileImpl) IsDir(path string) bool { + return isDir(path) +} + +func isDir(path string) bool { + file, err := Fs.Stat(path) + return err == nil && file.IsDir() +} + +func (f FileImpl) IsDirEmpty(path string) (bool, error) { + if !f.IsDir(path) { + return false, errors.Errorf("specific path is not a dir: %s", path) + } + file, err := os.Open(path) + if err != nil { + return false, err + } + defer file.Close() + _, err = file.Readdir(1) + if err != nil && err == io.EOF { + return true, nil + } + return false, err +} + +func (f FileImpl) IsFile(path string) bool { + file, err := Fs.Stat(path) + return err == nil && !file.IsDir() +} + +func (f FileImpl) GetRealPathBySymbolicLink(symbolicLink string) (string, error) { + realPath, err := filepath.EvalSymlinks(symbolicLink) + if err != nil { + return "", errors.Errorf("failed to get real path of symlink %s: %s", symbolicLink, err) + } + return realPath, nil +} + +func (f FileImpl) GetDirectoryUsedBytes(path string, timeout time.Duration) (int64, error) { + command := fmt.Sprintf(computeDirectoryUsedCommandTpl, path) + executeResult, err := libShell.NewCommand(command).WithOutputType(shell.StdOutput).WithTimeout(timeout).Execute() + if err != nil { + return 0, errors.Wrap(err, "get directory used bytes") + } + output := strings.TrimSpace(executeResult.Output) + used, err := strconv.ParseInt(output, 10, 64) + if err != nil { + return 0, errors.Errorf("failed to get directory used bytes, invalid output %s: %s", output, err) + } + // KB -> B + return used * 1024, nil +} + +type MatchRegexFunc func(reg string, content string) (matched bool, err error) + +type GetFileTimeFunc func(info os.FileInfo) (fileTime time.Time, err error) + +func GetFileModTime(info os.FileInfo) (fileTime time.Time, err error) { + return info.ModTime(), nil +} + +// FindFilesByRegexAndMTime 按照 regex 以及 mTime 查询(mTime 为 0 那么忽略该条件),行为模仿了 linux find 命令 +// 目前实现如下逻辑 +// -mtime +duration : 列出在 now - duration 之前(不含本身)被更改过内容的文件名 +// -mtime -duration : 列出在 now ~ now - duration 之内(不含本身)被更改过内容的文件名 +func (f FileImpl) FindFilesByRegexAndMTime( + ctx context.Context, + dir string, + fileRegex string, + matchFunc MatchRegexFunc, + mTime time.Duration, + now time.Time, + getFileTime GetFileTimeFunc, +) ([]os.FileInfo, error) { + dir = strings.TrimSpace(dir) + if dir == "" { + return nil, errors.Errorf("dir is empty") + } + if fileRegex == "" { + fileRegex = ".*" + } + + ctxLog := log.WithContext(ctx) + skipMTime := mTime == 0 + isMTimeUnSigned := mTime > 0 + if mTime > 0 { + mTime = -1 * mTime + } + checkPoint := now.Add(mTime) + matchedFiles := make([]os.FileInfo, 0) + + err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + ctxLog.WithError(err).Errorf("prevent panic by handling failure accessing a path %q", path) + return err + } + isMatchedRegex, err := matchFunc(fileRegex, info.Name()) + if err != nil { + return err + } + infoTime, err := getFileTime(info) + if err != nil { + return err + } + + isMatchedMTime := skipMTime || + (!skipMTime && ((isMTimeUnSigned && infoTime.Before(checkPoint)) || + (!isMTimeUnSigned && (infoTime.After(checkPoint) || infoTime.Equal(checkPoint))))) + + if info.Mode().IsRegular() && isMatchedRegex && isMatchedMTime { + matchedFiles = append(matchedFiles, info) + } + + ctxLog.WithField("path", path).Debugf("visited file or dirs") + return nil + }) + if err != nil { + return nil, err + } + return matchedFiles, nil +} + +type MatchMTimeFunc func(mTime, startTime, endTime time.Time) (matched bool, err error) +type MatchRegexpsFunc func(regs []string, content string) (matched bool, err error) + +type FindFilesParam struct { + Dir string + FileRegexps []string + MatchRegex MatchRegexpsFunc + StartTime time.Time + EndTime time.Time + GetFileTime GetFileTimeFunc + MatchMTime MatchMTimeFunc +} + +func (f FileImpl) FindFilesByRegexAndTimeSpan( + ctx context.Context, + param FindFilesParam, +) ([]os.FileInfo, error) { + dir := strings.TrimSpace(param.Dir) + if dir == "" || len(param.FileRegexps) == 0 { + return nil, nil + } + + ctxLog := log.WithContext(ctx) + + matchedFiles := make([]os.FileInfo, 0) + + err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + ctxLog.WithError(err).Errorf("prevent panic by handling failure accessing a path %q", path) + return err + } + isMatchedRegex, err := param.MatchRegex(param.FileRegexps, info.Name()) + if err != nil { + return err + } + infoTime, err := param.GetFileTime(info) + if err != nil { + return err + } + + isMatchedMTime, err := param.MatchMTime(infoTime, param.StartTime, param.EndTime) + if err != nil { + return err + } + + if info.Mode().IsRegular() && isMatchedRegex && isMatchedMTime { + matchedFiles = append(matchedFiles, info) + } + + ctxLog.WithField("path", path).Debugf("visited file or dirs") + return nil + }) + if err != nil { + return nil, err + } + return matchedFiles, nil +} + +// GetFileStatInfo 获取文件 stat 信息,包含 fileId,devId 等 +func (f FileImpl) GetFileStatInfo(ctx context.Context, file *os.File) (*FileInfoEx, error) { + ctxLog := log.WithContext(ctx) + ret, err := GetFileInfo(file) + if err != nil { + ctxLog.WithError(err).Error("failed get file info") + return nil, err + } + return ret, nil +} + +type PermissionType string + +const ( + AccessRead PermissionType = "ACCESS_READ" + AccessWrite PermissionType = "ACCESS_WRITE" + AccessExecute PermissionType = "ACCESS_EXECUTE" +) + +type CheckDirPermissionParam struct { + Directory string `json:"directory"` // directory to check permission + User string `json:"user"` // host user to check directory permissions, e.g. admin + Permission PermissionType `json:"permission"` // expected permission of storage directory +} + +type CheckDirectoryPermissionResult string + +const ( + CheckFailed CheckDirectoryPermissionResult = "CHECK_FAILED" + DirectoryNotExists CheckDirectoryPermissionResult = "DIRECTORY_NOT_EXISTS" + HasPermission CheckDirectoryPermissionResult = "HAS_PERMISSION" + NoPermission CheckDirectoryPermissionResult = "NO_PERMISSION" +) + +const checkDirectoryPermissionCommand = "if [ -\"%s\" \"%s\" ]; then echo 0; else echo 1; fi" +const hasPermissionOutput = "0" + +var filePermissionShellValue = map[PermissionType]string{ + AccessRead: "r", + AccessWrite: "w", + AccessExecute: "x", +} + +func (f FileImpl) CheckDirectoryPermission(ctx context.Context, dir string, user string, permissionType PermissionType) (CheckDirectoryPermissionResult, error) { + ctxlog := log.WithContext(ctx).WithFields(log.Fields{ + "directory": dir, + "user": user, + "permissionType": permissionType, + }) + + exists, err := fileExists(dir) + if err != nil { + ctxlog.WithError(err).Info("check directory permission failed, cannot check directory exists") + return CheckFailed, nil + } + if !exists { + ctxlog.Info("check directory permission done, directory not exists") + return DirectoryNotExists, nil + } + if !isDir(dir) { + ctxlog.Info("check directory permission done, path is not directory") + return DirectoryNotExists, nil + } + + cmd := fmt.Sprintf(checkDirectoryPermissionCommand, filePermissionShellValue[permissionType], dir) + executeResult, err := libShell.NewCommand(cmd).WithContext(ctx).WithUser(user).WithOutputType(shell.StdOutput).Execute() + if err != nil { + ctxlog.WithError(err).Info("check directory permission failed, cannot check directory permission") + return CheckFailed, nil + } + if strings.TrimSpace(executeResult.Output) == hasPermissionOutput { + ctxlog.Info("check directory permission done, directory has permission") + return HasPermission, nil + } else { + ctxlog.Info("check directory permission done, directory has no permission") + return NoPermission, nil + } +} + +func (f FileImpl) GetAllParentDirectories(ctx context.Context, path string) []string { + curDir := path + allParents := make([]string, 0) + for curDir != "/" { + curDir = filepath.Dir(curDir) + allParents = append(allParents, curDir) + } + return allParents +} diff --git a/lib/file/fileinfo.go b/lib/file/fileinfo.go new file mode 100644 index 0000000000000000000000000000000000000000..09830cf0e2a142c6c369f8ae8c0df395638900e0 --- /dev/null +++ b/lib/file/fileinfo.go @@ -0,0 +1,33 @@ +package file + +import ( + "os" + "time" +) + +type FileInfoEx struct { + os.FileInfo + fileId uint64 + devId uint64 + createTime time.Time +} + +func (f *FileInfoEx) FileId() uint64 { + return f.fileId +} + +func (f *FileInfoEx) DevId() uint64 { + return f.devId +} + +func (f *FileInfoEx) CreateTime() time.Time { + return f.createTime +} + +func GetFileInfo(f *os.File) (*FileInfoEx, error) { + info, err := f.Stat() + if err != nil { + return nil, err + } + return toFileInfoEx(info), nil +} diff --git a/lib/file/fileinfo_darwin.go b/lib/file/fileinfo_darwin.go new file mode 100644 index 0000000000000000000000000000000000000000..18c5dd5a4c9a6a3312800b5bd206ba7705ed43da --- /dev/null +++ b/lib/file/fileinfo_darwin.go @@ -0,0 +1,20 @@ +//go:build darwin +// +build darwin + +package file + +import ( + "os" + "syscall" + "time" +) + +func toFileInfoEx(info os.FileInfo) *FileInfoEx { + sysInfo, _ := info.Sys().(*syscall.Stat_t) + return &FileInfoEx{ + FileInfo: info, + fileId: sysInfo.Ino, + devId: uint64(sysInfo.Dev), + createTime: time.Unix(sysInfo.Ctimespec.Unix()), + } +} diff --git a/lib/file/fileinfo_linux.go b/lib/file/fileinfo_linux.go new file mode 100644 index 0000000000000000000000000000000000000000..ea7e44eed6534b8b7f1069332e711200d68a0071 --- /dev/null +++ b/lib/file/fileinfo_linux.go @@ -0,0 +1,20 @@ +//go:build linux +// +build linux + +package file + +import ( + "os" + "syscall" + "time" +) + +func toFileInfoEx(info os.FileInfo) *FileInfoEx { + sysInfo, _ := info.Sys().(*syscall.Stat_t) + return &FileInfoEx{ + FileInfo: info, + fileId: sysInfo.Ino, + devId: sysInfo.Dev, + createTime: time.Unix(sysInfo.Ctim.Unix()), + } +} diff --git a/lib/file/fs.go b/lib/file/fs.go new file mode 100644 index 0000000000000000000000000000000000000000..3e20481ed08118db26d7a5f24a6aab0fe789320d --- /dev/null +++ b/lib/file/fs.go @@ -0,0 +1,13 @@ +// All codes interacting with file system should be put here. +// Invoke file system operations via afero's Fs interface. +// This is convenient for mock testing. + +package file + +import ( + "github.com/spf13/afero" +) + +// OsFs is a Fs implementation that uses functions provided by the os package. +// When in unit testing, replace this with afero.MemMapFs. +var Fs afero.Fs = afero.OsFs{} diff --git a/lib/file/link.go b/lib/file/link.go new file mode 100644 index 0000000000000000000000000000000000000000..4c9ff02c10bbaf0ba3a7aa23e6fc5613d4a234fc --- /dev/null +++ b/lib/file/link.go @@ -0,0 +1,61 @@ +package file + +import ( + "os" + + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + "github.com/spf13/afero" +) + +func (f FileImpl) CreateSymbolicLink(sourcePath string, targetPath string) error { + linker, ok := Fs.(afero.Symlinker) + if !ok { + err := errors.New("symlink not supported by current file system") + log.WithError(err).Error("create symbolic link failed") + return err + } + err := linker.SymlinkIfPossible(sourcePath, targetPath) + if err != nil { + log.WithFields(log.Fields{ + "source": sourcePath, + "target": targetPath, + }).WithError(err).Info("create symbolic link failed") + return errors.Errorf("failed to create symbolic link: %s", err) + } + log.WithFields(log.Fields{ + "source": sourcePath, + "target": targetPath, + }).Info("create symbolic link done") + return nil +} + +func (f FileImpl) SymbolicLinkExists(linkPath string) (bool, error) { + linker, ok := Fs.(afero.Symlinker) + if !ok { + err := errors.New("symlink not supported by current file system") + log.WithError(err).Error("check symbolic link exists failed") + return false, err + } + + fileInfo, lstatCalled, err := linker.LstatIfPossible(linkPath) + if err != nil { + if os.IsNotExist(err) { + log.WithError(err).Warn("symbolicLinkExists.lstatIfPossible failed") + return false, nil + } + log.WithError(err).Error("check symbolic link exists failed") + return false, errors.Errorf("failed to check symbolic link, lstat failed: %s", err) + } + if !lstatCalled { + log.Infof("lstat not called, file %v is not symbolic link", linkPath) + return false, nil + } + if fileInfo.Mode()&os.ModeSymlink != 0 { + log.Infof("file %v is symbolic link", linkPath) + return true, nil + } else { + log.Infof("file %v is not symbolic link", linkPath) + return false, nil + } +} diff --git a/lib/file/link_test.go b/lib/file/link_test.go new file mode 100644 index 0000000000000000000000000000000000000000..e5e2407ce60ea338055694bc96938a370d2b4cd8 --- /dev/null +++ b/lib/file/link_test.go @@ -0,0 +1,37 @@ +package file + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/oceanbase/obagent/tests/testutil" +) + +func TestFileImpl_SymbolicLinkExists(t *testing.T) { + f := FileImpl{} + exists, err := f.SymbolicLinkExists("/home/admin/oceanbase/cgroup") + assert.NoError(t, err) + assert.False(t, exists) + + tmpDir1, err := ioutil.TempDir(testutil.TempDir, "a") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir1) + tmpDir2, err := ioutil.TempDir(testutil.TempDir, "b") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir2) + linkPath := filepath.Join(tmpDir2, "test") + err = f.CreateSymbolicLink(tmpDir1, linkPath) + assert.NoError(t, err) + exists, err = f.SymbolicLinkExists(linkPath) + assert.NoError(t, err) + assert.True(t, exists) + +} diff --git a/lib/file/mount.go b/lib/file/mount.go new file mode 100644 index 0000000000000000000000000000000000000000..e89a68e0c74af737e9713c2528b7017f56ddfa4f --- /dev/null +++ b/lib/file/mount.go @@ -0,0 +1,23 @@ +package file + +import ( + "context" + + "github.com/moby/sys/mountinfo" +) + +func (f FileImpl) Mount(ctx context.Context, source, target, mType, options string) error { + return mount(ctx, source, target, mType, options) +} + +func (f FileImpl) Unmount(ctx context.Context, path string) error { + return unmount(ctx, path) +} + +func (f FileImpl) IsMountPoint(ctx context.Context, fileName string) (bool, error) { + return mountinfo.Mounted(fileName) +} + +func (f FileImpl) GetMountInfos(ctx context.Context, filter mountinfo.FilterFunc) ([]*mountinfo.Info, error) { + return mountinfo.GetMounts(filter) +} diff --git a/lib/file/mount_darwin.go b/lib/file/mount_darwin.go new file mode 100644 index 0000000000000000000000000000000000000000..c4290c053040fccf0897c2de94fed669cf2c4164 --- /dev/null +++ b/lib/file/mount_darwin.go @@ -0,0 +1,14 @@ +//go:build darwin +// +build darwin + +package file + +import "context" + +func mount(ctx context.Context, device, target, mType, options string) error { + panic("mount not supported on this platform") +} + +func unmount(ctx context.Context, path string) error { + panic("mount not supported on this platform") +} diff --git a/lib/file/mount_linux.go b/lib/file/mount_linux.go new file mode 100644 index 0000000000000000000000000000000000000000..a08fd245eddd32be3d006dfc6d133ee6cc3d6fcd --- /dev/null +++ b/lib/file/mount_linux.go @@ -0,0 +1,19 @@ +//go:build linux +// +build linux + +package file + +import ( + "context" + + mount_util "github.com/moby/sys/mount" +) + +func mount(ctx context.Context, device, target, mType, options string) error { + + return mount_util.Mount(device, target, mType, options) +} + +func unmount(ctx context.Context, path string) error { + return mount_util.Unmount(path) +} diff --git a/lib/goroutinepool/goroutinepool.go b/lib/goroutinepool/goroutinepool.go new file mode 100644 index 0000000000000000000000000000000000000000..f0900dbdd37463adcaa0baef0d665274207515e0 --- /dev/null +++ b/lib/goroutinepool/goroutinepool.go @@ -0,0 +1,105 @@ +package goroutinepool + +import ( + "errors" + "fmt" + "sync" + "sync/atomic" + "time" + + log "github.com/sirupsen/logrus" +) + +type GoroutinePool struct { + jobQueneSize, runPoolSize int + queue chan job + close chan bool + closewg sync.WaitGroup + closed uint32 +} + +type job struct { + name string + fn func() error +} + +func (gr *GoroutinePool) Put(jobName string, fn func() error) error { + if atomic.LoadUint32(&gr.closed) == 1 { + return errors.New("goruntinuepool closed") + } + gr.queue <- job{ + name: jobName, + fn: fn, + } + return nil +} + +func (gr *GoroutinePool) PutWithTimeout(jobName string, fn func() error, duration time.Duration) error { + if atomic.LoadUint32(&gr.closed) == 1 { + return errors.New("goruntinuepool closed") + } + timeoutWrapper := func() error { + finished := make(chan bool, 1) + var err error + go func() { + err = fn() + finished <- true + }() + + after := time.After(duration) + select { + case <-finished: + return err + case <-after: + log.Errorf("job %s timeout", jobName) + return errors.New(fmt.Sprintf("job %s timeout", jobName)) + } + } + gr.queue <- job{ + fn: timeoutWrapper, + name: jobName, + } + return nil +} + +func (gr *GoroutinePool) Close() { + atomic.StoreUint32(&gr.closed, 1) + close(gr.close) + gr.closewg.Wait() +} + +func NewGoroutinePool(poolName string, runPoolSize, jobQueneSize int) (*GoroutinePool, error) { + if runPoolSize > jobQueneSize { + return nil, errors.New("goroutinepool worker size should be less than job quene size") + } + if runPoolSize <= 0 || jobQueneSize <= 0 { + return nil, errors.New("run pool size or job queue size should be greater than 0") + } + pool := &GoroutinePool{ + jobQueneSize: jobQueneSize, + runPoolSize: runPoolSize, + queue: make(chan job, jobQueneSize), + close: make(chan bool), + } + + pool.closewg.Add(runPoolSize) + for i := 0; i < runPoolSize; i++ { + go func(i int) { + defer pool.closewg.Done() + for { + select { + case <-pool.close: + log.Infof("#gorountine-pool-%s-%d, closed", poolName, i) + return + case job := <-pool.queue: + log.Debugf("#gorountine-pool-%s-%d, got a job %s", poolName, i, job.name) + if err := job.fn(); err != nil { + log.Errorf("#gorountine-pool-%s-%d, job %s, got err: %+v", poolName, i, job.name, err) + } + log.Debugf("#gorountine-pool-%s-%d, %s job done", poolName, i, job.name) + } + } + }(i) + } + return pool, nil +} diff --git a/lib/goroutinepool/goroutinepool_test.go b/lib/goroutinepool/goroutinepool_test.go new file mode 100644 index 0000000000000000000000000000000000000000..7267708d998f99b4d6a0cdd8b251a2ac3b29fa96 --- /dev/null +++ b/lib/goroutinepool/goroutinepool_test.go @@ -0,0 +1,89 @@ +package goroutinepool + +import ( + "errors" + "sync" + "testing" + "time" + + log "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" +) + +func TestGorourinepool_without_error(t *testing.T) { + pool, err := NewGoroutinePool("TEST-POOL", 2, 10) + assert.Nil(t, err) + wg := sync.WaitGroup{} + wg.Add(3) + for i := 0; i < 3; i++ { + idx := i + pool.Put("TASK", func() error { + defer wg.Done() + log.Infof("TASK ID:%d", idx+1) + return nil + }) + } + wg.Wait() + pool.Close() +} + +func TestGorourinepool_with_error(t *testing.T) { + pool, err := NewGoroutinePool("TEST-POOL", 1, 10) + assert.Nil(t, err) + wg := sync.WaitGroup{} + wg.Add(3) + for i := 0; i < 3; i++ { + index := i + pool.Put("TASK", func() error { + defer wg.Done() + log.Infof("TASK ID:%d", index+1) + if index == 0 { + return errors.New("task execute failed") + } + return nil + }) + } + wg.Wait() + pool.Close() +} + +func TestGorourinepool_with_timeout(t *testing.T) { + pool, err := NewGoroutinePool("TEST-POOL", 3, 10) + assert.Nil(t, err) + for i := 0; i < 3; i++ { + index := i + pool.PutWithTimeout("TASK", func() error { + time.Sleep(time.Millisecond * 10) + log.Infof("TASK ID:%d", index+1) + return nil + }, time.Millisecond) + } + + t1 := time.Now() + pool.Close() + dur := time.Now().Sub(t1) + log.Infof("pool exited, duration:%+v", dur) + assert.True(t, dur < time.Millisecond*5) +} + +func TestGorourinepool_without_timeout(t *testing.T) { + pool, err := NewGoroutinePool("TEST-POOL-WITHOUT-TIMEOUT", 3, 10) + assert.Nil(t, err) + for i := 0; i < 3; i++ { + index := i + pool.Put("TASK", func() error { + time.Sleep(time.Millisecond * 10) + log.Infof("TASK ID:%d", index+1) + return nil + }) + } + + t1 := time.Now() + + time.Sleep(time.Millisecond) + + pool.Close() + dur := time.Now().Sub(t1) + log.Infof("pool exited, duration:%+v", dur) + assert.True(t, dur >= time.Millisecond*10) +} diff --git a/lib/http/client.go b/lib/http/client.go new file mode 100644 index 0000000000000000000000000000000000000000..d5f75f0bc38d0eab1c4489422e393b1cdcbafda7 --- /dev/null +++ b/lib/http/client.go @@ -0,0 +1,99 @@ +package http + +import ( + "bytes" + "context" + "encoding/json" + "io/ioutil" + "net" + "net/http" + "strings" + "time" +) + +func CanConnect(network, addr string, timeout time.Duration) bool { + conn, err := net.DialTimeout(network, addr, timeout) + ret := err == nil + if conn != nil { + _ = conn.Close() + } + return ret +} + +type ApiClient struct { + protocol string + host string + hc *http.Client +} + +func NewSocketApiClient(socket string, timeout time.Duration) *ApiClient { + return &ApiClient{ + protocol: "http", + host: "socket", + hc: &http.Client{ + Timeout: timeout, + Transport: &http.Transport{ + DisableKeepAlives: true, + DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { + return net.Dial("unix", socket) + }, + }, + }, + } +} + +const ContentTypeJson = "application/json" + +func (ac *ApiClient) Call(api string, param interface{}, retPtr interface{}) error { + var inputData []byte + var err error + if param != nil { + inputData, err = json.Marshal(param) + if err != nil { + return EncodeRequestFailedErr.NewError().WithCause(err) + } + } + reader := bytes.NewReader(inputData) + resp, err := ac.hc.Post(ac.url(api), ContentTypeJson, reader) + if err != nil { + return ApiRequestFailedErr.NewError(api).WithCause(err) + } + defer ac.hc.CloseIdleConnections() + defer resp.Body.Close() + + envelop := OcpAgentResponse{ + Data: retPtr, + } + bs, err := ioutil.ReadAll(resp.Body) + if err != nil { + return ApiRequestFailedErr.NewError(api).WithCause(err) + } + cpResp := make([]byte, len(bs)) + copy(cpResp, bs) + respReader := bytes.NewReader(bs) + + if resp.StatusCode != http.StatusOK { + err = json.NewDecoder(respReader).Decode(&envelop) + if err == nil { + return ApiRequestGotFailResultErr.NewError(api, envelop.Error.Code, envelop.Error.Message) + } + return ApiRequestGotFailResultErr.WithMessageTemplate("api %s, resp code %d, status %s, decode resp %s err %s"). + NewError(api, resp.StatusCode, resp.Status, cpResp, err) + } + err = json.NewDecoder(respReader).Decode(&envelop) + if err != nil { + return DecodeResultFailedErr.WithMessageTemplate("api %s, decode resp %s").NewError(api, cpResp).WithCause(err) + } + if envelop.Successful { + return nil + } + return ApiRequestGotFailResultErr.NewError(api, envelop.Error.Code, envelop.Error.Message) +} + +func (ac *ApiClient) url(api string) string { + url := ac.protocol + "://" + ac.host + if !strings.HasPrefix(api, "/") { + url += "/" + } + return url + api +} diff --git a/lib/http/client_test.go b/lib/http/client_test.go new file mode 100644 index 0000000000000000000000000000000000000000..43c833d1d311ab8e57f583b9e55d610a5a396b9a --- /dev/null +++ b/lib/http/client_test.go @@ -0,0 +1,57 @@ +package http + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "testing" + "time" +) + +type Struct struct { + Value string +} + +func TestClient(t *testing.T) { + listener := NewListener() + listener.AddHandler("/hello", NewFuncHandler(func(s string) Struct { + return Struct{Value: "hello " + s} + })) + listener.AddHandler("/error", NewFuncHandler(func() error { + return errors.New("error") + })) + defer listener.Close() + socket := filepath.Join(os.TempDir(), fmt.Sprintf("test_%d.sock", time.Now().UnixNano())) + err := listener.StartSocket(socket) + if err != nil { + t.Error(err) + } + defer os.Remove(socket) + + if !CanConnect("unix", socket, time.Second) { + t.Error("CanConnect should return true") + } + + client := NewSocketApiClient(socket, time.Second) + ret := &Struct{} + err = client.Call("hello", "world", ret) + if err != nil { + t.Error(err) + } + if ret.Value != "hello world" { + t.Error("parse result wrong") + } + + err = client.Call("/not_exists", nil, nil) + if err == nil { + t.Error("should error on not_exists api") + } + fmt.Println(err) + + err = client.Call("/error", nil, nil) + if err == nil { + t.Error("should error on not_exists api") + } + fmt.Println(err) +} diff --git a/lib/http/error.go b/lib/http/error.go new file mode 100644 index 0000000000000000000000000000000000000000..caf5b863a84f62504e4ccf424b4d2df363f09d3a --- /dev/null +++ b/lib/http/error.go @@ -0,0 +1,11 @@ +package http + +import "github.com/oceanbase/obagent/lib/errors" + +var ( + EncodeRequestFailedErr = errors.InvalidArgument.NewCode("api/client", "encode_request_failed") + DecodeResultFailedErr = errors.Internal.NewCode("api/client", "decode_result_failed") + NoApiClientErr = errors.FailedPrecondition.NewCode("api/client", "no_api_client") + ApiRequestFailedErr = errors.Internal.NewCode("api/client", "api_request_failed") + ApiRequestGotFailResultErr = errors.Internal.NewCode("api/client", "api_request_got_fail_result") +) diff --git a/lib/http/http.go b/lib/http/http.go new file mode 100644 index 0000000000000000000000000000000000000000..f155cb51c0333116739784a13e61c9eed62dbdeb --- /dev/null +++ b/lib/http/http.go @@ -0,0 +1,126 @@ +package http + +import ( + "bytes" + "context" + "fmt" + "io" + "net" + "net/http" + "os" + "strings" + "sync" + + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + "golang.org/x/net/proxy" +) + +type Http interface { + DownloadFile(path string, url string) error +} + +var globalHttpClient = &http.Client{} +var httpClientLock = sync.Mutex{} + +func GetGlobalHttpClient() *http.Client { + httpClientLock.Lock() + defer httpClientLock.Unlock() + return globalHttpClient +} + +type HttpImpl struct { +} + +func UnsetSocksProxy() { + _ = SetSocksProxy("") +} + +func SetSocksProxy(proxyAddr string) error { + httpClientLock.Lock() + defer httpClientLock.Unlock() + proxyAddr = strings.TrimSpace(proxyAddr) + if proxyAddr == "" { + log.Info("unset http socks proxy") + globalHttpClient.Transport = nil + return nil + } + dialer, err := proxy.SOCKS5("tcp", proxyAddr, nil, proxy.Direct) + if err != nil { + return err + } + log.Infof("set http socks proxy to %s", proxyAddr) + globalHttpClient.Transport = &http.Transport{ + DialContext: func(ctx context.Context, network, address string) (net.Conn, error) { + log.Infof("http connect to %s %s via socks proxy %s", network, address, proxyAddr) + return dialer.Dial(network, address) + }, + DisableKeepAlives: true, + } + return nil +} + +func (h HttpImpl) DownloadFile(path string, url string) error { + log.WithField("url", url).Info("download file from url start") + targetFile, err := os.Create(path) + if err != nil { + return errors.Errorf("failed to download file from url, cannot open file %s: %s", path, err) + } + defer targetFile.Close() + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return errors.Errorf("failed to download file from url, cannot create request: %s", err) + } + resp, err := GetGlobalHttpClient().Do(req) + if err != nil { + return errors.Errorf("failed to download file from url, cannot send request: %s", err) + } + defer resp.Body.Close() + if resp.StatusCode >= 400 { + buf := bytes.NewBufferString("") + _, _ = io.Copy(buf, resp.Body) + result := buf.String() + return errors.Errorf("failed to download file from url, error response: %s", result) + } + + reader := &DownloadFileReader{ + Reader: resp.Body, + Total: resp.ContentLength, + Current: 0, + Progress: 0, + } + _, err = io.Copy(targetFile, reader) + if err != nil { + return errors.Errorf("failed to download file from url, cannot save response: %s", err) + } + log.WithField("url", url).Info("download file from url done") + return nil +} + +// DownloadFileReader custom a reader so that we can print the download progress +type DownloadFileReader struct { + io.Reader + Total int64 // total bytes to download + Current int64 // current downloaded bytes + Progress int // current max progress, range [0, 100] +} + +func (r *DownloadFileReader) Read(p []byte) (int, error) { + n, err := r.Reader.Read(p) + if err != nil { + return n, err + } + r.Current += int64(n) + progress := float64(r.Current*10000/r.Total) / 100 + // print log if the progress proceeds 1% + if int(progress) > r.Progress { + log.WithFields(log.Fields{ + "total": r.Total, + "current": r.Current, + "progress": fmt.Sprintf("%.2f%%", progress), + }).Info("current progress") + r.Progress = int(progress) + } + return n, err +} diff --git a/lib/http/http_command.go b/lib/http/http_command.go new file mode 100644 index 0000000000000000000000000000000000000000..2658c7a33093f08cbd638fcf3495f369be1c269b --- /dev/null +++ b/lib/http/http_command.go @@ -0,0 +1,138 @@ +package http + +import ( + "context" + "encoding/json" + "io" + "net/http" + "reflect" + "time" + + log "github.com/sirupsen/logrus" + + errors2 "github.com/oceanbase/obagent/errors" + "github.com/oceanbase/obagent/lib/command" + "github.com/oceanbase/obagent/lib/errors" + "github.com/oceanbase/obagent/lib/system" + "github.com/oceanbase/obagent/lib/trace" +) + +type TaskToken struct { + Token string `json:"token"` +} + +var serverIp, _ = system.SystemImpl{}.GetLocalIpAddress() + +type reqKey string + +const ( + RequestTimeKey reqKey = "request_time" +) + +func NewFuncHandler(fun interface{}) http.HandlerFunc { + return NewHandler(command.WrapFunc(fun)) +} + +func NewHandler(cmd command.Command) http.HandlerFunc { + return func(writer http.ResponseWriter, request *http.Request) { + defer request.Body.Close() + ctx := context.WithValue(trace.ContextWithTraceId(request), RequestTimeKey, time.Now()) + request = request.WithContext(ctx) + + cmdParam, err := parseJsonParam(request.Body, cmd.DefaultParam()) + if err != nil { + writeError(err, request, writer) + return + } + log.WithContext(ctx).Infof("handling command request %s %v", request.URL.Path, cmdParam) + execContext := command.NewInputExecutionContext(ctx, cmdParam) + err = cmd.Execute(execContext) + if err != nil { + writeError(err, request, writer) + return + } + result, err := execContext.Output().Get() + if err != nil { + writeError(err, request, writer) + return + } + writeOk(result, request, writer) + } +} + +func parseJsonParam(reader io.Reader, defaultParam interface{}) (interface{}, error) { + v := reflect.ValueOf(defaultParam) + if !v.IsValid() { + return nil, nil + } + v1 := reflect.New(v.Type()) + v1.Elem().Set(v) + err := json.NewDecoder(reader).Decode(v1.Interface()) + if err != nil { + return nil, err + } + return v1.Elem().Interface(), nil +} + +func writeData(ctx context.Context, result interface{}, writer http.ResponseWriter) { + data, _ := json.Marshal(result) + writer.Header().Set("Connection", "close") + _, err := writer.Write(data) + if err != nil { + log.WithContext(ctx).WithError(err).Error("write data to client got error. maybe client closed connection.") + } +} + +func writeOk(result interface{}, request *http.Request, writer http.ResponseWriter) { + envelop := wrapEnvelop(result, nil, request) + ctx := trace.ContextWithTraceId(request) + writeData(ctx, envelop, writer) + log.WithContext(ctx).Infof("command request %s succeed", request.URL.Path) +} + +func wrapEnvelop(result interface{}, err error, request *http.Request) OcpAgentResponse { + value := request.Context().Value(RequestTimeKey) + var requestTime time.Time + var ok bool + var duration int + if requestTime, ok = value.(time.Time); ok { + duration = int(time.Since(requestTime).Milliseconds()) + } + var apiErr *ApiError + success := true + code := 0 + statusCode := 200 + if err != nil { + success = false + statusCode = errors.HttpCode(err, http.StatusInternalServerError) + if e, ok := err.(*errors2.OcpAgentError); ok { + code = e.ErrorCode.Code + statusCode = e.ErrorCode.Kind + } else if e, ok := err.(*errors.Error); ok { + code = e.Kind().HttpCode + statusCode = code + } + apiErr = &ApiError{ + Code: code, + Message: err.Error(), + } + } + return OcpAgentResponse{ + Successful: success, + Timestamp: requestTime, + Duration: duration, + TraceId: trace.GetTraceId(request), + Server: serverIp, + Data: result, + Error: apiErr, + Status: statusCode, + } +} + +func writeError(err error, request *http.Request, writer http.ResponseWriter) { + envelop := wrapEnvelop(nil, err, request) + writer.WriteHeader(envelop.Status) + ctx := trace.ContextWithTraceId(request) + writeData(ctx, envelop, writer) + log.WithContext(ctx).Warnf("command request %s got error: %v", request.URL.Path, err) +} diff --git a/lib/http/http_command_test.go b/lib/http/http_command_test.go new file mode 100644 index 0000000000000000000000000000000000000000..143d28af897eae4f5fc7e0d1426440ef5cd9819a --- /dev/null +++ b/lib/http/http_command_test.go @@ -0,0 +1,120 @@ +package http + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +type St struct { + A string + B int +} + +func Test_parseJsonParam(t *testing.T) { + var str string + v, err := parseJsonParam(strings.NewReader(`"str"`), str) + if err != nil { + t.Error(err) + } + if v != "str" { + t.Error("bad value") + } + + v, err = parseJsonParam(strings.NewReader(`"str"`), &str) + if err != nil { + t.Error(err) + } + if *(v.(*string)) != "str" { + t.Error("bad value") + } + + v, err = parseJsonParam(strings.NewReader(`null`), &str) + if err != nil { + t.Error(err) + } + if v.(*string) != nil { + println(*v.(*string)) + t.Error("bad value") + } + + v, err = parseJsonParam(strings.NewReader(`"str2"`), &str) + if err != nil { + t.Error(err) + } + if *v.(*string) != "str2" { + t.Error("bad value") + } + + var sl []string + var m map[string]string + + v, err = parseJsonParam(strings.NewReader(`["a"]`), sl) + if err != nil { + t.Error(err) + } + if len(v.([]string)) != 1 { + t.Error("bad value") + } + + v, err = parseJsonParam(strings.NewReader(`{"a":"1"}`), m) + if err != nil { + t.Error(err) + } + if len(v.(map[string]string)) != 1 { + t.Error("bad value") + } + + var st = St{ + A: "A", + B: 123, + } + v, err = parseJsonParam(strings.NewReader(`{"B":234}`), st) + if err != nil { + t.Error(err) + } + if v.(St).A != "A" || v.(St).B != 234 { + t.Error("bad value") + } + v, err = parseJsonParam(strings.NewReader(`{"A":"111"}`), &st) + if err != nil { + t.Error(err) + } + if v.(*St).A != "111" || v.(*St).B != 123 { + t.Error("bad value") + } + + //httptest.NewServer().Close() +} + +func TestNewFuncHandler(t *testing.T) { + handler := NewFuncHandler(func(n int) int { + return n * 2 + }) + resp := httptest.NewRecorder() + req, err := http.NewRequest("POST", "/", bytes.NewReader([]byte("7"))) + if err != nil { + t.Error(err) + } + handler.ServeHTTP(resp, req) + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Error(err) + } + envelop := OcpAgentResponse{ + Data: 0, + } + err = json.Unmarshal(body, &envelop) + if err != nil { + t.Error(err) + } + if envelop.Data != 14 { + t.Error("bad value") + } + fmt.Println(string(body)) +} diff --git a/lib/http/http_test.go b/lib/http/http_test.go new file mode 100644 index 0000000000000000000000000000000000000000..f65968726d22c38dc388489c81b730a06a6d3625 --- /dev/null +++ b/lib/http/http_test.go @@ -0,0 +1,18 @@ +package http + +import ( + "testing" +) + +func TestProxy(t *testing.T) { + t.Skip() + err := SetSocksProxy("127.0.0.1:1081") + if err != nil { + t.Errorf("set proxy err: %v", err) + } + resp, err := GetGlobalHttpClient().Get("http://127.0.0.1:8080/") + if err != nil { + t.Errorf("request failed, %v", err) + } + println(resp.Status) +} diff --git a/api/server/listener.go b/lib/http/listener.go similarity index 74% rename from api/server/listener.go rename to lib/http/listener.go index 341484b13396533016f6f4ae9a0a9f89c575ef4c..1952935fa9d550a31e78470dd7282a9277bdec32 100644 --- a/api/server/listener.go +++ b/lib/http/listener.go @@ -1,16 +1,4 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - -package server +package http import ( "context" @@ -106,18 +94,11 @@ func (l *Listener) AddHandler(path string, h http.Handler) { } func (l *Listener) Close() { - //var err error _ = l.srv.Close() if l.tcpListener != nil { _ = l.tcpListener.Close() - //if err != nil { - // log.WithError(err).Warn("close tcpListener got error") - //} } if l.unixListener != nil { _ = l.unixListener.Close() - //if err != nil { - // log.WithError(err).Warn("close unixListener got error") - //} } } diff --git a/api/server/listener_test.go b/lib/http/listener_test.go similarity index 67% rename from api/server/listener_test.go rename to lib/http/listener_test.go index b370b4175f79ae2622fa8f549e6b530bc51f9fa6..daf0bb3a8115623b965f78cc185bf81448228351 100644 --- a/api/server/listener_test.go +++ b/lib/http/listener_test.go @@ -1,16 +1,4 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - -package server +package http import ( "fmt" @@ -57,7 +45,6 @@ func TestStartErr(t *testing.T) { err := listener1.StartTCP("127.0.0.11:9998") if err == nil { fmt.Println("run StartTCP on bad address should failed") - //t.Error("run StartTCP on bad address should failed") defer listener1.Close() } fmt.Println(err) diff --git a/api/response/response.go b/lib/http/response.go similarity index 65% rename from api/response/response.go rename to lib/http/response.go index 34c881d4a8cc80a2fdd514e77fcc3302d9e44e09..0639da4d0600d944eb16a0bce21b180e62573053 100644 --- a/api/response/response.go +++ b/lib/http/response.go @@ -1,16 +1,4 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - -package response +package http import ( "encoding/json" @@ -19,11 +7,13 @@ import ( "reflect" "time" + "github.com/go-playground/validator/v10" + "github.com/oceanbase/obagent/errors" ) -// AgentResponse defines basic API return structure for HTTP responses. -type AgentResponse struct { +// OcpAgentResponse defines basic API return structure for HTTP responses. +type OcpAgentResponse struct { Successful bool `json:"successful"` // Whether request successful or not Timestamp time.Time `json:"timestamp"` // Request handling timestamp (server time) Duration int `json:"duration"` // Request handling time cost (ms) @@ -34,7 +24,7 @@ type AgentResponse struct { Error *ApiError `json:"error,omitempty"` // Error payload when response is failed } -type AgentResponseJson struct { +type ocpAgentResponseJson struct { Successful bool `json:"successful"` // Whether request successful or not Timestamp time.Time `json:"timestamp"` // Request handling timestamp (server time) Duration int `json:"duration"` // Request handling time cost (ms) @@ -45,8 +35,8 @@ type AgentResponseJson struct { Error *ApiError `json:"error,omitempty"` // Error payload when response is failed } -func (r *AgentResponse) UnmarshalJSON(b []byte) error { - j := AgentResponseJson{} +func (r *OcpAgentResponse) UnmarshalJSON(b []byte) error { + j := ocpAgentResponseJson{} err := json.Unmarshal(b, &j) if err != nil { return err @@ -61,8 +51,14 @@ func (r *AgentResponse) UnmarshalJSON(b []byte) error { v := reflect.ValueOf(r.Data) if !v.IsValid() { err = json.Unmarshal(j.Data, &r.Data) + if err != nil { + return err + } } else if v.Type().Kind() == reflect.Ptr { err = json.Unmarshal(j.Data, r.Data) + if err != nil { + return err + } } else { tmp := reflect.New(v.Type()).Interface() err = json.Unmarshal(j.Data, tmp) @@ -79,9 +75,9 @@ type IterableData struct { } type ApiError struct { - Code int `json:"code"` // Error code - Message string `json:"message"` // Error message - SubErrors []interface{} `json:"subErrors"` // Sub errors + Code int `json:"code"` // Error code + Message string `json:"message"` // Error message + SubErrors []interface{} `json:"subErrors,omitempty"` // Sub errors } func (a ApiError) String() string { @@ -98,12 +94,20 @@ type ApiFieldError struct { Message string `json:"message"` } +func NewApiFieldError(fieldError validator.FieldError) ApiFieldError { + return ApiFieldError{ + Tag: fieldError.Tag(), + Field: fieldError.Field(), + Message: fieldError.Error(), + } +} + type ApiUnknownError struct { Error error `json:"error"` } -func NewSuccessResponse(data interface{}) AgentResponse { - return AgentResponse{ +func NewSuccessResponse(data interface{}) OcpAgentResponse { + return OcpAgentResponse{ Successful: true, Timestamp: time.Now(), Status: http.StatusOK, @@ -112,8 +116,8 @@ func NewSuccessResponse(data interface{}) AgentResponse { } } -func NewErrorResponse(err *errors.AgentError) AgentResponse { - return AgentResponse{ +func NewErrorResponse(err *errors.OcpAgentError) OcpAgentResponse { + return OcpAgentResponse{ Successful: false, Timestamp: time.Now(), Status: err.ErrorCode.Kind, @@ -125,8 +129,42 @@ func NewErrorResponse(err *errors.AgentError) AgentResponse { } } -func BuildResponse(data interface{}, err error) AgentResponse { - agenterr, ok := err.(*errors.AgentError) +func NewSubErrorsResponse(subErrors []interface{}) OcpAgentResponse { + allValidationError := true + for _, subError := range subErrors { + if _, ok := subError.(ApiFieldError); !ok { + allValidationError = false + } + } + + var status int + var code int + var message string + if allValidationError { + status = errors.ErrBadRequest.Kind + code = errors.ErrBadRequest.Code + message = "validation error" + } else { + status = errors.ErrUnexpected.Kind + code = errors.ErrUnexpected.Code + message = "unhandled error" + } + + return OcpAgentResponse{ + Successful: false, + Timestamp: time.Now(), + Status: status, + Data: nil, + Error: &ApiError{ + Code: code, + Message: message, + SubErrors: subErrors, + }, + } +} + +func BuildResponse(data interface{}, err error) OcpAgentResponse { + agenterr, ok := err.(*errors.OcpAgentError) if !ok && err != nil { agenterr = errors.Occur(errors.ErrUnexpected, err) } diff --git a/api/response/response_test.go b/lib/http/response_test.go similarity index 55% rename from api/response/response_test.go rename to lib/http/response_test.go index b69d92252fb661524d7bd1b12d3ec487bfffc28a..277b90df2d3c41e3c6a8da1e9c2565ac95cbe0f0 100644 --- a/api/response/response_test.go +++ b/lib/http/response_test.go @@ -1,16 +1,4 @@ -// Copyright (c) 2021 OceanBase -// obagent is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan PSL v2. -// You may obtain a copy of Mulan PSL v2 at: -// -// http://license.coscl.org.cn/MulanPSL2 -// -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, -// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, -// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - -package response +package http import ( "encoding/json" @@ -24,7 +12,7 @@ type S struct { } func TestJson(t *testing.T) { - resp := AgentResponse{ + resp := OcpAgentResponse{ Data: S{}, } err := json.Unmarshal([]byte(`{"Data":{"A":"11","B":123}}`), &resp) @@ -36,7 +24,7 @@ func TestJson(t *testing.T) { } ps := &S{} - resp = AgentResponse{ + resp = OcpAgentResponse{ Data: ps, } err = json.Unmarshal([]byte(`{"Data":{"A":"22","B":321}}`), &resp) @@ -47,7 +35,7 @@ func TestJson(t *testing.T) { t.Error("data wrong") } - resp = AgentResponse{} + resp = OcpAgentResponse{} err = json.Unmarshal([]byte(`{"Data":{"A":"33","B":999}}`), &resp) if err != nil { t.Error(err) diff --git a/lib/http/status.go b/lib/http/status.go new file mode 100644 index 0000000000000000000000000000000000000000..088b600e8b4a34f14f434f10bd109165f27c9b25 --- /dev/null +++ b/lib/http/status.go @@ -0,0 +1,78 @@ +package http + +import ( + "encoding/json" + "sync/atomic" +) + +type State int32 + +const ( + Unknown State = 0 + Starting State = 1 + Running State = 2 + Stopping State = 3 + Stopped State = 4 +) + +var stateString = []string{"unknown", "starting", "running", "stopping", "stopped"} + +func (s State) String() string { + return stateString[int(s)] +} + +func (s State) MarshalJSON() ([]byte, error) { + return json.Marshal(s.String()) +} + +func (s *State) UnmarshalJSON(b []byte) error { + var str string + err := json.Unmarshal(b, &str) + if err != nil { + return err + } + for i := 0; i < len(stateString); i++ { + if stateString[i] == str { + *s = State(i) + return nil + } + } + *s = Unknown + return nil +} + +// Status info api response +type Status struct { + //service state + State State `json:"state"` + //service version + Version string `json:"version"` + //service pid + Pid int `json:"pid"` + //timestamp when service started + StartAt int64 `json:"startAt"` + // Ports process occupied ports + Ports []int `json:"ports"` +} + +type StateHolder struct { + state State +} + +func NewStateHolder(init State) *StateHolder { + return &StateHolder{ + state: init, + } +} + +func (s *StateHolder) Get() State { + return State(atomic.LoadInt32((*int32)(&s.state))) +} + +func (s *StateHolder) Set(new State) { + atomic.StoreInt32((*int32)(&s.state), int32(new)) +} + +func (s *StateHolder) Cas(old, new State) bool { + return atomic.CompareAndSwapInt32((*int32)(&s.state), int32(old), int32(new)) +} diff --git a/lib/log_analyzer/agent_log_analyzer.go b/lib/log_analyzer/agent_log_analyzer.go new file mode 100644 index 0000000000000000000000000000000000000000..9738aa89e1a0fca9c81a894b9cb63c905bd35038 --- /dev/null +++ b/lib/log_analyzer/agent_log_analyzer.go @@ -0,0 +1,60 @@ +package log_analyzer + +import ( + "os" + "regexp" + "strings" + "time" + + "github.com/oceanbase/obagent/monitor/message" +) + +const agentLogTimeFormat = "2006-01-02T15:04:05.99999-07:00" + +type AgentLogAnalyzer struct { + name string +} + +var agentLogPattern = regexp.MustCompile(`^(?P