├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yaml │ ├── documentation.yaml │ ├── feature_request.yaml │ └── question.yaml ├── dependabot.yml └── workflows │ ├── go.yml │ └── golangci-lint.yml ├── .gitignore ├── .golangci.yml ├── COPYING ├── Makefile ├── README.md ├── config.go ├── config_test.go ├── convert ├── bytes_common.go ├── bytes_common_test.go ├── bytes_iec.go ├── bytes_iec_test.go ├── bytes_si.go └── bytes_si_test.go ├── examples ├── check_example │ ├── main.go │ └── main_test.go └── check_example2 │ ├── main.go │ └── main_test.go ├── exit.go ├── exit_test.go ├── go.mod ├── go.sum ├── perfdata ├── list.go ├── list_test.go ├── type.go └── type_test.go ├── result ├── overall.go ├── overall_test.go ├── worst.go └── worst_test.go ├── status.go ├── status_test.go ├── testhelper └── for_main.go ├── threshold.go ├── threshold_test.go └── timeout.go /.github/ISSUE_TEMPLATE/bug_report.yaml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report 3 | title: "[Bug]: " 4 | labels: ["bug", "needs-triage"] 5 | body: 6 | - type: checkboxes 7 | id: terms 8 | attributes: 9 | label: Please try to fill out as much of the information below as you can. Thank you! 10 | options: 11 | - label: Yes, I've searched similar issues on GitHub and didn't find any. 12 | required: true 13 | - type: input 14 | id: app_version 15 | attributes: 16 | label: Which version contains the bug? 17 | placeholder: 1.0.0 18 | - type: textarea 19 | id: description 20 | attributes: 21 | label: Describe the bug 22 | description: Please provide a concise description of the bug, add any relevant output or error messages. You can use markdown. 23 | - type: textarea 24 | id: recreate 25 | attributes: 26 | label: How to recreate the bug? 27 | description: Please provide the steps to recreate the issue. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation.yaml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | description: Suggest documentation improvements 3 | title: "[Documentation]: " 4 | labels: ["documentation"] 5 | body: 6 | - type: textarea 7 | id: description 8 | attributes: 9 | label: Describe the improvements you'd like. 10 | description: Please provide as much context as possible. You can use markdown. 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yaml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Request a feature or enhancement 3 | title: "[Feature]: " 4 | labels: ["feature", "needs-triage"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Please try to fill out as much of the information below as you can. Thank you! 10 | **Note:** If you want to sponsor new features, contact us at info@netways.de 11 | - type: textarea 12 | id: description 13 | attributes: 14 | label: Describe the feature request 15 | description: Please provide a concise description of the feature. You can use markdown. 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.yaml: -------------------------------------------------------------------------------- 1 | name: Question 2 | description: Ask a question 3 | title: "[Question]: " 4 | labels: ["question"] 5 | body: 6 | - type: textarea 7 | id: description 8 | attributes: 9 | label: Ask a question 10 | description: Please provide as much context as possible. You can use markdown. 11 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | time: '10:00' 8 | open-pull-requests-limit: 10 9 | 10 | - package-ecosystem: "github-actions" 11 | directory: "/" 12 | schedule: 13 | interval: monthly 14 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | branches: [ master ] 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Set up Go 15 | uses: actions/setup-go@v5 16 | 17 | - name: Check out code into the Go module directory 18 | uses: actions/checkout@v4 19 | 20 | - name: Vet 21 | run: go vet ./... 22 | 23 | - name: Test 24 | run: go test -v ./... 25 | -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | 8 | jobs: 9 | golangci: 10 | name: lint 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: golangci-lint 16 | uses: golangci/golangci-lint-action@v8 17 | with: 18 | version: v2.1.6 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Editors 2 | /.idea/ 3 | [._]*.sw[a-p] 4 | *~ 5 | 6 | # Binaries for programs and plugins 7 | /build/ 8 | /dist/ 9 | *.exe 10 | *.exe~ 11 | *.dll 12 | *.so 13 | *.dylib 14 | 15 | # Test artifacts 16 | httpmock-record.yml 17 | 18 | # Test binary, built with `go test -c` 19 | *.test 20 | *.profile 21 | *.benchmark 22 | 23 | # Output of the go coverage tool, specifically when used with LiteIDE 24 | *.out 25 | coverage.html -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | run: 3 | tests: false 4 | linters: 5 | default: all 6 | disable: 7 | - cyclop 8 | - depguard 9 | - err113 10 | - exhaustruct 11 | - forbidigo 12 | - forcetypeassert 13 | - gochecknoglobals 14 | - gochecknoinits 15 | - godot 16 | - godox 17 | - lll 18 | - mnd 19 | - musttag 20 | - nakedret 21 | - nlreturn 22 | - nolintlint 23 | - nonamedreturns 24 | - tagliatelle 25 | - varnamelen 26 | - wrapcheck 27 | exclusions: 28 | generated: lax 29 | presets: 30 | - comments 31 | - common-false-positives 32 | - legacy 33 | - std-error-handling 34 | rules: 35 | - linters: 36 | - nestif 37 | path: result/overall.go 38 | - linters: 39 | - ireturn 40 | path: convert/bytes_common.go 41 | paths: 42 | - third_party$ 43 | - builtin$ 44 | - examples/ 45 | formatters: 46 | enable: 47 | - gofmt 48 | - goimports 49 | exclusions: 50 | generated: lax 51 | paths: 52 | - third_party$ 53 | - builtin$ 54 | - examples$ 55 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test coverage lint vet 2 | 3 | build: 4 | go build 5 | lint: 6 | go fmt $(go list ./... | grep -v /vendor/) 7 | vet: 8 | go vet $(go list ./... | grep -v /vendor/) 9 | test: 10 | go test -v -cover ./... 11 | coverage: 12 | go test -v -cover -coverprofile=coverage.out ./... &&\ 13 | go tool cover -html=coverage.out -o coverage.html 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | go-check 2 | ======== 3 | 4 | [![GitHub release (latest by date)](https://img.shields.io/github/v/release/NETWAYS/go-check?label=version)](https://github.com/NETWAYS/go-check/releases) 5 | [![GoDoc](https://img.shields.io/static/v1?label=godoc&message=reference&color=blue)](https://pkg.go.dev/github.com/NETWAYS/go-check) 6 | [![Test Status](https://github.com/NETWAYS/go-check/workflows/Go/badge.svg)](https://github.com/NETWAYS/go-check/actions?query=workflow%3AGo) 7 | ![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/NETWAYS/go-check) 8 | ![GitHub](https://img.shields.io/github/license/NETWAYS/go-check?color=green) 9 | 10 | go-check is a library to help with development of monitoring plugins for tools like Icinga. 11 | 12 | See the [documentation on pkg.go.dev](https://pkg.go.dev/github.com/NETWAYS/go-check) for more details and examples. 13 | 14 | # Usage 15 | 16 | ## Simple Example 17 | 18 | ```go 19 | package main 20 | 21 | import ( 22 | "github.com/NETWAYS/go-check" 23 | ) 24 | 25 | func main() { 26 | config := check.NewConfig() 27 | config.Name = "check_test" 28 | config.Readme = `Test Plugin` 29 | config.Version = "1.0.0" 30 | 31 | _ = config.FlagSet.StringP("hostname", "H", "localhost", "Hostname to check") 32 | 33 | config.ParseArguments() 34 | 35 | // Some checking should be done here, when --help is not passed 36 | check.Exitf(check.OK, "Everything is fine - answer=%d", 42) 37 | // Output: 38 | // OK - Everything is fine - answer=42 39 | } 40 | ``` 41 | 42 | ## Exit Codes 43 | 44 | ``` 45 | check.Exitf(OK, "Everything is fine - value=%d", 42) // OK, 0 46 | 47 | check.ExitRaw(check.Critical, "CRITICAL", "|", "percent_packet_loss=100") // CRITICAL, 2 48 | 49 | err := fmt.Errorf("connection to %s has been timed out", "localhost:12345") 50 | 51 | check.ExitError(err) // UNKNOWN, 3 52 | ``` 53 | 54 | ## Timeout Handling 55 | 56 | ``` 57 | checkPluginTimeoutInSeconds := 10 58 | go check.HandleTimeout(checkPluginTimeoutInSeconds) 59 | ``` 60 | 61 | ## Thresholds 62 | 63 | Threshold objects represent monitoring plugin thresholds that have methods to evaluate if a given input is within the range. 64 | 65 | They can be created with the ParseThreshold parser. 66 | 67 | https://github.com/monitoring-plugins/monitoring-plugin-guidelines/blob/main/definitions/01.range_expressions.md 68 | 69 | ``` 70 | warnThreshold, err := check.ParseThreshold("~:3") 71 | 72 | if err != nil { 73 | return t, err 74 | } 75 | 76 | if warnThreshold.DoesViolate(3.6) { 77 | fmt.Println("Not great, not terrible.") 78 | } 79 | ``` 80 | 81 | ## Perfdata 82 | 83 | The Perfdata object represents monitoring plugin performance data that relates to the actual execution of a host or service check. 84 | 85 | https://github.com/monitoring-plugins/monitoring-plugin-guidelines/blob/main/monitoring_plugins_interface/03.Output.md#performance-data 86 | 87 | ``` 88 | var pl perfdata.PerfdataList 89 | 90 | pl.Add(&perfdata.Perfdata{ 91 | Label: "process.cpu.percent", 92 | Value: 25, 93 | Uom: "%", 94 | Warn: 50, 95 | Crit: 90, 96 | Min: 0, 97 | Max: 100}) 98 | 99 | fmt.Println(pl.String()) 100 | ``` 101 | 102 | ## Results 103 | 104 | ``` 105 | allStates = []int{0,2,3,0,1,2} 106 | 107 | switch result.WorstState(allStates...) { 108 | case 0: 109 | rc = check.OK 110 | case 1: 111 | rc = check.Warning 112 | case 2: 113 | rc = check.Critical 114 | default: 115 | rc = check.Unknown 116 | } 117 | ``` 118 | 119 | ## Partial Results 120 | 121 | ``` 122 | o := Overall{} 123 | o.Add(0, "Something is OK") 124 | 125 | pr := PartialResult{ 126 | Output: "My Subcheck", 127 | } 128 | 129 | if err := pr.SetState(check.OK); err != nil { 130 | fmt.Printf(%s, err) 131 | } 132 | 133 | o.AddSubcheck(pr) 134 | 135 | fmt.Println(o.GetOutput()) 136 | 137 | // states: ok=1 138 | // [OK] Something is OK 139 | // \_ [OK] My Subcheck 140 | ``` 141 | 142 | 143 | # Examples 144 | 145 | A few plugins using go-check: 146 | 147 | * [check_cloud_aws](https://github.com/NETWAYS/check_cloud_aws) 148 | * [check_logstash](https://github.com/NETWAYS/check_logstash) 149 | * [check_sentinelone](https://github.com/NETWAYS/check_sentinelone) 150 | * [check_sophos_central](https://github.com/NETWAYS/check_sophos_central) 151 | 152 | # License 153 | 154 | Copyright (c) 2020 [NETWAYS GmbH](mailto:info@netways.de) 155 | 156 | This library is distributed under the GPL-2.0 or newer license found in the [COPYING](./COPYING) 157 | file. 158 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package check 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path" 7 | "reflect" 8 | 9 | flag "github.com/spf13/pflag" 10 | ) 11 | 12 | // Config represents a configuration for a monitoring plugin's CLI 13 | type Config struct { 14 | // Name of the monitoring plugin 15 | Name string 16 | // README represents the help text for the CLI usage 17 | Readme string 18 | // Output for the --version flag 19 | Version string 20 | // Default for the --timeout flag 21 | Timeout int 22 | // Default for the --verbose flag 23 | Verbose bool 24 | // Default for the --debug flag 25 | Debug bool 26 | // Enable predefined --version output 27 | PrintVersion bool 28 | // Enable predefined default flags for the monitoring plugin 29 | DefaultFlags bool 30 | // Enable predefined default functions (e.g. Timeout handler) for the monitoring plugin 31 | DefaultHelper bool 32 | // Additional CLI flags for the monitoring plugin 33 | FlagSet *flag.FlagSet 34 | } 35 | 36 | // NewConfig returns a Config struct with some defaults 37 | func NewConfig() *Config { 38 | c := &Config{} 39 | c.Name = path.Base(os.Args[0]) 40 | 41 | c.FlagSet = flag.NewFlagSet(c.Name, flag.ContinueOnError) 42 | c.FlagSet.SortFlags = false 43 | c.FlagSet.SetOutput(os.Stdout) 44 | 45 | c.FlagSet.Usage = func() { 46 | fmt.Printf("Usage of %s\n", c.Name) 47 | 48 | if c.Readme != "" { 49 | fmt.Println() 50 | fmt.Println(c.Readme) 51 | } 52 | 53 | fmt.Println() 54 | fmt.Println("Arguments:") 55 | c.FlagSet.PrintDefaults() 56 | } 57 | 58 | // set some defaults 59 | c.DefaultFlags = true 60 | c.Timeout = 30 61 | c.DefaultHelper = true 62 | 63 | return c 64 | } 65 | 66 | // ParseArguments parses the command line arguments given by os.Args 67 | func (c *Config) ParseArguments() { 68 | c.ParseArray(os.Args[1:]) 69 | } 70 | 71 | // ParseArray parses a list of command line arguments 72 | func (c *Config) ParseArray(arguments []string) { 73 | if c.DefaultFlags { 74 | c.addDefaultFlags() 75 | } 76 | 77 | err := c.FlagSet.Parse(arguments) 78 | if err != nil { 79 | ExitError(err) 80 | } 81 | 82 | if c.PrintVersion { 83 | fmt.Println(c.Name, "version", c.Version) 84 | BaseExit(Unknown) 85 | } 86 | 87 | if c.DefaultHelper { 88 | c.EnableTimeoutHandler() 89 | } 90 | } 91 | 92 | // addDefaultFlags adds various default flags to the monitoring plugin 93 | func (c *Config) addDefaultFlags() { 94 | c.FlagSet.IntVarP(&c.Timeout, "timeout", "t", c.Timeout, "Abort the check after n seconds") 95 | c.FlagSet.BoolVarP(&c.Debug, "debug", "d", false, "Enable debug mode") 96 | c.FlagSet.BoolVarP(&c.Verbose, "verbose", "v", false, "Enable verbose mode") 97 | c.FlagSet.BoolVarP(&c.PrintVersion, "version", "V", false, "Print version and exit") 98 | 99 | c.DefaultFlags = false 100 | } 101 | 102 | // LoadFromEnv can be used to load struct values from 'env' tags. 103 | // Mainly used to avoid passing secrets via the CLI 104 | // 105 | // type Config struct { 106 | // Token string `env:"BEARER_TOKEN"` 107 | // } 108 | func LoadFromEnv(config interface{}) { 109 | configValue := reflect.ValueOf(config).Elem() 110 | configType := configValue.Type() 111 | 112 | for i := range configValue.NumField() { 113 | field := configType.Field(i) 114 | tag := field.Tag.Get("env") 115 | 116 | // If there's no "env" tag, skip this field. 117 | if tag == "" { 118 | continue 119 | } 120 | 121 | envValue := os.Getenv(tag) 122 | 123 | if envValue == "" { 124 | continue 125 | } 126 | 127 | // Potential for adding different types, for now we only use strings 128 | // since the main use case is credentials 129 | // nolint: exhaustive, gocritic 130 | switch field.Type.Kind() { 131 | case reflect.String: 132 | configValue.Field(i).SetString(envValue) 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /config_test.go: -------------------------------------------------------------------------------- 1 | package check 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func ExampleConfig() { 9 | config := NewConfig() 10 | config.Name = "check_test" 11 | config.Readme = `Test Plugin` 12 | config.Version = "1.0.0" 13 | 14 | _ = config.FlagSet.StringP("hostname", "H", "localhost", "Hostname to check") 15 | 16 | config.ParseArguments() 17 | 18 | // Some checking should be done here 19 | 20 | Exitf(OK, "Everything is fine - answer=%d", 42) 21 | 22 | // Output: [OK] - Everything is fine - answer=42 23 | // would exit with code 0 24 | } 25 | 26 | type ConfigForTesting struct { 27 | Auth string `env:"AUTH"` 28 | Bearer string `env:"EXAMPLE"` 29 | OneMoreThanTags string 30 | } 31 | 32 | func TestLoadFromEnv(t *testing.T) { 33 | c := ConfigForTesting{} 34 | 35 | err := os.Setenv("EXAMPLE", "foobar") 36 | defer os.Unsetenv("EXAMPLE") // just to not create any side effects 37 | 38 | if err != nil { 39 | t.Fatalf("expected no error, got %v", err) 40 | } 41 | 42 | LoadFromEnv(&c) 43 | 44 | if c.Bearer != "foobar" { 45 | t.Fatalf("expected %v, got %v", c.Bearer, "foobar") 46 | } 47 | 48 | if c.Auth != "" { 49 | t.Fatalf("expected %v, got %v", c.Auth, "") 50 | } 51 | 52 | if c.OneMoreThanTags != "" { 53 | t.Fatalf("expected %v, got %v", c.OneMoreThanTags, "") 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /convert/bytes_common.go: -------------------------------------------------------------------------------- 1 | package convert 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | type ByteAny interface { 11 | HumanReadable() string 12 | Bytes() uint64 13 | fmt.Stringer 14 | } 15 | 16 | // ExponentialFold defines the value to fold back to the previous exponent for better display of a small value. 17 | const ExponentialFold = 2 18 | 19 | // ParseBytes parses a strings and returns the proper ByteAny implementation based on the unit specified. 20 | // 21 | // When a plain value or B unit is used, ByteIEC is returned. 22 | func ParseBytes(value string) (ByteAny, error) { 23 | value = strings.TrimSpace(value) 24 | 25 | // Split number and unit by first non-numeric rune 26 | firstNonNumeric := func(c rune) bool { return !(c >= '0' && c <= '9' || c == '.') } //nolint: staticcheck 27 | 28 | i := strings.IndexFunc(value, firstNonNumeric) 29 | 30 | var unit string 31 | 32 | if i > 0 { 33 | unit = strings.TrimSpace(value[i:]) 34 | value = value[0:i] 35 | } 36 | 37 | // Parse value to float64 38 | number, err := strconv.ParseFloat(value, 64) // nolint:gomnd 39 | if err != nil { 40 | return nil, fmt.Errorf("provided value could not be parsed as float64: %s", value) 41 | } 42 | 43 | // Assume byte when no unit given 44 | if unit == "" { 45 | unit = "B" 46 | } 47 | 48 | // check for known units in ByteIECUnits 49 | for exponent, u := range ByteIECUnits { 50 | if u == unit { 51 | // convert to bytes and return type 52 | return BytesIEC(number * math.Pow(IECBase, float64(exponent))), nil 53 | } 54 | } 55 | 56 | // check for known units in ByteSIUnits 57 | for exponent, u := range ByteSIUnits { 58 | if u == unit { 59 | // convert to bytes and return type 60 | return BytesSI(number * math.Pow(SIBase, float64(exponent))), nil 61 | } 62 | } 63 | 64 | return nil, fmt.Errorf("invalid unit: %s", unit) 65 | } 66 | 67 | // humanReadable searches for the closest feasible unit for displaying the byte value to a human. 68 | // 69 | // Meant as a universal function to be used by the implementations, with base and a list of unit names. 70 | // 71 | // A special behavior is that resulting values smaller than 2 are displayed with the lower exponent. 72 | // If the input value is 0, humanReadable will always return "0B" 73 | // 74 | // Examples: 75 | // 76 | // 1073741824B -> 1000KB 77 | // 2147483648B -> 2MB 78 | // 0 -> 0MB 79 | func humanReadable(b uint64, units []string, base float64) (float64, string) { 80 | if b == 0 { 81 | return 0, "B" 82 | } 83 | 84 | exponent := math.Log(float64(b)) / math.Log(base) 85 | 86 | // Round to the unit scaled exponent 87 | unitExponent := math.Floor(exponent) 88 | 89 | // Ensure we only scale to the maximum known unit 90 | maxScale := float64(len(units) - 1) 91 | if unitExponent > maxScale { 92 | unitExponent = maxScale 93 | } 94 | 95 | value := math.Pow(base, exponent-unitExponent) 96 | 97 | // When resulting value is smaller than 2 calculate 1XXXM(i)B instead of 1.XXG(i)B 98 | if math.Round(value*base)/base < ExponentialFold { 99 | unitExponent-- 100 | value = math.Pow(base, exponent-unitExponent) 101 | } 102 | 103 | return value, units[int(unitExponent)] 104 | } 105 | -------------------------------------------------------------------------------- /convert/bytes_common_test.go: -------------------------------------------------------------------------------- 1 | package convert 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestParseBytes(t *testing.T) { 8 | b, err := ParseBytes("1024") 9 | if err != nil { 10 | t.Fatalf("expected no error, got %v", err) 11 | } 12 | if _, ok := b.(BytesIEC); !ok { 13 | t.Fatalf("expected type BytesIEC, got %T", b) 14 | } 15 | if b.Bytes() != 1024 { 16 | t.Fatalf("expected 1024 bytes, got %d", b.Bytes()) 17 | } 18 | 19 | b, err = ParseBytes("1MB") 20 | if err != nil { 21 | t.Fatalf("expected no error, got %v", err) 22 | } 23 | if _, ok := b.(BytesSI); !ok { 24 | t.Fatalf("expected type BytesSI, got %T", b) 25 | } 26 | if b.Bytes() != 1000*1000 { 27 | t.Fatalf("expected 1000000 bytes, got %d", b.Bytes()) 28 | } 29 | 30 | b, err = ParseBytes("1 MiB") 31 | if err != nil { 32 | t.Fatalf("expected no error, got %v", err) 33 | } 34 | if _, ok := b.(BytesIEC); !ok { 35 | t.Fatalf("expected type BytesIEC, got %T", b) 36 | } 37 | if b.Bytes() != 1024*1024 { 38 | t.Fatalf("expected 1048576 bytes, got %d", b.Bytes()) 39 | } 40 | 41 | b, err = ParseBytes("100MB") 42 | if err != nil { 43 | t.Fatalf("expected no error, got %v", err) 44 | } 45 | 46 | b, err = ParseBytes("100MiB") 47 | if err != nil { 48 | t.Fatalf("expected no error, got %v", err) 49 | } 50 | 51 | b, err = ParseBytes(" 23 GiB ") 52 | if err != nil { 53 | t.Fatalf("expected no error, got %v", err) 54 | } 55 | if _, ok := b.(BytesIEC); !ok { 56 | t.Fatalf("expected type BytesIEC, got %T", b) 57 | } 58 | if b.Bytes() != 23*1024*1024*1024 { 59 | t.Fatalf("expected 24742653952 bytes, got %d", b.Bytes()) 60 | } 61 | 62 | b, err = ParseBytes("1.2.3.4MB") 63 | if err == nil { 64 | t.Fatalf("expected error, got nil") 65 | } 66 | if b != nil { 67 | t.Fatalf("expected nil, got %v", b) 68 | } 69 | 70 | b, err = ParseBytes("1PHD") 71 | if err == nil { 72 | t.Fatalf("expected error, got nil") 73 | } 74 | if b != nil { 75 | t.Fatalf("expected nil, got %v", b) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /convert/bytes_iec.go: -------------------------------------------------------------------------------- 1 | package convert 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | ) 7 | 8 | // ByteIECUnits lists known units we can convert to based on uint64. 9 | // 10 | // See https://en.wikipedia.org/wiki/Byte#Unit_symbol 11 | var ByteIECUnits = []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB"} 12 | 13 | // BytesIEC is the IEC (1024) unit implementation of byte conversion. 14 | type BytesIEC uint64 15 | 16 | // IECBase is the exponential base for IEC units. 17 | const IECBase = 1024 18 | 19 | // HumanReadable returns the biggest sensible unit for the byte value with 2 decimal precision. 20 | // 21 | // When value is smaller than 2 render it with a lower scale. 22 | func (b BytesIEC) HumanReadable() string { 23 | value, unit := humanReadable(uint64(b), ByteIECUnits, IECBase) 24 | 25 | // Remove trailing zero decimals and any left over decimal dot 26 | s := strings.TrimRight(strings.TrimRight(strconv.FormatFloat(value, 'f', 2, 64), "0"), ".") 27 | 28 | return s + unit 29 | } 30 | 31 | func (b BytesIEC) String() string { 32 | return b.HumanReadable() 33 | } 34 | 35 | // Bytes returns the value as uint64. 36 | func (b BytesIEC) Bytes() uint64 { 37 | return uint64(b) 38 | } 39 | -------------------------------------------------------------------------------- /convert/bytes_iec_test.go: -------------------------------------------------------------------------------- 1 | package convert 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestBytesIEC_HumanReadable(t *testing.T) { 8 | if BytesIEC(0).HumanReadable() != "0B" { 9 | t.Fatalf("expected '0B', got %s", BytesIEC(0).HumanReadable()) 10 | } 11 | if BytesIEC(999).HumanReadable() != "999B" { 12 | t.Fatalf("expected '999B', got %s", BytesIEC(999).HumanReadable()) 13 | } 14 | if BytesIEC(999*1024).HumanReadable() != "999KiB" { 15 | t.Fatalf("expected '999KiB', got %s", BytesIEC(999*1024).HumanReadable()) 16 | } 17 | if BytesIEC(999*1024*1024).HumanReadable() != "999MiB" { 18 | t.Fatalf("expected '999MiB', got %s", BytesIEC(999*1024*1024).HumanReadable()) 19 | } 20 | if BytesIEC(999*1024*1024*1024).HumanReadable() != "999GiB" { 21 | t.Fatalf("expected '999GiB', got %s", BytesIEC(999*1024*1024*1024).HumanReadable()) 22 | } 23 | if BytesIEC(999*1024*1024*1024*1024).HumanReadable() != "999TiB" { 24 | t.Fatalf("expected '999TiB', got %s", BytesIEC(999*1024*1024*1024*1024).HumanReadable()) 25 | } 26 | if BytesIEC(4*1024*1024*1024*1024*1024).HumanReadable() != "4PiB" { 27 | t.Fatalf("expected '4PiB', got %s", BytesIEC(4*1024*1024*1024*1024*1024).HumanReadable()) 28 | } 29 | if BytesIEC(4*1024*1024*1024*1024*1024*1024).HumanReadable() != "4096PiB" { 30 | t.Fatalf("expected '4096PiB', got %s", BytesIEC(4*1024*1024*1024*1024*1024*1024).HumanReadable()) 31 | } 32 | if BytesIEC(1263*1024*1024).HumanReadable() != "1263MiB" { 33 | t.Fatalf("expected '1263MiB', got %s", BytesIEC(1263*1024*1024).HumanReadable()) 34 | } 35 | if BytesIEC(100*1024*1024).HumanReadable() != "100MiB" { 36 | t.Fatalf("expected '100MiB', got %s", BytesIEC(100*1024*1024).HumanReadable()) 37 | } 38 | if BytesIEC(129032519).HumanReadable() != "123.05MiB" { 39 | t.Fatalf("expected '123.05MiB', got %s", BytesIEC(129032519).HumanReadable()) 40 | } 41 | if BytesIEC(15756365824).HumanReadable() != "14.67GiB" { 42 | t.Fatalf("expected '14.67GiB', got %s", BytesIEC(15756365824).HumanReadable()) 43 | } 44 | if BytesIEC(1024*1024).HumanReadable() != "1024KiB" { 45 | t.Fatalf("expected '1024KiB', got %s", BytesIEC(1024*1024).HumanReadable()) 46 | } 47 | if BytesIEC(2*1024*1024).HumanReadable() != "2MiB" { 48 | t.Fatalf("expected '2MiB', got %s", BytesIEC(2*1024*1024).HumanReadable()) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /convert/bytes_si.go: -------------------------------------------------------------------------------- 1 | package convert 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | ) 7 | 8 | // ByteSIUnits lists known units we can convert to based on uint64. 9 | // 10 | // See https://en.wikipedia.org/wiki/Byte#Unit_symbol 11 | var ByteSIUnits = []string{"B", "KB", "MB", "GB", "TB", "PB"} 12 | 13 | // BytesSI is the SI (1000) unit implementation of byte conversion. 14 | type BytesSI uint64 15 | 16 | // SIBase is the exponential base for SI units. 17 | const SIBase = 1000 18 | 19 | // HumanReadable returns the biggest sensible unit for the byte value with 2 decimal precision. 20 | // 21 | // When value is smaller than 2 render it with a lower scale. 22 | func (b BytesSI) HumanReadable() string { 23 | value, unit := humanReadable(uint64(b), ByteSIUnits, SIBase) 24 | 25 | // Remove trailing zero decimals and any left over decimal dot 26 | s := strings.TrimRight(strings.TrimRight(strconv.FormatFloat(value, 'f', 2, 64), "0"), ".") 27 | 28 | return s + unit 29 | } 30 | 31 | // String returns a text representation of the current value. 32 | func (b BytesSI) String() string { 33 | return b.HumanReadable() 34 | } 35 | 36 | // Bytes returns the value as uint64. 37 | func (b BytesSI) Bytes() uint64 { 38 | return uint64(b) 39 | } 40 | -------------------------------------------------------------------------------- /convert/bytes_si_test.go: -------------------------------------------------------------------------------- 1 | package convert 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestBytesSI_HumanReadable(t *testing.T) { 8 | if BytesSI(0).HumanReadable() != "0B" { 9 | t.Fatalf("expected '0B', got %s", BytesSI(0).HumanReadable()) 10 | } 11 | if BytesSI(999).HumanReadable() != "999B" { 12 | t.Fatalf("expected '999B', got %s", BytesSI(999).HumanReadable()) 13 | } 14 | if BytesSI(999*1000).HumanReadable() != "999KB" { 15 | t.Fatalf("expected '999KB', got %s", BytesSI(999*1000).HumanReadable()) 16 | } 17 | if BytesSI(999*1000*1000).HumanReadable() != "999MB" { 18 | t.Fatalf("expected '999MB', got %s", BytesSI(999*1000*1000).HumanReadable()) 19 | } 20 | if BytesSI(999*1000*1000*1000).HumanReadable() != "999GB" { 21 | t.Fatalf("expected '999GB', got %s", BytesSI(999*1000*1000*1000).HumanReadable()) 22 | } 23 | if BytesSI(999*1000*1000*1000*1000).HumanReadable() != "999TB" { 24 | t.Fatalf("expected '999TB', got %s", BytesSI(999*1000*1000*1000*1000).HumanReadable()) 25 | } 26 | if BytesSI(4*1000*1000*1000*1000*1000).HumanReadable() != "4PB" { 27 | t.Fatalf("expected '4PB', got %s", BytesSI(4*1000*1000*1000*1000*1000).HumanReadable()) 28 | } 29 | if BytesSI(4*1000*1000*1000*1000*1000*1000).HumanReadable() != "4000PB" { 30 | t.Fatalf("expected '4000PB', got %s", BytesSI(4*1000*1000*1000*1000*1000*1000).HumanReadable()) 31 | } 32 | if BytesSI(4*1000*1000*1000*1000).HumanReadable() != "4TB" { 33 | t.Fatalf("expected '4TB', got %s", BytesSI(4*1000*1000*1000*1000).HumanReadable()) 34 | } 35 | if BytesSI(4*1000*1000*1000*1000*1000).HumanReadable() != "4PB" { 36 | t.Fatalf("expected '4PB', got %s", BytesSI(4*1000*1000*1000*1000*1000).HumanReadable()) 37 | } 38 | if BytesSI(1263*1000*1000).HumanReadable() != "1263MB" { 39 | t.Fatalf("expected '1263MB', got %s", BytesSI(1263*1000*1000).HumanReadable()) 40 | } 41 | if BytesSI(123050*1000).HumanReadable() != "123.05MB" { 42 | t.Fatalf("expected '123.05MB', got %s", BytesSI(123050*1000).HumanReadable()) 43 | } 44 | if BytesSI(14670*1000*1000).HumanReadable() != "14.67GB" { 45 | t.Fatalf("expected '14.67GB', got %s", BytesSI(14670*1000*1000).HumanReadable()) 46 | } 47 | if BytesSI(1000*1000).HumanReadable() != "1000KB" { 48 | t.Fatalf("expected '1000KB', got %s", BytesSI(1000*1000).HumanReadable()) 49 | } 50 | if BytesSI(2*1000*1000).HumanReadable() != "2MB" { 51 | t.Fatalf("expected '2MB', got %s", BytesSI(2*1000*1000).HumanReadable()) 52 | } 53 | if BytesSI(3*1000*1000).HumanReadable() != "3MB" { 54 | t.Fatalf("expected '3MB', got %s", BytesSI(3*1000*1000).HumanReadable()) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /examples/check_example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/NETWAYS/go-check" 5 | "log" 6 | ) 7 | 8 | func main() { 9 | defer check.CatchPanic() 10 | config := check.NewConfig() 11 | config.Name = "check_test" 12 | config.Readme = `Test Plugin` 13 | config.Version = "1.0.0" 14 | config.Timeout = 10 15 | 16 | value := config.FlagSet.IntP("value", "", 10, "test value") 17 | warning := config.FlagSet.IntP("warning", "w", 20, "warning threshold") 18 | critical := config.FlagSet.IntP("critical", "c", 50, "critical threshold") 19 | 20 | config.ParseArguments() 21 | 22 | if config.Debug { 23 | log.Println("Start logging") 24 | } 25 | // time.Sleep(20 * time.Second) 26 | 27 | if *value > *critical { 28 | check.Exitf(check.Critical, "value is %d", *value) 29 | } else if *value > *warning { 30 | check.Exitf(check.Warning, "value is %d", *value) 31 | } else { 32 | check.Exitf(check.OK, "value is %d", *value) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/check_example/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/NETWAYS/go-check/testhelper" 9 | ) 10 | 11 | func TestMyMain(t *testing.T) { 12 | actual := testhelper.RunMainTest(main, "--help") 13 | expected := `would exit with code 3` 14 | 15 | if !strings.Contains(actual, expected) { 16 | t.Fatalf("expected %v, got %v", expected, actual) 17 | } 18 | 19 | actual = testhelper.RunMainTest(main, "--warning", "20") 20 | expected = "[OK] - value is 10" 21 | 22 | if !strings.Contains(actual, expected) { 23 | t.Fatalf("expected %v, got %v", expected, actual) 24 | } 25 | 26 | actual = testhelper.RunMainTest(main, "--warning", "10", "--value", "11") 27 | expected = "[WARNING] - value is 11" 28 | 29 | if !strings.Contains(actual, expected) { 30 | t.Fatalf("expected %v, got %v", expected, actual) 31 | } 32 | } 33 | 34 | func TestMain(m *testing.M) { 35 | testhelper.EnableTestMode() 36 | os.Exit(m.Run()) 37 | } 38 | -------------------------------------------------------------------------------- /examples/check_example2/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/NETWAYS/go-check" 5 | "github.com/NETWAYS/go-check/perfdata" 6 | "github.com/NETWAYS/go-check/result" 7 | ) 8 | 9 | func main() { 10 | defer check.CatchPanic() 11 | 12 | var overall result.Overall 13 | 14 | check1 := result.PartialResult{} 15 | 16 | check1.Output = "Check1" 17 | err := check1.SetState(check.OK) 18 | 19 | if err != nil { 20 | check.ExitError(err) 21 | } 22 | 23 | check1.Perfdata.Add(&perfdata.Perfdata{ 24 | Label: "foo", 25 | Value: 23, 26 | }) 27 | 28 | check2 := result.PartialResult{} 29 | 30 | check2.Output = "Check2" 31 | err = check2.SetState(check.Warning) 32 | 33 | if err != nil { 34 | check.ExitError(err) 35 | } 36 | 37 | check2.Perfdata.Add(&perfdata.Perfdata{ 38 | Label: "bar", 39 | Value: 42, 40 | }) 41 | check2.Perfdata.Add(&perfdata.Perfdata{ 42 | Label: "foo2 bar", 43 | Value: 46, 44 | }) 45 | 46 | overall.AddSubcheck(check1) 47 | overall.AddSubcheck(check2) 48 | 49 | check.ExitRaw(overall.GetStatus(), overall.GetOutput()) 50 | } 51 | -------------------------------------------------------------------------------- /examples/check_example2/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/NETWAYS/go-check/testhelper" 8 | ) 9 | 10 | func TestMyMain(t *testing.T) { 11 | actual := testhelper.RunMainTest(main) 12 | 13 | expected := `[WARNING] - states: warning=1 ok=1 14 | \_ [OK] Check1 15 | \_ [WARNING] Check2 16 | |foo=23 bar=42 'foo2 bar'=46 17 | 18 | would exit with code 1 19 | ` 20 | 21 | if actual != expected { 22 | t.Fatalf("expected %v, got %v", expected, actual) 23 | } 24 | } 25 | 26 | func TestMain(m *testing.M) { 27 | testhelper.EnableTestMode() 28 | os.Exit(m.Run()) 29 | } 30 | -------------------------------------------------------------------------------- /exit.go: -------------------------------------------------------------------------------- 1 | package check 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "runtime/debug" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | // AllowExit lets you disable the call to os.Exit() in ExitXxx() functions of this package. 12 | // 13 | // This should be used carefully and most likely only for testing. 14 | var AllowExit = true 15 | 16 | // PrintStack prints the error stack when recovering from a panic with CatchPanic() 17 | var PrintStack = true 18 | 19 | // Exitf prints the plugin output using formatting and exits the program. 20 | // 21 | // Output is the formatting string, and the rest of the arguments help adding values. 22 | // 23 | // Also see fmt package: https://golang.org/pkg/fmt 24 | func Exitf(rc int, output string, args ...interface{}) { 25 | ExitRaw(rc, fmt.Sprintf(output, args...)) 26 | } 27 | 28 | // ExitRaw prints the plugin output with the state prefixed and exits the program. 29 | // 30 | // Example: 31 | // 32 | // OK - everything is fine 33 | func ExitRaw(rc int, output ...string) { 34 | var text strings.Builder 35 | 36 | text.WriteString("[" + StatusText(rc) + "] -") 37 | 38 | for _, s := range output { 39 | text.WriteString(" " + s) 40 | } 41 | 42 | text.WriteString("\n") 43 | 44 | _, _ = os.Stdout.WriteString(text.String()) 45 | 46 | BaseExit(rc) 47 | } 48 | 49 | // BaseExit exits the process with a given return code. 50 | // 51 | // Can be controlled with the global AllowExit 52 | func BaseExit(rc int) { 53 | if AllowExit { 54 | os.Exit(rc) 55 | } 56 | 57 | _, _ = os.Stdout.WriteString("would exit with code " + strconv.Itoa(rc) + "\n") 58 | } 59 | 60 | // ExitError exists with an Unknown state while reporting the error 61 | func ExitError(err error) { 62 | Exitf(Unknown, "%s (%T)", err.Error(), err) 63 | } 64 | 65 | // CatchPanic is a general function for defer, to capture any panic that occurred during runtime of a check 66 | // 67 | // The function will recover from the condition and exit with a proper UNKNOWN status, while showing error 68 | // and the call stack. 69 | func CatchPanic() { 70 | // This can be enabled when working with a debugger 71 | // ppid := os.Getppid() 72 | // if parent, err := ps.FindProcess(ppid); err == nil { 73 | // if parent.Executable() == "dlv" { 74 | // // seems to be a debugger, don't continue with recover 75 | // return 76 | // } 77 | // } 78 | if r := recover(); r != nil { 79 | output := fmt.Sprint("Golang encountered a panic: ", r) 80 | if PrintStack { 81 | output += "\n\n" + string(debug.Stack()) 82 | } 83 | 84 | ExitRaw(Unknown, output) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /exit_test.go: -------------------------------------------------------------------------------- 1 | package check 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "testing" 7 | ) 8 | 9 | func ExampleExit() { 10 | Exitf(OK, "Everything is fine - value=%d", 42) 11 | // Output: [OK] - Everything is fine - value=42 12 | // would exit with code 0 13 | } 14 | 15 | func ExampleExitf() { 16 | Exitf(OK, "Everything is fine - value=%d", 42) 17 | // Output: [OK] - Everything is fine - value=42 18 | // would exit with code 0 19 | } 20 | 21 | func ExampleExitRaw() { 22 | ExitRaw(OK, "Everything is fine") 23 | // Output: [OK] - Everything is fine 24 | // would exit with code 0 25 | } 26 | 27 | func ExampleExitError() { 28 | err := fmt.Errorf("connection to %s has been timed out", "localhost:12345") 29 | ExitError(err) 30 | // Output: [UNKNOWN] - connection to localhost:12345 has been timed out (*errors.errorString) 31 | // would exit with code 3 32 | } 33 | 34 | func ExampleCatchPanic() { 35 | defer CatchPanic() 36 | 37 | panic("something bad happened") 38 | // Output: [UNKNOWN] - Golang encountered a panic: something bad happened 39 | // would exit with code 3 40 | } 41 | 42 | func TestMain(m *testing.M) { 43 | // disable actual exit 44 | AllowExit = false 45 | 46 | // disable stack trace for the example 47 | PrintStack = false 48 | 49 | os.Exit(m.Run()) 50 | } 51 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/NETWAYS/go-check 2 | 3 | go 1.22 4 | 5 | require github.com/spf13/pflag v1.0.6 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 2 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 3 | -------------------------------------------------------------------------------- /perfdata/list.go: -------------------------------------------------------------------------------- 1 | package perfdata 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | // PerfdataList can store multiple perfdata and brings a simple fmt.Stringer interface 8 | // nolint: golint, revive 9 | type PerfdataList []*Perfdata 10 | 11 | // String returns string representations of all Perfdata 12 | func (l *PerfdataList) String() string { 13 | var out strings.Builder 14 | 15 | for _, p := range *l { 16 | pfDataString, err := p.ValidatedString() 17 | 18 | // Ignore perfdata points which fail to format 19 | if err == nil { 20 | out.WriteString(" ") 21 | out.WriteString(pfDataString) 22 | } 23 | } 24 | 25 | return strings.Trim(out.String(), " ") 26 | } 27 | 28 | // Add adds a Perfdata pointer to the list 29 | func (l *PerfdataList) Add(p *Perfdata) { 30 | *l = append(*l, p) 31 | } 32 | -------------------------------------------------------------------------------- /perfdata/list_test.go: -------------------------------------------------------------------------------- 1 | package perfdata 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func ExamplePerfdataList() { 9 | list := PerfdataList{} 10 | list.Add(&Perfdata{Label: "test1", Value: 23}) 11 | list.Add(&Perfdata{Label: "test2", Value: 42}) 12 | 13 | fmt.Println(list) 14 | 15 | // Output: 16 | // [test1=23 test2=42] 17 | } 18 | 19 | func TestPerfdataListFormating(t *testing.T) { 20 | list := PerfdataList{} 21 | list.Add(&Perfdata{Label: "test1", Value: 23}) 22 | list.Add(&Perfdata{Label: "test2", Value: 42}) 23 | 24 | actual := list.String() 25 | expected := "test1=23 test2=42" 26 | 27 | if actual != expected { 28 | t.Fatalf("expected %v, got %v", expected, actual) 29 | } 30 | } 31 | 32 | func BenchmarkPerfdataListFormating(b *testing.B) { 33 | b.ReportAllocs() 34 | 35 | list := PerfdataList{} 36 | list.Add(&Perfdata{Label: "test1", Value: 23}) 37 | list.Add(&Perfdata{Label: "test2", Value: 42}) 38 | 39 | for i := 0; i < b.N; i++ { 40 | l := list.String() 41 | _ = l 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /perfdata/type.go: -------------------------------------------------------------------------------- 1 | package perfdata 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "math" 7 | "strings" 8 | 9 | "github.com/NETWAYS/go-check" 10 | ) 11 | 12 | // Replace not allowed characters inside a label 13 | var replacer = strings.NewReplacer("=", "_", "`", "_", "'", "_", "\"", "_") 14 | 15 | // formatNumeric returns a string representation of various possible numerics 16 | // 17 | // This supports most internal types of Go and all fmt.Stringer interfaces. 18 | // Returns an error in some known cases where the value of a data type does not 19 | // represent a valid measurement, e.g INF for floats 20 | // This error can probably ignored in most cases and the perfdata point omitted, 21 | // but silently dropping the value and returning the empty strings seems like bad style 22 | func formatNumeric(value interface{}) (string, error) { 23 | switch v := value.(type) { 24 | case float64: 25 | if math.IsInf(v, 0) { 26 | return "", errors.New("Perfdata value is infinite") 27 | } 28 | 29 | if math.IsNaN(v) { 30 | return "", errors.New("Perfdata value is infinite") 31 | } 32 | 33 | return check.FormatFloat(v), nil 34 | case float32: 35 | if math.IsInf(float64(v), 0) { 36 | return "", errors.New("Perfdata value is infinite") 37 | } 38 | 39 | if math.IsNaN(float64(v)) { 40 | return "", errors.New("Perfdata value is infinite") 41 | } 42 | 43 | return check.FormatFloat(float64(v)), nil 44 | case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: 45 | return fmt.Sprintf("%d", v), nil 46 | default: 47 | panic(fmt.Sprintf("unsupported type for perfdata: %T", value)) 48 | } 49 | } 50 | 51 | // Perfdata represents all properties of performance data for Icinga 52 | // 53 | // Implements fmt.Stringer to return the plaintext format for a plugin output. 54 | // 55 | // For examples of Uom see: 56 | // 57 | // https://www.monitoring-plugins.org/doc/guidelines.html#AEN201 58 | // 59 | // https://github.com/Icinga/icinga2/blob/master/lib/base/perfdatavalue.cpp 60 | // 61 | // https://icinga.com/docs/icinga-2/latest/doc/05-service-monitoring/#unit-of-measurement-uom 62 | type Perfdata struct { 63 | Label string 64 | Value interface{} 65 | // Uom is the unit-of-measurement, see links above for details. 66 | Uom string 67 | Warn *check.Threshold 68 | Crit *check.Threshold 69 | Min interface{} 70 | Max interface{} 71 | } 72 | 73 | // String returns the proper format for the plugin output 74 | // on errors (occurs with invalid data, the empty string is returned 75 | func (p Perfdata) String() string { 76 | tmp, _ := p.ValidatedString() 77 | return tmp 78 | } 79 | 80 | // ValidatedString returns the proper format for the plugin output 81 | // Returns an error in some known cases where the value of a data type does not 82 | // represent a valid measurement, see the explanation for "formatNumeric" for 83 | // perfdata values. 84 | func (p Perfdata) ValidatedString() (string, error) { 85 | var sb strings.Builder 86 | 87 | // Add quotes if string contains any whitespace 88 | if strings.ContainsAny(p.Label, "\t\n\f\r ") { 89 | sb.WriteString(`'` + replacer.Replace(p.Label) + `'` + "=") 90 | } else { 91 | sb.WriteString(replacer.Replace(p.Label) + "=") 92 | } 93 | 94 | pfVal, err := formatNumeric(p.Value) 95 | if err != nil { 96 | return "", err 97 | } 98 | 99 | sb.WriteString(pfVal) 100 | sb.WriteString(p.Uom) 101 | 102 | // Thresholds 103 | for _, value := range []*check.Threshold{p.Warn, p.Crit} { 104 | sb.WriteString(";") 105 | 106 | if value != nil { 107 | sb.WriteString(value.String()) 108 | } 109 | } 110 | 111 | // Limits 112 | for _, value := range []interface{}{p.Min, p.Max} { 113 | sb.WriteString(";") 114 | 115 | if value != nil { 116 | pfVal, err := formatNumeric(value) 117 | // Attention: we ignore limits if they are faulty 118 | if err == nil { 119 | sb.WriteString(pfVal) 120 | } 121 | } 122 | } 123 | 124 | return strings.TrimRight(sb.String(), ";"), nil 125 | } 126 | -------------------------------------------------------------------------------- /perfdata/type_test.go: -------------------------------------------------------------------------------- 1 | package perfdata 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | 7 | "github.com/NETWAYS/go-check" 8 | ) 9 | 10 | func BenchmarkPerfdataString(b *testing.B) { 11 | b.ReportAllocs() 12 | 13 | perf := Perfdata{ 14 | Label: "test test=test", 15 | Value: 10.1, 16 | Uom: "%", 17 | Warn: &check.Threshold{Upper: 80}, 18 | Crit: &check.Threshold{Upper: 90}, 19 | Min: 0, 20 | Max: 100} 21 | 22 | for i := 0; i < b.N; i++ { 23 | p := perf.String() 24 | _ = p 25 | } 26 | } 27 | 28 | func TestRenderPerfdata(t *testing.T) { 29 | testcases := map[string]struct { 30 | perf Perfdata 31 | expected string 32 | }{ 33 | "simple": { 34 | perf: Perfdata{ 35 | Label: "test", 36 | Value: 2, 37 | }, 38 | expected: "test=2", 39 | }, 40 | "with-quotes": { 41 | perf: Perfdata{ 42 | Label: "te's\"t", 43 | Value: 2, 44 | }, 45 | expected: "te_s_t=2", 46 | }, 47 | "with-special-chars": { 48 | perf: Perfdata{ 49 | Label: "test_🖥️_'test", 50 | Value: 2, 51 | }, 52 | expected: "test_🖥️__test=2", 53 | }, 54 | "with-uom": { 55 | perf: Perfdata{ 56 | Label: "test", 57 | Value: 2, 58 | Uom: "%", 59 | }, 60 | expected: "test=2%", 61 | }, 62 | "with-thresholds": { 63 | perf: Perfdata{ 64 | Label: "foo bar", 65 | Value: 2.76, 66 | Uom: "m", 67 | Warn: &check.Threshold{Lower: 10, Upper: 25, Inside: true}, 68 | Crit: &check.Threshold{Lower: 15, Upper: 20, Inside: false}, 69 | }, 70 | expected: "'foo bar'=2.76m;@10:25;15:20", 71 | }, 72 | } 73 | 74 | testcasesWithErrors := map[string]struct { 75 | perf Perfdata 76 | expected string 77 | }{ 78 | "invalid-value": { 79 | perf: Perfdata{ 80 | Label: "to infinity", 81 | Value: math.Inf(+1), 82 | }, 83 | expected: "", 84 | }, 85 | } 86 | 87 | for name, tc := range testcases { 88 | t.Run(name, func(t *testing.T) { 89 | pfVal, err := tc.perf.ValidatedString() 90 | if err != nil { 91 | t.Fatalf("expected no error, got %v", err) 92 | } 93 | 94 | if tc.expected != pfVal { 95 | t.Fatalf("expected %v, got %v", tc.expected, pfVal) 96 | } 97 | }) 98 | } 99 | 100 | for name, tc := range testcasesWithErrors { 101 | t.Run(name, func(t *testing.T) { 102 | pfVal, err := tc.perf.ValidatedString() 103 | if err == nil { 104 | t.Fatalf("expected error, got none") 105 | } 106 | 107 | if tc.expected != pfVal { 108 | t.Fatalf("expected %v, got %v", tc.expected, pfVal) 109 | } 110 | }) 111 | } 112 | } 113 | 114 | type pfFormatTest struct { 115 | Result string 116 | InputValue any 117 | } 118 | 119 | func TestFormatNumeric(t *testing.T) { 120 | testdata := []pfFormatTest{ 121 | { 122 | Result: "10", 123 | InputValue: 10, 124 | }, 125 | { 126 | Result: "-10", 127 | InputValue: -10, 128 | }, 129 | { 130 | Result: "10", 131 | InputValue: uint8(10), 132 | }, 133 | { 134 | Result: "1234.567", 135 | InputValue: float64(1234.567), 136 | }, 137 | { 138 | Result: "3456.789", 139 | InputValue: float32(3456.789), 140 | }, 141 | { 142 | Result: "1234567890.988", 143 | InputValue: 1234567890.9877, 144 | }, 145 | } 146 | 147 | for _, val := range testdata { 148 | formatted, err := formatNumeric(val.InputValue) 149 | if err != nil { 150 | t.Fatalf("expected no error, got %v", err) 151 | } 152 | 153 | if val.Result != formatted { 154 | t.Fatalf("expected %v, got %v", formatted, val.Result) 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /result/overall.go: -------------------------------------------------------------------------------- 1 | // result tries to 2 | package result 3 | 4 | import ( 5 | "errors" 6 | "fmt" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/NETWAYS/go-check" 11 | "github.com/NETWAYS/go-check/perfdata" 12 | ) 13 | 14 | // The "width" of the indentation which is added on every level 15 | const indentationOffset = 2 16 | 17 | // Overall is a singleton for a monitoring plugin that has several partial results (or sub-results) 18 | // 19 | // Design decisions: A check plugin has a single Overall (singleton), 20 | // each partial thing which is tested, gets its own subcheck. 21 | // 22 | // The results of these may be relevant to the overall status in the end 23 | // or not, e.g. if a plugin tries two different methods for something and 24 | // one suffices, but one fails, the whole check might be OK and only the subcheck 25 | // Warning or Critical. 26 | type Overall struct { 27 | oks int 28 | warnings int 29 | criticals int 30 | unknowns int 31 | Summary string 32 | stateSetExplicitly bool 33 | Outputs []string // Deprecate this in a future version 34 | PartialResults []PartialResult 35 | } 36 | 37 | // PartialResult represents a sub-result for an Overall struct 38 | type PartialResult struct { 39 | Perfdata perfdata.PerfdataList 40 | PartialResults []PartialResult 41 | Output string 42 | state int // Result state, either set explicitly or derived from partialResults 43 | defaultState int // Default result state, if no partial results are available and no state is set explicitly 44 | stateSetExplicitly bool // nolint: unused 45 | defaultStateSet bool // nolint: unused 46 | } 47 | 48 | // Initializer for a PartialResult with "sane" defaults 49 | // Notable default compared to the nil object: the default state is set to Unknown 50 | func NewPartialResult() PartialResult { 51 | return PartialResult{ 52 | stateSetExplicitly: false, 53 | defaultState: check.Unknown, 54 | } 55 | } 56 | 57 | // String returns the status and output of the PartialResult 58 | func (s *PartialResult) String() string { 59 | return fmt.Sprintf("[%s] %s", check.StatusText(s.GetStatus()), s.Output) 60 | } 61 | 62 | // Add adds a return state explicitly 63 | // 64 | // Hint: This will set stateSetExplicitly to true 65 | func (o *Overall) Add(state int, output string) { 66 | switch state { 67 | case check.OK: 68 | o.oks++ 69 | case check.Warning: 70 | o.warnings++ 71 | case check.Critical: 72 | o.criticals++ 73 | default: 74 | o.unknowns++ 75 | } 76 | 77 | // TODO: Might be a bit obscure that the Add method also sets stateSetExplicitly 78 | o.stateSetExplicitly = true 79 | 80 | o.Outputs = append(o.Outputs, fmt.Sprintf("[%s] %s", check.StatusText(state), output)) 81 | } 82 | 83 | // AddSubcheck adds a PartialResult to the Overall 84 | func (o *Overall) AddSubcheck(subcheck PartialResult) { 85 | o.PartialResults = append(o.PartialResults, subcheck) 86 | } 87 | 88 | // AddSubcheck adds a PartialResult to the PartialResult 89 | func (s *PartialResult) AddSubcheck(subcheck PartialResult) { 90 | s.PartialResults = append(s.PartialResults, subcheck) 91 | } 92 | 93 | // GetStatus returns the current state (ok, warning, critical, unknown) of the Overall 94 | func (o *Overall) GetStatus() int { 95 | if o.stateSetExplicitly { 96 | // nolint: gocritic 97 | if o.criticals > 0 { 98 | return check.Critical 99 | } else if o.unknowns > 0 { 100 | return check.Unknown 101 | } else if o.warnings > 0 { 102 | return check.Warning 103 | } else if o.oks > 0 { 104 | return check.OK 105 | } 106 | 107 | return check.Unknown 108 | } 109 | 110 | // state not set explicitly! 111 | if len(o.PartialResults) == 0 { 112 | return check.Unknown 113 | } 114 | 115 | var ( 116 | criticals int 117 | warnings int 118 | oks int 119 | unknowns int 120 | ) 121 | 122 | for _, sc := range o.PartialResults { 123 | switch sc.GetStatus() { 124 | case check.Critical: 125 | criticals++ 126 | case check.Warning: 127 | warnings++ 128 | case check.Unknown: 129 | unknowns++ 130 | case check.OK: 131 | oks++ 132 | } 133 | } 134 | 135 | if criticals > 0 { 136 | return check.Critical 137 | } 138 | 139 | if unknowns > 0 { 140 | return check.Unknown 141 | } 142 | 143 | if warnings > 0 { 144 | return check.Warning 145 | } 146 | 147 | if oks > 0 { 148 | return check.OK 149 | } 150 | 151 | return check.Unknown 152 | } 153 | 154 | // GetSummary returns a text representation of the current state of the Overall 155 | // nolint: funlen 156 | func (o *Overall) GetSummary() string { 157 | if o.Summary != "" { 158 | return o.Summary 159 | } 160 | 161 | // Was the state set explicitly? 162 | if o.stateSetExplicitly { 163 | // Yes, so lets generate it from the sum of the overall states 164 | if o.criticals > 0 { 165 | o.Summary += fmt.Sprintf("critical=%d ", o.criticals) 166 | } 167 | 168 | if o.unknowns > 0 { 169 | o.Summary += fmt.Sprintf("unknown=%d ", o.unknowns) 170 | } 171 | 172 | if o.warnings > 0 { 173 | o.Summary += fmt.Sprintf("warning=%d ", o.warnings) 174 | } 175 | 176 | if o.oks > 0 { 177 | o.Summary += fmt.Sprintf("ok=%d ", o.oks) 178 | } 179 | 180 | if o.Summary == "" { 181 | o.Summary = "No status information" 182 | return o.Summary 183 | } 184 | } 185 | 186 | if !o.stateSetExplicitly { 187 | // No, so lets combine the partial ones 188 | if len(o.PartialResults) == 0 { 189 | // Oh, we actually don't have those either 190 | o.Summary = "No status information" 191 | return o.Summary 192 | } 193 | 194 | var ( 195 | criticals int 196 | warnings int 197 | oks int 198 | unknowns int 199 | ) 200 | 201 | for _, sc := range o.PartialResults { 202 | switch sc.GetStatus() { 203 | case check.Critical: 204 | criticals++ 205 | case check.Warning: 206 | warnings++ 207 | case check.Unknown: 208 | unknowns++ 209 | case check.OK: 210 | oks++ 211 | } 212 | } 213 | 214 | if criticals > 0 { 215 | o.Summary += fmt.Sprintf("critical=%d ", criticals) 216 | } 217 | 218 | if unknowns > 0 { 219 | o.Summary += fmt.Sprintf("unknowns=%d ", unknowns) 220 | } 221 | 222 | if warnings > 0 { 223 | o.Summary += fmt.Sprintf("warning=%d ", warnings) 224 | } 225 | 226 | if oks > 0 { 227 | o.Summary += fmt.Sprintf("ok=%d ", oks) 228 | } 229 | } 230 | 231 | o.Summary = "states: " + strings.TrimSpace(o.Summary) 232 | 233 | return o.Summary 234 | } 235 | 236 | // GetOutput returns a text representation of the current outputs of the Overall 237 | func (o *Overall) GetOutput() string { 238 | var output strings.Builder 239 | 240 | output.WriteString(o.GetSummary() + "\n") 241 | 242 | for _, extra := range o.Outputs { 243 | output.WriteString(extra + "\n") 244 | } 245 | 246 | if o.PartialResults != nil { 247 | var pdata strings.Builder 248 | 249 | // Generate indeted output and perfdata for all partialResults 250 | for i := range o.PartialResults { 251 | output.WriteString(o.PartialResults[i].getOutput(0)) 252 | pdata.WriteString(" " + o.PartialResults[i].getPerfdata()) 253 | } 254 | 255 | pdataString := strings.Trim(pdata.String(), " ") 256 | 257 | if len(pdataString) > 0 { 258 | output.WriteString("|" + pdataString + "\n") 259 | } 260 | } 261 | 262 | return output.String() 263 | } 264 | 265 | // SetDefaultState sets a new default state for a PartialResult 266 | func (s *PartialResult) SetDefaultState(state int) error { 267 | if state < check.OK || state > check.Unknown { 268 | return errors.New("Default State is not a valid result state. Got " + strconv.Itoa(state) + " which is not valid") 269 | } 270 | 271 | s.defaultState = state 272 | s.defaultStateSet = true 273 | 274 | return nil 275 | } 276 | 277 | // SetState sets a state for a PartialResult 278 | func (s *PartialResult) SetState(state int) error { 279 | if state < check.OK || state > check.Unknown { 280 | return errors.New("Default State is not a valid result state. Got " + strconv.Itoa(state) + " which is not valid") 281 | } 282 | 283 | s.state = state 284 | s.stateSetExplicitly = true 285 | 286 | return nil 287 | } 288 | 289 | // GetStatus returns the current state (ok, warning, critical, unknown) of the PartialResult 290 | // nolint: unused 291 | func (s *PartialResult) GetStatus() int { 292 | if s.stateSetExplicitly { 293 | return s.state 294 | } 295 | 296 | if len(s.PartialResults) == 0 { 297 | if s.defaultStateSet { 298 | return s.defaultState 299 | } 300 | 301 | return check.Unknown 302 | } 303 | 304 | states := make([]int, len(s.PartialResults)) 305 | 306 | for i := range s.PartialResults { 307 | states[i] = s.PartialResults[i].GetStatus() 308 | } 309 | 310 | return WorstState(states...) 311 | } 312 | 313 | // getPerfdata returns all subsequent perfdata as a concatenated string 314 | func (s *PartialResult) getPerfdata() string { 315 | var output strings.Builder 316 | 317 | if len(s.Perfdata) > 0 { 318 | output.WriteString(s.Perfdata.String()) 319 | } 320 | 321 | if s.PartialResults != nil { 322 | for _, ss := range s.PartialResults { 323 | output.WriteString(" " + ss.getPerfdata()) 324 | } 325 | } 326 | 327 | return strings.TrimSpace(output.String()) 328 | } 329 | 330 | // getOutput generates indented output for all subsequent PartialResults 331 | func (s *PartialResult) getOutput(indentLevel int) string { 332 | var output strings.Builder 333 | 334 | prefix := strings.Repeat(" ", indentLevel) 335 | output.WriteString(prefix + "\\_ " + s.String() + "\n") 336 | 337 | if s.PartialResults != nil { 338 | for _, ss := range s.PartialResults { 339 | output.WriteString(ss.getOutput(indentLevel + indentationOffset)) 340 | } 341 | } 342 | 343 | return output.String() 344 | } 345 | -------------------------------------------------------------------------------- /result/overall_test.go: -------------------------------------------------------------------------------- 1 | package result 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/NETWAYS/go-check" 9 | "github.com/NETWAYS/go-check/perfdata" 10 | ) 11 | 12 | func TestOverall_AddOK(t *testing.T) { 13 | overall := Overall{} 14 | overall.Add(0, "test ok") 15 | 16 | if overall.oks != 1 { 17 | t.Fatalf("expected 1, got %d", overall.oks) 18 | } 19 | 20 | expectedOutputs := []string{"[OK] test ok"} 21 | if !reflect.DeepEqual(overall.Outputs, expectedOutputs) { 22 | t.Fatalf("expected %v, got %v", expectedOutputs, overall.Outputs) 23 | } 24 | } 25 | 26 | func TestOverall_AddWarning(t *testing.T) { 27 | overall := Overall{} 28 | overall.Add(1, "test warning") 29 | 30 | if overall.warnings != 1 { 31 | t.Fatalf("expected 1, got %d", overall.warnings) 32 | } 33 | 34 | expectedOutputs := []string{"[WARNING] test warning"} 35 | if !reflect.DeepEqual(overall.Outputs, expectedOutputs) { 36 | t.Fatalf("expected %v, got %v", expectedOutputs, overall.Outputs) 37 | } 38 | } 39 | 40 | func TestOverall_AddCritical(t *testing.T) { 41 | overall := Overall{} 42 | overall.Add(2, "test critical") 43 | 44 | if overall.criticals != 1 { 45 | t.Fatalf("expected 1, got %d", overall.criticals) 46 | } 47 | 48 | expectedOutputs := []string{"[CRITICAL] test critical"} 49 | if !reflect.DeepEqual(overall.Outputs, expectedOutputs) { 50 | t.Fatalf("expected %v, got %v", expectedOutputs, overall.Outputs) 51 | } 52 | } 53 | 54 | func TestOverall_AddUnknown(t *testing.T) { 55 | overall := Overall{} 56 | overall.Add(3, "test unknown") 57 | 58 | if overall.unknowns != 1 { 59 | t.Fatalf("expected 1, got %d", overall.unknowns) 60 | } 61 | 62 | expectedOutputs := []string{"[UNKNOWN] test unknown"} 63 | if !reflect.DeepEqual(overall.Outputs, expectedOutputs) { 64 | t.Fatalf("expected %v, got %v", expectedOutputs, overall.Outputs) 65 | } 66 | } 67 | 68 | func TestOverall_GetStatus_GetSummary(t *testing.T) { 69 | testcases := []struct { 70 | actual Overall 71 | expectedSummary string 72 | expectedStatus int 73 | }{ 74 | { 75 | actual: Overall{}, 76 | expectedSummary: "No status information", 77 | expectedStatus: 3, 78 | }, 79 | { 80 | actual: Overall{oks: 1, stateSetExplicitly: true}, 81 | expectedSummary: "states: ok=1", 82 | expectedStatus: 0, 83 | }, 84 | { 85 | actual: Overall{criticals: 2, oks: 1, warnings: 2, unknowns: 1, stateSetExplicitly: true}, 86 | expectedSummary: "states: critical=2 unknown=1 warning=2 ok=1", 87 | expectedStatus: 2, 88 | }, 89 | { 90 | actual: Overall{unknowns: 2, oks: 1, warnings: 2, stateSetExplicitly: true}, 91 | expectedSummary: "states: unknown=2 warning=2 ok=1", 92 | expectedStatus: 3, 93 | }, 94 | { 95 | actual: Overall{oks: 1, warnings: 2, stateSetExplicitly: true}, 96 | expectedSummary: "states: warning=2 ok=1", 97 | expectedStatus: 1, 98 | }, 99 | { 100 | actual: Overall{Summary: "foobar"}, 101 | expectedSummary: "foobar", 102 | expectedStatus: 3, 103 | }, 104 | } 105 | 106 | for _, test := range testcases { 107 | if test.expectedStatus != test.actual.GetStatus() { 108 | t.Fatalf("expected %d, got %d", test.expectedStatus, test.actual.GetStatus()) 109 | } 110 | 111 | if test.expectedSummary != test.actual.GetSummary() { 112 | t.Fatalf("expected %s, got %s", test.expectedSummary, test.actual.GetSummary()) 113 | } 114 | } 115 | } 116 | 117 | func TestOverall_GetOutput(t *testing.T) { 118 | var overall Overall 119 | 120 | overall = Overall{} 121 | overall.Add(0, "First OK") 122 | overall.Add(0, "Second OK") 123 | 124 | expected := "states: ok=2\n[OK] First OK\n[OK] Second OK\n" 125 | 126 | if expected != overall.GetOutput() { 127 | t.Fatalf("expected %s, got %s", expected, overall.GetOutput()) 128 | } 129 | 130 | overall = Overall{} 131 | overall.Add(0, "State OK") 132 | 133 | expected = "states: ok=1\n[OK] State OK\n" 134 | 135 | if expected != overall.GetOutput() { 136 | t.Fatalf("expected %s, got %s", expected, overall.GetOutput()) 137 | } 138 | 139 | // TODO: compress when only one state 140 | overall = Overall{} 141 | overall.Add(0, "First OK") 142 | overall.Add(2, "Second Critical") 143 | overall.Summary = "Custom Summary" 144 | 145 | expected = "Custom Summary\n[OK] First OK\n[CRITICAL] Second Critical\n" 146 | 147 | if expected != overall.GetOutput() { 148 | t.Fatalf("expected %s, got %s", expected, overall.GetOutput()) 149 | } 150 | } 151 | 152 | func ExampleOverall_Add() { 153 | overall := Overall{} 154 | overall.Add(check.OK, "One element is good") 155 | overall.Add(check.Critical, "The other is critical") 156 | 157 | fmt.Printf("%#v\n", overall) 158 | // Output: result.Overall{oks:1, warnings:0, criticals:1, unknowns:0, Summary:"", stateSetExplicitly:true, Outputs:[]string{"[OK] One element is good", "[CRITICAL] The other is critical"}, PartialResults:[]result.PartialResult(nil)} 159 | } 160 | 161 | func ExampleOverall_GetOutput() { 162 | overall := Overall{} 163 | overall.Add(check.OK, "One element is good") 164 | overall.Add(check.Critical, "The other is critical") 165 | 166 | fmt.Println(overall.GetOutput()) 167 | // Output: 168 | // states: critical=1 ok=1 169 | // [OK] One element is good 170 | // [CRITICAL] The other is critical 171 | } 172 | 173 | func ExampleOverall_GetStatus() { 174 | overall := Overall{} 175 | overall.Add(check.OK, "One element is good") 176 | overall.Add(check.Critical, "The other is critical") 177 | 178 | fmt.Println(overall.GetStatus()) 179 | // Output: 2 180 | } 181 | 182 | func ExampleOverall_withSubchecks() { 183 | var overall Overall 184 | 185 | example_perfdata := perfdata.Perfdata{Label: "pd_test", Value: 5, Uom: "s"} 186 | pd_list := perfdata.PerfdataList{} 187 | pd_list.Add(&example_perfdata) 188 | 189 | subcheck := PartialResult{ 190 | Output: "Subcheck1 Test", 191 | Perfdata: pd_list, 192 | } 193 | 194 | subcheck.SetState(check.OK) 195 | 196 | overall.AddSubcheck(subcheck) 197 | overall.Add(0, "bla") 198 | 199 | fmt.Println(overall.GetOutput()) 200 | // Output: 201 | // states: ok=1 202 | // [OK] bla 203 | // \_ [OK] Subcheck1 Test 204 | // |pd_test=5s 205 | } 206 | 207 | func TestOverall_withEnhancedSubchecks(t *testing.T) { 208 | var overall Overall 209 | 210 | example_perfdata := perfdata.Perfdata{Label: "pd_test", Value: 5, Uom: "s"} 211 | example_perfdata2 := perfdata.Perfdata{ 212 | Label: "pd_test2", 213 | Value: 1099511627776, 214 | Uom: "kB", 215 | Warn: &check.Threshold{Inside: true, Lower: 3.14, Upper: 0x66666666666}, 216 | Crit: &check.Threshold{Inside: false, Lower: 07777777777777, Upper: 0xFFFFFFFFFFFFFFFFFFFF}, 217 | Max: uint64(18446744073709551615), 218 | } 219 | example_perfdata3 := perfdata.Perfdata{Label: "kl;jr2if;l2rkjasdf", Value: 5, Uom: "m"} 220 | example_perfdata4 := perfdata.Perfdata{Label: "asdf", Value: uint64(18446744073709551615), Uom: "B"} 221 | 222 | pd_list := perfdata.PerfdataList{} 223 | pd_list.Add(&example_perfdata) 224 | pd_list.Add(&example_perfdata2) 225 | 226 | pd_list2 := perfdata.PerfdataList{} 227 | pd_list2.Add(&example_perfdata3) 228 | pd_list2.Add(&example_perfdata4) 229 | 230 | subcheck := PartialResult{ 231 | Output: "Subcheck1 Test", 232 | Perfdata: pd_list, 233 | } 234 | 235 | subcheck.SetState(check.OK) 236 | 237 | subcheck2 := PartialResult{ 238 | Output: "Subcheck2 Test", 239 | Perfdata: pd_list2, 240 | } 241 | 242 | subcheck2.SetState(check.Warning) 243 | 244 | overall.AddSubcheck(subcheck) 245 | overall.AddSubcheck(subcheck2) 246 | 247 | resString := overall.GetOutput() 248 | //nolint:lll 249 | expectedString := `states: warning=1 ok=1 250 | \_ [OK] Subcheck1 Test 251 | \_ [WARNING] Subcheck2 Test 252 | |pd_test=5s pd_test2=1099511627776kB;@3.14:7036874417766;549755813887:1208925819614629174706176;;18446744073709551615 kl;jr2if;l2rkjasdf=5m asdf=18446744073709551615B 253 | ` 254 | 255 | if expectedString != resString { 256 | t.Fatalf("expected %s, got %s", expectedString, resString) 257 | } 258 | 259 | if check.Warning != overall.GetStatus() { 260 | t.Fatalf("expected %d, got %d", check.Warning, overall.GetStatus()) 261 | } 262 | } 263 | 264 | func TestOverall_withSubchecks_Simple_Output(t *testing.T) { 265 | var overall Overall 266 | 267 | subcheck2 := PartialResult{ 268 | Output: "SubSubcheck", 269 | } 270 | 271 | subcheck2.SetState(check.OK) 272 | 273 | subcheck := PartialResult{ 274 | Output: "PartialResult", 275 | } 276 | 277 | subcheck.SetState(check.OK) 278 | 279 | subcheck.PartialResults = append(subcheck.PartialResults, subcheck2) 280 | 281 | overall.AddSubcheck(subcheck) 282 | 283 | output := overall.GetOutput() 284 | 285 | resString := `states: ok=1 286 | \_ [OK] PartialResult 287 | \_ [OK] SubSubcheck 288 | ` 289 | 290 | if output != resString { 291 | t.Fatalf("expected %s, got %s", output, resString) 292 | } 293 | } 294 | 295 | func TestOverall_withSubchecks_Perfdata(t *testing.T) { 296 | var overall Overall 297 | 298 | subcheck2 := PartialResult{ 299 | Output: "SubSubcheck", 300 | } 301 | 302 | subcheck2.SetState(check.OK) 303 | 304 | subcheck := PartialResult{ 305 | Output: "PartialResult", 306 | } 307 | 308 | subcheck.SetState(check.OK) 309 | 310 | perf1 := perfdata.Perfdata{ 311 | Label: "foo", 312 | Value: 3, 313 | } 314 | perf2 := perfdata.Perfdata{ 315 | Label: "bar", 316 | Value: 300, 317 | Uom: "%", 318 | } 319 | 320 | subcheck2.Perfdata.Add(&perf1) 321 | subcheck2.Perfdata.Add(&perf2) 322 | subcheck.PartialResults = append(subcheck.PartialResults, subcheck2) 323 | 324 | overall.AddSubcheck(subcheck) 325 | 326 | res := `states: ok=1 327 | \_ [OK] PartialResult 328 | \_ [OK] SubSubcheck 329 | |foo=3 bar=300% 330 | ` 331 | 332 | if res != overall.GetOutput() { 333 | t.Fatalf("expected %s, got %s", res, overall.GetOutput()) 334 | } 335 | 336 | if 0 != overall.GetStatus() { 337 | t.Fatalf("expected %d, got %d", 0, overall.GetStatus()) 338 | } 339 | } 340 | 341 | func TestOverall_withSubchecks_PartialResult(t *testing.T) { 342 | var overall Overall 343 | 344 | subcheck3 := PartialResult{ 345 | Output: "SubSubSubcheck", 346 | } 347 | 348 | subcheck3.SetState(check.Critical) 349 | 350 | subcheck2 := PartialResult{ 351 | Output: "SubSubcheck", 352 | } 353 | 354 | subcheck := PartialResult{ 355 | Output: "PartialResult", 356 | } 357 | 358 | perf1 := perfdata.Perfdata{ 359 | Label: "foo", 360 | Value: 3, 361 | } 362 | perf2 := perfdata.Perfdata{ 363 | Label: "bar", 364 | Value: 300, 365 | Uom: "%", 366 | } 367 | perf3 := perfdata.Perfdata{ 368 | Label: "baz", 369 | Value: 23, 370 | Uom: "B", 371 | } 372 | 373 | subcheck3.Perfdata.Add(&perf3) 374 | subcheck2.Perfdata.Add(&perf1) 375 | subcheck2.Perfdata.Add(&perf2) 376 | subcheck2.PartialResults = append(subcheck.PartialResults, subcheck3) 377 | subcheck.PartialResults = append(subcheck.PartialResults, subcheck2) 378 | 379 | overall.AddSubcheck(subcheck) 380 | 381 | res := `states: critical=1 382 | \_ [CRITICAL] PartialResult 383 | \_ [CRITICAL] SubSubcheck 384 | \_ [CRITICAL] SubSubSubcheck 385 | |foo=3 bar=300% baz=23B 386 | ` 387 | 388 | if res != overall.GetOutput() { 389 | t.Fatalf("expected %s, got %s", res, overall.GetOutput()) 390 | } 391 | 392 | if check.Critical != overall.GetStatus() { 393 | t.Fatalf("expected %d, got %d", 2, overall.GetStatus()) 394 | } 395 | } 396 | 397 | func TestOverall_withSubchecks_PartialResultStatus(t *testing.T) { 398 | var overall Overall 399 | 400 | subcheck := PartialResult{ 401 | Output: "Subcheck", 402 | } 403 | 404 | subcheck.SetState(check.OK) 405 | 406 | subsubcheck := PartialResult{ 407 | Output: "SubSubcheck", 408 | } 409 | 410 | subsubcheck.SetState(check.Warning) 411 | 412 | subsubsubcheck := PartialResult{ 413 | Output: "SubSubSubcheck", 414 | } 415 | 416 | subsubsubcheck.SetState(check.Critical) 417 | 418 | subsubcheck.AddSubcheck(subsubsubcheck) 419 | subcheck.AddSubcheck(subsubcheck) 420 | overall.AddSubcheck(subcheck) 421 | 422 | res := `states: ok=1 423 | \_ [OK] Subcheck 424 | \_ [WARNING] SubSubcheck 425 | \_ [CRITICAL] SubSubSubcheck 426 | ` 427 | 428 | if res != overall.GetOutput() { 429 | t.Fatalf("expected %s, got %s", res, overall.GetOutput()) 430 | } 431 | 432 | if 0 != overall.GetStatus() { 433 | t.Fatalf("expected %d, got %d", 0, overall.GetStatus()) 434 | } 435 | } 436 | 437 | func TestSubchecksPerfdata(t *testing.T) { 438 | var overall Overall 439 | 440 | check1 := PartialResult{ 441 | Output: "Check1", 442 | Perfdata: perfdata.PerfdataList{ 443 | &perfdata.Perfdata{ 444 | Label: "foo", 445 | Value: 23, 446 | }, 447 | &perfdata.Perfdata{ 448 | Label: "bar", 449 | Value: 42, 450 | }, 451 | }, 452 | } 453 | 454 | check1.SetState(check.OK) 455 | 456 | check2 := PartialResult{ 457 | Output: "Check2", 458 | Perfdata: perfdata.PerfdataList{ 459 | &perfdata.Perfdata{ 460 | Label: "foo2 bar", 461 | Value: 46, 462 | }, 463 | }, 464 | } 465 | 466 | check2.SetState(check.Warning) 467 | 468 | overall.AddSubcheck(check1) 469 | overall.AddSubcheck(check2) 470 | 471 | resultString := "states: warning=1 ok=1\n\\_ [OK] Check1\n\\_ [WARNING] Check2\n|foo=23 bar=42 'foo2 bar'=46\n" 472 | 473 | if resultString != overall.GetOutput() { 474 | t.Fatalf("expected %s, got %s", resultString, overall.GetOutput()) 475 | } 476 | } 477 | 478 | func TestDefaultStates1(t *testing.T) { 479 | var overall Overall 480 | 481 | subcheck := PartialResult{} 482 | 483 | subcheck.SetDefaultState(check.OK) 484 | 485 | overall.AddSubcheck(subcheck) 486 | 487 | if check.OK != overall.GetStatus() { 488 | t.Fatalf("expected %d, got %d", check.OK, overall.GetStatus()) 489 | } 490 | } 491 | 492 | func TestDefaultStates2(t *testing.T) { 493 | var overall Overall 494 | 495 | subcheck := PartialResult{} 496 | 497 | overall.AddSubcheck(subcheck) 498 | 499 | if check.Unknown != subcheck.GetStatus() { 500 | t.Fatalf("expected %d, got %d", check.Unknown, subcheck.GetStatus()) 501 | } 502 | 503 | if check.Unknown != overall.GetStatus() { 504 | t.Fatalf("expected %d, got %d", check.Unknown, overall.GetStatus()) 505 | } 506 | } 507 | 508 | func TestDefaultStates3(t *testing.T) { 509 | var overall Overall 510 | 511 | subcheck := PartialResult{} 512 | subcheck.SetDefaultState(check.OK) 513 | 514 | subcheck.SetState(check.Warning) 515 | 516 | overall.AddSubcheck(subcheck) 517 | 518 | if check.Warning != overall.GetStatus() { 519 | t.Fatalf("expected %d, got %d", check.Warning, overall.GetStatus()) 520 | } 521 | } 522 | 523 | func TestOverallOutputWithMultiLayerPartials(t *testing.T) { 524 | var overall Overall 525 | 526 | subcheck1 := PartialResult{} 527 | subcheck1.SetState(check.Warning) 528 | 529 | subcheck2 := PartialResult{} 530 | 531 | subcheck2_1 := PartialResult{} 532 | subcheck2_1.SetState(check.OK) 533 | 534 | subcheck2_2 := PartialResult{} 535 | subcheck2_2.SetState(check.Critical) 536 | 537 | subcheck2.AddSubcheck(subcheck2_1) 538 | subcheck2.AddSubcheck(subcheck2_2) 539 | 540 | overall.AddSubcheck(subcheck1) 541 | overall.AddSubcheck(subcheck2) 542 | 543 | resultString := "states: critical=1 warning=1\n\\_ [WARNING] \n\\_ [CRITICAL] \n \\_ [OK] \n \\_ [CRITICAL] \n" 544 | 545 | if resultString != overall.GetOutput() { 546 | t.Fatalf("expected %s, got %s", resultString, overall.GetOutput()) 547 | } 548 | 549 | if check.Critical != overall.GetStatus() { 550 | t.Fatalf("expected %d, got %d", check.Critical, overall.GetStatus()) 551 | } 552 | } 553 | -------------------------------------------------------------------------------- /result/worst.go: -------------------------------------------------------------------------------- 1 | package result 2 | 3 | import "github.com/NETWAYS/go-check" 4 | 5 | // Determines the worst state from a list of states 6 | // 7 | // Helps combining an overall states, only based on a 8 | // few numbers for various checks. 9 | // 10 | // Order of preference: Critical, Unknown, Warning, Ok 11 | func WorstState(states ...int) int { 12 | overall := -1 13 | // nolint: gocritic 14 | for _, state := range states { 15 | if state == check.Critical { 16 | overall = check.Critical 17 | } else if state == check.Unknown { 18 | if overall != check.Critical { 19 | overall = check.Unknown 20 | } 21 | } else if state > overall { 22 | overall = state 23 | } 24 | } 25 | 26 | if overall < 0 || overall > 3 { 27 | overall = check.Unknown 28 | } 29 | 30 | return overall 31 | } 32 | -------------------------------------------------------------------------------- /result/worst_test.go: -------------------------------------------------------------------------------- 1 | package result 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestWorstState2(t *testing.T) { 8 | if WorstState(3) != 3 { 9 | t.Fatalf("expected 3, got %d", WorstState(3)) 10 | } 11 | if WorstState(2) != 2 { 12 | t.Fatalf("expected 2, got %d", WorstState(2)) 13 | } 14 | if WorstState(1) != 1 { 15 | t.Fatalf("expected 1, got %d", WorstState(1)) 16 | } 17 | if WorstState(0) != 0 { 18 | t.Fatalf("expected 0, got %d", WorstState(0)) 19 | } 20 | 21 | if WorstState(0, 1, 2, 3) != 2 { 22 | t.Fatalf("expected 2, got %d", WorstState(0, 1, 2, 3)) 23 | } 24 | if WorstState(0, 1, 3) != 3 { 25 | t.Fatalf("expected 3, got %d", WorstState(0, 1, 3)) 26 | } 27 | if WorstState(1, 0, 0) != 1 { 28 | t.Fatalf("expected 1, got %d", WorstState(1, 0, 0)) 29 | } 30 | if WorstState(0, 0, 0) != 0 { 31 | t.Fatalf("expected 0, got %d", WorstState(0, 0, 0)) 32 | } 33 | 34 | if WorstState(-1) != 3 { 35 | t.Fatalf("expected 3, got %d", WorstState(-1)) 36 | } 37 | if WorstState(4) != 3 { 38 | t.Fatalf("expected 3, got %d", WorstState(4)) 39 | } 40 | } 41 | 42 | func BenchmarkWorstState(b *testing.B) { 43 | b.ReportAllocs() 44 | 45 | // Initialize slice for benchmarking 46 | states := make([]int, 0, 100) 47 | for i := 0; i < 100; i++ { 48 | states = append(states, i%4) 49 | } 50 | 51 | for i := 0; i < b.N; i++ { 52 | s := WorstState(states...) 53 | _ = s 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /status.go: -------------------------------------------------------------------------------- 1 | package check 2 | 3 | const ( 4 | // OK means everything is fine 5 | OK = 0 6 | OKString = "OK" 7 | // Warning means there is a problem the admin should review 8 | Warning = 1 9 | WarningString = "WARNING" 10 | // Critical means there is a problem that requires immediate action 11 | Critical = 2 12 | CriticalString = "CRITICAL" 13 | // Unknown means the status can not be determined, probably due to an error or something missing 14 | Unknown = 3 15 | UnknownString = "UNKNOWN" 16 | ) 17 | 18 | // StatusText returns the string corresponding to a state 19 | func StatusText(status int) string { 20 | switch status { 21 | case OK: 22 | return OKString 23 | case Warning: 24 | return WarningString 25 | case Critical: 26 | return CriticalString 27 | case Unknown: 28 | } 29 | 30 | return UnknownString 31 | } 32 | -------------------------------------------------------------------------------- /status_test.go: -------------------------------------------------------------------------------- 1 | package check 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestStatusText(t *testing.T) { 8 | testcases := map[string]struct { 9 | input int 10 | expected string 11 | }{ 12 | "OK": { 13 | input: 0, 14 | expected: "OK", 15 | }, 16 | "WARNING": { 17 | input: 1, 18 | expected: "WARNING", 19 | }, 20 | "CRITICAL": { 21 | input: 2, 22 | expected: "CRITICAL", 23 | }, 24 | "UNKNOWN": { 25 | input: 3, 26 | expected: "UNKNOWN", 27 | }, 28 | } 29 | 30 | for name, tc := range testcases { 31 | t.Run(name, func(t *testing.T) { 32 | actual := StatusText(tc.input) 33 | 34 | if actual != tc.expected { 35 | t.Fatalf("expected %v, got %v", tc.expected, actual) 36 | } 37 | }) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /testhelper/for_main.go: -------------------------------------------------------------------------------- 1 | package testhelper 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "os" 7 | 8 | "github.com/NETWAYS/go-check" 9 | ) 10 | 11 | // Execute main function from a main package, while capturing its stdout 12 | // 13 | // You will need to pass `main`, since the function won't have access to the package's namespace 14 | func RunMainTest(f func(), args ...string) string { 15 | base := []string{"check_with_go_test"} 16 | origArgs := os.Args 17 | 18 | os.Args = append(base, args...) //nolint: gocritic 19 | stdout := CaptureStdout(f) 20 | os.Args = origArgs 21 | 22 | return stdout 23 | } 24 | 25 | // Enable test mode by disabling the default exit behavior, so go test won't fail with plugin states 26 | func EnableTestMode() { 27 | // disable actual exit 28 | check.AllowExit = false 29 | 30 | // disable stack trace for the example 31 | check.PrintStack = false 32 | } 33 | 34 | // Disable test mode behavior again 35 | // 36 | // Optional after testing has been done 37 | func DisableTestMode() { 38 | // disable actual exit 39 | check.AllowExit = true 40 | 41 | // disable stack trace for the example 42 | check.PrintStack = true 43 | } 44 | 45 | // Capture the output of the go program while running function f 46 | // 47 | // Source https://gist.github.com/mindscratch/0faa78bd3c0005d080bf 48 | func CaptureStdout(f func()) string { 49 | old := os.Stdout 50 | r, w, _ := os.Pipe() 51 | os.Stdout = w 52 | 53 | f() 54 | 55 | w.Close() 56 | 57 | os.Stdout = old 58 | 59 | var buf bytes.Buffer 60 | _, _ = io.Copy(&buf, r) 61 | 62 | return buf.String() 63 | } 64 | -------------------------------------------------------------------------------- /threshold.go: -------------------------------------------------------------------------------- 1 | package check 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "regexp" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | // Defining a threshold for any numeric value 12 | // 13 | // Format: [@]start:end 14 | // 15 | // Threshold Generate an alert if x... 16 | // 10 < 0 or > 10, (outside the range of {0 .. 10}) 17 | // 10: < 10, (outside {10 .. ∞}) 18 | // ~:10 > 10, (outside the range of {-∞ .. 10}) 19 | // 10:20 < 10 or > 20, (outside the range of {10 .. 20}) 20 | // @10:20 ≥ 10 and ≤ 20, (inside the range of {10 .. 20}) 21 | // 22 | // Reference: https://www.monitoring-plugins.org/doc/guidelines.html#THRESHOLDFORMAT 23 | type Threshold struct { 24 | Inside bool 25 | Lower float64 26 | Upper float64 27 | } 28 | 29 | var ( 30 | thresholdNumberRe = regexp.MustCompile(`(-?\d+(?:\.\d+)?|~)`) 31 | thresholdRe = regexp.MustCompile(fmt.Sprintf(`^(@)?(?:%s:)?(?:%s)?$`, 32 | thresholdNumberRe.String(), thresholdNumberRe.String())) 33 | PosInf = math.Inf(1) 34 | NegInf = math.Inf(-1) 35 | ) 36 | 37 | // Parse a Threshold from a string. 38 | // 39 | // See Threshold for details. 40 | func ParseThreshold(spec string) (t *Threshold, err error) { 41 | t = &Threshold{} 42 | 43 | parts := thresholdRe.FindStringSubmatch(spec) 44 | if spec == "" || len(parts) == 0 { 45 | err = fmt.Errorf("could not parse threshold: %s", spec) 46 | return 47 | } 48 | 49 | // @ at the beginning 50 | if parts[1] != "" { 51 | t.Inside = true 52 | } 53 | 54 | var v float64 55 | 56 | // Lower bound 57 | if parts[2] == "~" { 58 | t.Lower = NegInf 59 | } else if parts[2] != "" { 60 | v, err = strconv.ParseFloat(parts[2], 64) 61 | if err != nil { 62 | err = fmt.Errorf("can not parse lower bound '%s': %w", parts[2], err) 63 | return 64 | } 65 | 66 | t.Lower = v 67 | } 68 | 69 | // Upper bound 70 | if parts[3] == "~" || (parts[3] == "" && parts[2] != "") { 71 | t.Upper = PosInf 72 | } else if parts[3] != "" { 73 | v, err = strconv.ParseFloat(parts[3], 64) 74 | if err != nil { 75 | err = fmt.Errorf("can not parse upper bound '%s': %w", parts[3], err) 76 | return 77 | } 78 | 79 | t.Upper = v 80 | } 81 | 82 | return 83 | } 84 | 85 | // String returns the plain representation of the Threshold 86 | func (t Threshold) String() (s string) { 87 | s = BoundaryToString(t.Upper) 88 | 89 | // remove upper ~, which is the default 90 | if s == "~" { 91 | s = "" 92 | } 93 | 94 | if t.Lower != 0 { 95 | s = BoundaryToString(t.Lower) + ":" + s 96 | } 97 | 98 | if t.Inside { 99 | s = "@" + s 100 | } 101 | 102 | return 103 | } 104 | 105 | // Compares a value against the threshold, and returns true if the value violates the threshold. 106 | func (t Threshold) DoesViolate(value float64) bool { 107 | if t.Inside { 108 | return value >= t.Lower && value <= t.Upper 109 | } 110 | 111 | return value < t.Lower || value > t.Upper 112 | } 113 | 114 | // BoundaryToString returns the string representation of a Threshold boundary. 115 | func BoundaryToString(value float64) (s string) { 116 | s = FormatFloat(value) 117 | 118 | // In the threshold context, the sign derives from lower and upper bound, we only need the ~ notation 119 | if s == "+Inf" || s == "-Inf" { 120 | s = "~" 121 | } 122 | 123 | return 124 | } 125 | 126 | // FormatFloat returns a string representation of floats, avoiding scientific notation and removes trailing zeros. 127 | func FormatFloat(value float64) string { 128 | return strings.TrimRight(strings.TrimRight(fmt.Sprintf("%.3f", value), "0"), ".") // remove trailing 0 and trailing dot 129 | } 130 | -------------------------------------------------------------------------------- /threshold_test.go: -------------------------------------------------------------------------------- 1 | package check 2 | 3 | import ( 4 | "math" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | var testThresholds = map[string]*Threshold{ 10 | "10": {Lower: 0, Upper: 10}, 11 | "10:": {Lower: 10, Upper: PosInf}, 12 | "~:10": {Lower: NegInf, Upper: 10}, 13 | "10:20": {Lower: 10, Upper: 20}, 14 | "@10:20": {Lower: 10, Upper: 20, Inside: true}, 15 | "-10:10": {Lower: -10, Upper: 10}, 16 | "-10.001:10.001": {Lower: -10.001, Upper: 10.001}, 17 | "": nil, 18 | } 19 | 20 | func TestBoundaryToString(t *testing.T) { 21 | if BoundaryToString(10) != "10" { 22 | t.Fatalf("expected '10', got %s", BoundaryToString(10)) 23 | } 24 | if BoundaryToString(10.1) != "10.1" { 25 | t.Fatalf("expected '10.1', got %s", BoundaryToString(10.1)) 26 | } 27 | if BoundaryToString(10.001) != "10.001" { 28 | t.Fatalf("expected '10.001', got %s", BoundaryToString(10.001)) 29 | } 30 | } 31 | 32 | func TestParseThreshold(t *testing.T) { 33 | for spec, ref := range testThresholds { 34 | th, err := ParseThreshold(spec) 35 | 36 | if ref == nil { 37 | if err == nil { 38 | t.Errorf("Expected error, got nil") 39 | } 40 | } else { 41 | if err != nil { 42 | t.Fatalf("expected no error, got %v", err) 43 | } 44 | 45 | if !reflect.DeepEqual(ref, th) { 46 | t.Fatalf("expected %v, got %v for spec %s", ref, th, spec) 47 | } 48 | if th.String() != spec { 49 | t.Fatalf("expected %s, got %s for spec %s", spec, th.String(), spec) 50 | } 51 | } 52 | } 53 | } 54 | 55 | func TestThreshold_String(t *testing.T) { 56 | for spec, ref := range testThresholds { 57 | if ref != nil { 58 | if spec != ref.String() { 59 | t.Fatalf("expected %v, got %v", ref.String(), spec) 60 | } 61 | } 62 | } 63 | } 64 | 65 | // Threshold Generate an alert if x... 66 | // 10 < 0 or > 10, (outside the range of {0 .. 10}) 67 | // 10: < 10, (outside {10 .. ∞}) 68 | // ~:10 > 10, (outside the range of {-∞ .. 10}) 69 | // 10:20 < 10 or > 20, (outside the range of {10 .. 20}) 70 | // @10:20 ≥ 10 and ≤ 20, (inside the range of {10 .. 20}) 71 | func TestThreshold_DoesViolate(t *testing.T) { 72 | thr, err := ParseThreshold("10") 73 | if err != nil { 74 | t.Fatalf("expected no error, got %v", err) 75 | } 76 | if !thr.DoesViolate(11) { 77 | t.Fatalf("expected true, got false") 78 | } 79 | if thr.DoesViolate(10) { 80 | t.Fatalf("expected false, got true") 81 | } 82 | if thr.DoesViolate(0) { 83 | t.Fatalf("expected false, got true") 84 | } 85 | if !thr.DoesViolate(-1) { 86 | t.Fatalf("expected true, got false") 87 | } 88 | 89 | thr, err = ParseThreshold("10:") 90 | if err != nil { 91 | t.Fatalf("expected no error, got %v", err) 92 | } 93 | if thr.DoesViolate(3000) { 94 | t.Fatalf("expected false, got true") 95 | } 96 | if thr.DoesViolate(10) { 97 | t.Fatalf("expected false, got true") 98 | } 99 | if !thr.DoesViolate(9) { 100 | t.Fatalf("expected true, got false") 101 | } 102 | if !thr.DoesViolate(0) { 103 | t.Fatalf("expected true, got false") 104 | } 105 | if !thr.DoesViolate(-1) { 106 | t.Fatalf("expected true, got false") 107 | } 108 | 109 | thr, err = ParseThreshold("~:10") 110 | if err != nil { 111 | t.Fatalf("expected no error, got %v", err) 112 | } 113 | if thr.DoesViolate(-3000) { 114 | t.Fatalf("expected false, got true") 115 | } 116 | if thr.DoesViolate(0) { 117 | t.Fatalf("expected false, got true") 118 | } 119 | if thr.DoesViolate(10) { 120 | t.Fatalf("expected false, got true") 121 | } 122 | if !thr.DoesViolate(11) { 123 | t.Fatalf("expected true, got false") 124 | } 125 | if !thr.DoesViolate(3000) { 126 | t.Fatalf("expected true, got false") 127 | } 128 | 129 | thr, err = ParseThreshold("10:20") 130 | if err != nil { 131 | t.Fatalf("expected no error, got %v", err) 132 | } 133 | if thr.DoesViolate(10) { 134 | t.Fatalf("expected false, got true") 135 | } 136 | if thr.DoesViolate(15) { 137 | t.Fatalf("expected false, got true") 138 | } 139 | if thr.DoesViolate(20) { 140 | t.Fatalf("expected false, got true") 141 | } 142 | if !thr.DoesViolate(9) { 143 | t.Fatalf("expected true, got false") 144 | } 145 | if !thr.DoesViolate(-1) { 146 | t.Fatalf("expected true, got false") 147 | } 148 | if !thr.DoesViolate(20.1) { 149 | t.Fatalf("expected true, got false") 150 | } 151 | if !thr.DoesViolate(3000) { 152 | t.Fatalf("expected true, got false") 153 | } 154 | 155 | thr, err = ParseThreshold("@10:20") 156 | if err != nil { 157 | t.Fatalf("expected no error, got %v", err) 158 | } 159 | if !thr.DoesViolate(10) { 160 | t.Fatalf("expected true, got false") 161 | } 162 | if !thr.DoesViolate(15) { 163 | t.Fatalf("expected true, got false") 164 | } 165 | if !thr.DoesViolate(20) { 166 | t.Fatalf("expected true, got false") 167 | } 168 | if thr.DoesViolate(9) { 169 | t.Fatalf("expected false, got true") 170 | } 171 | if thr.DoesViolate(-1) { 172 | t.Fatalf("expected false, got true") 173 | } 174 | if thr.DoesViolate(20.1) { 175 | t.Fatalf("expected false, got true") 176 | } 177 | if thr.DoesViolate(3000) { 178 | t.Fatalf("expected false, got true") 179 | } 180 | } 181 | 182 | func TestFormatFloat(t *testing.T) { 183 | if FormatFloat(1000000000000) != "1000000000000" { 184 | t.Fatalf("expected '1000000000000', got %s", FormatFloat(1000000000000)) 185 | } 186 | if FormatFloat(1000000000) != "1000000000" { 187 | t.Fatalf("expected '1000000000', got %s", FormatFloat(1000000000)) 188 | } 189 | if FormatFloat(1234567890.9877) != "1234567890.988" { 190 | t.Fatalf("expected '1234567890.988', got %s", FormatFloat(1234567890.9877)) 191 | } 192 | if FormatFloat(math.Inf(-1)) != "-Inf" { 193 | t.Fatalf("expected '-Inf', got %s", FormatFloat(math.Inf(-1))) 194 | } 195 | if FormatFloat(math.Inf(1)) != "+Inf" { 196 | t.Fatalf("expected '+Inf', got %s", FormatFloat(math.Inf(1))) 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /timeout.go: -------------------------------------------------------------------------------- 1 | package check 2 | 3 | import ( 4 | "os" 5 | "os/signal" 6 | "syscall" 7 | "time" 8 | ) 9 | 10 | var timeoutEnabled bool 11 | 12 | // Start the timeout and signal handler in a goroutine 13 | func (c *Config) EnableTimeoutHandler() { 14 | go HandleTimeout(c.Timeout) 15 | } 16 | 17 | // Helper for a goroutine, to wait for signals and timeout, and exit with a proper code 18 | func HandleTimeout(timeout int) { 19 | if timeoutEnabled { 20 | // signal handling has already been set up 21 | return 22 | } 23 | 24 | signals := make(chan os.Signal, 1) 25 | signal.Notify(signals, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP) 26 | 27 | if timeout < 1 { 28 | Exitf(Unknown, "Invalid timeout: %d", timeout) 29 | } 30 | 31 | timedOut := time.After(time.Duration(timeout) * time.Second) 32 | timeoutEnabled = true 33 | 34 | select { 35 | case s := <-signals: 36 | Exitf(Unknown, "Received signal: %s", s) 37 | case <-timedOut: 38 | ExitRaw(Unknown, "Timeout reached") 39 | } 40 | } 41 | --------------------------------------------------------------------------------