├── .devops
└── go1.20编译二进制文件.yml
├── .github
└── workflows
│ └── release.yaml
├── .gitignore
├── .goreleaser.yaml
├── LICENSE
├── README.md
├── agent
├── agent.go
├── funcs.go
└── runner.go
├── asset
└── changelog.md
├── build.sh
├── conf.d
├── config.toml
├── p.exec
│ └── exec.toml
├── p.filechange
│ └── filechange.toml
├── p.http
│ └── http.toml
├── p.journaltail
│ └── journaltail.toml
├── p.mtime
│ └── mtime.toml
├── p.net
│ └── net.toml
├── p.ping
│ └── ping.toml
├── p.procnum
│ └── procnum.toml
└── p.sfilter
│ └── sfilter.toml
├── config
├── config.go
├── duration.go
├── http_config.go
├── inline.go
└── version.go
├── docker
└── Dockerfile.goreleaser
├── engine
├── cache.go
└── engine.go
├── go.mod
├── go.sum
├── logger
└── logger.go
├── main.go
├── pkg
├── cfg
│ ├── cfg.go
│ └── scan.go
├── choice
│ └── choice.go
├── cmdx
│ ├── cmd_notwindows.go
│ ├── cmd_windows.go
│ └── cmdx.go
├── filter
│ └── filter.go
├── netx
│ └── netx.go
├── osx
│ ├── osx.go
│ └── proc.go
├── runtimex
│ └── stack.go
├── safe
│ └── queue.go
├── shell
│ └── shellquote.go
└── tls
│ ├── common.go
│ └── config.go
├── plugins
├── exec
│ └── exec.go
├── filechange
│ └── filechange.go
├── http
│ ├── http.go
│ └── tls.go
├── journaltail
│ └── journaltail.go
├── mtime
│ └── mtime.go
├── net
│ └── net.go
├── ping
│ └── ping.go
├── plugins.go
├── procnum
│ ├── native_finder.go
│ ├── native_finder_notwindows.go
│ ├── native_finder_windows.go
│ ├── process.go
│ ├── procnum.go
│ ├── win_service_notwindows.go
│ └── win_service_windows.go
└── sfilter
│ └── sfilter.go
├── scripts
├── demo.sh
├── df.sh
├── greplog.sh
└── ulimit.sh
├── types
└── event.go
└── winx
├── winx_posix.go
└── winx_windows.go
/.devops/go1.20编译二进制文件.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | name: go1.20编译二进制文件
3 | description: 显示二进制文件信息,可使用scp节点复制到其他服务器
4 | global:
5 | concurrent: 1
6 | trigger:
7 | webhook: gitlink@1.0.0
8 | event:
9 | - ref: create_tag
10 | ruleset:
11 | - param-ref: tag
12 | operator: REG_EXP
13 | value: '"v.*"'
14 | ruleset-operator: AND
15 | workflow:
16 | - ref: git_clone_0
17 | name: git clone
18 | task: git_clone@1.2.9
19 | input:
20 | remote_url: '"https://gitlink.org.cn/UlricQin/catpaw.git"'
21 | ref: '"refs/heads/master"'
22 | commit_id: '""'
23 | depth: 1
24 | needs:
25 | - start
26 | - ref: start
27 | name: 开始
28 | task: start
29 | - ref: end
30 | name: 结束
31 | task: end
32 | needs:
33 | - shell_0
34 | - ref: golang_build_node_0
35 | name: golang_build_node
36 | task: yystopf/golang_build_node@0.0.2
37 | input:
38 | workspace: git_clone_0.git_path
39 | out_bin_name: '"build-test-bin"'
40 | goos: '"linux"'
41 | goarch: '"amd64"'
42 | needs:
43 | - git_clone_0
44 | - ref: shell_0
45 | name: 显示go build bin文件
46 | image: docker.jianmuhub.com/library/alpine:3.17.0
47 | env:
48 | GO_BIN_FILE: golang_build_node_0.bin_dir
49 | script:
50 | - ls -lsh ${GO_BIN_FILE}
51 | needs:
52 | - golang_build_node_0
53 |
54 |
--------------------------------------------------------------------------------
/.github/workflows/release.yaml:
--------------------------------------------------------------------------------
1 | name: Release
2 | permissions: write-all
3 |
4 | on:
5 | push:
6 | tags:
7 | - 'v*'
8 | env:
9 | GO_VERSION: 1.22
10 |
11 | jobs:
12 | goreleaser:
13 | runs-on: ubuntu-20.04
14 | steps:
15 | - name: Checkout Source Code
16 | uses: actions/checkout@v3
17 | with:
18 | fetch-depth: 0
19 | - name: Setup Go Environment
20 | uses: actions/setup-go@v3
21 | with:
22 | go-version: ${{ env.GO_VERSION }}
23 | - uses: docker/login-action@v2
24 | with:
25 | username: ${{ secrets.DOCKERHUB_USERNAME }}
26 | password: ${{ secrets.DOCKERHUB_TOKEN }}
27 | - name: Run GoReleaser
28 | uses: goreleaser/goreleaser-action@v3
29 | with:
30 | version: '~> v1'
31 | args: release --rm-dist
32 | distribution: goreleaser
33 | env:
34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
35 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /catpaw*
2 | /nohup.out
3 | /vendor
4 | /dist
5 |
6 | .idea
7 | .DS_Store
8 | .vscode
9 | .log
--------------------------------------------------------------------------------
/.goreleaser.yaml:
--------------------------------------------------------------------------------
1 | before:
2 | hooks:
3 | # You may remove this if you don't use go modules.
4 | - go mod tidy
5 |
6 | snapshot:
7 | name_template: '{{ .Tag }}'
8 | checksum:
9 | name_template: 'checksums.txt'
10 | changelog:
11 | skip: true
12 |
13 | builds:
14 | - id: build
15 | main: ./
16 | binary: catpaw
17 | env:
18 | - CGO_ENABLED=0
19 | goos:
20 | - linux
21 | - windows
22 | goarch:
23 | - arm64
24 | - amd64
25 | ldflags:
26 | - -s -w
27 | - -X github.com/cprobe/catpaw/config.Version={{ .Tag }}-{{.Commit}}
28 |
29 | archives:
30 | - id: archive
31 | rlcp: true
32 | builds:
33 | - build
34 | format: tar.gz
35 | format_overrides:
36 | - goos: windows
37 | format: zip
38 | name_template: "{{ .ProjectName }}-v{{ .Version }}-{{ .Os }}-{{ .Arch }}"
39 | wrap_in_directory: true
40 | files:
41 | - conf.d/*
42 | - LICENSE
43 | - README.md
44 |
45 |
46 | release:
47 | github:
48 | owner: cprobe
49 | name: catpaw
50 | name_template: "v{{ .Version }}"
51 |
52 | dockers:
53 | - image_templates:
54 | - flashcatcloud/catpaw:{{ .Tag }}-amd64
55 | goos: linux
56 | goarch: amd64
57 | ids:
58 | - build
59 | dockerfile: docker/Dockerfile.goreleaser
60 | extra_files:
61 | - LICENSE
62 | use: buildx
63 | build_flag_templates:
64 | - "--platform=linux/amd64"
65 |
66 | - image_templates:
67 | - flashcatcloud/catpaw:{{ .Tag }}-arm64v8
68 | goos: linux
69 | goarch: arm64
70 | ids:
71 | - build
72 | dockerfile: docker/Dockerfile.goreleaser
73 | extra_files:
74 | - LICENSE
75 | use: buildx
76 | build_flag_templates:
77 | - "--platform=linux/arm64/v8"
78 |
79 | docker_manifests:
80 | - name_template: flashcatcloud/catpaw:{{ .Tag }}
81 | image_templates:
82 | - flashcatcloud/catpaw:{{ .Tag }}-amd64
83 | - flashcatcloud/catpaw:{{ .Tag }}-arm64v8
84 |
85 | - name_template: flashcatcloud/catpaw:latest
86 | image_templates:
87 | - flashcatcloud/catpaw:{{ .Tag }}-amd64
88 | - flashcatcloud/catpaw:{{ .Tag }}-arm64v8
89 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU AFFERO GENERAL PUBLIC LICENSE
2 | Version 3, 19 November 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU Affero General Public License is a free, copyleft license for
11 | software and other kinds of works, specifically designed to ensure
12 | cooperation with the community in the case of network server software.
13 |
14 | The licenses for most software and other practical works are designed
15 | to take away your freedom to share and change the works. By contrast,
16 | our General Public Licenses are intended to guarantee your freedom to
17 | share and change all versions of a program--to make sure it remains free
18 | software for all its users.
19 |
20 | When we speak of free software, we are referring to freedom, not
21 | price. Our General Public Licenses are designed to make sure that you
22 | have the freedom to distribute copies of free software (and charge for
23 | them if you wish), that you receive source code or can get it if you
24 | want it, that you can change the software or use pieces of it in new
25 | free programs, and that you know you can do these things.
26 |
27 | Developers that use our General Public Licenses protect your rights
28 | with two steps: (1) assert copyright on the software, and (2) offer
29 | you this License which gives you legal permission to copy, distribute
30 | and/or modify the software.
31 |
32 | A secondary benefit of defending all users' freedom is that
33 | improvements made in alternate versions of the program, if they
34 | receive widespread use, become available for other developers to
35 | incorporate. Many developers of free software are heartened and
36 | encouraged by the resulting cooperation. However, in the case of
37 | software used on network servers, this result may fail to come about.
38 | The GNU General Public License permits making a modified version and
39 | letting the public access it on a server without ever releasing its
40 | source code to the public.
41 |
42 | The GNU Affero General Public License is designed specifically to
43 | ensure that, in such cases, the modified source code becomes available
44 | to the community. It requires the operator of a network server to
45 | provide the source code of the modified version running there to the
46 | users of that server. Therefore, public use of a modified version, on
47 | a publicly accessible server, gives the public access to the source
48 | code of the modified version.
49 |
50 | An older license, called the Affero General Public License and
51 | published by Affero, was designed to accomplish similar goals. This is
52 | a different license, not a version of the Affero GPL, but Affero has
53 | released a new version of the Affero GPL which permits relicensing under
54 | this license.
55 |
56 | The precise terms and conditions for copying, distribution and
57 | modification follow.
58 |
59 | TERMS AND CONDITIONS
60 |
61 | 0. Definitions.
62 |
63 | "This License" refers to version 3 of the GNU Affero General Public License.
64 |
65 | "Copyright" also means copyright-like laws that apply to other kinds of
66 | works, such as semiconductor masks.
67 |
68 | "The Program" refers to any copyrightable work licensed under this
69 | License. Each licensee is addressed as "you". "Licensees" and
70 | "recipients" may be individuals or organizations.
71 |
72 | To "modify" a work means to copy from or adapt all or part of the work
73 | in a fashion requiring copyright permission, other than the making of an
74 | exact copy. The resulting work is called a "modified version" of the
75 | earlier work or a work "based on" the earlier work.
76 |
77 | A "covered work" means either the unmodified Program or a work based
78 | on the Program.
79 |
80 | To "propagate" a work means to do anything with it that, without
81 | permission, would make you directly or secondarily liable for
82 | infringement under applicable copyright law, except executing it on a
83 | computer or modifying a private copy. Propagation includes copying,
84 | distribution (with or without modification), making available to the
85 | public, and in some countries other activities as well.
86 |
87 | To "convey" a work means any kind of propagation that enables other
88 | parties to make or receive copies. Mere interaction with a user through
89 | a computer network, with no transfer of a copy, is not conveying.
90 |
91 | An interactive user interface displays "Appropriate Legal Notices"
92 | to the extent that it includes a convenient and prominently visible
93 | feature that (1) displays an appropriate copyright notice, and (2)
94 | tells the user that there is no warranty for the work (except to the
95 | extent that warranties are provided), that licensees may convey the
96 | work under this License, and how to view a copy of this License. If
97 | the interface presents a list of user commands or options, such as a
98 | menu, a prominent item in the list meets this criterion.
99 |
100 | 1. Source Code.
101 |
102 | The "source code" for a work means the preferred form of the work
103 | for making modifications to it. "Object code" means any non-source
104 | form of a work.
105 |
106 | A "Standard Interface" means an interface that either is an official
107 | standard defined by a recognized standards body, or, in the case of
108 | interfaces specified for a particular programming language, one that
109 | is widely used among developers working in that language.
110 |
111 | The "System Libraries" of an executable work include anything, other
112 | than the work as a whole, that (a) is included in the normal form of
113 | packaging a Major Component, but which is not part of that Major
114 | Component, and (b) serves only to enable use of the work with that
115 | Major Component, or to implement a Standard Interface for which an
116 | implementation is available to the public in source code form. A
117 | "Major Component", in this context, means a major essential component
118 | (kernel, window system, and so on) of the specific operating system
119 | (if any) on which the executable work runs, or a compiler used to
120 | produce the work, or an object code interpreter used to run it.
121 |
122 | The "Corresponding Source" for a work in object code form means all
123 | the source code needed to generate, install, and (for an executable
124 | work) run the object code and to modify the work, including scripts to
125 | control those activities. However, it does not include the work's
126 | System Libraries, or general-purpose tools or generally available free
127 | programs which are used unmodified in performing those activities but
128 | which are not part of the work. For example, Corresponding Source
129 | includes interface definition files associated with source files for
130 | the work, and the source code for shared libraries and dynamically
131 | linked subprograms that the work is specifically designed to require,
132 | such as by intimate data communication or control flow between those
133 | subprograms and other parts of the work.
134 |
135 | The Corresponding Source need not include anything that users
136 | can regenerate automatically from other parts of the Corresponding
137 | Source.
138 |
139 | The Corresponding Source for a work in source code form is that
140 | same work.
141 |
142 | 2. Basic Permissions.
143 |
144 | All rights granted under this License are granted for the term of
145 | copyright on the Program, and are irrevocable provided the stated
146 | conditions are met. This License explicitly affirms your unlimited
147 | permission to run the unmodified Program. The output from running a
148 | covered work is covered by this License only if the output, given its
149 | content, constitutes a covered work. This License acknowledges your
150 | rights of fair use or other equivalent, as provided by copyright law.
151 |
152 | You may make, run and propagate covered works that you do not
153 | convey, without conditions so long as your license otherwise remains
154 | in force. You may convey covered works to others for the sole purpose
155 | of having them make modifications exclusively for you, or provide you
156 | with facilities for running those works, provided that you comply with
157 | the terms of this License in conveying all material for which you do
158 | not control copyright. Those thus making or running the covered works
159 | for you must do so exclusively on your behalf, under your direction
160 | and control, on terms that prohibit them from making any copies of
161 | your copyrighted material outside their relationship with you.
162 |
163 | Conveying under any other circumstances is permitted solely under
164 | the conditions stated below. Sublicensing is not allowed; section 10
165 | makes it unnecessary.
166 |
167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
168 |
169 | No covered work shall be deemed part of an effective technological
170 | measure under any applicable law fulfilling obligations under article
171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
172 | similar laws prohibiting or restricting circumvention of such
173 | measures.
174 |
175 | When you convey a covered work, you waive any legal power to forbid
176 | circumvention of technological measures to the extent such circumvention
177 | is effected by exercising rights under this License with respect to
178 | the covered work, and you disclaim any intention to limit operation or
179 | modification of the work as a means of enforcing, against the work's
180 | users, your or third parties' legal rights to forbid circumvention of
181 | technological measures.
182 |
183 | 4. Conveying Verbatim Copies.
184 |
185 | You may convey verbatim copies of the Program's source code as you
186 | receive it, in any medium, provided that you conspicuously and
187 | appropriately publish on each copy an appropriate copyright notice;
188 | keep intact all notices stating that this License and any
189 | non-permissive terms added in accord with section 7 apply to the code;
190 | keep intact all notices of the absence of any warranty; and give all
191 | recipients a copy of this License along with the Program.
192 |
193 | You may charge any price or no price for each copy that you convey,
194 | and you may offer support or warranty protection for a fee.
195 |
196 | 5. Conveying Modified Source Versions.
197 |
198 | You may convey a work based on the Program, or the modifications to
199 | produce it from the Program, in the form of source code under the
200 | terms of section 4, provided that you also meet all of these conditions:
201 |
202 | a) The work must carry prominent notices stating that you modified
203 | it, and giving a relevant date.
204 |
205 | b) The work must carry prominent notices stating that it is
206 | released under this License and any conditions added under section
207 | 7. This requirement modifies the requirement in section 4 to
208 | "keep intact all notices".
209 |
210 | c) You must license the entire work, as a whole, under this
211 | License to anyone who comes into possession of a copy. This
212 | License will therefore apply, along with any applicable section 7
213 | additional terms, to the whole of the work, and all its parts,
214 | regardless of how they are packaged. This License gives no
215 | permission to license the work in any other way, but it does not
216 | invalidate such permission if you have separately received it.
217 |
218 | d) If the work has interactive user interfaces, each must display
219 | Appropriate Legal Notices; however, if the Program has interactive
220 | interfaces that do not display Appropriate Legal Notices, your
221 | work need not make them do so.
222 |
223 | A compilation of a covered work with other separate and independent
224 | works, which are not by their nature extensions of the covered work,
225 | and which are not combined with it such as to form a larger program,
226 | in or on a volume of a storage or distribution medium, is called an
227 | "aggregate" if the compilation and its resulting copyright are not
228 | used to limit the access or legal rights of the compilation's users
229 | beyond what the individual works permit. Inclusion of a covered work
230 | in an aggregate does not cause this License to apply to the other
231 | parts of the aggregate.
232 |
233 | 6. Conveying Non-Source Forms.
234 |
235 | You may convey a covered work in object code form under the terms
236 | of sections 4 and 5, provided that you also convey the
237 | machine-readable Corresponding Source under the terms of this License,
238 | in one of these ways:
239 |
240 | a) Convey the object code in, or embodied in, a physical product
241 | (including a physical distribution medium), accompanied by the
242 | Corresponding Source fixed on a durable physical medium
243 | customarily used for software interchange.
244 |
245 | b) Convey the object code in, or embodied in, a physical product
246 | (including a physical distribution medium), accompanied by a
247 | written offer, valid for at least three years and valid for as
248 | long as you offer spare parts or customer support for that product
249 | model, to give anyone who possesses the object code either (1) a
250 | copy of the Corresponding Source for all the software in the
251 | product that is covered by this License, on a durable physical
252 | medium customarily used for software interchange, for a price no
253 | more than your reasonable cost of physically performing this
254 | conveying of source, or (2) access to copy the
255 | Corresponding Source from a network server at no charge.
256 |
257 | c) Convey individual copies of the object code with a copy of the
258 | written offer to provide the Corresponding Source. This
259 | alternative is allowed only occasionally and noncommercially, and
260 | only if you received the object code with such an offer, in accord
261 | with subsection 6b.
262 |
263 | d) Convey the object code by offering access from a designated
264 | place (gratis or for a charge), and offer equivalent access to the
265 | Corresponding Source in the same way through the same place at no
266 | further charge. You need not require recipients to copy the
267 | Corresponding Source along with the object code. If the place to
268 | copy the object code is a network server, the Corresponding Source
269 | may be on a different server (operated by you or a third party)
270 | that supports equivalent copying facilities, provided you maintain
271 | clear directions next to the object code saying where to find the
272 | Corresponding Source. Regardless of what server hosts the
273 | Corresponding Source, you remain obligated to ensure that it is
274 | available for as long as needed to satisfy these requirements.
275 |
276 | e) Convey the object code using peer-to-peer transmission, provided
277 | you inform other peers where the object code and Corresponding
278 | Source of the work are being offered to the general public at no
279 | charge under subsection 6d.
280 |
281 | A separable portion of the object code, whose source code is excluded
282 | from the Corresponding Source as a System Library, need not be
283 | included in conveying the object code work.
284 |
285 | A "User Product" is either (1) a "consumer product", which means any
286 | tangible personal property which is normally used for personal, family,
287 | or household purposes, or (2) anything designed or sold for incorporation
288 | into a dwelling. In determining whether a product is a consumer product,
289 | doubtful cases shall be resolved in favor of coverage. For a particular
290 | product received by a particular user, "normally used" refers to a
291 | typical or common use of that class of product, regardless of the status
292 | of the particular user or of the way in which the particular user
293 | actually uses, or expects or is expected to use, the product. A product
294 | is a consumer product regardless of whether the product has substantial
295 | commercial, industrial or non-consumer uses, unless such uses represent
296 | the only significant mode of use of the product.
297 |
298 | "Installation Information" for a User Product means any methods,
299 | procedures, authorization keys, or other information required to install
300 | and execute modified versions of a covered work in that User Product from
301 | a modified version of its Corresponding Source. The information must
302 | suffice to ensure that the continued functioning of the modified object
303 | code is in no case prevented or interfered with solely because
304 | modification has been made.
305 |
306 | If you convey an object code work under this section in, or with, or
307 | specifically for use in, a User Product, and the conveying occurs as
308 | part of a transaction in which the right of possession and use of the
309 | User Product is transferred to the recipient in perpetuity or for a
310 | fixed term (regardless of how the transaction is characterized), the
311 | Corresponding Source conveyed under this section must be accompanied
312 | by the Installation Information. But this requirement does not apply
313 | if neither you nor any third party retains the ability to install
314 | modified object code on the User Product (for example, the work has
315 | been installed in ROM).
316 |
317 | The requirement to provide Installation Information does not include a
318 | requirement to continue to provide support service, warranty, or updates
319 | for a work that has been modified or installed by the recipient, or for
320 | the User Product in which it has been modified or installed. Access to a
321 | network may be denied when the modification itself materially and
322 | adversely affects the operation of the network or violates the rules and
323 | protocols for communication across the network.
324 |
325 | Corresponding Source conveyed, and Installation Information provided,
326 | in accord with this section must be in a format that is publicly
327 | documented (and with an implementation available to the public in
328 | source code form), and must require no special password or key for
329 | unpacking, reading or copying.
330 |
331 | 7. Additional Terms.
332 |
333 | "Additional permissions" are terms that supplement the terms of this
334 | License by making exceptions from one or more of its conditions.
335 | Additional permissions that are applicable to the entire Program shall
336 | be treated as though they were included in this License, to the extent
337 | that they are valid under applicable law. If additional permissions
338 | apply only to part of the Program, that part may be used separately
339 | under those permissions, but the entire Program remains governed by
340 | this License without regard to the additional permissions.
341 |
342 | When you convey a copy of a covered work, you may at your option
343 | remove any additional permissions from that copy, or from any part of
344 | it. (Additional permissions may be written to require their own
345 | removal in certain cases when you modify the work.) You may place
346 | additional permissions on material, added by you to a covered work,
347 | for which you have or can give appropriate copyright permission.
348 |
349 | Notwithstanding any other provision of this License, for material you
350 | add to a covered work, you may (if authorized by the copyright holders of
351 | that material) supplement the terms of this License with terms:
352 |
353 | a) Disclaiming warranty or limiting liability differently from the
354 | terms of sections 15 and 16 of this License; or
355 |
356 | b) Requiring preservation of specified reasonable legal notices or
357 | author attributions in that material or in the Appropriate Legal
358 | Notices displayed by works containing it; or
359 |
360 | c) Prohibiting misrepresentation of the origin of that material, or
361 | requiring that modified versions of such material be marked in
362 | reasonable ways as different from the original version; or
363 |
364 | d) Limiting the use for publicity purposes of names of licensors or
365 | authors of the material; or
366 |
367 | e) Declining to grant rights under trademark law for use of some
368 | trade names, trademarks, or service marks; or
369 |
370 | f) Requiring indemnification of licensors and authors of that
371 | material by anyone who conveys the material (or modified versions of
372 | it) with contractual assumptions of liability to the recipient, for
373 | any liability that these contractual assumptions directly impose on
374 | those licensors and authors.
375 |
376 | All other non-permissive additional terms are considered "further
377 | restrictions" within the meaning of section 10. If the Program as you
378 | received it, or any part of it, contains a notice stating that it is
379 | governed by this License along with a term that is a further
380 | restriction, you may remove that term. If a license document contains
381 | a further restriction but permits relicensing or conveying under this
382 | License, you may add to a covered work material governed by the terms
383 | of that license document, provided that the further restriction does
384 | not survive such relicensing or conveying.
385 |
386 | If you add terms to a covered work in accord with this section, you
387 | must place, in the relevant source files, a statement of the
388 | additional terms that apply to those files, or a notice indicating
389 | where to find the applicable terms.
390 |
391 | Additional terms, permissive or non-permissive, may be stated in the
392 | form of a separately written license, or stated as exceptions;
393 | the above requirements apply either way.
394 |
395 | 8. Termination.
396 |
397 | You may not propagate or modify a covered work except as expressly
398 | provided under this License. Any attempt otherwise to propagate or
399 | modify it is void, and will automatically terminate your rights under
400 | this License (including any patent licenses granted under the third
401 | paragraph of section 11).
402 |
403 | However, if you cease all violation of this License, then your
404 | license from a particular copyright holder is reinstated (a)
405 | provisionally, unless and until the copyright holder explicitly and
406 | finally terminates your license, and (b) permanently, if the copyright
407 | holder fails to notify you of the violation by some reasonable means
408 | prior to 60 days after the cessation.
409 |
410 | Moreover, your license from a particular copyright holder is
411 | reinstated permanently if the copyright holder notifies you of the
412 | violation by some reasonable means, this is the first time you have
413 | received notice of violation of this License (for any work) from that
414 | copyright holder, and you cure the violation prior to 30 days after
415 | your receipt of the notice.
416 |
417 | Termination of your rights under this section does not terminate the
418 | licenses of parties who have received copies or rights from you under
419 | this License. If your rights have been terminated and not permanently
420 | reinstated, you do not qualify to receive new licenses for the same
421 | material under section 10.
422 |
423 | 9. Acceptance Not Required for Having Copies.
424 |
425 | You are not required to accept this License in order to receive or
426 | run a copy of the Program. Ancillary propagation of a covered work
427 | occurring solely as a consequence of using peer-to-peer transmission
428 | to receive a copy likewise does not require acceptance. However,
429 | nothing other than this License grants you permission to propagate or
430 | modify any covered work. These actions infringe copyright if you do
431 | not accept this License. Therefore, by modifying or propagating a
432 | covered work, you indicate your acceptance of this License to do so.
433 |
434 | 10. Automatic Licensing of Downstream Recipients.
435 |
436 | Each time you convey a covered work, the recipient automatically
437 | receives a license from the original licensors, to run, modify and
438 | propagate that work, subject to this License. You are not responsible
439 | for enforcing compliance by third parties with this License.
440 |
441 | An "entity transaction" is a transaction transferring control of an
442 | organization, or substantially all assets of one, or subdividing an
443 | organization, or merging organizations. If propagation of a covered
444 | work results from an entity transaction, each party to that
445 | transaction who receives a copy of the work also receives whatever
446 | licenses to the work the party's predecessor in interest had or could
447 | give under the previous paragraph, plus a right to possession of the
448 | Corresponding Source of the work from the predecessor in interest, if
449 | the predecessor has it or can get it with reasonable efforts.
450 |
451 | You may not impose any further restrictions on the exercise of the
452 | rights granted or affirmed under this License. For example, you may
453 | not impose a license fee, royalty, or other charge for exercise of
454 | rights granted under this License, and you may not initiate litigation
455 | (including a cross-claim or counterclaim in a lawsuit) alleging that
456 | any patent claim is infringed by making, using, selling, offering for
457 | sale, or importing the Program or any portion of it.
458 |
459 | 11. Patents.
460 |
461 | A "contributor" is a copyright holder who authorizes use under this
462 | License of the Program or a work on which the Program is based. The
463 | work thus licensed is called the contributor's "contributor version".
464 |
465 | A contributor's "essential patent claims" are all patent claims
466 | owned or controlled by the contributor, whether already acquired or
467 | hereafter acquired, that would be infringed by some manner, permitted
468 | by this License, of making, using, or selling its contributor version,
469 | but do not include claims that would be infringed only as a
470 | consequence of further modification of the contributor version. For
471 | purposes of this definition, "control" includes the right to grant
472 | patent sublicenses in a manner consistent with the requirements of
473 | this License.
474 |
475 | Each contributor grants you a non-exclusive, worldwide, royalty-free
476 | patent license under the contributor's essential patent claims, to
477 | make, use, sell, offer for sale, import and otherwise run, modify and
478 | propagate the contents of its contributor version.
479 |
480 | In the following three paragraphs, a "patent license" is any express
481 | agreement or commitment, however denominated, not to enforce a patent
482 | (such as an express permission to practice a patent or covenant not to
483 | sue for patent infringement). To "grant" such a patent license to a
484 | party means to make such an agreement or commitment not to enforce a
485 | patent against the party.
486 |
487 | If you convey a covered work, knowingly relying on a patent license,
488 | and the Corresponding Source of the work is not available for anyone
489 | to copy, free of charge and under the terms of this License, through a
490 | publicly available network server or other readily accessible means,
491 | then you must either (1) cause the Corresponding Source to be so
492 | available, or (2) arrange to deprive yourself of the benefit of the
493 | patent license for this particular work, or (3) arrange, in a manner
494 | consistent with the requirements of this License, to extend the patent
495 | license to downstream recipients. "Knowingly relying" means you have
496 | actual knowledge that, but for the patent license, your conveying the
497 | covered work in a country, or your recipient's use of the covered work
498 | in a country, would infringe one or more identifiable patents in that
499 | country that you have reason to believe are valid.
500 |
501 | If, pursuant to or in connection with a single transaction or
502 | arrangement, you convey, or propagate by procuring conveyance of, a
503 | covered work, and grant a patent license to some of the parties
504 | receiving the covered work authorizing them to use, propagate, modify
505 | or convey a specific copy of the covered work, then the patent license
506 | you grant is automatically extended to all recipients of the covered
507 | work and works based on it.
508 |
509 | A patent license is "discriminatory" if it does not include within
510 | the scope of its coverage, prohibits the exercise of, or is
511 | conditioned on the non-exercise of one or more of the rights that are
512 | specifically granted under this License. You may not convey a covered
513 | work if you are a party to an arrangement with a third party that is
514 | in the business of distributing software, under which you make payment
515 | to the third party based on the extent of your activity of conveying
516 | the work, and under which the third party grants, to any of the
517 | parties who would receive the covered work from you, a discriminatory
518 | patent license (a) in connection with copies of the covered work
519 | conveyed by you (or copies made from those copies), or (b) primarily
520 | for and in connection with specific products or compilations that
521 | contain the covered work, unless you entered into that arrangement,
522 | or that patent license was granted, prior to 28 March 2007.
523 |
524 | Nothing in this License shall be construed as excluding or limiting
525 | any implied license or other defenses to infringement that may
526 | otherwise be available to you under applicable patent law.
527 |
528 | 12. No Surrender of Others' Freedom.
529 |
530 | If conditions are imposed on you (whether by court order, agreement or
531 | otherwise) that contradict the conditions of this License, they do not
532 | excuse you from the conditions of this License. If you cannot convey a
533 | covered work so as to satisfy simultaneously your obligations under this
534 | License and any other pertinent obligations, then as a consequence you may
535 | not convey it at all. For example, if you agree to terms that obligate you
536 | to collect a royalty for further conveying from those to whom you convey
537 | the Program, the only way you could satisfy both those terms and this
538 | License would be to refrain entirely from conveying the Program.
539 |
540 | 13. Remote Network Interaction; Use with the GNU General Public License.
541 |
542 | Notwithstanding any other provision of this License, if you modify the
543 | Program, your modified version must prominently offer all users
544 | interacting with it remotely through a computer network (if your version
545 | supports such interaction) an opportunity to receive the Corresponding
546 | Source of your version by providing access to the Corresponding Source
547 | from a network server at no charge, through some standard or customary
548 | means of facilitating copying of software. This Corresponding Source
549 | shall include the Corresponding Source for any work covered by version 3
550 | of the GNU General Public License that is incorporated pursuant to the
551 | following paragraph.
552 |
553 | Notwithstanding any other provision of this License, you have
554 | permission to link or combine any covered work with a work licensed
555 | under version 3 of the GNU General Public License into a single
556 | combined work, and to convey the resulting work. The terms of this
557 | License will continue to apply to the part which is the covered work,
558 | but the work with which it is combined will remain governed by version
559 | 3 of the GNU General Public License.
560 |
561 | 14. Revised Versions of this License.
562 |
563 | The Free Software Foundation may publish revised and/or new versions of
564 | the GNU Affero General Public License from time to time. Such new versions
565 | will be similar in spirit to the present version, but may differ in detail to
566 | address new problems or concerns.
567 |
568 | Each version is given a distinguishing version number. If the
569 | Program specifies that a certain numbered version of the GNU Affero General
570 | Public License "or any later version" applies to it, you have the
571 | option of following the terms and conditions either of that numbered
572 | version or of any later version published by the Free Software
573 | Foundation. If the Program does not specify a version number of the
574 | GNU Affero General Public License, you may choose any version ever published
575 | by the Free Software Foundation.
576 |
577 | If the Program specifies that a proxy can decide which future
578 | versions of the GNU Affero General Public License can be used, that proxy's
579 | public statement of acceptance of a version permanently authorizes you
580 | to choose that version for the Program.
581 |
582 | Later license versions may give you additional or different
583 | permissions. However, no additional obligations are imposed on any
584 | author or copyright holder as a result of your choosing to follow a
585 | later version.
586 |
587 | 15. Disclaimer of Warranty.
588 |
589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
597 |
598 | 16. Limitation of Liability.
599 |
600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
608 | SUCH DAMAGES.
609 |
610 | 17. Interpretation of Sections 15 and 16.
611 |
612 | If the disclaimer of warranty and limitation of liability provided
613 | above cannot be given local legal effect according to their terms,
614 | reviewing courts shall apply local law that most closely approximates
615 | an absolute waiver of all civil liability in connection with the
616 | Program, unless a warranty or assumption of liability accompanies a
617 | copy of the Program in return for a fee.
618 |
619 | END OF TERMS AND CONDITIONS
620 |
621 | How to Apply These Terms to Your New Programs
622 |
623 | If you develop a new program, and you want it to be of the greatest
624 | possible use to the public, the best way to achieve this is to make it
625 | free software which everyone can redistribute and change under these terms.
626 |
627 | To do so, attach the following notices to the program. It is safest
628 | to attach them to the start of each source file to most effectively
629 | state the exclusion of warranty; and each file should have at least
630 | the "copyright" line and a pointer to where the full notice is found.
631 |
632 |
633 | Copyright (C)
634 |
635 | This program is free software: you can redistribute it and/or modify
636 | it under the terms of the GNU Affero General Public License as published
637 | by the Free Software Foundation, either version 3 of the License, or
638 | (at your option) any later version.
639 |
640 | This program is distributed in the hope that it will be useful,
641 | but WITHOUT ANY WARRANTY; without even the implied warranty of
642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
643 | GNU Affero General Public License for more details.
644 |
645 | You should have received a copy of the GNU Affero General Public License
646 | along with this program. If not, see .
647 |
648 | Also add information on how to contact you by electronic and paper mail.
649 |
650 | If your software can interact with users remotely through a computer
651 | network, you should also make sure that it provides a way for users to
652 | get its source. For example, if your program is a web application, its
653 | interface could display a "Source" link that leads users to an archive
654 | of the code. There are many ways you could offer source, and different
655 | solutions will be better for different programs; see section 13 for the
656 | specific requirements.
657 |
658 | You should also get your employer (if you work as a programmer) or school,
659 | if any, to sign a "copyright disclaimer" for the program, if necessary.
660 | For more information on this, and how to apply and follow the GNU AGPL, see
661 | .
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## 简介
2 | 这是一个极为轻量的事件监控系统,用于探测一些异常事件并生成告警。通常和 Flashduty 协同使用(当然,你也可以写个接收事件的小服务来做告警分发),catpaw 负责产生事件,Flashduty 负责发送事件。
3 |
4 | ## 插件简介
5 | catpaw 是插件机制,提供了不同功能的插件用于不同的监控场景。
6 |
7 | ### exec
8 | 自定义脚本执行的插件。脚本用什么语言写都可以,只要按照规定的格式输出即可。
9 |
10 | ### filechange
11 | 监控近期是否有文件发生变化,比如 `/etc/shadow` 等重要文件。
12 |
13 | ### http
14 | 监控 HTTP URL,检查返回的状态码和内容是否符合预期。
15 |
16 | ### journaltail
17 | 使用 journalctl 命令检查日志,如果日志里有关键字就产生事件。
18 |
19 | ### mtime
20 | 递归检查某个目录下的所有文件的 mtime,如果有文件在近期发生变化就产生事件。
21 |
22 | ### net
23 | 通过 tcp、udp 方式探测远端端口是否可用。
24 |
25 | ### ping
26 | 通过 icmp 方式探测远端主机是否可用。
27 |
28 | ### procnum
29 | 检查某个进程的数量,如果数量不够(通常是进程挂了)就产生事件。
30 |
31 | ### sfilter
32 | 执行脚本,检查输出,只要输出中包含关键字就产生事件。
33 |
34 | ## 使用场景
35 |
36 | - 不想引入大型监控系统,不想有太多依赖,就想对一些重要的事情做一些简单的监控。
37 | - 监控系统的自监控。为了避免循环依赖,对监控系统做监控,通常需要另一个系统,catpaw 轻量,合适。
38 | - 对一些日志、字符串、事件文本做监控,直接读取匹配了关键字就告警。
39 |
40 | ## 安装
41 |
42 | 从 [github releases](https://github.com/cprobe/catpaw/releases) 页面下载编译好的二进制。
43 |
44 | ## 使用
45 |
46 | 首先你需要注册一个 Flashduty 账号。
47 |
48 | - [Flashduty产品介绍](https://flashcat.cloud/product/flashduty/)
49 | - [Flashduty免费注册](https://console.flashcat.cloud/)
50 |
51 | 然后在集成中心创建一个“标准告警事件”的集成,随便起个名字,保存,就可以得到一个 webhook 地址。如果搞不定,Flashduty 页面右上角有较为详细的文档和视频教程。
52 |
53 | 把 webhook 地址配置到 catpaw 的配置文件中:`conf.d/config.toml`,配置到 flashduty 下面的 url 字段。然后,就可以启动 catpaw 玩耍了。catpaw 有几个命令行参数,通过 `./catpaw --help` 可以看到。
54 |
55 | 当然了,具体要监控什么,需要去修改各个插件的配置,每个插件的配置文件在 `conf.d` 目录下,比如 `conf.d/p.http` 就是 http 插件的配置文件。里边有详尽的注释。
56 |
57 | ## 交流
58 |
59 | 可以加我微信:`picobyte` 进群交流。备注 `catpaw`。
60 |
61 |
--------------------------------------------------------------------------------
/agent/agent.go:
--------------------------------------------------------------------------------
1 | package agent
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 | "sync"
7 |
8 | "flashcat.cloud/catpaw/config"
9 | "flashcat.cloud/catpaw/logger"
10 | "flashcat.cloud/catpaw/pkg/choice"
11 | "flashcat.cloud/catpaw/plugins"
12 | "github.com/BurntSushi/toml"
13 | "github.com/toolkits/pkg/file"
14 |
15 | // auto registry
16 | _ "flashcat.cloud/catpaw/plugins/exec"
17 | _ "flashcat.cloud/catpaw/plugins/filechange"
18 | _ "flashcat.cloud/catpaw/plugins/http"
19 | _ "flashcat.cloud/catpaw/plugins/journaltail"
20 | _ "flashcat.cloud/catpaw/plugins/mtime"
21 | _ "flashcat.cloud/catpaw/plugins/net"
22 | _ "flashcat.cloud/catpaw/plugins/ping"
23 | _ "flashcat.cloud/catpaw/plugins/procnum"
24 | _ "flashcat.cloud/catpaw/plugins/sfilter"
25 | )
26 |
27 | type PluginConfig struct {
28 | Source string // file || http
29 | Digest string
30 | FileContent []byte
31 | }
32 |
33 | type Agent struct {
34 | pluginFilters map[string]struct{}
35 | pluginConfigs map[string]*PluginConfig
36 | pluginRunners map[string]*PluginRunner
37 | sync.RWMutex
38 | }
39 |
40 | func New() *Agent {
41 | return &Agent{
42 | pluginFilters: parseFilter(config.Config.Plugins),
43 | pluginConfigs: make(map[string]*PluginConfig),
44 | pluginRunners: make(map[string]*PluginRunner),
45 | }
46 | }
47 |
48 | func (a *Agent) Start() {
49 | logger.Logger.Info("agent starting")
50 |
51 | pcs, err := loadFileConfigs()
52 | if err != nil {
53 | logger.Logger.Error("load file configs fail:", err)
54 | return
55 | }
56 |
57 | for name, pc := range pcs {
58 | a.LoadPlugin(name, pc)
59 | }
60 |
61 | logger.Logger.Info("agent started")
62 | }
63 |
64 | func (a *Agent) LoadPlugin(name string, pc *PluginConfig) {
65 | if len(a.pluginFilters) > 0 {
66 | // need filter by --plugins
67 | _, has := a.pluginFilters[name]
68 | if !has {
69 | return
70 | }
71 | }
72 |
73 | logger.Logger.Infof("%s: loading...", name)
74 |
75 | creator, has := plugins.PluginCreators[name]
76 | if !has {
77 | logger.Logger.Infof("%s: plugin not supported", name)
78 | return
79 | }
80 |
81 | pluginObject := creator()
82 | err := toml.Unmarshal(pc.FileContent, pluginObject)
83 | if err != nil {
84 | logger.Logger.Errorf("%s: unmarshal plugin config fail: %v", name, err)
85 | return
86 | }
87 |
88 | // structs will have value after toml.Unmarshal
89 | // apply partial configuration if some fields are not set
90 | err = plugins.MayApplyPartials(pluginObject)
91 | if err != nil {
92 | logger.Logger.Errorf("%s: apply partial config fail: %v", name, err)
93 | return
94 | }
95 |
96 | runner := newPluginRunner(name, pluginObject)
97 | runner.start()
98 |
99 | a.Lock()
100 | a.pluginRunners[name] = runner
101 | a.pluginConfigs[name] = pc
102 | a.Unlock()
103 | }
104 |
105 | func (a *Agent) DelPlugin(name string) {
106 | a.Lock()
107 | defer a.Unlock()
108 |
109 | if runner, has := a.pluginRunners[name]; has {
110 | runner.stop()
111 | delete(a.pluginRunners, name)
112 | delete(a.pluginConfigs, name)
113 | }
114 | }
115 |
116 | func (a *Agent) RunningPlugins() []string {
117 | a.RLock()
118 | defer a.RUnlock()
119 |
120 | cnt := len(a.pluginRunners)
121 | ret := make([]string, 0, cnt)
122 |
123 | for name := range a.pluginRunners {
124 | ret = append(ret, name)
125 | }
126 |
127 | return ret
128 | }
129 |
130 | func (a *Agent) GetPluginConfig(name string) *PluginConfig {
131 | a.RLock()
132 | defer a.RUnlock()
133 |
134 | return a.pluginConfigs[name]
135 | }
136 |
137 | func (a *Agent) Stop() {
138 | logger.Logger.Info("agent stopping")
139 |
140 | a.Lock()
141 | defer a.Unlock()
142 |
143 | for name := range a.pluginRunners {
144 | a.pluginRunners[name].stop()
145 | delete(a.pluginRunners, name)
146 | delete(a.pluginConfigs, name)
147 | }
148 |
149 | logger.Logger.Info("agent stopped")
150 | }
151 |
152 | func (a *Agent) HandleChangedPlugin(names []string) {
153 | for _, name := range names {
154 | pc := a.GetPluginConfig(name)
155 | if pc.Source != "file" {
156 | // not supported
157 | continue
158 | }
159 |
160 | mtime, err := getMTime(name)
161 | if err != nil {
162 | logger.Logger.Errorw("get mtime fail:"+err.Error(), "plugin:", name)
163 | continue
164 | }
165 |
166 | if mtime == -1 {
167 | // files deleted
168 | a.DelPlugin(name)
169 | continue
170 | }
171 |
172 | if pc.Digest == fmt.Sprint(mtime) {
173 | // not changed
174 | continue
175 | }
176 |
177 | // configuration changed
178 | // delete old plugin
179 | a.DelPlugin(name)
180 |
181 | bs, err := getFileContent(name)
182 | if err != nil {
183 | logger.Logger.Errorw("get file content fail:"+err.Error(), "plugin:", name)
184 | continue
185 | }
186 |
187 | if bs == nil {
188 | // files deleted
189 | continue
190 | }
191 |
192 | a.LoadPlugin(name, &PluginConfig{
193 | Source: "file",
194 | Digest: fmt.Sprint(mtime),
195 | FileContent: bs,
196 | })
197 | }
198 | }
199 |
200 | func (a *Agent) Reload() {
201 | logger.Logger.Info("agent reloading")
202 |
203 | names := a.RunningPlugins()
204 | a.HandleChangedPlugin(names)
205 | a.HandleNewPlugin(names)
206 |
207 | logger.Logger.Info("agent reloaded")
208 | }
209 |
210 | func (a *Agent) HandleNewPlugin(names []string) {
211 | dirs, err := file.DirsUnder(config.Config.ConfigDir)
212 | if err != nil {
213 | logger.Logger.Error("failed to get config dirs:", err)
214 | return
215 | }
216 |
217 | for _, dir := range dirs {
218 | if !strings.HasPrefix(dir, "p.") {
219 | continue
220 | }
221 |
222 | name := dir[len("p."):]
223 |
224 | if choice.Contains(name, names) {
225 | // already running
226 | continue
227 | }
228 |
229 | mtime, err := getMTime(name)
230 | if err != nil {
231 | logger.Logger.Error("get mtime fail:", err)
232 | continue
233 | }
234 |
235 | if mtime == -1 {
236 | continue
237 | }
238 |
239 | bs, err := getFileContent(name)
240 | if err != nil {
241 | logger.Logger.Error("get file content fail:", err)
242 | continue
243 | }
244 |
245 | if bs == nil {
246 | continue
247 | }
248 |
249 | a.LoadPlugin(name, &PluginConfig{
250 | Source: "file",
251 | Digest: fmt.Sprint(mtime),
252 | FileContent: bs,
253 | })
254 | }
255 | }
256 |
--------------------------------------------------------------------------------
/agent/funcs.go:
--------------------------------------------------------------------------------
1 | package agent
2 |
3 | import (
4 | "fmt"
5 | "path"
6 | "sort"
7 | "strings"
8 |
9 | "flashcat.cloud/catpaw/config"
10 | "github.com/toolkits/pkg/file"
11 | )
12 |
13 | func loadFileConfigs() (map[string]*PluginConfig, error) {
14 | dirs, err := file.DirsUnder(config.Config.ConfigDir)
15 | if err != nil {
16 | return nil, fmt.Errorf("failed to get config dirs: %v", err)
17 | }
18 |
19 | ret := make(map[string]*PluginConfig)
20 |
21 | for _, dir := range dirs {
22 | if !strings.HasPrefix(dir, "p.") {
23 | continue
24 | }
25 |
26 | // use this as map key
27 | name := dir[len("p."):]
28 |
29 | pluginDir := path.Join(config.Config.ConfigDir, dir)
30 | files, err := file.FilesUnder(pluginDir)
31 | if err != nil {
32 | return nil, fmt.Errorf("failed to list files under %s: %v", pluginDir, err)
33 | }
34 |
35 | if len(files) == 0 {
36 | continue
37 | }
38 |
39 | sort.Strings(files)
40 |
41 | var maxmt int64
42 | var bytes []byte
43 | for i := 0; i < len(files); i++ {
44 | if !strings.HasSuffix(files[i], ".toml") {
45 | continue
46 | }
47 |
48 | filepath := path.Join(pluginDir, files[i])
49 | mtime, err := file.FileMTime(filepath)
50 | if err != nil {
51 | return nil, fmt.Errorf("failed to get mtime of %s: %v", filepath, err)
52 | }
53 |
54 | if mtime > maxmt {
55 | maxmt = mtime
56 | }
57 |
58 | if i > 0 {
59 | bytes = append(bytes, '\n')
60 | bytes = append(bytes, '\n')
61 | }
62 |
63 | bs, err := file.ReadBytes(filepath)
64 | if err != nil {
65 | return nil, fmt.Errorf("failed to read %s: %v", filepath, err)
66 | }
67 |
68 | bytes = append(bytes, bs...)
69 | }
70 |
71 | ret[name] = &PluginConfig{
72 | Digest: fmt.Sprint(maxmt),
73 | FileContent: bytes,
74 | Source: "file",
75 | }
76 | }
77 |
78 | return ret, nil
79 | }
80 |
81 | // return -1 means no configuration files under plugin directory
82 | func getMTime(name string) (int64, error) {
83 | pluginDir := path.Join(config.Config.ConfigDir, "p."+name)
84 |
85 | files, err := file.FilesUnder(pluginDir)
86 | if err != nil {
87 | return 0, fmt.Errorf("failed to list files under %s: %v", pluginDir, err)
88 | }
89 |
90 | var maxmt int64 = -1
91 | for i := 0; i < len(files); i++ {
92 | if !strings.HasSuffix(files[i], ".toml") {
93 | continue
94 | }
95 |
96 | filepath := path.Join(pluginDir, files[i])
97 | mtime, err := file.FileMTime(filepath)
98 | if err != nil {
99 | return 0, fmt.Errorf("failed to get mtime of %s: %v", filepath, err)
100 | }
101 |
102 | if mtime > maxmt {
103 | maxmt = mtime
104 | }
105 | }
106 |
107 | return maxmt, nil
108 | }
109 |
110 | // get plugin configuration file content
111 | func getFileContent(name string) ([]byte, error) {
112 | pluginDir := path.Join(config.Config.ConfigDir, "p."+name)
113 |
114 | files, err := file.FilesUnder(pluginDir)
115 | if err != nil {
116 | return nil, fmt.Errorf("failed to list files under %s: %v", pluginDir, err)
117 | }
118 |
119 | if len(files) == 0 {
120 | return nil, nil
121 | }
122 |
123 | sort.Strings(files)
124 |
125 | var bytes []byte
126 | for i := 0; i < len(files); i++ {
127 | if !strings.HasSuffix(files[i], ".toml") {
128 | continue
129 | }
130 |
131 | filepath := path.Join(pluginDir, files[i])
132 |
133 | if i > 0 {
134 | bytes = append(bytes, '\n')
135 | bytes = append(bytes, '\n')
136 | }
137 |
138 | bs, err := file.ReadBytes(filepath)
139 | if err != nil {
140 | return nil, fmt.Errorf("failed to read %s: %v", filepath, err)
141 | }
142 |
143 | bytes = append(bytes, bs...)
144 | }
145 |
146 | return bytes, nil
147 | }
148 |
149 | func parseFilter(filterStr string) map[string]struct{} {
150 | filters := strings.Split(filterStr, ":")
151 | filtermap := make(map[string]struct{})
152 | for i := 0; i < len(filters); i++ {
153 | if strings.TrimSpace(filters[i]) == "" {
154 | continue
155 | }
156 | filtermap[filters[i]] = struct{}{}
157 | }
158 | return filtermap
159 | }
160 |
--------------------------------------------------------------------------------
/agent/runner.go:
--------------------------------------------------------------------------------
1 | package agent
2 |
3 | import (
4 | "time"
5 |
6 | "flashcat.cloud/catpaw/config"
7 | "flashcat.cloud/catpaw/engine"
8 | "flashcat.cloud/catpaw/logger"
9 | "flashcat.cloud/catpaw/pkg/runtimex"
10 | "flashcat.cloud/catpaw/pkg/safe"
11 | "flashcat.cloud/catpaw/plugins"
12 | "flashcat.cloud/catpaw/types"
13 | )
14 |
15 | type PluginRunner struct {
16 | pluginName string
17 | pluginObject plugins.Plugin
18 | quitChan []chan struct{}
19 | Instances []plugins.Instance
20 | }
21 |
22 | func newPluginRunner(pluginName string, p plugins.Plugin) *PluginRunner {
23 | return &PluginRunner{
24 | pluginName: pluginName,
25 | pluginObject: p,
26 | }
27 | }
28 |
29 | func (r *PluginRunner) stop() {
30 | for i := 0; i < len(r.Instances); i++ {
31 | r.quitChan[i] <- struct{}{}
32 | plugins.MayDrop(r.Instances[i])
33 | }
34 | }
35 |
36 | func (r *PluginRunner) start() {
37 | r.Instances = plugins.MayGetInstances(r.pluginObject)
38 | r.quitChan = make([]chan struct{}, len(r.Instances))
39 | for i := 0; i < len(r.Instances); i++ {
40 | r.quitChan[i] = make(chan struct{}, 1)
41 | ins := r.Instances[i]
42 | ch := r.quitChan[i]
43 | go r.startInstancePlugin(ins, ch)
44 | time.Sleep(50 * time.Millisecond)
45 | }
46 | }
47 |
48 | func (r *PluginRunner) startInstancePlugin(instance plugins.Instance, ch chan struct{}) {
49 | interval := instance.GetInterval()
50 | if interval == 0 {
51 | interval = r.pluginObject.GetInterval()
52 | if interval == 0 {
53 | interval = config.Config.Global.Interval
54 | }
55 | }
56 |
57 | if err := instance.InitInternalConfig(); err != nil {
58 | logger.Logger.Errorw("init internal config fail: "+err.Error(), "plugin", r.pluginName)
59 | return
60 | }
61 |
62 | timer := time.NewTimer(0)
63 | defer timer.Stop()
64 |
65 | var start time.Time
66 |
67 | for {
68 | select {
69 | case <-ch:
70 | close(ch)
71 | return
72 | case <-timer.C:
73 | start = time.Now()
74 | r.gatherInstancePlugin(instance)
75 | next := time.Duration(interval) - time.Since(start)
76 | if next < 0 {
77 | next = 0
78 | }
79 | timer.Reset(next)
80 | }
81 | }
82 | }
83 |
84 | func (r *PluginRunner) gatherInstancePlugin(ins plugins.Instance) {
85 | defer func() {
86 | if rc := recover(); rc != nil {
87 | logger.Logger.Errorw("gather instance plugin panic: "+string(runtimex.Stack(3)), "plugin", r.pluginName)
88 | }
89 | }()
90 |
91 | queue := safe.NewQueue[*types.Event]()
92 | plugins.MayGather(ins, queue)
93 | if queue.Len() > 0 {
94 | engine.PushRawEvents(r.pluginName, r.pluginObject, ins, queue)
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/asset/changelog.md:
--------------------------------------------------------------------------------
1 | ## v0.1.2
2 |
3 | - New: add journaltail plugin
4 | - New: add script greplog.sh
5 |
6 | ## v0.3.0
7 |
8 | - New: add mtime plugin
9 |
10 | ## v0.4.0
11 |
12 | - New: add sfilter plugin
13 | - Fix: remove configuration keywords of plugin journaltail
14 |
15 | ## v0.6.0
16 |
17 | - New: add filechange plugin
18 |
19 | ## v0.7.0
20 |
21 | - New: add procnum plugin
22 |
23 | ## v0.8.0
24 |
25 | - New: refactor http/net/ping plugin configurations
26 |
27 |
--------------------------------------------------------------------------------
/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | export GOPROXY=https://goproxy.cn
4 |
5 | if ! go build; then
6 | echo "build failed"
7 | exit 1
8 | fi
9 |
10 | version=$(./catpaw -version)
11 | echo "version: $version"
12 |
13 | rm -rf dist/catpaw-v${version}-linux-amd64
14 | mkdir -p dist/catpaw-v${version}-linux-amd64
15 |
16 | cp catpaw dist/catpaw-v${version}-linux-amd64/
17 | cp -r conf.d dist/catpaw-v${version}-linux-amd64/
18 | cp -r scripts dist/catpaw-v${version}-linux-amd64/
19 |
20 | cd dist
21 | tar -zcvf catpaw-v${version}-linux-amd64.tar.gz catpaw-v${version}-linux-amd64
22 |
23 | echo "build success"
--------------------------------------------------------------------------------
/conf.d/config.toml:
--------------------------------------------------------------------------------
1 | [global]
2 | interval = "30s"
3 |
4 | [global.labels]
5 | from_agent = "catpaw"
6 | from_hostname = "$hostname"
7 | from_hostip = "$ip"
8 |
9 | [log]
10 | level = "info"
11 | # format = "json"
12 | # output = "stdout"
13 | # fields = {}
14 |
15 | [flashduty]
16 | url = "https://api.flashcat.cloud/event/push/alert/standard?integration_key=x"
17 | timeout = "10s"
18 |
--------------------------------------------------------------------------------
/conf.d/p.exec/exec.toml:
--------------------------------------------------------------------------------
1 | # script stdout example:
2 | # 1. It's an array
3 | # 2. event_status choice: Critical, Warning, Info, Ok
4 | # [
5 | # {
6 | # "event_status": "Warning",
7 | # "labels": {
8 | # "check": "oom killed",
9 | # },
10 | # "title_rule": "$check",
11 | # "description": "kernel: Out of memory: Kill process 9163 (mysqld) score 511 or sacrifice child"
12 | # }
13 | # ]
14 |
15 | [[instances]]
16 | # # commands, support glob
17 | commands = [
18 | # "/opt/catpaw/scripts/*.sh"
19 | ]
20 |
21 | # # script timeout
22 | # timeout = "10s"
23 |
24 | # # Concurrent requests to make per target
25 | # concurrency = 5
26 |
27 | # # gather interval
28 | # interval = "30s"
29 |
30 | # # Optional append labels
31 | # labels = { env="production", team="devops" }
32 |
33 | [instances.alerting]
34 | ## Enable alerting or not
35 | enabled = true
36 | ## Same functionality as Prometheus keyword 'for'
37 | for_duration = 0
38 | ## Minimum interval duration between notifications
39 | repeat_interval = "5m"
40 | ## Maximum number of notifications
41 | repeat_number = 3
42 | ## Whether notify recovery event
43 | recovery_notification = true
44 |
--------------------------------------------------------------------------------
/conf.d/p.filechange/filechange.toml:
--------------------------------------------------------------------------------
1 | [[instances]]
2 | time_span = "3m"
3 | filepaths = ["/etc/shadow"]
4 | check = "文件变化检测"
5 | interval = "30s"
6 |
7 | [instances.alerting]
8 | ## Enable alerting or not
9 | enabled = true
10 | ## Same functionality as Prometheus keyword 'for'
11 | for_duration = 0
12 | ## Minimum interval duration between notifications
13 | repeat_interval = "5m"
14 | ## Maximum number of notifications
15 | repeat_number = 3
16 | ## Whether notify recovery event
17 | recovery_notification = true
18 | ## Choice: Critical, Warning, Info
19 | default_severity = "Warning"
20 |
--------------------------------------------------------------------------------
/conf.d/p.http/http.toml:
--------------------------------------------------------------------------------
1 | [[partials]]
2 | id = "default"
3 |
4 | # # Concurrent requests to make per instance
5 | # concurrency = 10
6 |
7 | ## Set http_proxy (catpaw uses the system wide proxy settings if it's is not set)
8 | # http_proxy = "http://localhost:8888"
9 |
10 | ## Interface to use when dialing an address
11 | # interface = "eth0"
12 |
13 | ## HTTP Request Method
14 | # method = "GET"
15 |
16 | ## Set timeout (default 5 seconds)
17 | # timeout = "5s"
18 |
19 | ## Whether to follow redirects from the server (defaults to false)
20 | # follow_redirects = false
21 |
22 | ## Optional HTTP Basic Auth Credentials
23 | # basic_auth_user = "username"
24 | # basic_auth_pass = "pa$$word"
25 |
26 | ## Optional headers
27 | # headers = ["Header-Key-1", "Header-Value-1", "Header-Key-2", "Header-Value-2"]
28 |
29 | ## Optional HTTP Request Body
30 | # payload = '''
31 | # {'fake':'data'}
32 | # '''
33 |
34 | [[instances]]
35 | targets = [
36 | # "https://baidu.com",
37 | # "http://127.0.0.1:8888/request",
38 | ]
39 |
40 | partial = "default"
41 |
42 | # # gather interval
43 | # interval = "30s"
44 |
45 | # # Optional append labels
46 | # labels = { env="production", team="devops" }
47 |
48 | ## Optional TLS Config
49 | # use_tls = false
50 | # tls_ca = "/etc/catpaw/ca.pem"
51 | # tls_cert = "/etc/catpaw/cert.pem"
52 | # tls_key = "/etc/catpaw/key.pem"
53 | ## Use TLS but skip chain & host verification
54 | # insecure_skip_verify = false
55 |
56 | [instances.expect]
57 | ## Optional expected response status code.
58 | response_status_code = ["20*", "30*"]
59 | ## Optional substring match in body of the response (case sensitive)
60 | response_substring = "html"
61 | ## Optional alert when cert will expire in x hours
62 | cert_expire_threshold = "72h"
63 |
64 | [instances.alerting]
65 | ## Enable alerting or not
66 | enabled = true
67 | ## Same functionality as Prometheus keyword 'for'
68 | for_duration = 0
69 | ## Minimum interval duration between notifications
70 | repeat_interval = "5m"
71 | ## Maximum number of notifications
72 | repeat_number = 3
73 | ## Whether notify recovery event
74 | recovery_notification = true
75 | ## Choice: Critical, Warning, Info
76 | default_severity = "Warning"
77 |
--------------------------------------------------------------------------------
/conf.d/p.journaltail/journaltail.toml:
--------------------------------------------------------------------------------
1 | [[instances]]
2 | # journalctl -S -${time_span}
3 | time_span = "1m"
4 | # relationship: or
5 | filter_include = ["*Out of memory*", "*nf_conntrack: table full, dropping packets*"]
6 | filter_exclude = []
7 | # check rule name
8 | check = "Critical System Errors"
9 | # # gather interval
10 | interval = "30s"
11 |
12 | [instances.alerting]
13 | ## Enable alerting or not
14 | enabled = true
15 | ## Same functionality as Prometheus keyword 'for'
16 | for_duration = 0
17 | ## Minimum interval duration between notifications
18 | repeat_interval = "5m"
19 | ## Maximum number of notifications
20 | repeat_number = 3
21 | ## Whether notify recovery event
22 | recovery_notification = true
23 | ## Choice: Critical, Warning, Info
24 | default_severity = "Warning"
25 |
--------------------------------------------------------------------------------
/conf.d/p.mtime/mtime.toml:
--------------------------------------------------------------------------------
1 | [[instances]]
2 | time_span = "3m"
3 | directory = "/tmp"
4 | check = "递归检测新文件或文件变化"
5 | interval = "30s"
6 |
7 | [instances.alerting]
8 | ## Enable alerting or not
9 | enabled = true
10 | ## Same functionality as Prometheus keyword 'for'
11 | for_duration = 0
12 | ## Minimum interval duration between notifications
13 | repeat_interval = "5m"
14 | ## Maximum number of notifications
15 | repeat_number = 3
16 | ## Whether notify recovery event
17 | recovery_notification = true
18 | ## Choice: Critical, Warning, Info
19 | default_severity = "Warning"
20 |
--------------------------------------------------------------------------------
/conf.d/p.net/net.toml:
--------------------------------------------------------------------------------
1 | [[partials]]
2 | id = "default"
3 |
4 | # # Concurrent requests to make per instance
5 | # concurrency = 10
6 |
7 | ## Set connect timeout (default 5 seconds)
8 | # timeout = "1s"
9 |
10 | ## Set read timeout (only used if expecting a response)
11 | # read_timeout = "1s"
12 |
13 | # # Concurrent requests to make per instance
14 | # concurrency = 10
15 |
16 | ## Protocol, must be "tcp" or "udp"
17 | ## NOTE: because the "udp" protocol does not respond to requests, it requires
18 | ## a send/expect string pair (see below).
19 | # protocol = "tcp"
20 |
21 | ## The following options are required for UDP checks. For TCP, they are
22 | ## optional. The plugin will send the given string to the server and then
23 | ## expect to receive the given 'expect' string back.
24 | ## string sent to the server
25 | # send = "ssh"
26 | ## expected string in answer
27 | # expect = "ssh"
28 |
29 |
30 | [[instances]]
31 | targets = [
32 | # "127.0.0.1:22",
33 | # "localhost:6379",
34 | # ":9090"
35 | ]
36 |
37 | partial = "default"
38 |
39 | # # gather interval
40 | # interval = "30s"
41 |
42 | # # Optional append labels
43 | # labels = { env="production", team="devops" }
44 |
45 | [instances.alerting]
46 | ## Enable alerting or not
47 | enabled = true
48 | ## Same functionality as Prometheus keyword 'for'
49 | for_duration = 0
50 | ## Minimum interval duration between notifications
51 | repeat_interval = "5m"
52 | ## Maximum number of notifications
53 | repeat_number = 3
54 | ## Whether notify recovery event
55 | recovery_notification = true
56 | ## Choice: Critical, Warning, Info
57 | default_severity = "Warning"
58 |
--------------------------------------------------------------------------------
/conf.d/p.ping/ping.toml:
--------------------------------------------------------------------------------
1 | [[partials]]
2 | id = "default"
3 |
4 | # # Concurrent requests to make per instance
5 | # concurrency = 10
6 |
7 | ## Number of ping packets to send per interval. Corresponds to the "-c"
8 | ## option of the ping command.
9 | # count = 3
10 |
11 | ## Time to wait between sending ping packets in seconds. Operates like the
12 | ## "-i" option of the ping command.
13 | # ping_interval = 0.2
14 |
15 | ## If set, the time to wait for a ping response in seconds. Operates like
16 | ## the "-W" option of the ping command.
17 | # timeout = 2.0
18 |
19 | ## Interface or source address to send ping from. Operates like the -I or -S
20 | ## option of the ping command.
21 | # interface = ""
22 |
23 | ## Use only IPv6 addresses when resolving a hostname.
24 | # ipv6 = false
25 |
26 | ## Number of data bytes to be sent. Corresponds to the "-s"
27 | ## option of the ping command.
28 | # size = 56
29 |
30 | # alert if packet loss is above this threshold
31 | alert_if_packet_loss_percent_ge = 1.0
32 |
33 | [[instances]]
34 | targets = [
35 | "127.0.0.1",
36 | ]
37 |
38 | partial = "default"
39 |
40 | # # gather interval
41 | # interval = "30s"
42 |
43 | # # Optional append labels
44 | # labels = { env="production", team="devops" }
45 |
46 | [instances.alerting]
47 | ## Enable alerting or not
48 | enabled = true
49 | ## Same functionality as Prometheus keyword 'for'
50 | for_duration = 0
51 | ## Minimum interval duration between notifications
52 | repeat_interval = "5m"
53 | ## Maximum number of notifications
54 | repeat_number = 3
55 | ## Whether notify recovery event
56 | recovery_notification = true
57 | ## Choice: Critical, Warning, Info
58 | default_severity = "Warning"
59 |
--------------------------------------------------------------------------------
/conf.d/p.procnum/procnum.toml:
--------------------------------------------------------------------------------
1 | [[instances]]
2 | # # executable name (ie, pgrep )
3 | # search_exec_substring = ""
4 |
5 | # # pattern as argument for pgrep (ie, pgrep -f )
6 | search_cmdline_substring = ""
7 |
8 | # # windows service name
9 | # search_win_service = ""
10 |
11 | alert_if_num_lt = 1
12 | check = "进程存活检测(进程数量检测)"
13 | interval = "30s"
14 |
15 | [instances.alerting]
16 | ## Enable alerting or not
17 | enabled = true
18 | ## Same functionality as Prometheus keyword 'for'
19 | for_duration = 0
20 | ## Minimum interval duration between notifications
21 | repeat_interval = "5m"
22 | ## Maximum number of notifications
23 | repeat_number = 3
24 | ## Whether notify recovery event
25 | recovery_notification = true
26 | ## Choice: Critical, Warning, Info
27 | default_severity = "Warning"
28 |
--------------------------------------------------------------------------------
/conf.d/p.sfilter/sfilter.toml:
--------------------------------------------------------------------------------
1 | [[instances]]
2 | # # e.g. /path/to/sfilter-scripts/system-health.sh
3 | command = ""
4 | # # script timeout
5 | timeout = "10s"
6 | # check rule name
7 | check = "Check system health state"
8 |
9 | # support glob
10 | filter_include = ["*WARNING*", "*CRITICAL*"]
11 | filter_exclude = []
12 |
13 | # # gather interval
14 | interval = "30s"
15 |
16 | [instances.alerting]
17 | ## Enable alerting or not
18 | enabled = true
19 | ## Same functionality as Prometheus keyword 'for'
20 | for_duration = 0
21 | ## Minimum interval duration between notifications
22 | repeat_interval = "5m"
23 | ## Maximum number of notifications
24 | repeat_number = 3
25 | ## Whether notify recovery event
26 | recovery_notification = true
27 | ## Choice: Critical, Warning, Info
28 | default_severity = "Warning"
29 |
--------------------------------------------------------------------------------
/config/config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | "net/http"
7 | "os"
8 | "path"
9 | "strings"
10 | "time"
11 |
12 | "flashcat.cloud/catpaw/pkg/cfg"
13 | "github.com/jackpal/gateway"
14 | "github.com/toolkits/pkg/file"
15 | )
16 |
17 | type Global struct {
18 | Interval Duration `toml:"interval"`
19 | Labels map[string]string `toml:"labels"`
20 | LabelHasHostname bool `toml:"label_has_hostname"`
21 | }
22 |
23 | type LogConfig struct {
24 | Level string `toml:"level"`
25 | Format string `toml:"format"`
26 | Output string `toml:"output"`
27 | Fields map[string]interface{} `toml:"fields"`
28 | }
29 |
30 | type Flashduty struct {
31 | Url string `toml:"url"`
32 | Timeout Duration `toml:"timeout"`
33 | Client *http.Client `toml:"-"`
34 | }
35 |
36 | type ConfigType struct {
37 | ConfigDir string `toml:"-"`
38 | TestMode bool `toml:"-"`
39 | Plugins string `toml:"-"`
40 | Url string `toml:"-"`
41 | Loglevel string `toml:"-"`
42 |
43 | Global Global `toml:"global"`
44 | LogConfig LogConfig `toml:"log"`
45 | Flashduty Flashduty `toml:"flashduty"`
46 | }
47 |
48 | var Config *ConfigType
49 |
50 | func InitConfig(configDir string, testMode bool, interval int64, plugins, url, loglevel string) error {
51 | configFile := path.Join(configDir, "config.toml")
52 | if !file.IsExist(configFile) {
53 | return fmt.Errorf("configuration file(%s) not found", configFile)
54 | }
55 |
56 | Config = &ConfigType{
57 | ConfigDir: configDir,
58 | TestMode: testMode,
59 | Plugins: plugins,
60 | Url: url,
61 | Loglevel: loglevel,
62 | }
63 |
64 | if err := cfg.LoadConfigByDir(configDir, Config); err != nil {
65 | return fmt.Errorf("failed to load configs of directory: %s error:%s", configDir, err)
66 | }
67 |
68 | if interval > 0 {
69 | Config.Global.Interval = Duration(time.Second * time.Duration(interval))
70 | }
71 |
72 | if Config.Global.Interval == 0 {
73 | Config.Global.Interval = Duration(30 * time.Second)
74 | }
75 |
76 | if Config.Loglevel != "" {
77 | Config.LogConfig.Level = Config.Loglevel
78 | }
79 |
80 | if Config.LogConfig.Level == "" {
81 | Config.LogConfig.Level = "info"
82 | }
83 |
84 | if Config.LogConfig.Format == "" {
85 | Config.LogConfig.Format = "json"
86 | }
87 |
88 | if len(Config.LogConfig.Output) == 0 {
89 | Config.LogConfig.Output = "stdout"
90 | }
91 |
92 | if Config.LogConfig.Fields == nil {
93 | Config.LogConfig.Fields = make(map[string]interface{})
94 | }
95 |
96 | if Config.Flashduty.Timeout == 0 {
97 | Config.Flashduty.Timeout = Duration(10 * time.Second)
98 | }
99 |
100 | if Config.Url != "" {
101 | Config.Flashduty.Url = Config.Url
102 | }
103 |
104 | Config.Flashduty.Client = &http.Client{
105 | Timeout: time.Duration(Config.Flashduty.Timeout),
106 | }
107 |
108 | if Config.Global.Labels == nil {
109 | Config.Global.Labels = make(map[string]string)
110 | }
111 |
112 | for k, v := range Config.Global.Labels {
113 | if !strings.Contains(v, "$") {
114 | continue
115 | }
116 |
117 | if strings.Contains(v, "$hostname") {
118 | Config.Global.LabelHasHostname = true
119 | continue
120 | }
121 |
122 | if strings.Contains(v, "$ip") {
123 | ip, err := GetOutboundIP()
124 | if err != nil {
125 | return fmt.Errorf("failed to get outbound ip: %v", err)
126 | }
127 | Config.Global.Labels[k] = strings.Replace(v, "$ip", fmt.Sprint(ip), -1)
128 | }
129 |
130 | Config.Global.Labels[k] = os.Expand(Config.Global.Labels[k], GetEnv)
131 | }
132 |
133 | return nil
134 | }
135 |
136 | func GetEnv(key string) string {
137 | v := os.Getenv(key)
138 | var envVarEscaper = strings.NewReplacer(
139 | `"`, `\"`,
140 | `\`, `\\`,
141 | )
142 | return envVarEscaper.Replace(v)
143 | }
144 |
145 | // Get preferred outbound ip of this machine
146 | func GetOutboundIP() (net.IP, error) {
147 | gateway, err := gateway.DiscoverGateway()
148 | if err != nil {
149 | return nil, fmt.Errorf("failed to detect gateway: %v", err)
150 | }
151 |
152 | gatewayip := fmt.Sprint(gateway)
153 | if gatewayip == "" {
154 | return nil, fmt.Errorf("failed to detect gateway: empty")
155 | }
156 |
157 | conn, err := net.Dial("udp", gatewayip+":80")
158 | if err != nil {
159 | return nil, fmt.Errorf("failed to get outbound ip: %v", err)
160 | }
161 | defer conn.Close()
162 |
163 | localAddr := conn.LocalAddr().(*net.UDPAddr)
164 |
165 | return localAddr.IP, nil
166 | }
167 |
--------------------------------------------------------------------------------
/config/duration.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "fmt"
5 | "math"
6 | "strconv"
7 | "strings"
8 | "time"
9 | )
10 |
11 | // Duration is a time.Duration
12 | type Duration time.Duration
13 |
14 | // UnmarshalTOML parses the duration from the TOML config file
15 | func (d *Duration) UnmarshalTOML(b []byte) error {
16 | // convert to string
17 | durStr := string(b)
18 |
19 | // Value is a TOML number (e.g. 3, 10, 3.5)
20 | // First try parsing as integer seconds
21 | sI, err := strconv.ParseInt(durStr, 10, 64)
22 | if err == nil {
23 | dur := time.Second * time.Duration(sI)
24 | *d = Duration(dur)
25 | return nil
26 | }
27 | // Second try parsing as float seconds
28 | sF, err := strconv.ParseFloat(durStr, 64)
29 | if err == nil {
30 | dur := time.Second * time.Duration(sF)
31 | *d = Duration(dur)
32 | return nil
33 | }
34 |
35 | // Finally, try value is a TOML string (e.g. "3s", 3s) or literal (e.g. '3s')
36 | durStr = strings.ReplaceAll(durStr, "'", "")
37 | durStr = strings.ReplaceAll(durStr, "\"", "")
38 | if durStr == "" {
39 | durStr = "0s"
40 | }
41 |
42 | dur, err := time.ParseDuration(durStr)
43 | if err != nil {
44 | return err
45 | }
46 |
47 | *d = Duration(dur)
48 | return nil
49 | }
50 |
51 | func (d *Duration) UnmarshalText(text []byte) error {
52 | return d.UnmarshalTOML(text)
53 | }
54 |
55 | func (d *Duration) HumanString() string {
56 | duration := time.Duration(*d)
57 | if duration.Seconds() < 60.0 {
58 | return fmt.Sprintf("%d seconds", int64(duration.Seconds()))
59 | }
60 | if duration.Minutes() < 60.0 {
61 | remainingSeconds := math.Mod(duration.Seconds(), 60)
62 | return fmt.Sprintf("%d minutes %d seconds", int64(duration.Minutes()), int64(remainingSeconds))
63 | }
64 | if duration.Hours() < 24.0 {
65 | remainingMinutes := math.Mod(duration.Minutes(), 60)
66 | remainingSeconds := math.Mod(duration.Seconds(), 60)
67 | return fmt.Sprintf("%d hours %d minutes %d seconds",
68 | int64(duration.Hours()), int64(remainingMinutes), int64(remainingSeconds))
69 | }
70 | remainingHours := math.Mod(duration.Hours(), 24)
71 | remainingMinutes := math.Mod(duration.Minutes(), 60)
72 | remainingSeconds := math.Mod(duration.Seconds(), 60)
73 | return fmt.Sprintf("%d days %d hours %d minutes %d seconds",
74 | int64(duration.Hours()/24), int64(remainingHours),
75 | int64(remainingMinutes), int64(remainingSeconds))
76 | }
77 |
--------------------------------------------------------------------------------
/config/http_config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "net/url"
7 | "time"
8 |
9 | "flashcat.cloud/catpaw/pkg/tls"
10 | )
11 |
12 | type HTTPConfig struct {
13 | Interface string `toml:"interface"`
14 | Method string `toml:"method"`
15 | Timeout Duration `toml:"timeout"`
16 | FollowRedirects *bool `toml:"follow_redirects"`
17 | BasicAuthUser string `toml:"basic_auth_user"`
18 | BasicAuthPass string `toml:"basic_auth_pass"`
19 | Headers []string `toml:"headers"`
20 | Payload string `toml:"payload"`
21 | HTTPProxy string `toml:"http_proxy"`
22 | tls.ClientConfig
23 | }
24 |
25 | type proxyFunc func(req *http.Request) (*url.URL, error)
26 |
27 | func (hc *HTTPConfig) GetProxy() (proxyFunc, error) {
28 | if len(hc.HTTPProxy) > 0 {
29 | address, err := url.Parse(hc.HTTPProxy)
30 | if err != nil {
31 | return nil, fmt.Errorf("error parsing proxy url %q: %w", hc.HTTPProxy, err)
32 | }
33 | return http.ProxyURL(address), nil
34 | }
35 | return http.ProxyFromEnvironment, nil
36 | }
37 |
38 | func (hc *HTTPConfig) GetTimeout() Duration {
39 | if hc.Timeout == 0 {
40 | return Duration(time.Second * 5)
41 | }
42 | return hc.Timeout
43 | }
44 |
45 | func (hc *HTTPConfig) GetMethod() string {
46 | if hc.Method == "" {
47 | return "GET"
48 | }
49 | return hc.Method
50 | }
51 |
--------------------------------------------------------------------------------
/config/inline.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import "flashcat.cloud/catpaw/types"
4 |
5 | type Alerting struct {
6 | Enabled bool `toml:"enabled"`
7 |
8 | // like prometheus `for`
9 | ForDuration Duration `toml:"for_duration"`
10 |
11 | // repeat interval
12 | RepeatInterval Duration `toml:"repeat_interval"`
13 |
14 | // maximum number of notifications
15 | RepeatNumber int `toml:"repeat_number"`
16 |
17 | // whether send recovery notification
18 | RecoveryNotification bool `toml:"recovery_notification"`
19 |
20 | // alert severity
21 | DefaultSeverity string `toml:"default_severity"`
22 | }
23 |
24 | type InternalConfig struct {
25 | // append labels to every event
26 | Labels map[string]string `toml:"labels"`
27 |
28 | // gather interval
29 | Interval Duration `toml:"interval"`
30 |
31 | // alerting rule
32 | Alerting Alerting `toml:"alerting"`
33 |
34 | // whether instance initialized
35 | initialized bool `toml:"-"`
36 | }
37 |
38 | func (ic *InternalConfig) GetLabels() map[string]string {
39 | if ic.Labels != nil {
40 | return ic.Labels
41 | }
42 |
43 | return map[string]string{}
44 | }
45 |
46 | func (ic *InternalConfig) GetInitialized() bool {
47 | return ic.initialized
48 | }
49 |
50 | func (ic *InternalConfig) SetInitialized() {
51 | ic.initialized = true
52 | }
53 |
54 | func (ic *InternalConfig) GetInterval() Duration {
55 | return ic.Interval
56 | }
57 |
58 | func (ic *InternalConfig) InitInternalConfig() error {
59 | // maybe compile some glob/regex pattern here
60 | return nil
61 | }
62 |
63 | func (ic *InternalConfig) GetAlerting() Alerting {
64 | return ic.Alerting
65 | }
66 |
67 | func (ic *InternalConfig) GetDefaultSeverity() string {
68 | if ic.Alerting.DefaultSeverity == "" {
69 | return types.EventStatusWarning
70 | }
71 |
72 | return ic.Alerting.DefaultSeverity
73 | }
74 |
--------------------------------------------------------------------------------
/config/version.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | var Version = "0.8.0"
4 |
--------------------------------------------------------------------------------
/docker/Dockerfile.goreleaser:
--------------------------------------------------------------------------------
1 | FROM --platform=$TARGETPLATFORM ubuntu:23.04
2 |
3 | WORKDIR /app
4 | ADD catpaw /app
5 | # COPY conf.d /app/conf.d
6 |
7 | CMD ["/app/catpaw", "-h"]
--------------------------------------------------------------------------------
/engine/cache.go:
--------------------------------------------------------------------------------
1 | package engine
2 |
3 | import (
4 | "sync"
5 |
6 | "flashcat.cloud/catpaw/types"
7 | )
8 |
9 | type EventCache struct {
10 | sync.RWMutex
11 | records map[string]*types.Event
12 | }
13 |
14 | var Events = &EventCache{records: make(map[string]*types.Event)}
15 |
16 | func (c *EventCache) Get(key string) *types.Event {
17 | c.RLock()
18 | defer c.RUnlock()
19 | return c.records[key]
20 | }
21 |
22 | func (c *EventCache) Set(val *types.Event) {
23 | c.Lock()
24 | defer c.Unlock()
25 | c.records[val.AlertKey] = val
26 | }
27 |
28 | func (c *EventCache) Del(key string) {
29 | c.Lock()
30 | defer c.Unlock()
31 | delete(c.records, key)
32 | }
33 |
--------------------------------------------------------------------------------
/engine/engine.go:
--------------------------------------------------------------------------------
1 | package engine
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "io"
8 | "net/http"
9 | "os"
10 | "sort"
11 | "strings"
12 | "time"
13 |
14 | "flashcat.cloud/catpaw/config"
15 | "flashcat.cloud/catpaw/logger"
16 | "flashcat.cloud/catpaw/pkg/safe"
17 | "flashcat.cloud/catpaw/plugins"
18 | "flashcat.cloud/catpaw/types"
19 | "github.com/toolkits/pkg/str"
20 | )
21 |
22 | func PushRawEvents(pluginName string, pluginObj plugins.Plugin, ins plugins.Instance, queue *safe.Queue[*types.Event]) {
23 | if queue.Len() == 0 {
24 | return
25 | }
26 |
27 | now := time.Now().Unix()
28 | events := queue.PopBackAll()
29 |
30 | for i := range events {
31 | if events[i] == nil {
32 | continue
33 | }
34 |
35 | err := clean(events[i], now, pluginName, pluginObj, ins)
36 | if err != nil {
37 | logger.Logger.Errorf("clean raw event fail: %v, event: %v", err.Error(), events[i])
38 | continue
39 | }
40 |
41 | logger.Logger.Debugf("event:%s: raw data received. event object: %v", events[i].AlertKey, events[i])
42 |
43 | if !ins.GetAlerting().Enabled {
44 | continue
45 | }
46 |
47 | if events[i].EventStatus == types.EventStatusOk {
48 | handleRecoveryEvent(ins, events[i])
49 | } else {
50 | handleAlertEvent(ins, events[i])
51 | }
52 | }
53 | }
54 |
55 | // 处理恢复事件
56 | func handleRecoveryEvent(ins plugins.Instance, event *types.Event) {
57 | old := Events.Get(event.AlertKey)
58 | if old == nil {
59 | // 之前没有产生Event,当下的情况也是正常的,这是大多数场景,忽略即可,无需做任何处理
60 | return
61 | }
62 |
63 | // 之前产生了告警,现在恢复了,事件就可以从缓存删除了
64 | Events.Del(old.AlertKey)
65 |
66 | // 不过,也得看具体 alerting 的配置,如果不需要发送恢复通知,则忽略
67 | if ins.GetAlerting().RecoveryNotification && old.LastSent > 0 {
68 | event.LastSent = event.EventTime
69 | event.FirstFireTime = old.FirstFireTime
70 | event.NotifyCount = old.NotifyCount + 1
71 | forward(event)
72 | }
73 | }
74 |
75 | // 处理告警事件
76 | func handleAlertEvent(ins plugins.Instance, event *types.Event) {
77 | alerting := ins.GetAlerting()
78 | old := Events.Get(event.AlertKey)
79 | if old == nil {
80 | // 第一次产生告警事件
81 | event.FirstFireTime = event.EventTime
82 |
83 | // 无论如何,这个事件都得缓存起来
84 | Events.Set(event)
85 |
86 | // 要不要发?分两种情况。ForDuration 是 0 则立马发,否则等待 ForDuration 时间后再发
87 | if alerting.ForDuration == 0 {
88 | event.LastSent = event.EventTime
89 | event.NotifyCount++
90 | forward(event)
91 | return
92 | }
93 |
94 | return
95 | }
96 |
97 | // old != nil 这已经不是第一次产生告警事件了
98 | // 如果 ForDuration 没有满足,则不能继续发送
99 | if alerting.ForDuration > 0 && event.EventTime-old.FirstFireTime < int64(alerting.ForDuration/config.Duration(time.Second)) {
100 | return
101 | }
102 |
103 | // ForDuration 满足了,可以继续发送了
104 | // 首先看是否达到最大发送次数
105 | if alerting.RepeatNumber > 0 && old.NotifyCount >= int64(alerting.RepeatNumber) {
106 | return
107 | }
108 |
109 | // 其次看发送频率,不能发的太快了
110 | if alerting.RepeatInterval > 0 && event.EventTime-old.LastSent < int64(alerting.RepeatInterval/config.Duration(time.Second)) {
111 | return
112 | }
113 |
114 | // 最后,可以发了
115 | event.LastSent = event.EventTime
116 | event.NotifyCount = old.NotifyCount + 1
117 | Events.Set(event)
118 | forward(event)
119 | }
120 |
121 | func clean(event *types.Event, now int64, pluginName string, pluginObj plugins.Plugin, ins plugins.Instance) error {
122 | if event.EventTime == 0 {
123 | event.EventTime = now
124 | }
125 |
126 | if !types.EventStatusValid(event.EventStatus) {
127 | return fmt.Errorf("invalid event_status: %s", event.EventStatus)
128 | }
129 |
130 | if event.Labels == nil {
131 | event.Labels = make(map[string]string)
132 | }
133 |
134 | // append label: from_plugin
135 | event.Labels["from_plugin"] = pluginName
136 |
137 | // append label from plugin
138 | plLabels := pluginObj.GetLabels()
139 | for k, v := range plLabels {
140 | event.Labels[k] = v
141 | }
142 |
143 | // append label from instance
144 | insLabels := ins.GetLabels()
145 | for k, v := range insLabels {
146 | event.Labels[k] = v
147 | }
148 |
149 | // append label: global labels
150 | var (
151 | hostname string
152 | err error
153 | )
154 |
155 | if config.Config.Global.LabelHasHostname {
156 | hostname, err = os.Hostname()
157 | if err != nil {
158 | return fmt.Errorf("failed to get hostname: %s", err.Error())
159 | }
160 | }
161 |
162 | for key := range config.Config.Global.Labels {
163 | if strings.Contains(config.Config.Global.Labels[key], "$hostname") {
164 | event.Labels[key] = strings.ReplaceAll(config.Config.Global.Labels[key], "$hostname", hostname)
165 | } else {
166 | event.Labels[key] = config.Config.Global.Labels[key]
167 | }
168 | }
169 |
170 | count := len(event.Labels)
171 | keys := make([]string, 0, count)
172 | for k := range event.Labels {
173 | keys = append(keys, k)
174 | }
175 |
176 | sort.Strings(keys)
177 |
178 | var sb strings.Builder
179 | for _, k := range keys {
180 | sb.WriteString(k)
181 | sb.WriteString(":")
182 | sb.WriteString(event.Labels[k])
183 | sb.WriteString(":")
184 | }
185 |
186 | event.AlertKey = str.MD5(sb.String())
187 |
188 | return nil
189 | }
190 |
191 | func forward(event *types.Event) {
192 | if config.Config.TestMode {
193 | printStdout(event)
194 | return
195 | }
196 |
197 | bs, err := json.Marshal(event)
198 | if err != nil {
199 | logger.Logger.Errorf("event:%s: forward: marshal fail: %s", event.AlertKey, err.Error())
200 | }
201 |
202 | req, err := http.NewRequest("POST", config.Config.Flashduty.Url, bytes.NewReader(bs))
203 | if err != nil {
204 | logger.Logger.Errorf("event:%s: forward: new request fail: %s", event.AlertKey, err.Error())
205 | return
206 | }
207 |
208 | req.Header.Set("Content-Type", "application/json")
209 |
210 | res, err := config.Config.Flashduty.Client.Do(req)
211 | if err != nil {
212 | logger.Logger.Errorf("event:%s: forward: do request fail: %s", event.AlertKey, err.Error())
213 | return
214 | }
215 |
216 | var body []byte
217 | if res.Body != nil {
218 | defer res.Body.Close()
219 | body, err = io.ReadAll(res.Body)
220 | if err != nil {
221 | logger.Logger.Errorf("event:%s: forward: read response fail: %s", event.AlertKey, err.Error())
222 | return
223 | }
224 | }
225 |
226 | logger.Logger.Infof("event:%s: forward: done, request payload: %s, response status code: %d, response body: %s", event.AlertKey, string(bs), res.StatusCode, string(body))
227 | }
228 |
229 | func printStdout(event *types.Event) {
230 | var sb strings.Builder
231 |
232 | sb.WriteString(fmt.Sprint(event.EventTime))
233 | sb.WriteString(" ")
234 | sb.WriteString(time.Unix(event.EventTime, 0).Format("15:04:05"))
235 | sb.WriteString(" ")
236 | sb.WriteString(event.AlertKey)
237 | sb.WriteString(" ")
238 | sb.WriteString(event.EventStatus)
239 | sb.WriteString(" ")
240 |
241 | for k, v := range event.Labels {
242 | sb.WriteString(fmt.Sprintf("%s=%s", k, v))
243 | sb.WriteString(",")
244 | }
245 |
246 | sb.WriteString(" ")
247 | sb.WriteString(event.TitleRule)
248 | sb.WriteString(" ")
249 | sb.WriteString(event.Description)
250 |
251 | fmt.Println(sb.String())
252 | }
253 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module flashcat.cloud/catpaw
2 |
3 | go 1.20
4 |
5 | require (
6 | github.com/chai2010/winsvc v0.0.0-20200705094454-db7ec320025c
7 | github.com/gobwas/glob v0.2.3
8 | github.com/jackpal/gateway v1.0.10
9 | github.com/koding/multiconfig v0.0.0-20171124222453-69c27309b2d7
10 | github.com/toolkits/pkg v1.3.3
11 | go.uber.org/zap v1.24.0
12 | )
13 |
14 | require (
15 | github.com/BurntSushi/toml v1.3.0 // indirect
16 | github.com/fatih/camelcase v1.0.0 // indirect
17 | github.com/fatih/structs v1.1.0 // indirect
18 | github.com/go-ole/go-ole v1.2.6 // indirect
19 | github.com/google/uuid v1.3.0 // indirect
20 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
21 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
22 | github.com/prometheus-community/pro-bing v0.2.0 // indirect
23 | github.com/shirou/gopsutil/v3 v3.23.6 // indirect
24 | github.com/shoenig/go-m1cpu v0.1.6 // indirect
25 | github.com/tklauser/go-sysconf v0.3.11 // indirect
26 | github.com/tklauser/numcpus v0.6.0 // indirect
27 | github.com/yusufpapurcu/wmi v1.2.3 // indirect
28 | go.uber.org/atomic v1.7.0 // indirect
29 | go.uber.org/automaxprocs v1.4.0 // indirect
30 | go.uber.org/multierr v1.6.0 // indirect
31 | golang.org/x/net v0.10.0 // indirect
32 | golang.org/x/sync v0.2.0 // indirect
33 | golang.org/x/sys v0.9.0 // indirect
34 | gopkg.in/yaml.v2 v2.4.0 // indirect
35 | )
36 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/BurntSushi/toml v1.3.0 h1:Ws8e5YmnrGEHzZEzg0YvK/7COGYtTC5PbaH9oSSbgfA=
2 | github.com/BurntSushi/toml v1.3.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
3 | github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
4 | github.com/chai2010/winsvc v0.0.0-20200705094454-db7ec320025c h1:ZgxF2fGttmsetibm9Tc91TAUWzRZSfjJPstNxU6jWyU=
5 | github.com/chai2010/winsvc v0.0.0-20200705094454-db7ec320025c/go.mod h1:b9Xy0A0C/binZARjeVfHEr+gHzQUVztL71bTms7PRIM=
6 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
7 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
8 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
9 | github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8=
10 | github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc=
11 | github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
12 | github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
13 | github.com/garyburd/redigo v1.6.2/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
14 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
15 | github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U=
16 | github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
17 | github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
18 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
19 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
20 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
21 | github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
22 | github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
23 | github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
24 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
25 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
26 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
27 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
28 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
29 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
30 | github.com/jackpal/gateway v1.0.10 h1:7g3fDo4Cd3RnTu6PzAfw6poO4Y81uNxrxFQFsBFSzJM=
31 | github.com/jackpal/gateway v1.0.10/go.mod h1:+uPBgIllrbkwYCAoDkGSZbjvpre/bGYAFCYIcrH+LHs=
32 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
33 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
34 | github.com/koding/multiconfig v0.0.0-20171124222453-69c27309b2d7 h1:SWlt7BoQNASbhTUD0Oy5yysI2seJ7vWuGUp///OM4TM=
35 | github.com/koding/multiconfig v0.0.0-20171124222453-69c27309b2d7/go.mod h1:Y2SaZf2Rzd0pXkLVhLlCiAXFCLSXAIbTKDivVgff/AM=
36 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
37 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
38 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
39 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
40 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
41 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
42 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
43 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
44 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
45 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
46 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
47 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
48 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
49 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
50 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
51 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
52 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
53 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
54 | github.com/prometheus-community/pro-bing v0.2.0 h1:hyK7yPFndU3LCDwEQJwPQUCjNkp1DGP/VxyzrWfXZUU=
55 | github.com/prometheus-community/pro-bing v0.2.0/go.mod h1:20arNb2S8rNG3EtmjHyZZU92cfbhQx7oCHZ9sulAV+I=
56 | github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62/go.mod h1:65XQgovT59RWatovFwnwocoUxiI/eENTnOY5GK3STuY=
57 | github.com/shirou/gopsutil/v3 v3.23.6 h1:5y46WPI9QBKBbK7EEccUPNXpJpNrvPuTD0O2zHEHT08=
58 | github.com/shirou/gopsutil/v3 v3.23.6/go.mod h1:j7QX50DrXYggrpN30W0Mo+I4/8U2UUIQrnrhqUeWrAU=
59 | github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
60 | github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
61 | github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
62 | github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
63 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
64 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
65 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
66 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
67 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
68 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
69 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
70 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
71 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
72 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
73 | github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM=
74 | github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI=
75 | github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms=
76 | github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4=
77 | github.com/toolkits/pkg v1.3.3 h1:qpQAQ18Jr47dv4NcBALlH0ad7L2PuqSh5K+nJKNg5lU=
78 | github.com/toolkits/pkg v1.3.3/go.mod h1:USXArTJlz1f1DCnQHNPYugO8GPkr1NRhP4eYQZQVshk=
79 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
80 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
81 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
82 | github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
83 | github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
84 | go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
85 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
86 | go.uber.org/automaxprocs v1.4.0 h1:CpDZl6aOlLhReez+8S3eEotD7Jx0Os++lemPlMULQP0=
87 | go.uber.org/automaxprocs v1.4.0/go.mod h1:/mTEdr7LvHhs0v7mjdxDreTz1OG5zdZGqgOnhWiR/+Q=
88 | go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
89 | go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
90 | go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
91 | go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
92 | go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
93 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
94 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
95 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
96 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
97 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
98 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
99 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
100 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
101 | golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
102 | golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
103 | golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
104 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
105 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
106 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
107 | golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
108 | golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
109 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
110 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
111 | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
112 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
113 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
114 | golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
115 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
116 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
117 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
118 | golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
119 | golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
120 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
121 | golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
122 | golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
123 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
124 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
125 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
126 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
127 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
128 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
129 | golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
130 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
131 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
132 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
133 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
134 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
135 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
136 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
137 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
138 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
139 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
140 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
141 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
142 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
143 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
144 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
145 |
--------------------------------------------------------------------------------
/logger/logger.go:
--------------------------------------------------------------------------------
1 | package logger
2 |
3 | import (
4 | "time"
5 |
6 | "flashcat.cloud/catpaw/config"
7 | "go.uber.org/zap"
8 | "go.uber.org/zap/zapcore"
9 | )
10 |
11 | var Logger *zap.SugaredLogger
12 |
13 | func Build() func() {
14 | c := config.Config.LogConfig
15 |
16 | loggerConfig := zap.NewProductionConfig()
17 | loggerConfig.EncoderConfig.TimeKey = "ts"
18 | loggerConfig.EncoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout(time.RFC3339)
19 | loggerConfig.DisableStacktrace = true
20 |
21 | switch c.Level {
22 | case "debug":
23 | loggerConfig.Level = zap.NewAtomicLevelAt(zap.DebugLevel)
24 | case "info":
25 | loggerConfig.Level = zap.NewAtomicLevelAt(zap.InfoLevel)
26 | case "warn":
27 | loggerConfig.Level = zap.NewAtomicLevelAt(zap.WarnLevel)
28 | case "error":
29 | loggerConfig.Level = zap.NewAtomicLevelAt(zap.ErrorLevel)
30 | case "fatal":
31 | loggerConfig.Level = zap.NewAtomicLevelAt(zap.FatalLevel)
32 | default:
33 | loggerConfig.Level = zap.NewAtomicLevelAt(zap.InfoLevel)
34 | }
35 |
36 | loggerConfig.Encoding = c.Format
37 | loggerConfig.OutputPaths = []string{c.Output}
38 | loggerConfig.InitialFields = c.Fields
39 |
40 | logger, err := loggerConfig.Build()
41 | if err != nil {
42 | panic(err)
43 | }
44 |
45 | Logger = logger.Sugar()
46 |
47 | return func() { Logger.Sync() }
48 | }
49 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "os"
7 | "os/signal"
8 | "runtime"
9 | "syscall"
10 |
11 | "flashcat.cloud/catpaw/agent"
12 | "flashcat.cloud/catpaw/config"
13 | "flashcat.cloud/catpaw/logger"
14 | "flashcat.cloud/catpaw/winx"
15 | "github.com/chai2010/winsvc"
16 | "github.com/toolkits/pkg/runner"
17 | )
18 |
19 | var (
20 | appPath string
21 | configDir = flag.String("configs", "conf.d", "Specify configuration directory.")
22 | testMode = flag.Bool("test", false, "Is test mode? Print results to stdout if --test given.")
23 | interval = flag.Int64("interval", 0, "Global interval(unit:Second).")
24 | showVersion = flag.Bool("version", false, "Show version.")
25 | plugins = flag.String("plugins", "", "e.g. plugin1:plugin2")
26 | Url = flag.String("url", "", "e.g. https://api.flashcat.cloud/event/push/alert/standard?integration_key=x")
27 | Loglevel = flag.String("loglevel", "", "e.g. debug, info, warn, error, fatal")
28 | )
29 |
30 | func init() {
31 | var err error
32 | if appPath, err = winsvc.GetAppPath(); err != nil {
33 | panic(err)
34 | }
35 | }
36 |
37 | func main() {
38 | flag.Parse()
39 |
40 | if *showVersion {
41 | fmt.Println(config.Version)
42 | os.Exit(0)
43 | }
44 |
45 | winx.Args(appPath)
46 |
47 | if err := config.InitConfig(*configDir, *testMode, *interval, *plugins, *Url, *Loglevel); err != nil {
48 | panic(err)
49 | }
50 |
51 | closefn := logger.Build()
52 | defer closefn()
53 |
54 | runner.Init()
55 | logger.Logger.Info("runner.binarydir: ", runner.Cwd)
56 | logger.Logger.Info("runner.configdir: ", *configDir)
57 | logger.Logger.Info("runner.hostname: ", runner.Hostname)
58 | logger.Logger.Info("runner.fd_limits: ", runner.FdLimits())
59 | logger.Logger.Info("runner.vm_limits: ", runner.VMLimits())
60 |
61 | agent := agent.New()
62 |
63 | if runtime.GOOS == "windows" && !winsvc.IsAnInteractiveSession() {
64 | if err := winsvc.RunAsService(winx.GetServiceName(), agent.Start, agent.Stop, false); err != nil {
65 | fmt.Println("failed to run windows service:", err)
66 | os.Exit(1)
67 | }
68 | return
69 | } else {
70 | agent.Start()
71 | }
72 |
73 | sc := make(chan os.Signal, 1)
74 | // syscall.SIGUSR2 == 0xc , not available on windows
75 | signal.Notify(sc, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGPIPE)
76 |
77 | EXIT:
78 | for {
79 | sig := <-sc
80 | logger.Logger.Info("received signal: ", sig.String())
81 | switch sig {
82 | case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT:
83 | break EXIT
84 | case syscall.SIGHUP:
85 | agent.Reload()
86 | case syscall.SIGPIPE:
87 | // https://pkg.go.dev/os/signal#hdr-SIGPIPE
88 | // do nothing
89 | }
90 | }
91 |
92 | agent.Stop()
93 | logger.Logger.Info("agent exited")
94 | }
95 |
--------------------------------------------------------------------------------
/pkg/cfg/cfg.go:
--------------------------------------------------------------------------------
1 | package cfg
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "path"
7 | "strings"
8 |
9 | "github.com/koding/multiconfig"
10 | "github.com/toolkits/pkg/file"
11 | )
12 |
13 | type ConfigFormat string
14 |
15 | const (
16 | YamlFormat ConfigFormat = "yaml"
17 | TomlFormat ConfigFormat = "toml"
18 | JsonFormat ConfigFormat = "json"
19 | )
20 |
21 | type ConfigWithFormat struct {
22 | Config string `json:"config"`
23 | Format ConfigFormat `json:"format"`
24 | checkSum string `json:"-"`
25 | }
26 |
27 | func (cwf *ConfigWithFormat) CheckSum() string {
28 | return cwf.checkSum
29 | }
30 |
31 | func (cwf *ConfigWithFormat) SetCheckSum(checkSum string) {
32 | cwf.checkSum = checkSum
33 | }
34 |
35 | func GuessFormat(fpath string) ConfigFormat {
36 | if strings.HasSuffix(fpath, ".json") {
37 | return JsonFormat
38 | }
39 | if strings.HasSuffix(fpath, ".yaml") || strings.HasSuffix(fpath, ".yml") {
40 | return YamlFormat
41 | }
42 | return TomlFormat
43 | }
44 |
45 | func LoadConfigByDir(configDir string, configPtr interface{}) error {
46 | var (
47 | tBuf []byte
48 | )
49 |
50 | loaders := []multiconfig.Loader{
51 | &multiconfig.TagLoader{},
52 | &multiconfig.EnvironmentLoader{},
53 | }
54 |
55 | files, err := file.FilesUnder(configDir)
56 | if err != nil {
57 | return fmt.Errorf("failed to list files under: %s : %v", configDir, err)
58 | }
59 | s := NewFileScanner()
60 | for _, fpath := range files {
61 | switch {
62 | case strings.HasSuffix(fpath, ".toml"):
63 | s.Read(path.Join(configDir, fpath))
64 | tBuf = append(tBuf, s.Data()...)
65 | tBuf = append(tBuf, []byte("\n")...)
66 | case strings.HasSuffix(fpath, ".json"):
67 | loaders = append(loaders, &multiconfig.JSONLoader{Path: path.Join(configDir, fpath)})
68 | case strings.HasSuffix(fpath, ".yaml") || strings.HasSuffix(fpath, ".yml"):
69 | loaders = append(loaders, &multiconfig.YAMLLoader{Path: path.Join(configDir, fpath)})
70 | }
71 | if s.Err() != nil {
72 | return s.Err()
73 | }
74 | }
75 |
76 | if len(tBuf) != 0 {
77 | loaders = append(loaders, &multiconfig.TOMLLoader{Reader: bytes.NewReader(tBuf)})
78 | }
79 |
80 | m := multiconfig.DefaultLoader{
81 | Loader: multiconfig.MultiLoader(loaders...),
82 | Validator: multiconfig.MultiValidator(&multiconfig.RequiredValidator{}),
83 | }
84 | return m.Load(configPtr)
85 | }
86 |
87 | func LoadConfigs(configs []ConfigWithFormat, configPtr interface{}) error {
88 | var (
89 | tBuf, yBuf, jBuf []byte
90 | )
91 | loaders := []multiconfig.Loader{
92 | &multiconfig.TagLoader{},
93 | &multiconfig.EnvironmentLoader{},
94 | }
95 | for _, c := range configs {
96 | switch c.Format {
97 | case TomlFormat:
98 | tBuf = append(tBuf, []byte("\n\n")...)
99 | tBuf = append(tBuf, []byte(c.Config)...)
100 | case YamlFormat:
101 | yBuf = append(yBuf, []byte(c.Config)...)
102 | case JsonFormat:
103 | jBuf = append(jBuf, []byte(c.Config)...)
104 | }
105 | }
106 |
107 | if len(tBuf) != 0 {
108 | loaders = append(loaders, &multiconfig.TOMLLoader{Reader: bytes.NewReader(tBuf)})
109 | }
110 | if len(yBuf) != 0 {
111 | loaders = append(loaders, &multiconfig.YAMLLoader{Reader: bytes.NewReader(yBuf)})
112 | }
113 | if len(jBuf) != 0 {
114 | loaders = append(loaders, &multiconfig.JSONLoader{Reader: bytes.NewReader(jBuf)})
115 | }
116 |
117 | m := multiconfig.DefaultLoader{
118 | Loader: multiconfig.MultiLoader(loaders...),
119 | Validator: multiconfig.MultiValidator(&multiconfig.RequiredValidator{}),
120 | }
121 | return m.Load(configPtr)
122 | }
123 |
124 | func LoadSingleConfig(c ConfigWithFormat, configPtr interface{}) error {
125 | loaders := []multiconfig.Loader{
126 | &multiconfig.TagLoader{},
127 | &multiconfig.EnvironmentLoader{},
128 | }
129 |
130 | switch c.Format {
131 | case TomlFormat:
132 | loaders = append(loaders, &multiconfig.TOMLLoader{Reader: bytes.NewReader([]byte(c.Config))})
133 | case YamlFormat:
134 | loaders = append(loaders, &multiconfig.YAMLLoader{Reader: bytes.NewReader([]byte(c.Config))})
135 | case JsonFormat:
136 | loaders = append(loaders, &multiconfig.JSONLoader{Reader: bytes.NewReader([]byte(c.Config))})
137 |
138 | }
139 |
140 | m := multiconfig.DefaultLoader{
141 | Loader: multiconfig.MultiLoader(loaders...),
142 | Validator: multiconfig.MultiValidator(&multiconfig.RequiredValidator{}),
143 | }
144 | return m.Load(configPtr)
145 | }
146 |
--------------------------------------------------------------------------------
/pkg/cfg/scan.go:
--------------------------------------------------------------------------------
1 | package cfg
2 |
3 | import (
4 | "io/ioutil"
5 | )
6 |
7 | type scanner struct {
8 | data []byte
9 | err error
10 | }
11 |
12 | func NewFileScanner() *scanner {
13 | return &scanner{}
14 | }
15 |
16 | func (s *scanner) Err() error {
17 | return s.err
18 | }
19 |
20 | func (s *scanner) Data() []byte {
21 | return s.data
22 | }
23 |
24 | func (s *scanner) Read(file string) {
25 | if s.err == nil {
26 | s.data, s.err = ioutil.ReadFile(file)
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/pkg/choice/choice.go:
--------------------------------------------------------------------------------
1 | // Package choice provides basic functions for working with
2 | // plugin options that must be one of several values.
3 | package choice
4 |
5 | import (
6 | "fmt"
7 | "strings"
8 | )
9 |
10 | // Contains return true if the choice in the list of choices.
11 | func Contains(choice string, choices []string) bool {
12 | for _, item := range choices {
13 | if item == choice {
14 | return true
15 | }
16 | }
17 | return false
18 | }
19 |
20 | // Contains return true if the choice in the list of choices.
21 | func ContainsPrefix(choice string, choices []string) bool {
22 | for _, item := range choices {
23 | if strings.HasPrefix(choice, item) {
24 | return true
25 | }
26 | }
27 | return false
28 | }
29 |
30 | // Check returns an error if a choice is not one of
31 | // the available choices.
32 | func Check(choice string, available []string) error {
33 | if !Contains(choice, available) {
34 | return fmt.Errorf("unknown choice %s", choice)
35 | }
36 | return nil
37 | }
38 |
39 | // CheckSlice returns an error if the choices is not a subset of
40 | // available.
41 | func CheckSlice(choices, available []string) error {
42 | for _, choice := range choices {
43 | err := Check(choice, available)
44 | if err != nil {
45 | return err
46 | }
47 | }
48 | return nil
49 | }
50 |
--------------------------------------------------------------------------------
/pkg/cmdx/cmd_notwindows.go:
--------------------------------------------------------------------------------
1 | //go:build !windows
2 | // +build !windows
3 |
4 | package cmdx
5 |
6 | import (
7 | "os/exec"
8 | "syscall"
9 | "time"
10 | )
11 |
12 | func CmdWait(cmd *exec.Cmd, timeout time.Duration) (error, bool) {
13 | var err error
14 |
15 | done := make(chan error)
16 | go func() {
17 | done <- cmd.Wait()
18 | }()
19 |
20 | select {
21 | case <-time.After(timeout):
22 | go func() {
23 | <-done // allow goroutine to exit
24 | }()
25 |
26 | // IMPORTANT: cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} is necessary before cmd.Start()
27 | err = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
28 | return err, true
29 | case err = <-done:
30 | return err, false
31 | }
32 | }
33 |
34 | func CmdStart(cmd *exec.Cmd) error {
35 | cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
36 | return cmd.Start()
37 | }
38 |
--------------------------------------------------------------------------------
/pkg/cmdx/cmd_windows.go:
--------------------------------------------------------------------------------
1 | //go:build windows
2 | // +build windows
3 |
4 | package cmdx
5 |
6 | import (
7 | "os/exec"
8 | "syscall"
9 | "time"
10 | )
11 |
12 | func CmdWait(cmd *exec.Cmd, timeout time.Duration) (error, bool) {
13 | var err error
14 |
15 | done := make(chan error)
16 | go func() {
17 | done <- cmd.Wait()
18 | }()
19 |
20 | select {
21 | case <-time.After(timeout):
22 | go func() {
23 | <-done // allow goroutine to exit
24 | }()
25 |
26 | err = cmd.Process.Signal(syscall.SIGKILL)
27 | return err, true
28 | case err = <-done:
29 | return err, false
30 | }
31 | }
32 |
33 | func CmdStart(cmd *exec.Cmd) error {
34 | return cmd.Start()
35 | }
36 |
--------------------------------------------------------------------------------
/pkg/cmdx/cmdx.go:
--------------------------------------------------------------------------------
1 | package cmdx
2 |
3 | import (
4 | "os/exec"
5 | "time"
6 | )
7 |
8 | func RunTimeout(cmd *exec.Cmd, timeout time.Duration) (error, bool) {
9 | err := CmdStart(cmd)
10 | if err != nil {
11 | return err, false
12 | }
13 |
14 | return CmdWait(cmd, timeout)
15 | }
16 |
--------------------------------------------------------------------------------
/pkg/filter/filter.go:
--------------------------------------------------------------------------------
1 | package filter
2 |
3 | import (
4 | "strings"
5 |
6 | "github.com/gobwas/glob"
7 | )
8 |
9 | type Filter interface {
10 | Match(string) bool
11 | }
12 |
13 | // Compile takes a list of string filters and returns a Filter interface
14 | // for matching a given string against the filter list. The filter list
15 | // supports glob matching too, ie:
16 | //
17 | // f, _ := Compile([]string{"cpu", "mem", "net*"})
18 | // f.Match("cpu") // true
19 | // f.Match("network") // true
20 | // f.Match("memory") // false
21 | func Compile(filters []string) (Filter, error) {
22 | // return if there is nothing to compile
23 | if len(filters) == 0 {
24 | return nil, nil
25 | }
26 |
27 | // check if we can compile a non-glob filter
28 | noGlob := true
29 | for _, filter := range filters {
30 | if HasMeta(filter) {
31 | noGlob = false
32 | break
33 | }
34 | }
35 |
36 | switch {
37 | case noGlob:
38 | // return non-globbing filter if not needed.
39 | return compileFilterNoGlob(filters), nil
40 | case len(filters) == 1:
41 | return glob.Compile(filters[0])
42 | default:
43 | return glob.Compile("{" + strings.Join(filters, ",") + "}")
44 | }
45 | }
46 |
47 | // HasMeta reports whether path contains any magic glob characters.
48 | func HasMeta(s string) bool {
49 | return strings.ContainsAny(s, "*?[")
50 | }
51 |
52 | type filter struct {
53 | m map[string]struct{}
54 | }
55 |
56 | func (f *filter) Match(s string) bool {
57 | _, ok := f.m[s]
58 | return ok
59 | }
60 |
61 | type filtersingle struct {
62 | s string
63 | }
64 |
65 | func (f *filtersingle) Match(s string) bool {
66 | return f.s == s
67 | }
68 |
69 | func compileFilterNoGlob(filters []string) Filter {
70 | if len(filters) == 1 {
71 | return &filtersingle{s: filters[0]}
72 | }
73 | out := filter{m: make(map[string]struct{})}
74 | for _, filter := range filters {
75 | out.m[filter] = struct{}{}
76 | }
77 | return &out
78 | }
79 |
80 | type IncludeExcludeFilter struct {
81 | include Filter
82 | exclude Filter
83 | includeDefault bool
84 | excludeDefault bool
85 | }
86 |
87 | func NewIncludeExcludeFilter(
88 | include []string,
89 | exclude []string,
90 | ) (Filter, error) {
91 | return NewIncludeExcludeFilterDefaults(include, exclude, true, false)
92 | }
93 |
94 | func NewIncludeExcludeFilterDefaults(
95 | include []string,
96 | exclude []string,
97 | includeDefault bool,
98 | excludeDefault bool,
99 | ) (Filter, error) {
100 | in, err := Compile(include)
101 | if err != nil {
102 | return nil, err
103 | }
104 |
105 | ex, err := Compile(exclude)
106 | if err != nil {
107 | return nil, err
108 | }
109 |
110 | return &IncludeExcludeFilter{in, ex, includeDefault, excludeDefault}, nil
111 | }
112 |
113 | func (f *IncludeExcludeFilter) Match(s string) bool {
114 | if f.include != nil {
115 | if !f.include.Match(s) {
116 | return false
117 | }
118 | } else if !f.includeDefault {
119 | return false
120 | }
121 |
122 | if f.exclude != nil {
123 | if f.exclude.Match(s) {
124 | return false
125 | }
126 | } else if f.excludeDefault {
127 | return false
128 | }
129 |
130 | return true
131 | }
132 |
--------------------------------------------------------------------------------
/pkg/netx/netx.go:
--------------------------------------------------------------------------------
1 | package netx
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | )
7 |
8 | func LocalAddressByInterfaceName(interfaceName string) (net.Addr, error) {
9 | i, err := net.InterfaceByName(interfaceName)
10 | if err != nil {
11 | return nil, err
12 | }
13 |
14 | addrs, err := i.Addrs()
15 | if err != nil {
16 | return nil, err
17 | }
18 |
19 | for _, addr := range addrs {
20 | if naddr, ok := addr.(*net.IPNet); ok {
21 | // leaving port set to zero to let kernel pick
22 | return &net.TCPAddr{IP: naddr.IP}, nil
23 | }
24 | }
25 |
26 | return nil, fmt.Errorf("cannot create local address for interface %q", interfaceName)
27 | }
28 |
--------------------------------------------------------------------------------
/pkg/osx/osx.go:
--------------------------------------------------------------------------------
1 | package osx
2 |
3 | import "os"
4 |
5 | // getEnv returns the value of an environment variable, or returns the provided fallback value
6 | func GetEnv(key, fallback string) string {
7 | if value, ok := os.LookupEnv(key); ok {
8 | return value
9 | }
10 | return fallback
11 | }
12 |
--------------------------------------------------------------------------------
/pkg/osx/proc.go:
--------------------------------------------------------------------------------
1 | package osx
2 |
3 | import "os"
4 |
5 | func GetHostProc() string {
6 | procPath := "/proc"
7 | if os.Getenv("HOST_PROC") != "" {
8 | procPath = os.Getenv("HOST_PROC")
9 | }
10 | return procPath
11 | }
12 |
--------------------------------------------------------------------------------
/pkg/runtimex/stack.go:
--------------------------------------------------------------------------------
1 | package runtimex
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "io/ioutil"
7 | "runtime"
8 | )
9 |
10 | var (
11 | dunno = []byte("???")
12 | centerDot = []byte("·")
13 | dot = []byte(".")
14 | slash = []byte("/")
15 | )
16 |
17 | // stack returns a nicely formatted stack frame, skipping skip frames.
18 | func Stack(skip int) []byte {
19 | buf := new(bytes.Buffer) // the returned data
20 | // As we loop, we open files and read them. These variables record the currently
21 | // loaded file.
22 | var lines [][]byte
23 | var lastFile string
24 | for i := skip; ; i++ { // Skip the expected number of frames
25 | pc, file, line, ok := runtime.Caller(i)
26 | if !ok {
27 | break
28 | }
29 | // Print this much at least. If we can't find the source, it won't show.
30 | fmt.Fprintf(buf, "%s:%d (0x%x)\n", file, line, pc)
31 | if file != lastFile {
32 | data, err := ioutil.ReadFile(file)
33 | if err != nil {
34 | continue
35 | }
36 | lines = bytes.Split(data, []byte{'\n'})
37 | lastFile = file
38 | }
39 | fmt.Fprintf(buf, "\t%s: %s\n", function(pc), source(lines, line))
40 | }
41 | return buf.Bytes()
42 | }
43 |
44 | // source returns a space-trimmed slice of the n'th line.
45 | func source(lines [][]byte, n int) []byte {
46 | n-- // in stack trace, lines are 1-indexed but our array is 0-indexed
47 | if n < 0 || n >= len(lines) {
48 | return dunno
49 | }
50 | return bytes.TrimSpace(lines[n])
51 | }
52 |
53 | // function returns, if possible, the name of the function containing the PC.
54 | func function(pc uintptr) []byte {
55 | fn := runtime.FuncForPC(pc)
56 | if fn == nil {
57 | return dunno
58 | }
59 | name := []byte(fn.Name())
60 | // The name includes the path name to the package, which is unnecessary
61 | // since the file name is already included. Plus, it has center dots.
62 | // That is, we see
63 | // runtime/debug.*T·ptrmethod
64 | // and want
65 | // *T.ptrmethod
66 | // Also the package path might contains dot (e.g. code.google.com/...),
67 | // so first eliminate the path prefix
68 | if lastslash := bytes.LastIndex(name, slash); lastslash >= 0 {
69 | name = name[lastslash+1:]
70 | }
71 | if period := bytes.Index(name, dot); period >= 0 {
72 | name = name[period+1:]
73 | }
74 | name = bytes.Replace(name, centerDot, dot, -1)
75 | return name
76 | }
77 |
--------------------------------------------------------------------------------
/pkg/safe/queue.go:
--------------------------------------------------------------------------------
1 | package safe
2 |
3 | import (
4 | "container/list"
5 | "sync"
6 | )
7 |
8 | // Queue is a thread-safe linkedlist
9 | type Queue[T any] struct {
10 | sync.RWMutex
11 | linkedlist *list.List
12 | }
13 |
14 | func NewQueue[T any]() *Queue[T] {
15 | return &Queue[T]{linkedlist: list.New()}
16 | }
17 |
18 | func (q *Queue[T]) PushFront(v T) *list.Element {
19 | q.Lock()
20 | e := q.linkedlist.PushFront(v)
21 | q.Unlock()
22 | return e
23 | }
24 |
25 | func (q *Queue[T]) PushFrontN(vs []T) {
26 | q.Lock()
27 | for _, item := range vs {
28 | q.linkedlist.PushFront(item)
29 | }
30 | q.Unlock()
31 | }
32 |
33 | func (q *Queue[T]) PopBack() *T {
34 | q.Lock()
35 | defer q.Unlock()
36 | if elem := q.linkedlist.Back(); elem != nil {
37 | item := q.linkedlist.Remove(elem)
38 | v, ok := item.(T)
39 | if !ok {
40 | return nil
41 | }
42 | return &v
43 | }
44 | return nil
45 | }
46 |
47 | func (q *Queue[T]) PopBackN(n int) []T {
48 | q.Lock()
49 | defer q.Unlock()
50 |
51 | count := q.linkedlist.Len()
52 | if count == 0 {
53 | return nil
54 | }
55 |
56 | if count > n {
57 | count = n
58 | }
59 |
60 | items := make([]T, 0, count)
61 | for i := 0; i < count; i++ {
62 | data := q.linkedlist.Remove(q.linkedlist.Back())
63 | item, ok := data.(T)
64 | if ok {
65 | items = append(items, item)
66 | }
67 | }
68 | return items
69 | }
70 |
71 | func (q *Queue[T]) PopBackAll() []T {
72 | q.Lock()
73 | defer q.Unlock()
74 | count := q.linkedlist.Len()
75 | if count == 0 {
76 | return nil
77 | }
78 |
79 | items := make([]T, 0, count)
80 | for i := 0; i < count; i++ {
81 | data := q.linkedlist.Remove(q.linkedlist.Back())
82 | item, ok := data.(T)
83 | if ok {
84 | items = append(items, item)
85 | }
86 | }
87 | return items
88 | }
89 |
90 | func (q *Queue[T]) RemoveAll() {
91 | q.Lock()
92 | q.linkedlist.Init()
93 | q.Unlock()
94 | }
95 |
96 | func (q *Queue[T]) Len() int {
97 | q.RLock()
98 | size := q.linkedlist.Len()
99 | q.RUnlock()
100 | return size
101 | }
102 |
103 | // QueueLimited is Queue with Limited Size
104 | type QueueLimited[T any] struct {
105 | maxSize int
106 | queue *Queue[T]
107 | }
108 |
109 | func NewQueueLimited[T any](maxSize int) *QueueLimited[T] {
110 | return &QueueLimited[T]{queue: NewQueue[T](), maxSize: maxSize}
111 | }
112 |
113 | func (ql *QueueLimited[T]) PushFront(v T) bool {
114 | if ql.queue.Len() >= ql.maxSize {
115 | return false
116 | }
117 |
118 | ql.queue.PushFront(v)
119 | return true
120 | }
121 |
122 | func (ql *QueueLimited[T]) PushFrontN(vs []T) bool {
123 | if ql.queue.Len() >= ql.maxSize {
124 | return false
125 | }
126 |
127 | ql.queue.PushFrontN(vs)
128 | return true
129 | }
130 |
131 | func (ql *QueueLimited[T]) PopBack() *T {
132 | return ql.queue.PopBack()
133 | }
134 |
135 | func (ql *QueueLimited[T]) PopBackN(n int) []T {
136 | return ql.queue.PopBackN(n)
137 | }
138 |
139 | func (ql *QueueLimited[T]) PopBackAll() []T {
140 | return ql.queue.PopBackAll()
141 | }
142 |
143 | func (ql *QueueLimited[T]) RemoveAll() {
144 | ql.queue.RemoveAll()
145 | }
146 |
147 | func (ql *QueueLimited[T]) Len() int {
148 | return ql.queue.Len()
149 | }
150 |
--------------------------------------------------------------------------------
/pkg/shell/shellquote.go:
--------------------------------------------------------------------------------
1 | package shell
2 |
3 | import (
4 | "bytes"
5 | "errors"
6 | "runtime"
7 | "strings"
8 | "unicode/utf8"
9 | )
10 |
11 | var (
12 | UnterminatedSingleQuoteError = errors.New("Unterminated single-quoted string")
13 | UnterminatedDoubleQuoteError = errors.New("Unterminated double-quoted string")
14 | UnterminatedEscapeError = errors.New("Unterminated backslash-escape")
15 | )
16 |
17 | var (
18 | splitChars = " \n\t"
19 | singleChar = '\''
20 | doubleChar = '"'
21 | escapeChar = '\\'
22 | doubleEscapeChars = "$`\"\n\\"
23 | )
24 |
25 | // Split splits a string according to /bin/sh's word-splitting rules. It
26 | // supports backslash-escapes, single-quotes, and double-quotes. Notably it does
27 | // not support the $” style of quoting. It also doesn't attempt to perform any
28 | // other sort of expansion, including brace expansion, shell expansion, or
29 | // pathname expansion.
30 | //
31 | // If the given input has an unterminated quoted string or ends in a
32 | // backslash-escape, one of UnterminatedSingleQuoteError,
33 | // UnterminatedDoubleQuoteError, or UnterminatedEscapeError is returned.
34 | func QuoteSplit(input string) (words []string, err error) {
35 | var buf bytes.Buffer
36 | words = make([]string, 0)
37 |
38 | for len(input) > 0 {
39 | // skip any splitChars at the start
40 | c, l := utf8.DecodeRuneInString(input)
41 | if strings.ContainsRune(splitChars, c) {
42 | input = input[l:]
43 | continue
44 | } else if c == escapeChar {
45 | // Look ahead for escaped newline so we can skip over it
46 | next := input[l:]
47 | if len(next) == 0 {
48 | err = UnterminatedEscapeError
49 | return
50 | }
51 | c2, l2 := utf8.DecodeRuneInString(next)
52 | if c2 == '\n' {
53 | input = next[l2:]
54 | continue
55 | }
56 | }
57 |
58 | var word string
59 | word, input, err = splitWord(input, &buf)
60 | if err != nil {
61 | return
62 | }
63 | words = append(words, word)
64 | }
65 | return
66 | }
67 |
68 | func splitWord(input string, buf *bytes.Buffer) (word string, remainder string, err error) {
69 | buf.Reset()
70 |
71 | raw:
72 | {
73 | cur := input
74 | for len(cur) > 0 {
75 | c, l := utf8.DecodeRuneInString(cur)
76 | cur = cur[l:]
77 | if c == singleChar {
78 | buf.WriteString(input[0 : len(input)-len(cur)-l])
79 | input = cur
80 | goto single
81 | } else if c == doubleChar {
82 | buf.WriteString(input[0 : len(input)-len(cur)-l])
83 | input = cur
84 | goto double
85 | } else if c == escapeChar {
86 | if runtime.GOOS == "windows" {
87 | buf.WriteString(input[0 : len(input)-len(cur)-l+1])
88 | } else {
89 | buf.WriteString(input[0 : len(input)-len(cur)-l])
90 | }
91 | input = cur
92 | goto escape
93 | } else if strings.ContainsRune(splitChars, c) {
94 | buf.WriteString(input[0 : len(input)-len(cur)-l])
95 | return buf.String(), cur, nil
96 | }
97 | }
98 | if len(input) > 0 {
99 | buf.WriteString(input)
100 | input = ""
101 | }
102 | goto done
103 | }
104 |
105 | escape:
106 | {
107 | if len(input) == 0 {
108 | return "", "", UnterminatedEscapeError
109 | }
110 | c, l := utf8.DecodeRuneInString(input)
111 | if c == '\n' {
112 | // a backslash-escaped newline is elided from the output entirely
113 | } else {
114 | buf.WriteString(input[:l])
115 | }
116 | input = input[l:]
117 | }
118 | goto raw
119 |
120 | single:
121 | {
122 | i := strings.IndexRune(input, singleChar)
123 | if i == -1 {
124 | return "", "", UnterminatedSingleQuoteError
125 | }
126 | buf.WriteString(input[0:i])
127 | input = input[i+1:]
128 | goto raw
129 | }
130 |
131 | double:
132 | {
133 | cur := input
134 | for len(cur) > 0 {
135 | c, l := utf8.DecodeRuneInString(cur)
136 | cur = cur[l:]
137 | if c == doubleChar {
138 | buf.WriteString(input[0 : len(input)-len(cur)-l])
139 | input = cur
140 | goto raw
141 | } else if c == escapeChar {
142 | // bash only supports certain escapes in double-quoted strings
143 | c2, l2 := utf8.DecodeRuneInString(cur)
144 | cur = cur[l2:]
145 | if strings.ContainsRune(doubleEscapeChars, c2) {
146 | buf.WriteString(input[0 : len(input)-len(cur)-l-l2])
147 | if c2 == '\n' {
148 | // newline is special, skip the backslash entirely
149 | } else {
150 | buf.WriteRune(c2)
151 | }
152 | input = cur
153 | }
154 | }
155 | }
156 | return "", "", UnterminatedDoubleQuoteError
157 | }
158 |
159 | done:
160 | return buf.String(), input, nil
161 | }
162 |
--------------------------------------------------------------------------------
/pkg/tls/common.go:
--------------------------------------------------------------------------------
1 | package tls
2 |
3 | import (
4 | "crypto/tls"
5 | "fmt"
6 | )
7 |
8 | var tlsVersionMap = map[string]uint16{
9 | "TLS10": tls.VersionTLS10,
10 | "TLS11": tls.VersionTLS11,
11 | "TLS12": tls.VersionTLS12,
12 | "TLS13": tls.VersionTLS13,
13 | }
14 |
15 | var tlsCipherMap = map[string]uint16{
16 | "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
17 | "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
18 | "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
19 | "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
20 | "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
21 | "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
22 | "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,
23 | "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
24 | "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,
25 | "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
26 | "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
27 | "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
28 | "TLS_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
29 | "TLS_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
30 | "TLS_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_RSA_WITH_AES_128_CBC_SHA256,
31 | "TLS_RSA_WITH_AES_128_CBC_SHA": tls.TLS_RSA_WITH_AES_128_CBC_SHA,
32 | "TLS_RSA_WITH_AES_256_CBC_SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA,
33 | "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
34 | "TLS_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
35 | "TLS_RSA_WITH_RC4_128_SHA": tls.TLS_RSA_WITH_RC4_128_SHA,
36 | "TLS_ECDHE_RSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA,
37 | "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA,
38 | "TLS_AES_128_GCM_SHA256": tls.TLS_AES_128_GCM_SHA256,
39 | "TLS_AES_256_GCM_SHA384": tls.TLS_AES_256_GCM_SHA384,
40 | "TLS_CHACHA20_POLY1305_SHA256": tls.TLS_CHACHA20_POLY1305_SHA256,
41 | }
42 |
43 | // ParseCiphers returns a `[]uint16` by received `[]string` key that represents ciphers from crypto/tls.
44 | // If some of ciphers in received list doesn't exists ParseCiphers returns nil with error
45 | func ParseCiphers(ciphers []string) ([]uint16, error) {
46 | suites := []uint16{}
47 |
48 | for _, cipher := range ciphers {
49 | v, ok := tlsCipherMap[cipher]
50 | if !ok {
51 | return nil, fmt.Errorf("unsupported cipher %q", cipher)
52 | }
53 | suites = append(suites, v)
54 | }
55 |
56 | return suites, nil
57 | }
58 |
59 | // ParseTLSVersion returns a `uint16` by received version string key that represents tls version from crypto/tls.
60 | // If version isn't supported ParseTLSVersion returns 0 with error
61 | func ParseTLSVersion(version string) (uint16, error) {
62 | if v, ok := tlsVersionMap[version]; ok {
63 | return v, nil
64 | }
65 | return 0, fmt.Errorf("unsupported version %q", version)
66 | }
67 |
--------------------------------------------------------------------------------
/pkg/tls/config.go:
--------------------------------------------------------------------------------
1 | package tls
2 |
3 | import (
4 | "crypto/tls"
5 | "crypto/x509"
6 | "fmt"
7 | "os"
8 | "strings"
9 |
10 | "flashcat.cloud/catpaw/pkg/choice"
11 | )
12 |
13 | // ClientConfig represents the standard client TLS config.
14 | type ClientConfig struct {
15 | UseTLS bool `toml:"use_tls"`
16 | TLSCA string `toml:"tls_ca"`
17 | TLSCert string `toml:"tls_cert"`
18 | TLSKey string `toml:"tls_key"`
19 | TLSKeyPwd string `toml:"tls_key_pwd"`
20 | InsecureSkipVerify bool `toml:"insecure_skip_verify"`
21 | ServerName string `toml:"tls_server_name"`
22 | TLSMinVersion string `toml:"tls_min_version"`
23 | TLSMaxVersion string `toml:"tls_max_version"`
24 | }
25 |
26 | // ServerConfig represents the standard server TLS config.
27 | type ServerConfig struct {
28 | TLSCert string `toml:"tls_cert"`
29 | TLSKey string `toml:"tls_key"`
30 | TLSKeyPwd string `toml:"tls_key_pwd"`
31 | TLSAllowedCACerts []string `toml:"tls_allowed_cacerts"`
32 | TLSCipherSuites []string `toml:"tls_cipher_suites"`
33 | TLSMinVersion string `toml:"tls_min_version"`
34 | TLSMaxVersion string `toml:"tls_max_version"`
35 | TLSAllowedDNSNames []string `toml:"tls_allowed_dns_names"`
36 | }
37 |
38 | // TLSConfig returns a tls.Config, may be nil without error if TLS is not
39 | // configured.
40 | func (c *ClientConfig) TLSConfig() (*tls.Config, error) {
41 | if !c.UseTLS {
42 | return nil, nil
43 | }
44 |
45 | tlsConfig := &tls.Config{
46 | InsecureSkipVerify: c.InsecureSkipVerify,
47 | Renegotiation: tls.RenegotiateNever,
48 | }
49 |
50 | if c.TLSCA != "" {
51 | pool, err := makeCertPool([]string{c.TLSCA})
52 | if err != nil {
53 | return nil, err
54 | }
55 | tlsConfig.RootCAs = pool
56 | }
57 |
58 | if c.TLSCert != "" && c.TLSKey != "" {
59 | err := loadCertificate(tlsConfig, c.TLSCert, c.TLSKey)
60 | if err != nil {
61 | return nil, err
62 | }
63 | }
64 |
65 | if c.ServerName != "" {
66 | tlsConfig.ServerName = c.ServerName
67 | }
68 |
69 | if c.TLSMinVersion == "1.0" {
70 | tlsConfig.MinVersion = tls.VersionTLS10
71 | } else if c.TLSMinVersion == "1.1" {
72 | tlsConfig.MinVersion = tls.VersionTLS11
73 | } else if c.TLSMinVersion == "1.2" {
74 | tlsConfig.MinVersion = tls.VersionTLS12
75 | } else if c.TLSMinVersion == "1.3" {
76 | tlsConfig.MinVersion = tls.VersionTLS13
77 | }
78 |
79 | if c.TLSMaxVersion == "1.0" {
80 | tlsConfig.MaxVersion = tls.VersionTLS10
81 | } else if c.TLSMaxVersion == "1.1" {
82 | tlsConfig.MaxVersion = tls.VersionTLS11
83 | } else if c.TLSMaxVersion == "1.2" {
84 | tlsConfig.MaxVersion = tls.VersionTLS12
85 | } else if c.TLSMaxVersion == "1.3" {
86 | tlsConfig.MaxVersion = tls.VersionTLS13
87 | }
88 |
89 | return tlsConfig, nil
90 | }
91 |
92 | // TLSConfig returns a tls.Config, may be nil without error if TLS is not
93 | // configured.
94 | func (c *ServerConfig) TLSConfig() (*tls.Config, error) {
95 | if c.TLSCert == "" && c.TLSKey == "" && len(c.TLSAllowedCACerts) == 0 {
96 | return nil, nil
97 | }
98 |
99 | tlsConfig := &tls.Config{}
100 |
101 | if len(c.TLSAllowedCACerts) != 0 {
102 | pool, err := makeCertPool(c.TLSAllowedCACerts)
103 | if err != nil {
104 | return nil, err
105 | }
106 | tlsConfig.ClientCAs = pool
107 | tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
108 | }
109 |
110 | if c.TLSCert != "" && c.TLSKey != "" {
111 | err := loadCertificate(tlsConfig, c.TLSCert, c.TLSKey)
112 | if err != nil {
113 | return nil, err
114 | }
115 | }
116 |
117 | if len(c.TLSCipherSuites) != 0 {
118 | cipherSuites, err := ParseCiphers(c.TLSCipherSuites)
119 | if err != nil {
120 | return nil, fmt.Errorf(
121 | "could not parse server cipher suites %s: %v", strings.Join(c.TLSCipherSuites, ","), err)
122 | }
123 | tlsConfig.CipherSuites = cipherSuites
124 | }
125 |
126 | if c.TLSMaxVersion != "" {
127 | version, err := ParseTLSVersion(c.TLSMaxVersion)
128 | if err != nil {
129 | return nil, fmt.Errorf(
130 | "could not parse tls max version %q: %v", c.TLSMaxVersion, err)
131 | }
132 | tlsConfig.MaxVersion = version
133 | }
134 |
135 | if c.TLSMinVersion != "" {
136 | version, err := ParseTLSVersion(c.TLSMinVersion)
137 | if err != nil {
138 | return nil, fmt.Errorf(
139 | "could not parse tls min version %q: %v", c.TLSMinVersion, err)
140 | }
141 | tlsConfig.MinVersion = version
142 | }
143 |
144 | if tlsConfig.MinVersion != 0 && tlsConfig.MaxVersion != 0 && tlsConfig.MinVersion > tlsConfig.MaxVersion {
145 | return nil, fmt.Errorf(
146 | "tls min version %q can't be greater than tls max version %q", tlsConfig.MinVersion, tlsConfig.MaxVersion)
147 | }
148 |
149 | // Since clientAuth is tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
150 | // there must be certs to validate.
151 | if len(c.TLSAllowedCACerts) > 0 && len(c.TLSAllowedDNSNames) > 0 {
152 | tlsConfig.VerifyPeerCertificate = c.verifyPeerCertificate
153 | }
154 |
155 | return tlsConfig, nil
156 | }
157 |
158 | func makeCertPool(certFiles []string) (*x509.CertPool, error) {
159 | pool := x509.NewCertPool()
160 | for _, certFile := range certFiles {
161 | pem, err := os.ReadFile(certFile)
162 | if err != nil {
163 | return nil, fmt.Errorf(
164 | "could not read certificate %q: %v", certFile, err)
165 | }
166 | if !pool.AppendCertsFromPEM(pem) {
167 | return nil, fmt.Errorf(
168 | "could not parse any PEM certificates %q: %v", certFile, err)
169 | }
170 | }
171 | return pool, nil
172 | }
173 |
174 | func loadCertificate(config *tls.Config, certFile, keyFile string) error {
175 | cert, err := tls.LoadX509KeyPair(certFile, keyFile)
176 | if err != nil {
177 | return fmt.Errorf(
178 | "could not load keypair %s:%s: %v", certFile, keyFile, err)
179 | }
180 |
181 | config.Certificates = []tls.Certificate{cert}
182 | config.BuildNameToCertificate()
183 | return nil
184 | }
185 |
186 | func (c *ServerConfig) verifyPeerCertificate(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
187 | // The certificate chain is client + intermediate + root.
188 | // Let's review the client certificate.
189 | cert, err := x509.ParseCertificate(rawCerts[0])
190 | if err != nil {
191 | return fmt.Errorf("could not validate peer certificate: %v", err)
192 | }
193 |
194 | for _, name := range cert.DNSNames {
195 | if choice.Contains(name, c.TLSAllowedDNSNames) {
196 | return nil
197 | }
198 | }
199 |
200 | return fmt.Errorf("peer certificate not in allowed DNS Name list: %v", cert.DNSNames)
201 | }
202 |
--------------------------------------------------------------------------------
/plugins/exec/exec.go:
--------------------------------------------------------------------------------
1 | package exec
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "io"
8 | osExec "os/exec"
9 | "path/filepath"
10 | "runtime"
11 | "strings"
12 | "sync"
13 | "time"
14 |
15 | "flashcat.cloud/catpaw/config"
16 | "flashcat.cloud/catpaw/logger"
17 | "flashcat.cloud/catpaw/pkg/cmdx"
18 | "flashcat.cloud/catpaw/pkg/safe"
19 | "flashcat.cloud/catpaw/pkg/shell"
20 | "flashcat.cloud/catpaw/plugins"
21 | "flashcat.cloud/catpaw/types"
22 | "github.com/toolkits/pkg/concurrent/semaphore"
23 | )
24 |
25 | const (
26 | pluginName string = "exec"
27 | maxStderrBytes int = 512
28 | )
29 |
30 | type Instance struct {
31 | config.InternalConfig
32 |
33 | Commands []string `toml:"commands"`
34 | Timeout config.Duration `toml:"timeout"`
35 | Concurrency int `toml:"concurrency"`
36 | }
37 |
38 | type ExecPlugin struct {
39 | config.InternalConfig
40 | Instances []*Instance `toml:"instances"`
41 | }
42 |
43 | func (p *ExecPlugin) GetInstances() []plugins.Instance {
44 | ret := make([]plugins.Instance, len(p.Instances))
45 | for i := 0; i < len(p.Instances); i++ {
46 | ret[i] = p.Instances[i]
47 | }
48 | return ret
49 | }
50 |
51 | func init() {
52 | plugins.Add(pluginName, func() plugins.Plugin {
53 | return &ExecPlugin{}
54 | })
55 | }
56 |
57 | func (ins *Instance) Gather(q *safe.Queue[*types.Event]) {
58 | if len(ins.Commands) == 0 {
59 | return
60 | }
61 |
62 | if ins.Timeout == 0 {
63 | ins.Timeout = config.Duration(10 * time.Second)
64 | }
65 |
66 | if ins.Concurrency == 0 {
67 | ins.Concurrency = 5
68 | }
69 |
70 | var commands []string
71 | for _, pattern := range ins.Commands {
72 | cmdAndArgs := strings.SplitN(pattern, " ", 2)
73 | if len(cmdAndArgs) == 0 {
74 | continue
75 | }
76 |
77 | matches, err := filepath.Glob(cmdAndArgs[0])
78 | if err != nil {
79 | logger.Logger.Errorw("failed to get filepath glob", "error", err, "pattern", cmdAndArgs[0])
80 | continue
81 | }
82 |
83 | if len(matches) == 0 {
84 | // There were no matches with the glob pattern, so let's assume
85 | // that the command is in PATH and just run it as it is
86 | commands = append(commands, pattern)
87 | } else {
88 | // There were matches, so we'll append each match together with
89 | // the arguments to the commands slice
90 | for _, match := range matches {
91 | if len(cmdAndArgs) == 1 {
92 | commands = append(commands, match)
93 | } else {
94 | commands = append(commands,
95 | strings.Join([]string{match, cmdAndArgs[1]}, " "))
96 | }
97 | }
98 | }
99 | }
100 |
101 | if len(commands) == 0 {
102 | logger.Logger.Warnln("no commands after parse")
103 | return
104 | }
105 |
106 | wg := new(sync.WaitGroup)
107 | se := semaphore.NewSemaphore(ins.Concurrency)
108 | for _, command := range commands {
109 | wg.Add(1)
110 | se.Acquire()
111 | go func(command string) {
112 | defer func() {
113 | se.Release()
114 | wg.Done()
115 | }()
116 | ins.gather(q, command)
117 | }(command)
118 | }
119 | wg.Wait()
120 | }
121 |
122 | func (ins *Instance) gather(q *safe.Queue[*types.Event], command string) {
123 | outbuf, errbuf, err := commandRun(command, time.Duration(ins.Timeout))
124 | if err != nil || len(errbuf) > 0 {
125 | logger.Logger.Errorw("failed to exec command", "command", command, "error", err, "stderr", string(errbuf), "stdout", string(outbuf))
126 | return
127 | }
128 |
129 | if len(outbuf) == 0 {
130 | logger.Logger.Warnw("exec command output is empty", "command", command)
131 | return
132 | }
133 |
134 | var events []*types.Event
135 | if err := json.Unmarshal(outbuf, &events); err != nil {
136 | logger.Logger.Errorw("failed to unmarshal command output", "command", command, "error", err, "output", string(outbuf))
137 | return
138 | }
139 |
140 | for i := range events {
141 | q.PushFront(events[i])
142 | }
143 | }
144 |
145 | func commandRun(command string, timeout time.Duration) ([]byte, []byte, error) {
146 | splitCmd, err := shell.QuoteSplit(command)
147 | if err != nil || len(splitCmd) == 0 {
148 | return nil, nil, fmt.Errorf("exec: unable to parse command, %s", err)
149 | }
150 |
151 | cmd := osExec.Command(splitCmd[0], splitCmd[1:]...)
152 |
153 | var (
154 | out bytes.Buffer
155 | stderr bytes.Buffer
156 | )
157 | cmd.Stdout = &out
158 | cmd.Stderr = &stderr
159 |
160 | runError, runTimeout := cmdx.RunTimeout(cmd, timeout)
161 | if runTimeout {
162 | return nil, nil, fmt.Errorf("exec %s timeout", command)
163 | }
164 |
165 | out = removeWindowsCarriageReturns(out)
166 | if stderr.Len() > 0 {
167 | stderr = removeWindowsCarriageReturns(stderr)
168 | stderr = truncate(stderr)
169 | }
170 |
171 | return out.Bytes(), stderr.Bytes(), runError
172 | }
173 |
174 | func truncate(buf bytes.Buffer) bytes.Buffer {
175 | // Limit the number of bytes.
176 | didTruncate := false
177 | if buf.Len() > maxStderrBytes {
178 | buf.Truncate(maxStderrBytes)
179 | didTruncate = true
180 | }
181 | if i := bytes.IndexByte(buf.Bytes(), '\n'); i > 0 {
182 | // Only show truncation if the newline wasn't the last character.
183 | if i < buf.Len()-1 {
184 | didTruncate = true
185 | }
186 | buf.Truncate(i)
187 | }
188 | if didTruncate {
189 | //nolint:errcheck,revive // Will always return nil or panic
190 | buf.WriteString("...")
191 | }
192 | return buf
193 | }
194 |
195 | // removeWindowsCarriageReturns removes all carriage returns from the input if the
196 | // OS is Windows. It does not return any errors.
197 | func removeWindowsCarriageReturns(b bytes.Buffer) bytes.Buffer {
198 | if runtime.GOOS == "windows" {
199 | var buf bytes.Buffer
200 | for {
201 | byt, err := b.ReadBytes(0x0D)
202 | byt = bytes.TrimRight(byt, "\x0d")
203 | if len(byt) > 0 {
204 | _, _ = buf.Write(byt)
205 | }
206 | if err == io.EOF {
207 | return buf
208 | }
209 | }
210 | }
211 | return b
212 | }
213 |
--------------------------------------------------------------------------------
/plugins/filechange/filechange.go:
--------------------------------------------------------------------------------
1 | package filechange
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "path/filepath"
7 | "strings"
8 | "time"
9 |
10 | "flashcat.cloud/catpaw/config"
11 | "flashcat.cloud/catpaw/logger"
12 | "flashcat.cloud/catpaw/pkg/safe"
13 | "flashcat.cloud/catpaw/plugins"
14 | "flashcat.cloud/catpaw/types"
15 | )
16 |
17 | const (
18 | pluginName string = "filechange"
19 | )
20 |
21 | type Instance struct {
22 | config.InternalConfig
23 |
24 | TimeSpan time.Duration `toml:"time_span"`
25 | Filepaths []string `toml:"filepaths"`
26 | Check string `toml:"check"`
27 | }
28 |
29 | type FileChangePlugin struct {
30 | config.InternalConfig
31 | Instances []*Instance `toml:"instances"`
32 | }
33 |
34 | func (p *FileChangePlugin) GetInstances() []plugins.Instance {
35 | ret := make([]plugins.Instance, len(p.Instances))
36 | for i := 0; i < len(p.Instances); i++ {
37 | ret[i] = p.Instances[i]
38 | }
39 | return ret
40 | }
41 |
42 | func init() {
43 | plugins.Add(pluginName, func() plugins.Plugin {
44 | return &FileChangePlugin{}
45 | })
46 | }
47 |
48 | func (ins *Instance) Gather(q *safe.Queue[*types.Event]) {
49 | if ins.TimeSpan == 0 {
50 | ins.TimeSpan = 3 * time.Minute
51 | }
52 |
53 | if ins.Check == "" {
54 | logger.Logger.Error("check is empty")
55 | return
56 | }
57 |
58 | if len(ins.Filepaths) == 0 {
59 | logger.Logger.Error("filepaths is empty")
60 | return
61 | }
62 |
63 | // get all files
64 | var fps []string
65 | for _, fp := range ins.Filepaths {
66 | matches, err := filepath.Glob(fp)
67 | if err != nil {
68 | logger.Logger.Errorf("glob %s error: %v", fp, err)
69 | continue
70 | }
71 |
72 | if len(matches) == 0 {
73 | continue
74 | }
75 |
76 | fps = append(fps, matches...)
77 | }
78 |
79 | // check mtime
80 | now := time.Now()
81 | files := make(map[string]time.Time)
82 |
83 | for _, fp := range fps {
84 | f, e := os.Stat(fp)
85 | if e != nil {
86 | logger.Logger.Errorf("stat %s error: %v", fp, e)
87 | continue
88 | }
89 |
90 | mtime := f.ModTime()
91 | if now.Sub(mtime) < ins.TimeSpan {
92 | files[fp] = mtime
93 | }
94 | }
95 |
96 | if len(files) == 0 {
97 | q.PushFront(ins.buildEvent(ins.Check, "files not changed"))
98 | return
99 | }
100 |
101 | var body strings.Builder
102 | body.WriteString(head)
103 |
104 | for fp, mtime := range files {
105 | body.WriteString("| ")
106 | body.WriteString(fp)
107 | body.WriteString(" | ")
108 | body.WriteString(mtime.Format("2006-01-02 15:04:05"))
109 | body.WriteString(" |\n")
110 | }
111 |
112 | title := fmt.Sprintf("files changed\n\nconfiguration.filepaths:`%s`\n", ins.Filepaths)
113 | q.PushFront(ins.buildEvent(ins.Check, "[MD]", title, body.String()).SetEventStatus(ins.GetDefaultSeverity()))
114 | }
115 |
116 | func (ins *Instance) buildEvent(check string, desc ...string) *types.Event {
117 | event := types.BuildEvent(map[string]string{"check": check}).SetTitleRule("$check")
118 | if len(desc) > 0 {
119 | event.SetDescription(strings.Join(desc, "\n"))
120 | }
121 | return event
122 | }
123 |
124 | var head = `| File | MTime |
125 | | :--| --: |
126 | `
127 |
--------------------------------------------------------------------------------
/plugins/http/http.go:
--------------------------------------------------------------------------------
1 | package http
2 |
3 | import (
4 | "fmt"
5 | "io"
6 | "net"
7 | "net/http"
8 | "net/url"
9 | "strings"
10 | "sync"
11 | "time"
12 |
13 | "flashcat.cloud/catpaw/config"
14 | "flashcat.cloud/catpaw/logger"
15 | "flashcat.cloud/catpaw/pkg/filter"
16 | "flashcat.cloud/catpaw/pkg/netx"
17 | "flashcat.cloud/catpaw/pkg/safe"
18 | "flashcat.cloud/catpaw/plugins"
19 | "flashcat.cloud/catpaw/types"
20 | "github.com/toolkits/pkg/concurrent/semaphore"
21 | )
22 |
23 | const pluginName = "http"
24 |
25 | type Expect struct {
26 | ResponseSubstring string `toml:"response_substring"`
27 | ResponseStatusCode []string `toml:"response_status_code"`
28 | ResponseStatusCodeFilter filter.Filter `toml:"-"` // compiled filter
29 | CertExpireThreshold config.Duration `toml:"cert_expire_threshold"`
30 | }
31 |
32 | type Partial struct {
33 | ID string `toml:"id"`
34 | Concurrency int `toml:"concurrency"`
35 | config.HTTPConfig
36 | }
37 |
38 | type Instance struct {
39 | config.InternalConfig
40 | Partial string `toml:"partial"`
41 |
42 | Targets []string `toml:"targets"`
43 | Concurrency int `toml:"concurrency"`
44 | Expect Expect `toml:"expect"`
45 |
46 | config.HTTPConfig
47 | client httpClient
48 | }
49 |
50 | type HttpPlugin struct {
51 | config.InternalConfig
52 | Partials []Partial `toml:"partials"`
53 | Instances []*Instance `toml:"instances"`
54 | }
55 |
56 | func (p *HttpPlugin) ApplyPartials() error {
57 | for i := 0; i < len(p.Instances); i++ {
58 | id := p.Instances[i].Partial
59 | if id != "" {
60 | for _, partial := range p.Partials {
61 | if partial.ID == id {
62 | // use partial config as default
63 | if p.Instances[i].Concurrency == 0 {
64 | p.Instances[i].Concurrency = partial.Concurrency
65 | }
66 |
67 | if p.Instances[i].HTTPConfig.HTTPProxy == "" {
68 | p.Instances[i].HTTPConfig.HTTPProxy = partial.HTTPProxy
69 | }
70 |
71 | if p.Instances[i].HTTPConfig.Interface == "" {
72 | p.Instances[i].HTTPConfig.Interface = partial.Interface
73 | }
74 |
75 | if p.Instances[i].HTTPConfig.Method == "" {
76 | p.Instances[i].HTTPConfig.Method = partial.Method
77 | }
78 |
79 | if p.Instances[i].HTTPConfig.Timeout == 0 {
80 | p.Instances[i].HTTPConfig.Timeout = partial.Timeout
81 | }
82 |
83 | if p.Instances[i].HTTPConfig.FollowRedirects == nil {
84 | p.Instances[i].HTTPConfig.FollowRedirects = partial.FollowRedirects
85 | }
86 |
87 | if p.Instances[i].HTTPConfig.BasicAuthUser == "" {
88 | p.Instances[i].HTTPConfig.BasicAuthUser = partial.BasicAuthUser
89 | }
90 |
91 | if p.Instances[i].HTTPConfig.BasicAuthPass == "" {
92 | p.Instances[i].HTTPConfig.BasicAuthPass = partial.BasicAuthPass
93 | }
94 |
95 | if len(p.Instances[i].HTTPConfig.Headers) == 0 {
96 | p.Instances[i].HTTPConfig.Headers = partial.Headers
97 | }
98 |
99 | if p.Instances[i].HTTPConfig.Payload == "" {
100 | p.Instances[i].HTTPConfig.Payload = partial.Payload
101 | }
102 |
103 | break
104 | }
105 | }
106 | }
107 | }
108 | return nil
109 | }
110 |
111 | func init() {
112 | plugins.Add(pluginName, func() plugins.Plugin {
113 | return &HttpPlugin{}
114 | })
115 | }
116 |
117 | type httpClient interface {
118 | Do(req *http.Request) (*http.Response, error)
119 | }
120 |
121 | func (ins *Instance) Init() error {
122 | if ins.Concurrency == 0 {
123 | ins.Concurrency = 10
124 | }
125 |
126 | var err error
127 | if len(ins.Expect.ResponseStatusCode) > 0 {
128 | ins.Expect.ResponseStatusCodeFilter, err = filter.Compile(ins.Expect.ResponseStatusCode)
129 | if err != nil {
130 | return err
131 | }
132 | }
133 |
134 | client, err := ins.createHTTPClient()
135 | if err != nil {
136 | return fmt.Errorf("failed to create http client: %v", err)
137 | }
138 |
139 | ins.client = client
140 |
141 | for _, target := range ins.Targets {
142 | addr, err := url.Parse(target)
143 | if err != nil {
144 | return fmt.Errorf("failed to parse http url: %s, error: %v", target, err)
145 | }
146 |
147 | if addr.Scheme != "http" && addr.Scheme != "https" {
148 | return fmt.Errorf("only http and https are supported, target: %s", target)
149 | }
150 | }
151 |
152 | return nil
153 | }
154 |
155 | func (ins *Instance) createHTTPClient() (*http.Client, error) {
156 | tlsCfg, err := ins.ClientConfig.TLSConfig()
157 | if err != nil {
158 | return nil, err
159 | }
160 |
161 | dialer := &net.Dialer{}
162 |
163 | if ins.Interface != "" {
164 | dialer.LocalAddr, err = netx.LocalAddressByInterfaceName(ins.Interface)
165 | if err != nil {
166 | return nil, err
167 | }
168 | }
169 |
170 | proxy, err := ins.GetProxy()
171 | if err != nil {
172 | return nil, err
173 | }
174 |
175 | trans := &http.Transport{
176 | Proxy: proxy,
177 | DialContext: dialer.DialContext,
178 | DisableKeepAlives: true,
179 | TLSClientConfig: tlsCfg,
180 | }
181 |
182 | if ins.UseTLS {
183 | trans.TLSClientConfig = tlsCfg
184 | }
185 |
186 | client := &http.Client{
187 | Transport: trans,
188 | Timeout: time.Duration(ins.GetTimeout()),
189 | }
190 |
191 | if ins.FollowRedirects != nil && *ins.FollowRedirects {
192 | client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
193 | if len(via) >= 10 {
194 | return fmt.Errorf("stopped after 10 redirects")
195 | }
196 | return http.ErrUseLastResponse
197 | }
198 | }
199 |
200 | return client, nil
201 | }
202 |
203 | func (h *HttpPlugin) GetInstances() []plugins.Instance {
204 | ret := make([]plugins.Instance, len(h.Instances))
205 | for i := 0; i < len(h.Instances); i++ {
206 | ret[i] = h.Instances[i]
207 | }
208 | return ret
209 | }
210 |
211 | func (ins *Instance) Gather(q *safe.Queue[*types.Event]) {
212 | if len(ins.Targets) == 0 {
213 | return
214 | }
215 |
216 | if !ins.GetInitialized() {
217 | if err := ins.Init(); err != nil {
218 | logger.Logger.Errorf("failed to init http plugin instance: %v", err)
219 | return
220 | } else {
221 | ins.SetInitialized()
222 | }
223 | }
224 |
225 | wg := new(sync.WaitGroup)
226 | se := semaphore.NewSemaphore(ins.Concurrency)
227 | for _, target := range ins.Targets {
228 | wg.Add(1)
229 | se.Acquire()
230 | go func(target string) {
231 | defer func() {
232 | se.Release()
233 | wg.Done()
234 | }()
235 | ins.gather(q, target)
236 | }(target)
237 | }
238 | wg.Wait()
239 | }
240 |
241 | func (ins *Instance) gather(q *safe.Queue[*types.Event], target string) {
242 | logger.Logger.Debug("http target: ", target)
243 |
244 | labels := map[string]string{
245 | "target": target,
246 | "method": ins.GetMethod(),
247 | }
248 |
249 | var payload io.Reader
250 | if ins.Payload != "" {
251 | payload = strings.NewReader(ins.Payload)
252 | }
253 |
254 | request, err := http.NewRequest(ins.Method, target, payload)
255 | if err != nil {
256 | logger.Logger.Errorw("failed to create http request", "error", err, "plugin", pluginName)
257 | return
258 | }
259 |
260 | for i := 0; i < len(ins.Headers); i += 2 {
261 | request.Header.Add(ins.Headers[i], ins.Headers[i+1])
262 | if ins.Headers[i] == "Host" {
263 | request.Host = ins.Headers[i+1]
264 | }
265 | }
266 |
267 | if ins.BasicAuthUser != "" || ins.BasicAuthPass != "" {
268 | request.SetBasicAuth(ins.BasicAuthUser, ins.BasicAuthPass)
269 | }
270 |
271 | // check connection
272 | resp, err := ins.client.Do(request)
273 |
274 | e := types.BuildEvent(map[string]string{
275 | "check": "HTTP check failed",
276 | }, labels)
277 |
278 | errString := "null. everything is ok"
279 | if err != nil {
280 | e.SetEventStatus(ins.GetDefaultSeverity())
281 | errString = err.Error()
282 | }
283 |
284 | e.SetTitleRule("$check").SetDescription(`[MD]
285 | - **target**: ` + target + `
286 | - **method**: ` + ins.GetMethod() + `
287 | - **error**: ` + errString + `
288 | `)
289 |
290 | q.PushFront(e)
291 |
292 | if err != nil {
293 | logger.Logger.Errorw("failed to send http request", "error", err, "plugin", pluginName, "target", target)
294 | return
295 | }
296 |
297 | // check tls cert
298 | if ins.Expect.CertExpireThreshold > 0 && strings.HasPrefix(target, "https://") && resp.TLS != nil {
299 | e := types.BuildEvent(map[string]string{
300 | "check": "TLS cert will expire soon",
301 | }, labels)
302 |
303 | certExpireTimestamp := getEarliestCertExpiry(resp.TLS).Unix()
304 | if certExpireTimestamp < time.Now().Add(time.Duration(ins.Expect.CertExpireThreshold)).Unix() {
305 | e.SetEventStatus(ins.GetDefaultSeverity())
306 | }
307 |
308 | e.SetTitleRule("$check").SetDescription(`[MD]
309 | - **target**: ` + target + `
310 | - **method**: ` + ins.GetMethod() + `
311 | - **cert expire threshold**: ` + ins.Expect.CertExpireThreshold.HumanString() + `
312 | - **cert expire at**: ` + time.Unix(certExpireTimestamp, 0).Format("2006-01-02 15:04:05") + `
313 | `)
314 |
315 | q.PushFront(e)
316 | }
317 |
318 | var body []byte
319 | if resp.Body != nil {
320 | defer resp.Body.Close()
321 | body, err = io.ReadAll(resp.Body)
322 | if err != nil {
323 | logger.Logger.Errorw("failed to read http response body", "error", err, "plugin", pluginName, "target", target)
324 | return
325 | }
326 | }
327 |
328 | statusCode := fmt.Sprint(resp.StatusCode)
329 |
330 | if len(ins.Expect.ResponseStatusCode) > 0 {
331 | e := types.BuildEvent(map[string]string{
332 | "check": "HTTP response status code not match",
333 | }, labels)
334 |
335 | if !ins.Expect.ResponseStatusCodeFilter.Match(statusCode) {
336 | e.SetEventStatus(ins.GetDefaultSeverity())
337 | }
338 |
339 | e.SetTitleRule("$check").SetDescription(fmt.Sprintf(ExpectResponseStatusCodeDesn, target, ins.GetMethod(), statusCode, ins.Expect.ResponseStatusCode, string(body)))
340 | q.PushFront(e)
341 | }
342 |
343 | if len(ins.Expect.ResponseSubstring) > 0 {
344 | e := types.BuildEvent(map[string]string{
345 | "check": "HTTP response body not match",
346 | }, labels)
347 |
348 | if !strings.Contains(string(body), ins.Expect.ResponseSubstring) {
349 | e.SetEventStatus(ins.GetDefaultSeverity())
350 | }
351 |
352 | e.SetTitleRule("$check").SetDescription(fmt.Sprintf(ExpectResponseSubstringDesn, target, ins.GetMethod(), statusCode, ins.Expect.ResponseSubstring, string(body)))
353 | q.PushFront(e)
354 | }
355 | }
356 |
357 | var ExpectResponseStatusCodeDesn = `[MD]
358 | - **target**: %s
359 | - **method**: %s
360 | - **status code**: %s
361 | - **expect code**: %v
362 |
363 | **response body**:
364 |
365 | ` + "```" + `
366 | %s
367 | ` + "```" + `
368 | `
369 |
370 | var ExpectResponseSubstringDesn = `[MD]
371 | - **target**: %s
372 | - **method**: %s
373 | - **status code**: %s
374 | - **expect substring**: %v
375 |
376 | **response body**:
377 |
378 | ` + "```" + `
379 | %s
380 | ` + "```" + `
381 | `
382 |
--------------------------------------------------------------------------------
/plugins/http/tls.go:
--------------------------------------------------------------------------------
1 | package http
2 |
3 | import (
4 | "crypto/tls"
5 | "time"
6 | )
7 |
8 | func getEarliestCertExpiry(state *tls.ConnectionState) time.Time {
9 | earliest := time.Time{}
10 | for _, cert := range state.PeerCertificates {
11 | if (earliest.IsZero() || cert.NotAfter.Before(earliest)) && !cert.NotAfter.IsZero() {
12 | earliest = cert.NotAfter
13 | }
14 | }
15 | return earliest
16 | }
17 |
--------------------------------------------------------------------------------
/plugins/journaltail/journaltail.go:
--------------------------------------------------------------------------------
1 | package journaltail
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "os/exec"
7 |
8 | "flashcat.cloud/catpaw/config"
9 | "flashcat.cloud/catpaw/logger"
10 | "flashcat.cloud/catpaw/pkg/filter"
11 | "flashcat.cloud/catpaw/pkg/safe"
12 | "flashcat.cloud/catpaw/plugins"
13 | "flashcat.cloud/catpaw/types"
14 | )
15 |
16 | const (
17 | pluginName string = "journaltail"
18 | )
19 |
20 | type Instance struct {
21 | config.InternalConfig
22 |
23 | TimeSpan string `toml:"time_span"`
24 | Check string `toml:"check"`
25 | FilterInclude []string `toml:"filter_include"`
26 | FilterExclude []string `toml:"filter_exclude"`
27 |
28 | filter filter.Filter
29 | }
30 |
31 | type JournaltailPlugin struct {
32 | config.InternalConfig
33 | Instances []*Instance `toml:"instances"`
34 | }
35 |
36 | func (p *JournaltailPlugin) GetInstances() []plugins.Instance {
37 | ret := make([]plugins.Instance, len(p.Instances))
38 | for i := 0; i < len(p.Instances); i++ {
39 | ret[i] = p.Instances[i]
40 | }
41 | return ret
42 | }
43 |
44 | func init() {
45 | plugins.Add(pluginName, func() plugins.Plugin {
46 | return &JournaltailPlugin{}
47 | })
48 | }
49 |
50 | func (ins *Instance) Gather(q *safe.Queue[*types.Event]) {
51 | if ins.TimeSpan == "" {
52 | ins.TimeSpan = "1m"
53 | }
54 |
55 | if ins.filter == nil {
56 | if len(ins.FilterInclude) == 0 && len(ins.FilterExclude) == 0 {
57 | logger.Logger.Error("filter_include and filter_exclude are empty")
58 | return
59 | }
60 |
61 | var err error
62 | ins.filter, err = filter.NewIncludeExcludeFilter(ins.FilterInclude, ins.FilterExclude)
63 | if err != nil {
64 | logger.Logger.Warnf("failed to create filter: %s", err)
65 | return
66 | }
67 | }
68 |
69 | if ins.Check == "" {
70 | logger.Logger.Error("check is empty")
71 | return
72 | }
73 |
74 | // go go go
75 | bin, err := exec.LookPath("journalctl")
76 | if err != nil {
77 | logger.Logger.Error("lookup journalctl fail: ", err)
78 | return
79 | }
80 |
81 | if bin == "" {
82 | logger.Logger.Error("journalctl not found")
83 | return
84 | }
85 |
86 | out, err := exec.Command(bin, "--since", fmt.Sprintf("-%s", ins.TimeSpan), "--no-pager", "--no-tail").Output()
87 | if err != nil {
88 | logger.Logger.Error("exec journalctl fail: ", err)
89 | return
90 | }
91 |
92 | var bs bytes.Buffer
93 | var triggered bool
94 |
95 | bs.WriteString("[MD]")
96 | bs.WriteString(fmt.Sprintf("- time_span: `%s`\n", ins.TimeSpan))
97 | bs.WriteString(fmt.Sprintf("- filter_include: `%s`\n", ins.FilterInclude))
98 | bs.WriteString(fmt.Sprintf("- filter_exclude: `%s`\n", ins.FilterExclude))
99 | bs.WriteString("\n")
100 | bs.WriteString("\n")
101 | bs.WriteString("**matched lines**:\n")
102 | bs.WriteString("\n```")
103 |
104 | for _, line := range bytes.Split(out, []byte("\n")) {
105 | if len(line) == 0 {
106 | continue
107 | }
108 |
109 | if !ins.filter.Match(string(line)) {
110 | continue
111 | }
112 |
113 | triggered = true
114 | bs.Write(line)
115 | bs.Write([]byte("\n"))
116 | }
117 |
118 | bs.WriteString("```")
119 |
120 | e := types.BuildEvent(map[string]string{
121 | "check": ins.Check,
122 | }).SetTitleRule("$check")
123 |
124 | if !triggered {
125 | q.PushFront(e)
126 | return
127 | }
128 |
129 | e.SetEventStatus(ins.GetDefaultSeverity())
130 | e.SetDescription(bs.String())
131 | q.PushFront(e)
132 | }
133 |
--------------------------------------------------------------------------------
/plugins/mtime/mtime.go:
--------------------------------------------------------------------------------
1 | package mtime
2 |
3 | import (
4 | "fmt"
5 | "io/fs"
6 | "path/filepath"
7 | "strings"
8 | "time"
9 |
10 | "flashcat.cloud/catpaw/config"
11 | "flashcat.cloud/catpaw/logger"
12 | "flashcat.cloud/catpaw/pkg/safe"
13 | "flashcat.cloud/catpaw/plugins"
14 | "flashcat.cloud/catpaw/types"
15 | "github.com/toolkits/pkg/file"
16 | )
17 |
18 | const (
19 | pluginName string = "mtime"
20 | )
21 |
22 | type Instance struct {
23 | config.InternalConfig
24 |
25 | TimeSpan time.Duration `toml:"time_span"`
26 | Directory string `toml:"directory"`
27 | Check string `toml:"check"`
28 | }
29 |
30 | type MTimePlugin struct {
31 | config.InternalConfig
32 | Instances []*Instance `toml:"instances"`
33 | }
34 |
35 | func (p *MTimePlugin) GetInstances() []plugins.Instance {
36 | ret := make([]plugins.Instance, len(p.Instances))
37 | for i := 0; i < len(p.Instances); i++ {
38 | ret[i] = p.Instances[i]
39 | }
40 | return ret
41 | }
42 |
43 | func init() {
44 | plugins.Add(pluginName, func() plugins.Plugin {
45 | return &MTimePlugin{}
46 | })
47 | }
48 |
49 | func (ins *Instance) Gather(q *safe.Queue[*types.Event]) {
50 | if ins.TimeSpan == 0 {
51 | ins.TimeSpan = 3 * time.Minute
52 | }
53 |
54 | if ins.Check == "" {
55 | logger.Logger.Error("check is empty")
56 | return
57 | }
58 |
59 | if !file.IsExist(ins.Directory) {
60 | logger.Logger.Warnf("directory %s not exist", ins.Directory)
61 | return
62 | }
63 |
64 | now := time.Now()
65 | files := make(map[string]time.Time)
66 |
67 | if err := filepath.WalkDir(ins.Directory, func(path string, di fs.DirEntry, err error) error {
68 | if err != nil {
69 | return err
70 | }
71 |
72 | if di.IsDir() {
73 | return nil
74 | }
75 |
76 | fileinfo, err := di.Info()
77 | if err != nil {
78 | return err
79 | }
80 |
81 | mtime := fileinfo.ModTime()
82 | if now.Sub(mtime) < ins.TimeSpan {
83 | files[path] = mtime
84 | }
85 |
86 | return nil
87 | }); err != nil {
88 | q.PushFront(ins.buildEvent(ins.Directory, ins.Check, fmt.Sprintf("walk directory %s error: %v", ins.Directory, err)).SetEventStatus(ins.GetDefaultSeverity()))
89 | return
90 | }
91 |
92 | if len(files) == 0 {
93 | q.PushFront(ins.buildEvent(ins.Directory, ins.Check, fmt.Sprintf("files not changed or created in directory %s", ins.Directory)))
94 | return
95 | }
96 |
97 | var body strings.Builder
98 | body.WriteString(head)
99 |
100 | for fp, mtime := range files {
101 | body.WriteString("| ")
102 | body.WriteString(fp)
103 | body.WriteString(" | ")
104 | body.WriteString(mtime.Format("2006-01-02 15:04:05"))
105 | body.WriteString(" |\n")
106 | }
107 |
108 | q.PushFront(ins.buildEvent(ins.Directory, ins.Check, body.String()).SetEventStatus(ins.GetDefaultSeverity()))
109 | }
110 |
111 | func (ins *Instance) buildEvent(dir, check string, desc ...string) *types.Event {
112 | event := types.BuildEvent(map[string]string{"directory": dir, "check": check}).SetTitleRule("$check")
113 | if len(desc) > 0 {
114 | event.SetDescription(desc[0])
115 | }
116 | return event
117 | }
118 |
119 | var head = `[MD]| File | MTime |
120 | | :--| --: |
121 | `
122 |
--------------------------------------------------------------------------------
/plugins/net/net.go:
--------------------------------------------------------------------------------
1 | package net
2 |
3 | import (
4 | "bufio"
5 | "errors"
6 | "fmt"
7 | "net"
8 | "net/textproto"
9 | "strings"
10 | "sync"
11 | "time"
12 |
13 | "flashcat.cloud/catpaw/config"
14 | "flashcat.cloud/catpaw/logger"
15 | "flashcat.cloud/catpaw/pkg/safe"
16 | "flashcat.cloud/catpaw/plugins"
17 | "flashcat.cloud/catpaw/types"
18 | "github.com/toolkits/pkg/concurrent/semaphore"
19 | )
20 |
21 | const pluginName = "net"
22 |
23 | type Partial struct {
24 | ID string `toml:"id"`
25 | Concurrency int `toml:"concurrency"`
26 | Timeout config.Duration `toml:"timeout"`
27 | ReadTimeout config.Duration `toml:"read_timeout"`
28 | Protocol string `toml:"protocol"`
29 | Send string `toml:"send"`
30 | Expect string `toml:"expect"`
31 | }
32 |
33 | type Instance struct {
34 | config.InternalConfig
35 | Partial string `toml:"partial"`
36 |
37 | Targets []string `toml:"targets"`
38 | Concurrency int `toml:"concurrency"`
39 | Timeout config.Duration `toml:"timeout"`
40 | ReadTimeout config.Duration `toml:"read_timeout"`
41 | Protocol string `toml:"protocol"`
42 | Send string `toml:"send"`
43 | Expect string `toml:"expect"`
44 | }
45 |
46 | type NETPlugin struct {
47 | config.InternalConfig
48 | Partials []Partial `toml:"partials"`
49 | Instances []*Instance `toml:"instances"`
50 | }
51 |
52 | func (p *NETPlugin) ApplyPartials() error {
53 | for i := 0; i < len(p.Instances); i++ {
54 | id := p.Instances[i].Partial
55 | if id != "" {
56 | for _, partial := range p.Partials {
57 | if partial.ID == id {
58 | // use partial config as default
59 | if p.Instances[i].Concurrency == 0 {
60 | p.Instances[i].Concurrency = partial.Concurrency
61 | }
62 |
63 | if p.Instances[i].Timeout == 0 {
64 | p.Instances[i].Timeout = partial.Timeout
65 | }
66 |
67 | if p.Instances[i].ReadTimeout == 0 {
68 | p.Instances[i].ReadTimeout = partial.ReadTimeout
69 | }
70 |
71 | if p.Instances[i].Protocol == "" {
72 | p.Instances[i].Protocol = partial.Protocol
73 | }
74 |
75 | if p.Instances[i].Send == "" {
76 | p.Instances[i].Send = partial.Send
77 | }
78 |
79 | if p.Instances[i].Expect == "" {
80 | p.Instances[i].Expect = partial.Expect
81 | }
82 |
83 | break
84 | }
85 | }
86 | }
87 | }
88 | return nil
89 | }
90 |
91 | func init() {
92 | plugins.Add(pluginName, func() plugins.Plugin {
93 | return &NETPlugin{}
94 | })
95 | }
96 |
97 | func (p *NETPlugin) GetInstances() []plugins.Instance {
98 | ret := make([]plugins.Instance, len(p.Instances))
99 | for i := 0; i < len(p.Instances); i++ {
100 | ret[i] = p.Instances[i]
101 | }
102 | return ret
103 | }
104 |
105 | func (ins *Instance) Init() error {
106 | if ins.Concurrency == 0 {
107 | ins.Concurrency = 10
108 | }
109 |
110 | if ins.Timeout == 0 {
111 | ins.Timeout = config.Duration(time.Second)
112 | }
113 |
114 | if ins.ReadTimeout == 0 {
115 | ins.ReadTimeout = config.Duration(time.Second)
116 | }
117 |
118 | if ins.Protocol == "" {
119 | ins.Protocol = "tcp"
120 | }
121 |
122 | if ins.Protocol != "tcp" && ins.Protocol != "udp" {
123 | return errors.New("bad protocol, only tcp and udp are supported")
124 | }
125 |
126 | if ins.Protocol == "udp" && ins.Send == "" {
127 | return errors.New("send string cannot be empty when protocol is udp")
128 | }
129 |
130 | if ins.Protocol == "udp" && ins.Expect == "" {
131 | return errors.New("expected string cannot be empty when protocol is udp")
132 | }
133 |
134 | for i := 0; i < len(ins.Targets); i++ {
135 | target := ins.Targets[i]
136 |
137 | host, port, err := net.SplitHostPort(target)
138 | if err != nil {
139 | return fmt.Errorf("failed to split host port, target: %s, error: %v", target, err)
140 | }
141 |
142 | if host == "" {
143 | ins.Targets[i] = "localhost:" + port
144 | }
145 |
146 | if port == "" {
147 | return errors.New("bad port, target: " + target)
148 | }
149 | }
150 |
151 | return nil
152 | }
153 |
154 | func (ins *Instance) Gather(q *safe.Queue[*types.Event]) {
155 | logger.Logger.Debug("net gather, targets: ", ins.Targets)
156 |
157 | if len(ins.Targets) == 0 {
158 | return
159 | }
160 |
161 | if !ins.GetInitialized() {
162 | if err := ins.Init(); err != nil {
163 | logger.Logger.Errorf("failed to init net plugin instance: %v", err)
164 | return
165 | } else {
166 | ins.SetInitialized()
167 | }
168 | }
169 |
170 | wg := new(sync.WaitGroup)
171 | se := semaphore.NewSemaphore(ins.Concurrency)
172 | for _, target := range ins.Targets {
173 | wg.Add(1)
174 | se.Acquire()
175 | go func(target string) {
176 | defer func() {
177 | se.Release()
178 | wg.Done()
179 | }()
180 | ins.gather(q, target)
181 | }(target)
182 | }
183 | wg.Wait()
184 | }
185 |
186 | func (ins *Instance) gather(q *safe.Queue[*types.Event], target string) {
187 | logger.Logger.Debug("net target: ", target)
188 |
189 | labels := map[string]string{
190 | "target": target,
191 | "protocol": ins.Protocol,
192 | }
193 |
194 | switch ins.Protocol {
195 | case "tcp":
196 | ins.TCPGather(target, labels, q)
197 | case "udp":
198 | ins.UDPGather(target, labels, q)
199 | }
200 | }
201 |
202 | func (ins *Instance) TCPGather(address string, labels map[string]string, q *safe.Queue[*types.Event]) {
203 | event := types.BuildEvent(map[string]string{
204 | "check": "tcp check",
205 | }, labels).SetTitleRule("$check").SetDescription(ins.buildDesc(address, "everything is ok"))
206 |
207 | conn, err := net.DialTimeout("tcp", address, time.Duration(ins.Timeout))
208 | if err != nil {
209 | q.PushFront(event.SetEventStatus(ins.GetDefaultSeverity()).SetDescription(ins.buildDesc(address, fmt.Sprintf("connection error: %v", err))))
210 | logger.Logger.Errorw("failed to send tcp request", "error", err, "plugin", pluginName, "target", address)
211 | return
212 | }
213 |
214 | defer conn.Close()
215 |
216 | // check expect string
217 | if ins.Send == "" {
218 | // no need check send and expect
219 | q.PushFront(event)
220 | return
221 | }
222 |
223 | msg := []byte(ins.Send)
224 | if _, err = conn.Write(msg); err != nil {
225 | q.PushFront(event.SetEventStatus(ins.GetDefaultSeverity()).SetDescription(ins.buildDesc(address, fmt.Sprintf("failed to send message: %s, error: %v", ins.Send, err))))
226 | return
227 | }
228 |
229 | // Read string if needed
230 | if ins.Expect != "" {
231 | // Set read timeout
232 | if err := conn.SetReadDeadline(time.Now().Add(time.Duration(ins.ReadTimeout))); err != nil {
233 | q.PushFront(event.SetEventStatus(ins.GetDefaultSeverity()).SetDescription(ins.buildDesc(address, fmt.Sprintf("failed to set read deadline, error: %v", err))))
234 | return
235 | }
236 |
237 | // Prepare reader
238 | reader := bufio.NewReader(conn)
239 | tp := textproto.NewReader(reader)
240 | // Read
241 | data, err := tp.ReadLine()
242 | if err != nil {
243 | q.PushFront(event.SetEventStatus(ins.GetDefaultSeverity()).SetDescription(ins.buildDesc(address, fmt.Sprintf("failed to read response line, error: %v", err))))
244 | return
245 | }
246 |
247 | if !strings.Contains(data, ins.Expect) {
248 | q.PushFront(event.SetEventStatus(ins.GetDefaultSeverity()).SetDescription(ins.buildDesc(address, fmt.Sprintf("response mismatch. expected: %s, real response: %s", ins.Expect, data))))
249 | return
250 | }
251 | }
252 |
253 | q.PushFront(event)
254 | }
255 |
256 | func (ins *Instance) UDPGather(address string, labels map[string]string, q *safe.Queue[*types.Event]) {
257 | event := types.BuildEvent(map[string]string{
258 | "check": "udp check",
259 | }, labels).SetTitleRule("$check").SetDescription(ins.buildDesc(address, "everything is ok"))
260 |
261 | udpAddr, err := net.ResolveUDPAddr("udp", address)
262 | if err != nil {
263 | message := fmt.Sprintf("resolve udp address(%s) error: %v", address, err)
264 | q.PushFront(event.SetEventStatus(ins.GetDefaultSeverity()).SetDescription(ins.buildDesc(address, message)))
265 | logger.Logger.Error(message)
266 | return
267 | }
268 |
269 | conn, err := net.DialUDP("udp", nil, udpAddr)
270 | if err != nil {
271 | message := fmt.Sprintf("dial udp address(%s) error: %v", address, err)
272 | q.PushFront(event.SetEventStatus(ins.GetDefaultSeverity()).SetDescription(ins.buildDesc(address, message)))
273 | logger.Logger.Error(message)
274 | return
275 | }
276 |
277 | defer conn.Close()
278 |
279 | if _, err = conn.Write([]byte(ins.Send)); err != nil {
280 | message := fmt.Sprintf("write string(%s) to udp address(%s) error: %v", ins.Send, address, err)
281 | q.PushFront(event.SetEventStatus(ins.GetDefaultSeverity()).SetDescription(ins.buildDesc(address, message)))
282 | logger.Logger.Error(message)
283 | return
284 | }
285 |
286 | if err = conn.SetReadDeadline(time.Now().Add(time.Duration(ins.ReadTimeout))); err != nil {
287 | message := fmt.Sprintf("set connection deadline to udp address(%s) error: %v", address, err)
288 | q.PushFront(event.SetEventStatus(ins.GetDefaultSeverity()).SetDescription(ins.buildDesc(address, message)))
289 | logger.Logger.Error(message)
290 | return
291 | }
292 |
293 | // Read
294 | buf := make([]byte, 1024)
295 | if _, _, err = conn.ReadFromUDP(buf); err != nil {
296 | message := fmt.Sprintf("read from udp address(%s) error: %v", address, err)
297 | q.PushFront(event.SetEventStatus(ins.GetDefaultSeverity()).SetDescription(ins.buildDesc(address, message)))
298 | logger.Logger.Error(message)
299 | return
300 | }
301 |
302 | if !strings.Contains(string(buf), ins.Expect) {
303 | message := fmt.Sprintf("response mismatch. expect: %s, real: %s", ins.Expect, string(buf))
304 | q.PushFront(event.SetEventStatus(ins.GetDefaultSeverity()).SetDescription(ins.buildDesc(address, message)))
305 | logger.Logger.Error(message)
306 | return
307 | }
308 |
309 | q.PushFront(event)
310 | }
311 |
312 | func (ins *Instance) buildDesc(target, message string) string {
313 | return `[MD]
314 | - **target**: ` + target + `
315 | - **protocol**: ` + ins.Protocol + `
316 | - **config.send**: ` + ins.Send + `
317 | - **config.expect**:` + ins.Expect + `
318 |
319 |
320 | **message**:
321 |
322 | ` + "```" + `
323 | ` + message + `
324 | ` + "```" + `
325 | `
326 | }
327 |
--------------------------------------------------------------------------------
/plugins/ping/ping.go:
--------------------------------------------------------------------------------
1 | package ping
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | "runtime"
7 | "strings"
8 | "sync"
9 | "time"
10 |
11 | "flashcat.cloud/catpaw/config"
12 | "flashcat.cloud/catpaw/logger"
13 | "flashcat.cloud/catpaw/pkg/safe"
14 | "flashcat.cloud/catpaw/plugins"
15 | "flashcat.cloud/catpaw/types"
16 | ping "github.com/prometheus-community/pro-bing"
17 | )
18 |
19 | const (
20 | pluginName = "ping"
21 | defaultPingDataBytesSize = 56
22 | )
23 |
24 | type Partial struct {
25 | ID string `toml:"id"`
26 | Concurrency int `toml:"concurrency"`
27 | Count int `toml:"count"` // ping -c
28 | PingInterval float64 `toml:"ping_interval"` // ping -i
29 | Timeout float64 `toml:"timeout"` // ping -W
30 | Interface string `toml:"interface"` // ping -I/-S
31 | IPv6 *bool `toml:"ipv6"` // Whether to resolve addresses using ipv6 or not.
32 | Size *int `toml:"size"` // Packet size
33 | AlertIfPacketLossPercentGe float64 `toml:"alert_if_packet_loss_percent_ge"`
34 | }
35 |
36 | type Instance struct {
37 | config.InternalConfig
38 | Partial string `toml:"partial"`
39 |
40 | Targets []string `toml:"targets"`
41 | Concurrency int `toml:"concurrency"`
42 | Count int `toml:"count"` // ping -c
43 | PingInterval float64 `toml:"ping_interval"` // ping -i
44 | Timeout float64 `toml:"timeout"` // ping -W
45 | Interface string `toml:"interface"` // ping -I/-S
46 | IPv6 *bool `toml:"ipv6"` // Whether to resolve addresses using ipv6 or not.
47 | Size *int `toml:"size"` // Packet size
48 | AlertIfPacketLossPercentGe float64 `toml:"alert_if_packet_loss_percent_ge"`
49 |
50 | calcInterval time.Duration
51 | calcTimeout time.Duration
52 | sourceAddress string
53 | }
54 |
55 | type PingPlugin struct {
56 | config.InternalConfig
57 | Partials []Partial `toml:"partials"`
58 | Instances []*Instance `toml:"instances"`
59 | }
60 |
61 | func (p *PingPlugin) ApplyPartials() error {
62 | for i := 0; i < len(p.Instances); i++ {
63 | id := p.Instances[i].Partial
64 | if id != "" {
65 | for _, partial := range p.Partials {
66 | if partial.ID == id {
67 | // use partial config as default
68 | if p.Instances[i].Concurrency == 0 {
69 | p.Instances[i].Concurrency = partial.Concurrency
70 | }
71 |
72 | if p.Instances[i].Count == 0 {
73 | p.Instances[i].Count = partial.Count
74 | }
75 |
76 | if p.Instances[i].PingInterval == 0 {
77 | p.Instances[i].PingInterval = partial.PingInterval
78 | }
79 |
80 | if p.Instances[i].Timeout == 0 {
81 | p.Instances[i].Timeout = partial.Timeout
82 | }
83 |
84 | if p.Instances[i].Interface == "" {
85 | p.Instances[i].Interface = partial.Interface
86 | }
87 |
88 | if p.Instances[i].IPv6 == nil {
89 | p.Instances[i].IPv6 = partial.IPv6
90 | }
91 |
92 | if p.Instances[i].Size == nil {
93 | p.Instances[i].Size = partial.Size
94 | }
95 |
96 | if p.Instances[i].AlertIfPacketLossPercentGe == 0 {
97 | p.Instances[i].AlertIfPacketLossPercentGe = partial.AlertIfPacketLossPercentGe
98 | }
99 |
100 | break
101 | }
102 | }
103 | }
104 | }
105 | return nil
106 | }
107 |
108 | func init() {
109 | plugins.Add(pluginName, func() plugins.Plugin {
110 | return &PingPlugin{}
111 | })
112 | }
113 |
114 | func (p *PingPlugin) GetInstances() []plugins.Instance {
115 | ret := make([]plugins.Instance, len(p.Instances))
116 | for i := 0; i < len(p.Instances); i++ {
117 | ret[i] = p.Instances[i]
118 | }
119 | return ret
120 | }
121 |
122 | func (ins *Instance) Init() error {
123 | if ins.Concurrency == 0 {
124 | ins.Concurrency = 10
125 | }
126 |
127 | if ins.Count < 1 {
128 | ins.Count = 5
129 | }
130 |
131 | if ins.PingInterval < 0.2 {
132 | ins.calcInterval = time.Duration(0.2 * float64(time.Second))
133 | } else {
134 | ins.calcInterval = time.Duration(ins.PingInterval * float64(time.Second))
135 | }
136 |
137 | if ins.Timeout == 0 {
138 | ins.calcTimeout = time.Duration(3) * time.Second
139 | } else {
140 | ins.calcTimeout = time.Duration(ins.Timeout * float64(time.Second))
141 | }
142 |
143 | if ins.Interface != "" {
144 | if addr := net.ParseIP(ins.Interface); addr != nil {
145 | ins.sourceAddress = ins.Interface
146 | } else {
147 | i, err := net.InterfaceByName(ins.Interface)
148 | if err != nil {
149 | return fmt.Errorf("failed to get interface: %v", err)
150 | }
151 |
152 | addrs, err := i.Addrs()
153 | if err != nil {
154 | return fmt.Errorf("failed to get the address of interface: %v", err)
155 | }
156 |
157 | ins.sourceAddress = addrs[0].(*net.IPNet).IP.String()
158 | }
159 | }
160 |
161 | return nil
162 | }
163 |
164 | func (ins *Instance) Gather(q *safe.Queue[*types.Event]) {
165 | logger.Logger.Debug("ping... targets: ", ins.Targets)
166 |
167 | if len(ins.Targets) == 0 {
168 | return
169 | }
170 |
171 | if !ins.GetInitialized() {
172 | if err := ins.Init(); err != nil {
173 | logger.Logger.Errorf("failed to init ping plugin instance: %v", err)
174 | return
175 | } else {
176 | ins.SetInitialized()
177 | }
178 | }
179 |
180 | wg := new(sync.WaitGroup)
181 | se := make(chan struct{}, ins.Concurrency)
182 | for _, target := range ins.Targets {
183 | wg.Add(1)
184 | se <- struct{}{}
185 | go func(target string) {
186 | defer func() {
187 | <-se
188 | wg.Done()
189 | }()
190 | ins.gather(q, target)
191 | }(target)
192 | }
193 | wg.Wait()
194 | close(se)
195 | }
196 |
197 | func (ins *Instance) gather(q *safe.Queue[*types.Event], target string) {
198 | logger.Logger.Debug("ping target: ", target)
199 |
200 | labels := map[string]string{
201 | "target": target,
202 | }
203 |
204 | event := types.BuildEvent(map[string]string{
205 | "check": "ping check",
206 | }, labels).SetTitleRule("$check").SetDescription(ins.buildDesc(target, "everything is ok"))
207 |
208 | stats, err := ins.ping(target)
209 | if err != nil {
210 | message := fmt.Sprintf("ping %s failed: %v", target, err)
211 | logger.Logger.Error(message)
212 | q.PushFront(event.SetEventStatus(ins.GetDefaultSeverity()).SetDescription(ins.buildDesc(target, message)))
213 | return
214 | }
215 |
216 | if stats.PacketsSent == 0 {
217 | message := fmt.Sprintf("no packets sent to %s", target)
218 | logger.Logger.Error(message)
219 | q.PushFront(event.SetEventStatus(ins.GetDefaultSeverity()).SetDescription(ins.buildDesc(target, message)))
220 | return
221 | }
222 |
223 | if stats.PacketsRecv == 0 {
224 | message := fmt.Sprintf("no packets received to %s", target)
225 | logger.Logger.Error(message)
226 | q.PushFront(event.SetEventStatus(ins.GetDefaultSeverity()).SetDescription(ins.buildDesc(target, message)))
227 | return
228 | }
229 |
230 | if stats.PacketLoss >= float64(ins.AlertIfPacketLossPercentGe) {
231 | message := fmt.Sprintf("packet loss is %f%%", stats.PacketLoss)
232 | logger.Logger.Error(message)
233 | q.PushFront(event.SetEventStatus(ins.GetDefaultSeverity()).SetDescription(ins.buildDesc(target, message)))
234 | return
235 | }
236 |
237 | q.PushFront(event)
238 | }
239 |
240 | type pingStats struct {
241 | ping.Statistics
242 | ttl int
243 | }
244 |
245 | func (ins *Instance) ping(destination string) (*pingStats, error) {
246 | ps := &pingStats{}
247 |
248 | pinger, err := ping.NewPinger(destination)
249 | if err != nil {
250 | return nil, fmt.Errorf("failed to create new pinger: %w", err)
251 | }
252 |
253 | pinger.SetPrivileged(true)
254 |
255 | if ins.IPv6 != nil && *ins.IPv6 {
256 | pinger.SetNetwork("ip6")
257 | }
258 |
259 | pinger.Size = defaultPingDataBytesSize
260 | if ins.Size != nil {
261 | pinger.Size = *ins.Size
262 | }
263 |
264 | pinger.Source = ins.sourceAddress
265 | pinger.Interval = ins.calcInterval
266 | pinger.Timeout = ins.calcTimeout
267 |
268 | // Get Time to live (TTL) of first response, matching original implementation
269 | once := &sync.Once{}
270 | pinger.OnRecv = func(pkt *ping.Packet) {
271 | once.Do(func() {
272 | ps.ttl = pkt.TTL
273 | })
274 | }
275 |
276 | pinger.Count = ins.Count
277 | err = pinger.Run()
278 | if err != nil {
279 | if strings.Contains(err.Error(), "operation not permitted") {
280 | if runtime.GOOS == "linux" {
281 | return nil, fmt.Errorf("permission changes required, enable CAP_NET_RAW capabilities (refer to the ping plugin's README.md for more info)")
282 | }
283 |
284 | return nil, fmt.Errorf("permission changes required, refer to the ping plugin's README.md for more info")
285 | }
286 | return nil, fmt.Errorf("%w", err)
287 | }
288 |
289 | ps.Statistics = *pinger.Statistics()
290 |
291 | return ps, nil
292 | }
293 |
294 | func (ins *Instance) buildDesc(target, message string) string {
295 | return `[MD]
296 | - target: ` + target + `
297 | - alert_if_packet_loss_percent_ge: ` + fmt.Sprint(ins.AlertIfPacketLossPercentGe) + `
298 |
299 |
300 | **message**:
301 |
302 | ` + "```" + `
303 | ` + message + `
304 | ` + "```" + `
305 | `
306 | }
307 |
--------------------------------------------------------------------------------
/plugins/plugins.go:
--------------------------------------------------------------------------------
1 | package plugins
2 |
3 | import (
4 | "flashcat.cloud/catpaw/config"
5 | "flashcat.cloud/catpaw/pkg/safe"
6 | "flashcat.cloud/catpaw/types"
7 | )
8 |
9 | type Instance interface {
10 | GetLabels() map[string]string
11 | GetInterval() config.Duration
12 | GetAlerting() config.Alerting
13 | InitInternalConfig() error
14 | }
15 |
16 | type Plugin interface {
17 | GetLabels() map[string]string
18 | GetInterval() config.Duration
19 | // GetAlerting() config.Alerting
20 | // InitInternalConfig() error
21 | }
22 |
23 | type IApplyPartials interface {
24 | ApplyPartials() error
25 | }
26 |
27 | type Gatherer interface {
28 | Gather(*safe.Queue[*types.Event])
29 | }
30 |
31 | func MayApplyPartials(p interface{}) error {
32 | if ap, ok := p.(IApplyPartials); ok {
33 | return ap.ApplyPartials()
34 | }
35 | return nil
36 | }
37 |
38 | type Dropper interface {
39 | Drop()
40 | }
41 |
42 | type InstancesGetter interface {
43 | GetInstances() []Instance
44 | }
45 |
46 | func MayGather(t interface{}, q *safe.Queue[*types.Event]) {
47 | if gather, ok := t.(Gatherer); ok {
48 | gather.Gather(q)
49 | }
50 | }
51 |
52 | func MayDrop(t interface{}) {
53 | if dropper, ok := t.(Dropper); ok {
54 | dropper.Drop()
55 | }
56 | }
57 |
58 | func MayGetInstances(t interface{}) []Instance {
59 | if instancesGetter, ok := t.(InstancesGetter); ok {
60 | return instancesGetter.GetInstances()
61 | }
62 | return nil
63 | }
64 |
65 | type Creator func() Plugin
66 |
67 | var PluginCreators = map[string]Creator{}
68 |
69 | func Add(name string, creator Creator) {
70 | PluginCreators[name] = creator
71 | }
72 |
--------------------------------------------------------------------------------
/plugins/procnum/native_finder.go:
--------------------------------------------------------------------------------
1 | package procnum
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "strconv"
7 | "strings"
8 |
9 | "github.com/shirou/gopsutil/v3/process"
10 | )
11 |
12 | type Filter func(p *process.Process) bool
13 |
14 | // NativeFinder uses gopsutil to find processes
15 | type NativeFinder struct {
16 | }
17 |
18 | func NewNativeFinder() PIDFinder {
19 | return &NativeFinder{}
20 | }
21 |
22 | // Uid will return all pids for the given user
23 | func (pg *NativeFinder) UID(user string) ([]PID, error) {
24 | var dst []PID
25 | procs, err := process.Processes()
26 | if err != nil {
27 | return dst, err
28 | }
29 | for _, p := range procs {
30 | username, err := p.Username()
31 | if err != nil {
32 | // skip, this can happen if we don't have permissions or
33 | // the pid no longer exists
34 | continue
35 | }
36 | if username == user {
37 | dst = append(dst, PID(p.Pid))
38 | }
39 | }
40 | return dst, nil
41 | }
42 |
43 | // PidFile returns the pid from the pid file given.
44 | func (pg *NativeFinder) PidFile(path string) ([]PID, error) {
45 | var pids []PID
46 | pidString, err := os.ReadFile(path)
47 | if err != nil {
48 | return pids, fmt.Errorf("failed to read pidfile '%s'. Error: '%s'",
49 | path, err)
50 | }
51 | pid, err := strconv.ParseInt(strings.TrimSpace(string(pidString)), 10, 32)
52 | if err != nil {
53 | return pids, err
54 | }
55 | pids = append(pids, PID(pid))
56 | return pids, nil
57 | }
58 |
59 | // FullPattern matches on the command line when the process was executed
60 | func (pg *NativeFinder) FullPattern(pattern string, filters ...Filter) ([]PID, error) {
61 | var pids []PID
62 |
63 | procs, err := pg.FastProcessList()
64 | if err != nil {
65 | return pids, err
66 | }
67 | PROCS:
68 | for _, p := range procs {
69 | for _, filter := range filters {
70 | if !filter(p) {
71 | continue PROCS
72 | }
73 | }
74 | cmd, err := p.Cmdline()
75 | if err != nil {
76 | // skip, this can be caused by the pid no longer existing
77 | // or you having no permissions to access it
78 | continue
79 | }
80 | if strings.Contains(cmd, pattern) {
81 | pids = append(pids, PID(p.Pid))
82 | }
83 | }
84 | return pids, err
85 |
86 | }
87 |
88 | func (pg *NativeFinder) FastProcessList() ([]*process.Process, error) {
89 | pids, err := process.Pids()
90 | if err != nil {
91 | return nil, err
92 | }
93 |
94 | result := make([]*process.Process, len(pids))
95 | for i, pid := range pids {
96 | result[i] = &process.Process{Pid: pid}
97 | }
98 | return result, nil
99 | }
100 |
--------------------------------------------------------------------------------
/plugins/procnum/native_finder_notwindows.go:
--------------------------------------------------------------------------------
1 | //go:build !windows
2 | // +build !windows
3 |
4 | package procnum
5 |
6 | import (
7 | "strings"
8 | )
9 |
10 | // Pattern matches on the process name
11 | func (pg *NativeFinder) Pattern(pattern string, filters ...Filter) ([]PID, error) {
12 | var pids []PID
13 | procs, err := pg.FastProcessList()
14 | if err != nil {
15 | return pids, err
16 | }
17 | PROCS:
18 | for _, p := range procs {
19 | for _, filter := range filters {
20 | if !filter(p) {
21 | continue PROCS
22 | }
23 | }
24 | name, err := p.Exe()
25 | if err != nil {
26 | // skip, this can be caused by the pid no longer existing
27 | // or you having no permissions to access it
28 | continue
29 | }
30 | if strings.Contains(name, pattern) {
31 | pids = append(pids, PID(p.Pid))
32 | }
33 | }
34 | return pids, err
35 | }
36 |
--------------------------------------------------------------------------------
/plugins/procnum/native_finder_windows.go:
--------------------------------------------------------------------------------
1 | package procnum
2 |
3 | import (
4 | "strings"
5 | )
6 |
7 | // Pattern matches on the process name
8 | func (pg *NativeFinder) Pattern(pattern string, filters ...Filter) ([]PID, error) {
9 | var pids []PID
10 | procs, err := pg.FastProcessList()
11 | if err != nil {
12 | return pids, err
13 | }
14 | PROCS:
15 | for _, p := range procs {
16 | for _, filter := range filters {
17 | if !filter(p) {
18 | continue PROCS
19 | }
20 | }
21 | name, err := p.Name()
22 | if err != nil {
23 | // skip, this can be caused by the pid no longer existing
24 | // or you having no permissions to access it
25 | continue
26 | }
27 | if strings.Contains(name, pattern) {
28 | pids = append(pids, PID(p.Pid))
29 | }
30 | }
31 | return pids, err
32 | }
33 |
--------------------------------------------------------------------------------
/plugins/procnum/process.go:
--------------------------------------------------------------------------------
1 | package procnum
2 |
3 | type PID int32
4 |
5 | type PIDFinder interface {
6 | PidFile(path string) ([]PID, error)
7 | Pattern(pattern string, filters ...Filter) ([]PID, error)
8 | UID(user string) ([]PID, error)
9 | FullPattern(path string, filters ...Filter) ([]PID, error)
10 | }
11 |
--------------------------------------------------------------------------------
/plugins/procnum/procnum.go:
--------------------------------------------------------------------------------
1 | package procnum
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 |
7 | "flashcat.cloud/catpaw/config"
8 | "flashcat.cloud/catpaw/logger"
9 | "flashcat.cloud/catpaw/pkg/safe"
10 | "flashcat.cloud/catpaw/plugins"
11 | "flashcat.cloud/catpaw/types"
12 | )
13 |
14 | const (
15 | pluginName string = "procnum"
16 | )
17 |
18 | type Instance struct {
19 | config.InternalConfig
20 |
21 | SearchExecSubstring string `toml:"search_exec_substring"`
22 | SearchCmdlineSubstring string `toml:"search_cmdline_substring"`
23 | SearchWinService string `toml:"search_win_service"`
24 |
25 | searchString string
26 |
27 | AlertIfNumLt int `toml:"alert_if_num_lt"`
28 | Check string `toml:"check"`
29 | }
30 |
31 | type ProcnumPlugin struct {
32 | config.InternalConfig
33 | Instances []*Instance `toml:"instances"`
34 | }
35 |
36 | func (p *ProcnumPlugin) GetInstances() []plugins.Instance {
37 | ret := make([]plugins.Instance, len(p.Instances))
38 | for i := 0; i < len(p.Instances); i++ {
39 | ret[i] = p.Instances[i]
40 | }
41 | return ret
42 | }
43 |
44 | func init() {
45 | plugins.Add(pluginName, func() plugins.Plugin {
46 | return &ProcnumPlugin{}
47 | })
48 | }
49 |
50 | func (ins *Instance) Init() error {
51 | if ins.SearchExecSubstring != "" {
52 | ins.searchString = ins.SearchExecSubstring
53 | } else if ins.SearchCmdlineSubstring != "" {
54 | ins.searchString = ins.SearchCmdlineSubstring
55 | } else if ins.SearchWinService != "" {
56 | ins.searchString = ins.SearchWinService
57 | }
58 |
59 | return nil
60 | }
61 |
62 | func (ins *Instance) Gather(q *safe.Queue[*types.Event]) {
63 | if !ins.GetInitialized() {
64 | if err := ins.Init(); err != nil {
65 | logger.Logger.Errorf("failed to init procnum plugin instance: %v", err)
66 | return
67 | } else {
68 | ins.SetInitialized()
69 | }
70 | }
71 |
72 | if ins.searchString == "" {
73 | return
74 | }
75 |
76 | if ins.Check == "" {
77 | logger.Logger.Error("check is empty")
78 | return
79 | }
80 |
81 | var (
82 | pids []PID
83 | err error
84 | )
85 |
86 | pg := NewNativeFinder()
87 | if ins.SearchExecSubstring != "" {
88 | pids, err = pg.Pattern(ins.SearchExecSubstring)
89 | } else if ins.SearchCmdlineSubstring != "" {
90 | pids, err = pg.FullPattern(ins.SearchCmdlineSubstring)
91 | } else if ins.SearchWinService != "" {
92 | pids, err = ins.winServicePIDs()
93 | } else {
94 | logger.Logger.Error("Oops... search string not found")
95 | return
96 | }
97 |
98 | if err != nil {
99 | q.PushFront(ins.buildEvent("Occur error: " + err.Error()).SetEventStatus(ins.GetDefaultSeverity()))
100 | return
101 | }
102 |
103 | logger.Logger.Debugf("search string: %s, pids: %v", ins.searchString, pids)
104 |
105 | if len(pids) < ins.AlertIfNumLt {
106 | s := fmt.Sprintf("The number of process is less than expected. real: %d, expected: %d", len(pids), ins.AlertIfNumLt)
107 | q.PushFront(ins.buildEvent("[MD]", s, `
108 | - search_exec_substring: `+ins.SearchExecSubstring+`
109 | - search_cmdline_substring: `+ins.SearchCmdlineSubstring+`
110 | - search_win_service: `+ins.SearchWinService+`
111 | `).SetEventStatus(ins.GetDefaultSeverity()))
112 | return
113 | }
114 |
115 | q.PushFront(ins.buildEvent())
116 | }
117 |
118 | func (ins *Instance) buildEvent(desc ...string) *types.Event {
119 | event := types.BuildEvent(map[string]string{"check": ins.Check}).SetTitleRule("$check")
120 | if len(desc) > 0 {
121 | event.SetDescription(strings.Join(desc, "\n"))
122 | }
123 | return event
124 | }
125 |
126 | func (ins *Instance) winServicePIDs() ([]PID, error) {
127 | var pids []PID
128 |
129 | pid, err := queryPidWithWinServiceName(ins.SearchWinService)
130 | if err != nil {
131 | return pids, err
132 | }
133 |
134 | pids = append(pids, PID(pid))
135 |
136 | return pids, nil
137 | }
138 |
--------------------------------------------------------------------------------
/plugins/procnum/win_service_notwindows.go:
--------------------------------------------------------------------------------
1 | //go:build !windows
2 | // +build !windows
3 |
4 | package procnum
5 |
6 | import (
7 | "fmt"
8 | )
9 |
10 | func queryPidWithWinServiceName(_ string) (uint32, error) {
11 | return 0, fmt.Errorf("os not support win_service option")
12 | }
13 |
--------------------------------------------------------------------------------
/plugins/procnum/win_service_windows.go:
--------------------------------------------------------------------------------
1 | //go:build windows
2 | // +build windows
3 |
4 | package procnum
5 |
6 | import (
7 | "unsafe"
8 |
9 | "golang.org/x/sys/windows"
10 | "golang.org/x/sys/windows/svc/mgr"
11 | )
12 |
13 | func getService(name string) (*mgr.Service, error) {
14 | m, err := mgr.Connect()
15 | if err != nil {
16 | return nil, err
17 | }
18 | defer m.Disconnect()
19 |
20 | srv, err := m.OpenService(name)
21 | if err != nil {
22 | return nil, err
23 | }
24 |
25 | return srv, nil
26 | }
27 |
28 | func queryPidWithWinServiceName(winServiceName string) (uint32, error) {
29 | srv, err := getService(winServiceName)
30 | if err != nil {
31 | return 0, err
32 | }
33 |
34 | var p *windows.SERVICE_STATUS_PROCESS
35 | var bytesNeeded uint32
36 | var buf []byte
37 |
38 | if err := windows.QueryServiceStatusEx(srv.Handle, windows.SC_STATUS_PROCESS_INFO, nil, 0, &bytesNeeded); err != windows.ERROR_INSUFFICIENT_BUFFER {
39 | return 0, err
40 | }
41 |
42 | buf = make([]byte, bytesNeeded)
43 | p = (*windows.SERVICE_STATUS_PROCESS)(unsafe.Pointer(&buf[0]))
44 | if err := windows.QueryServiceStatusEx(srv.Handle, windows.SC_STATUS_PROCESS_INFO, &buf[0], uint32(len(buf)), &bytesNeeded); err != nil {
45 | return 0, err
46 | }
47 |
48 | return p.ProcessId, nil
49 | }
50 |
--------------------------------------------------------------------------------
/plugins/sfilter/sfilter.go:
--------------------------------------------------------------------------------
1 | package sfilter
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "io"
7 | osExec "os/exec"
8 | "runtime"
9 | "time"
10 |
11 | "flashcat.cloud/catpaw/config"
12 | "flashcat.cloud/catpaw/logger"
13 | "flashcat.cloud/catpaw/pkg/cmdx"
14 | "flashcat.cloud/catpaw/pkg/filter"
15 | "flashcat.cloud/catpaw/pkg/safe"
16 | "flashcat.cloud/catpaw/pkg/shell"
17 | "flashcat.cloud/catpaw/plugins"
18 | "flashcat.cloud/catpaw/types"
19 | )
20 |
21 | const (
22 | pluginName string = "sfilter"
23 | maxStderrBytes int = 512
24 | )
25 |
26 | type Instance struct {
27 | config.InternalConfig
28 |
29 | Command string `toml:"command"`
30 | Timeout config.Duration `toml:"timeout"`
31 | Check string `toml:"check"`
32 | FilterInclude []string `toml:"filter_include"`
33 | FilterExclude []string `toml:"filter_exclude"`
34 |
35 | filter filter.Filter
36 | }
37 |
38 | type SFilterPlugin struct {
39 | config.InternalConfig
40 | Instances []*Instance `toml:"instances"`
41 | }
42 |
43 | func (p *SFilterPlugin) GetInstances() []plugins.Instance {
44 | ret := make([]plugins.Instance, len(p.Instances))
45 | for i := 0; i < len(p.Instances); i++ {
46 | ret[i] = p.Instances[i]
47 | }
48 | return ret
49 | }
50 |
51 | func init() {
52 | plugins.Add(pluginName, func() plugins.Plugin {
53 | return &SFilterPlugin{}
54 | })
55 | }
56 |
57 | func (ins *Instance) Gather(q *safe.Queue[*types.Event]) {
58 | if len(ins.Command) == 0 {
59 | return
60 | }
61 |
62 | if ins.Check == "" {
63 | logger.Logger.Warnln("configuration check is empty")
64 | return
65 | }
66 |
67 | if ins.Timeout == 0 {
68 | ins.Timeout = config.Duration(10 * time.Second)
69 | }
70 |
71 | if ins.filter == nil {
72 | if len(ins.FilterInclude) == 0 && len(ins.FilterExclude) == 0 {
73 | logger.Logger.Error("filter_include and filter_exclude are empty")
74 | return
75 | }
76 |
77 | var err error
78 | ins.filter, err = filter.NewIncludeExcludeFilter(ins.FilterInclude, ins.FilterExclude)
79 | if err != nil {
80 | logger.Logger.Warnf("failed to create filter: %s", err)
81 | return
82 | }
83 | }
84 |
85 | ins.gather(q, ins.Command)
86 | }
87 |
88 | func (ins *Instance) gather(q *safe.Queue[*types.Event], command string) {
89 | outbuf, errbuf, err := commandRun(command, time.Duration(ins.Timeout))
90 | if err != nil || len(errbuf) > 0 {
91 | logger.Logger.Errorw("failed to exec command", "command", command, "error", err, "stderr", string(errbuf), "stdout", string(outbuf))
92 | return
93 | }
94 |
95 | if len(outbuf) == 0 {
96 | logger.Logger.Warnw("exec command output is empty", "command", command)
97 | return
98 | }
99 |
100 | var bs bytes.Buffer
101 | var triggered bool
102 |
103 | bs.WriteString("[MD]")
104 | bs.WriteString(fmt.Sprintf("- filter_include: `%s`\n", ins.FilterInclude))
105 | bs.WriteString(fmt.Sprintf("- filter_exclude: `%s`\n", ins.FilterExclude))
106 | bs.WriteString("\n")
107 | bs.WriteString("\n")
108 | bs.WriteString("**matched lines**:\n")
109 | bs.WriteString("\n```")
110 |
111 | for _, line := range bytes.Split(outbuf, []byte("\n")) {
112 | if len(line) == 0 {
113 | continue
114 | }
115 |
116 | if !ins.filter.Match(string(line)) {
117 | continue
118 | }
119 |
120 | triggered = true
121 | bs.Write(line)
122 | bs.Write([]byte("\n"))
123 | }
124 |
125 | bs.WriteString("```")
126 |
127 | if !triggered {
128 | q.PushFront(types.BuildEvent(map[string]string{"check": ins.Check}).SetTitleRule("$check").SetDescription("everything is ok"))
129 | } else {
130 | q.PushFront(types.BuildEvent(map[string]string{"check": ins.Check}).SetTitleRule("$check").SetDescription(bs.String()).SetEventStatus(ins.GetDefaultSeverity()))
131 | }
132 | }
133 |
134 | func commandRun(command string, timeout time.Duration) ([]byte, []byte, error) {
135 | splitCmd, err := shell.QuoteSplit(command)
136 | if err != nil || len(splitCmd) == 0 {
137 | return nil, nil, fmt.Errorf("exec: unable to parse command, %s", err)
138 | }
139 |
140 | cmd := osExec.Command(splitCmd[0], splitCmd[1:]...)
141 |
142 | var (
143 | out bytes.Buffer
144 | stderr bytes.Buffer
145 | )
146 | cmd.Stdout = &out
147 | cmd.Stderr = &stderr
148 |
149 | runError, runTimeout := cmdx.RunTimeout(cmd, timeout)
150 | if runTimeout {
151 | return nil, nil, fmt.Errorf("exec %s timeout", command)
152 | }
153 |
154 | out = removeWindowsCarriageReturns(out)
155 | if stderr.Len() > 0 {
156 | stderr = removeWindowsCarriageReturns(stderr)
157 | stderr = truncate(stderr)
158 | }
159 |
160 | return out.Bytes(), stderr.Bytes(), runError
161 | }
162 |
163 | func truncate(buf bytes.Buffer) bytes.Buffer {
164 | // Limit the number of bytes.
165 | didTruncate := false
166 | if buf.Len() > maxStderrBytes {
167 | buf.Truncate(maxStderrBytes)
168 | didTruncate = true
169 | }
170 | if i := bytes.IndexByte(buf.Bytes(), '\n'); i > 0 {
171 | // Only show truncation if the newline wasn't the last character.
172 | if i < buf.Len()-1 {
173 | didTruncate = true
174 | }
175 | buf.Truncate(i)
176 | }
177 | if didTruncate {
178 | //nolint:errcheck,revive // Will always return nil or panic
179 | buf.WriteString("...")
180 | }
181 | return buf
182 | }
183 |
184 | // removeWindowsCarriageReturns removes all carriage returns from the input if the
185 | // OS is Windows. It does not return any errors.
186 | func removeWindowsCarriageReturns(b bytes.Buffer) bytes.Buffer {
187 | if runtime.GOOS == "windows" {
188 | var buf bytes.Buffer
189 | for {
190 | byt, err := b.ReadBytes(0x0D)
191 | byt = bytes.TrimRight(byt, "\x0d")
192 | if len(byt) > 0 {
193 | _, _ = buf.Write(byt)
194 | }
195 | if err == io.EOF {
196 | return buf
197 | }
198 | }
199 | }
200 | return b
201 | }
202 |
--------------------------------------------------------------------------------
/scripts/demo.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # use plugin exec
3 | echo '[
4 | {
5 | "event_status": "Warning",
6 | "labels": {
7 | "check": "script demo"
8 | },
9 | "title_rule": "$check",
10 | "description": "this is description, support markdown"
11 | }
12 | ]'
--------------------------------------------------------------------------------
/scripts/df.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # use plugin sfilter
3 |
4 | output=`df -hT`
5 | count=`echo "$output" | grep -c '100%'`
6 | if [ $count -gt 0 ]; then
7 | echo "$output"
8 | fi
--------------------------------------------------------------------------------
/scripts/greplog.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # use plugin exec
3 | # exec.toml example:
4 | # [[instances]]
5 | # commands = [
6 | # "/opt/catpaw/scripts/greplog.sh 'Out of memory'"
7 | # ]
8 | #
9 | # deprecated, use plugin journaltail instead
10 |
11 | if [ "$1" ]; then
12 | keyword=$1
13 | else
14 | keyword="Out of memory"
15 | fi
16 |
17 | output=$(journalctl -S-3m | grep "${keyword}")
18 | status="Ok"
19 | if [ -n "$output" ]; then
20 | status="Warning"
21 | fi
22 |
23 | echo '[
24 | {
25 | "event_status": "'${status}'",
26 | "labels": {
27 | "check": "grep keyword: '${keyword}'"
28 | },
29 | "title_rule": "$check",
30 | "description": "'${output}'"
31 | }
32 | ]'
33 |
--------------------------------------------------------------------------------
/scripts/ulimit.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # use plugin exec
3 |
4 | if [ "$1" ]; then
5 | threshhold=$1
6 | else
7 | threshhold=2048
8 | fi
9 |
10 | count=$(ulimit -n)
11 |
12 | status="Ok"
13 | if [ $count -lt $threshhold ]; then
14 | status="Warning"
15 | fi
16 |
17 | echo '[
18 | {
19 | "event_status": "'${status}'",
20 | "labels": {
21 | "check": "ulimit check"
22 | },
23 | "title_rule": "$check",
24 | "description": "ulimit -n: '${count}', too low, should be greater than '${threshhold}'"
25 | }
26 | ]'
--------------------------------------------------------------------------------
/types/event.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | const (
4 | EventStatusCritical = "Critical"
5 | EventStatusWarning = "Warning"
6 | EventStatusInfo = "Info"
7 | EventStatusOk = "Ok"
8 | )
9 |
10 | type Event struct {
11 | EventTime int64 `json:"event_time"`
12 | EventStatus string `json:"event_status"`
13 | AlertKey string `json:"alert_key"`
14 | Labels map[string]string `json:"labels"`
15 | TitleRule string `json:"title_rule"` // $a::b::$c
16 | Description string `json:"description"`
17 |
18 | // for internal use
19 | FirstFireTime int64 `json:"-"`
20 | NotifyCount int64 `json:"-"`
21 | LastSent int64 `json:"-"`
22 | }
23 |
24 | func EventStatusValid(status string) bool {
25 | switch status {
26 | case EventStatusCritical, EventStatusWarning, EventStatusInfo, EventStatusOk:
27 | return true
28 | default:
29 | return false
30 | }
31 | }
32 |
33 | func (e *Event) SetEventStatus(status string) *Event {
34 | e.EventStatus = status
35 | return e
36 | }
37 |
38 | func (e *Event) SetEventTime(t int64) *Event {
39 | e.EventTime = t
40 | return e
41 | }
42 |
43 | func (e *Event) SetTitleRule(rule string) *Event {
44 | e.TitleRule = rule
45 | return e
46 | }
47 |
48 | func (e *Event) SetDescription(desc string) *Event {
49 | e.Description = desc
50 | return e
51 | }
52 |
53 | func BuildEvent(labelMaps ...map[string]string) *Event {
54 | event := &Event{
55 | EventStatus: EventStatusOk,
56 | }
57 |
58 | event.Labels = make(map[string]string)
59 | for _, labelMap := range labelMaps {
60 | for k, v := range labelMap {
61 | event.Labels[k] = v
62 | }
63 | }
64 |
65 | return event
66 | }
67 |
--------------------------------------------------------------------------------
/winx/winx_posix.go:
--------------------------------------------------------------------------------
1 | //go:build !windows
2 |
3 | package winx
4 |
5 | func Args(appPath string) {
6 | }
7 |
8 | func GetServiceName() string {
9 | return ""
10 | }
11 |
--------------------------------------------------------------------------------
/winx/winx_windows.go:
--------------------------------------------------------------------------------
1 | //go:build windows
2 |
3 | package winx
4 |
5 | import (
6 | "flag"
7 | "fmt"
8 | "os"
9 |
10 | "github.com/chai2010/winsvc"
11 | )
12 |
13 | var (
14 | flagWinSvcName = flag.String("win-service-name", "catpaw", "Set windows service name")
15 | flagWinSvcDesc = flag.String("win-service-desc", "catpaw is a monitoring tool", "Set windows service description")
16 | flagWinSvcInstall = flag.Bool("win-service-install", false, "Install windows service")
17 | flagWinSvcUninstall = flag.Bool("win-service-uninstall", false, "Uninstall windows service")
18 | flagWinSvcStart = flag.Bool("win-service-start", false, "Start windows service")
19 | flagWinSvcStop = flag.Bool("win-service-stop", false, "Stop windows service")
20 | )
21 |
22 | func GetServiceName() string {
23 | return *flagWinSvcName
24 | }
25 |
26 | func Args(appPath string) {
27 | // install service
28 | if *flagWinSvcInstall {
29 | if err := winsvc.InstallService(appPath, *flagWinSvcName, *flagWinSvcDesc); err != nil {
30 | fmt.Println("failed to install service:", *flagWinSvcName, "error:", err)
31 | }
32 | fmt.Println("done")
33 | os.Exit(0)
34 | }
35 |
36 | // uninstall service
37 | if *flagWinSvcUninstall {
38 | if err := winsvc.RemoveService(*flagWinSvcName); err != nil {
39 | fmt.Println("failed to uninstall service:", *flagWinSvcName, "error:", err)
40 | }
41 | fmt.Println("done")
42 | os.Exit(0)
43 | }
44 |
45 | // start service
46 | if *flagWinSvcStart {
47 | if err := winsvc.StartService(*flagWinSvcName); err != nil {
48 | fmt.Println("failed to start service:", *flagWinSvcName, "error:", err)
49 | }
50 | fmt.Println("done")
51 | os.Exit(0)
52 | }
53 |
54 | // stop service
55 | if *flagWinSvcStop {
56 | if err := winsvc.StopService(*flagWinSvcName); err != nil {
57 | fmt.Println("failed to stop service:", *flagWinSvcName, "error:", err)
58 | }
59 | fmt.Println("done")
60 | os.Exit(0)
61 | }
62 | }
63 |
--------------------------------------------------------------------------------