├── .gitignore ├── LICENSE ├── README.md ├── README_CN.md ├── config.yml ├── requirements.txt └── ssh_exporter.py /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *.pyc 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | env/ 15 | py2env/ 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | .mypy_cache/ 31 | .idea/ 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *,cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # IPython Notebook 78 | .ipynb_checkpoints 79 | 80 | # pyenv 81 | .python-version 82 | 83 | # celery beat schedule file 84 | celerybeat-schedule 85 | 86 | # dotenv 87 | .env 88 | 89 | # virtualenv 90 | .venv/ 91 | venv/ 92 | ENV/ 93 | 94 | # Spyder project settings 95 | .spyderproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # Mac 101 | .DS_Store 102 | 103 | # vim 104 | *.swp 105 | 106 | # netCDF Files 107 | *.nc 108 | conda-requirements.txt 109 | 110 | tests/ 111 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 2.1, February 1999 3 | 4 | Copyright (C) 1991, 1999 Free Software Foundation, Inc. 5 | 51 Franklin Street, Suite 500, Boston, MA 02110-1335 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | [This is the first released version of the Lesser GPL. It also counts 10 | as the successor of the GNU Library Public License, version 2, hence 11 | the version number 2.1.] 12 | 13 | Preamble 14 | 15 | The licenses for most software are designed to take away your 16 | freedom to share and change it. By contrast, the GNU General Public 17 | Licenses are intended to guarantee your freedom to share and change 18 | free software--to make sure the software is free for all its users. 19 | 20 | This license, the Lesser General Public License, applies to some 21 | specially designated software packages--typically libraries--of the 22 | Free Software Foundation and other authors who decide to use it. You 23 | can use it too, but we suggest you first think carefully about whether 24 | this license or the ordinary General Public License is the better 25 | strategy to use in any particular case, based on the explanations below. 26 | 27 | When we speak of free software, we are referring to freedom of use, 28 | not price. Our General Public Licenses are designed to make sure that 29 | you have the freedom to distribute copies of free software (and charge 30 | for this service if you wish); that you receive source code or can get 31 | it if you want it; that you can change the software and use pieces of 32 | it in new free programs; and that you are informed that you can do 33 | these things. 34 | 35 | To protect your rights, we need to make restrictions that forbid 36 | distributors to deny you these rights or to ask you to surrender these 37 | rights. These restrictions translate to certain responsibilities for 38 | you if you distribute copies of the library or if you modify it. 39 | 40 | For example, if you distribute copies of the library, whether gratis 41 | or for a fee, you must give the recipients all the rights that we gave 42 | you. You must make sure that they, too, receive or can get the source 43 | code. If you link other code with the library, you must provide 44 | complete object files to the recipients, so that they can relink them 45 | with the library after making changes to the library and recompiling 46 | it. And you must show them these terms so they know their rights. 47 | 48 | We protect your rights with a two-step method: (1) we copyright the 49 | library, and (2) we offer you this license, which gives you legal 50 | permission to copy, distribute and/or modify the library. 51 | 52 | To protect each distributor, we want to make it very clear that 53 | there is no warranty for the free library. Also, if the library is 54 | modified by someone else and passed on, the recipients should know 55 | that what they have is not the original version, so that the original 56 | author's reputation will not be affected by problems that might be 57 | introduced by others. 58 | 59 | Finally, software patents pose a constant threat to the existence of 60 | any free program. We wish to make sure that a company cannot 61 | effectively restrict the users of a free program by obtaining a 62 | restrictive license from a patent holder. Therefore, we insist that 63 | any patent license obtained for a version of the library must be 64 | consistent with the full freedom of use specified in this license. 65 | 66 | Most GNU software, including some libraries, is covered by the 67 | ordinary GNU General Public License. This license, the GNU Lesser 68 | General Public License, applies to certain designated libraries, and 69 | is quite different from the ordinary General Public License. We use 70 | this license for certain libraries in order to permit linking those 71 | libraries into non-free programs. 72 | 73 | When a program is linked with a library, whether statically or using 74 | a shared library, the combination of the two is legally speaking a 75 | combined work, a derivative of the original library. The ordinary 76 | General Public License therefore permits such linking only if the 77 | entire combination fits its criteria of freedom. The Lesser General 78 | Public License permits more lax criteria for linking other code with 79 | the library. 80 | 81 | We call this license the "Lesser" General Public License because it 82 | does Less to protect the user's freedom than the ordinary General 83 | Public License. It also provides other free software developers Less 84 | of an advantage over competing non-free programs. These disadvantages 85 | are the reason we use the ordinary General Public License for many 86 | libraries. However, the Lesser license provides advantages in certain 87 | special circumstances. 88 | 89 | For example, on rare occasions, there may be a special need to 90 | encourage the widest possible use of a certain library, so that it becomes 91 | a de-facto standard. To achieve this, non-free programs must be 92 | allowed to use the library. A more frequent case is that a free 93 | library does the same job as widely used non-free libraries. In this 94 | case, there is little to gain by limiting the free library to free 95 | software only, so we use the Lesser General Public License. 96 | 97 | In other cases, permission to use a particular library in non-free 98 | programs enables a greater number of people to use a large body of 99 | free software. For example, permission to use the GNU C Library in 100 | non-free programs enables many more people to use the whole GNU 101 | operating system, as well as its variant, the GNU/Linux operating 102 | system. 103 | 104 | Although the Lesser General Public License is Less protective of the 105 | users' freedom, it does ensure that the user of a program that is 106 | linked with the Library has the freedom and the wherewithal to run 107 | that program using a modified version of the Library. 108 | 109 | The precise terms and conditions for copying, distribution and 110 | modification follow. Pay close attention to the difference between a 111 | "work based on the library" and a "work that uses the library". The 112 | former contains code derived from the library, whereas the latter must 113 | be combined with the library in order to run. 114 | 115 | GNU LESSER GENERAL PUBLIC LICENSE 116 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 117 | 118 | 0. This License Agreement applies to any software library or other 119 | program which contains a notice placed by the copyright holder or 120 | other authorized party saying it may be distributed under the terms of 121 | this Lesser General Public License (also called "this License"). 122 | Each licensee is addressed as "you". 123 | 124 | A "library" means a collection of software functions and/or data 125 | prepared so as to be conveniently linked with application programs 126 | (which use some of those functions and data) to form executables. 127 | 128 | The "Library", below, refers to any such software library or work 129 | which has been distributed under these terms. A "work based on the 130 | Library" means either the Library or any derivative work under 131 | copyright law: that is to say, a work containing the Library or a 132 | portion of it, either verbatim or with modifications and/or translated 133 | straightforwardly into another language. (Hereinafter, translation is 134 | included without limitation in the term "modification".) 135 | 136 | "Source code" for a work means the preferred form of the work for 137 | making modifications to it. For a library, complete source code means 138 | all the source code for all modules it contains, plus any associated 139 | interface definition files, plus the scripts used to control compilation 140 | and installation of the library. 141 | 142 | Activities other than copying, distribution and modification are not 143 | covered by this License; they are outside its scope. The act of 144 | running a program using the Library is not restricted, and output from 145 | such a program is covered only if its contents constitute a work based 146 | on the Library (independent of the use of the Library in a tool for 147 | writing it). Whether that is true depends on what the Library does 148 | and what the program that uses the Library does. 149 | 150 | 1. You may copy and distribute verbatim copies of the Library's 151 | complete source code as you receive it, in any medium, provided that 152 | you conspicuously and appropriately publish on each copy an 153 | appropriate copyright notice and disclaimer of warranty; keep intact 154 | all the notices that refer to this License and to the absence of any 155 | warranty; and distribute a copy of this License along with the 156 | Library. 157 | 158 | You may charge a fee for the physical act of transferring a copy, 159 | and you may at your option offer warranty protection in exchange for a 160 | fee. 161 | 162 | 2. You may modify your copy or copies of the Library or any portion 163 | of it, thus forming a work based on the Library, and copy and 164 | distribute such modifications or work under the terms of Section 1 165 | above, provided that you also meet all of these conditions: 166 | 167 | a) The modified work must itself be a software library. 168 | 169 | b) You must cause the files modified to carry prominent notices 170 | stating that you changed the files and the date of any change. 171 | 172 | c) You must cause the whole of the work to be licensed at no 173 | charge to all third parties under the terms of this License. 174 | 175 | d) If a facility in the modified Library refers to a function or a 176 | table of data to be supplied by an application program that uses 177 | the facility, other than as an argument passed when the facility 178 | is invoked, then you must make a good faith effort to ensure that, 179 | in the event an application does not supply such function or 180 | table, the facility still operates, and performs whatever part of 181 | its purpose remains meaningful. 182 | 183 | (For example, a function in a library to compute square roots has 184 | a purpose that is entirely well-defined independent of the 185 | application. Therefore, Subsection 2d requires that any 186 | application-supplied function or table used by this function must 187 | be optional: if the application does not supply it, the square 188 | root function must still compute square roots.) 189 | 190 | These requirements apply to the modified work as a whole. If 191 | identifiable sections of that work are not derived from the Library, 192 | and can be reasonably considered independent and separate works in 193 | themselves, then this License, and its terms, do not apply to those 194 | sections when you distribute them as separate works. But when you 195 | distribute the same sections as part of a whole which is a work based 196 | on the Library, the distribution of the whole must be on the terms of 197 | this License, whose permissions for other licensees extend to the 198 | entire whole, and thus to each and every part regardless of who wrote 199 | it. 200 | 201 | Thus, it is not the intent of this section to claim rights or contest 202 | your rights to work written entirely by you; rather, the intent is to 203 | exercise the right to control the distribution of derivative or 204 | collective works based on the Library. 205 | 206 | In addition, mere aggregation of another work not based on the Library 207 | with the Library (or with a work based on the Library) on a volume of 208 | a storage or distribution medium does not bring the other work under 209 | the scope of this License. 210 | 211 | 3. You may opt to apply the terms of the ordinary GNU General Public 212 | License instead of this License to a given copy of the Library. To do 213 | this, you must alter all the notices that refer to this License, so 214 | that they refer to the ordinary GNU General Public License, version 2, 215 | instead of to this License. (If a newer version than version 2 of the 216 | ordinary GNU General Public License has appeared, then you can specify 217 | that version instead if you wish.) Do not make any other change in 218 | these notices. 219 | 220 | Once this change is made in a given copy, it is irreversible for 221 | that copy, so the ordinary GNU General Public License applies to all 222 | subsequent copies and derivative works made from that copy. 223 | 224 | This option is useful when you wish to copy part of the code of 225 | the Library into a program that is not a library. 226 | 227 | 4. You may copy and distribute the Library (or a portion or 228 | derivative of it, under Section 2) in object code or executable form 229 | under the terms of Sections 1 and 2 above provided that you accompany 230 | it with the complete corresponding machine-readable source code, which 231 | must be distributed under the terms of Sections 1 and 2 above on a 232 | medium customarily used for software interchange. 233 | 234 | If distribution of object code is made by offering access to copy 235 | from a designated place, then offering equivalent access to copy the 236 | source code from the same place satisfies the requirement to 237 | distribute the source code, even though third parties are not 238 | compelled to copy the source along with the object code. 239 | 240 | 5. A program that contains no derivative of any portion of the 241 | Library, but is designed to work with the Library by being compiled or 242 | linked with it, is called a "work that uses the Library". Such a 243 | work, in isolation, is not a derivative work of the Library, and 244 | therefore falls outside the scope of this License. 245 | 246 | However, linking a "work that uses the Library" with the Library 247 | creates an executable that is a derivative of the Library (because it 248 | contains portions of the Library), rather than a "work that uses the 249 | library". The executable is therefore covered by this License. 250 | Section 6 states terms for distribution of such executables. 251 | 252 | When a "work that uses the Library" uses material from a header file 253 | that is part of the Library, the object code for the work may be a 254 | derivative work of the Library even though the source code is not. 255 | Whether this is true is especially significant if the work can be 256 | linked without the Library, or if the work is itself a library. The 257 | threshold for this to be true is not precisely defined by law. 258 | 259 | If such an object file uses only numerical parameters, data 260 | structure layouts and accessors, and small macros and small inline 261 | functions (ten lines or less in length), then the use of the object 262 | file is unrestricted, regardless of whether it is legally a derivative 263 | work. (Executables containing this object code plus portions of the 264 | Library will still fall under Section 6.) 265 | 266 | Otherwise, if the work is a derivative of the Library, you may 267 | distribute the object code for the work under the terms of Section 6. 268 | Any executables containing that work also fall under Section 6, 269 | whether or not they are linked directly with the Library itself. 270 | 271 | 6. As an exception to the Sections above, you may also combine or 272 | link a "work that uses the Library" with the Library to produce a 273 | work containing portions of the Library, and distribute that work 274 | under terms of your choice, provided that the terms permit 275 | modification of the work for the customer's own use and reverse 276 | engineering for debugging such modifications. 277 | 278 | You must give prominent notice with each copy of the work that the 279 | Library is used in it and that the Library and its use are covered by 280 | this License. You must supply a copy of this License. If the work 281 | during execution displays copyright notices, you must include the 282 | copyright notice for the Library among them, as well as a reference 283 | directing the user to the copy of this License. Also, you must do one 284 | of these things: 285 | 286 | a) Accompany the work with the complete corresponding 287 | machine-readable source code for the Library including whatever 288 | changes were used in the work (which must be distributed under 289 | Sections 1 and 2 above); and, if the work is an executable linked 290 | with the Library, with the complete machine-readable "work that 291 | uses the Library", as object code and/or source code, so that the 292 | user can modify the Library and then relink to produce a modified 293 | executable containing the modified Library. (It is understood 294 | that the user who changes the contents of definitions files in the 295 | Library will not necessarily be able to recompile the application 296 | to use the modified definitions.) 297 | 298 | b) Use a suitable shared library mechanism for linking with the 299 | Library. A suitable mechanism is one that (1) uses at run time a 300 | copy of the library already present on the user's computer system, 301 | rather than copying library functions into the executable, and (2) 302 | will operate properly with a modified version of the library, if 303 | the user installs one, as long as the modified version is 304 | interface-compatible with the version that the work was made with. 305 | 306 | c) Accompany the work with a written offer, valid for at 307 | least three years, to give the same user the materials 308 | specified in Subsection 6a, above, for a charge no more 309 | than the cost of performing this distribution. 310 | 311 | d) If distribution of the work is made by offering access to copy 312 | from a designated place, offer equivalent access to copy the above 313 | specified materials from the same place. 314 | 315 | e) Verify that the user has already received a copy of these 316 | materials or that you have already sent this user a copy. 317 | 318 | For an executable, the required form of the "work that uses the 319 | Library" must include any data and utility programs needed for 320 | reproducing the executable from it. However, as a special exception, 321 | the materials to be distributed need not include anything that is 322 | normally distributed (in either source or binary form) with the major 323 | components (compiler, kernel, and so on) of the operating system on 324 | which the executable runs, unless that component itself accompanies 325 | the executable. 326 | 327 | It may happen that this requirement contradicts the license 328 | restrictions of other proprietary libraries that do not normally 329 | accompany the operating system. Such a contradiction means you cannot 330 | use both them and the Library together in an executable that you 331 | distribute. 332 | 333 | 7. You may place library facilities that are a work based on the 334 | Library side-by-side in a single library together with other library 335 | facilities not covered by this License, and distribute such a combined 336 | library, provided that the separate distribution of the work based on 337 | the Library and of the other library facilities is otherwise 338 | permitted, and provided that you do these two things: 339 | 340 | a) Accompany the combined library with a copy of the same work 341 | based on the Library, uncombined with any other library 342 | facilities. This must be distributed under the terms of the 343 | Sections above. 344 | 345 | b) Give prominent notice with the combined library of the fact 346 | that part of it is a work based on the Library, and explaining 347 | where to find the accompanying uncombined form of the same work. 348 | 349 | 8. You may not copy, modify, sublicense, link with, or distribute 350 | the Library except as expressly provided under this License. Any 351 | attempt otherwise to copy, modify, sublicense, link with, or 352 | distribute the Library is void, and will automatically terminate your 353 | rights under this License. However, parties who have received copies, 354 | or rights, from you under this License will not have their licenses 355 | terminated so long as such parties remain in full compliance. 356 | 357 | 9. You are not required to accept this License, since you have not 358 | signed it. However, nothing else grants you permission to modify or 359 | distribute the Library or its derivative works. These actions are 360 | prohibited by law if you do not accept this License. Therefore, by 361 | modifying or distributing the Library (or any work based on the 362 | Library), you indicate your acceptance of this License to do so, and 363 | all its terms and conditions for copying, distributing or modifying 364 | the Library or works based on it. 365 | 366 | 10. Each time you redistribute the Library (or any work based on the 367 | Library), the recipient automatically receives a license from the 368 | original licensor to copy, distribute, link with or modify the Library 369 | subject to these terms and conditions. You may not impose any further 370 | restrictions on the recipients' exercise of the rights granted herein. 371 | You are not responsible for enforcing compliance by third parties with 372 | this License. 373 | 374 | 11. If, as a consequence of a court judgment or allegation of patent 375 | infringement or for any other reason (not limited to patent issues), 376 | conditions are imposed on you (whether by court order, agreement or 377 | otherwise) that contradict the conditions of this License, they do not 378 | excuse you from the conditions of this License. If you cannot 379 | distribute so as to satisfy simultaneously your obligations under this 380 | License and any other pertinent obligations, then as a consequence you 381 | may not distribute the Library at all. For example, if a patent 382 | license would not permit royalty-free redistribution of the Library by 383 | all those who receive copies directly or indirectly through you, then 384 | the only way you could satisfy both it and this License would be to 385 | refrain entirely from distribution of the Library. 386 | 387 | If any portion of this section is held invalid or unenforceable under any 388 | particular circumstance, the balance of the section is intended to apply, 389 | and the section as a whole is intended to apply in other circumstances. 390 | 391 | It is not the purpose of this section to induce you to infringe any 392 | patents or other property right claims or to contest validity of any 393 | such claims; this section has the sole purpose of protecting the 394 | integrity of the free software distribution system which is 395 | implemented by public license practices. Many people have made 396 | generous contributions to the wide range of software distributed 397 | through that system in reliance on consistent application of that 398 | system; it is up to the author/donor to decide if he or she is willing 399 | to distribute software through any other system and a licensee cannot 400 | impose that choice. 401 | 402 | This section is intended to make thoroughly clear what is believed to 403 | be a consequence of the rest of this License. 404 | 405 | 12. If the distribution and/or use of the Library is restricted in 406 | certain countries either by patents or by copyrighted interfaces, the 407 | original copyright holder who places the Library under this License may add 408 | an explicit geographical distribution limitation excluding those countries, 409 | so that distribution is permitted only in or among countries not thus 410 | excluded. In such case, this License incorporates the limitation as if 411 | written in the body of this License. 412 | 413 | 13. The Free Software Foundation may publish revised and/or new 414 | versions of the Lesser General Public License from time to time. 415 | Such new versions will be similar in spirit to the present version, 416 | but may differ in detail to address new problems or concerns. 417 | 418 | Each version is given a distinguishing version number. If the Library 419 | specifies a version number of this License which applies to it and 420 | "any later version", you have the option of following the terms and 421 | conditions either of that version or of any later version published by 422 | the Free Software Foundation. If the Library does not specify a 423 | license version number, you may choose any version ever published by 424 | the Free Software Foundation. 425 | 426 | 14. If you wish to incorporate parts of the Library into other free 427 | programs whose distribution conditions are incompatible with these, 428 | write to the author to ask for permission. For software which is 429 | copyrighted by the Free Software Foundation, write to the Free 430 | Software Foundation; we sometimes make exceptions for this. Our 431 | decision will be guided by the two goals of preserving the free status 432 | of all derivatives of our free software and of promoting the sharing 433 | and reuse of software generally. 434 | 435 | NO WARRANTY 436 | 437 | 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO 438 | WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 439 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR 440 | OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY 441 | KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE 442 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 443 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE 444 | LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME 445 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 446 | 447 | 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN 448 | WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY 449 | AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU 450 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR 451 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE 452 | LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING 453 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A 454 | FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF 455 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 456 | DAMAGES. 457 | 458 | END OF TERMS AND CONDITIONS 459 | 460 | How to Apply These Terms to Your New Libraries 461 | 462 | If you develop a new library, and you want it to be of the greatest 463 | possible use to the public, we recommend making it free software that 464 | everyone can redistribute and change. You can do so by permitting 465 | redistribution under these terms (or, alternatively, under the terms of the 466 | ordinary General Public License). 467 | 468 | To apply these terms, attach the following notices to the library. It is 469 | safest to attach them to the start of each source file to most effectively 470 | convey the exclusion of warranty; and each file should have at least the 471 | "copyright" line and a pointer to where the full notice is found. 472 | 473 | 474 | Copyright (C) 475 | 476 | This library is free software; you can redistribute it and/or 477 | modify it under the terms of the GNU Lesser General Public 478 | License as published by the Free Software Foundation; either 479 | version 2.1 of the License, or (at your option) any later version. 480 | 481 | This library is distributed in the hope that it will be useful, 482 | but WITHOUT ANY WARRANTY; without even the implied warranty of 483 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 484 | Lesser General Public License for more details. 485 | 486 | You should have received a copy of the GNU Lesser General Public 487 | License along with this library; if not, write to the Free Software 488 | Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335 USA 489 | 490 | Also add information on how to contact you by electronic and paper mail. 491 | 492 | You should also get your employer (if you work as a programmer) or your 493 | school, if any, to sign a "copyright disclaimer" for the library, if 494 | necessary. Here is a sample; alter the names: 495 | 496 | Yoyodyne, Inc., hereby disclaims all copyright interest in the 497 | library `Frob' (a library for tweaking knobs) written by James Random Hacker. 498 | 499 | , 1 April 1990 500 | Ty Coon, President of Vice 501 | 502 | That's all there is to it! 503 | 504 | 505 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SSH Exporter 2 | English | [中文](https://github.com/2018-11-27/ssh-exporter/blob/master/README_CN.md) 3 | 4 | ## Introduction 5 | 6 | SSH Exporter is a monitoring tool based on the Prometheus specification. It remotely collects system performance data, such as CPU usage, memory utilization, disk and network I/O, from target servers via the SSH protocol. The collected data is exposed as Prometheus-formatted metrics, allowing it to be scraped and stored by a Prometheus Server. 7 | 8 | ## Features 9 | 10 | - **Remote Monitoring**: Connects to remote servers via SSH, eliminating the need to install additional agents on the monitored servers. 11 | - **Comprehensive System Monitoring**: Supports monitoring of multiple performance indicators including CPU, memory, disk, and network. 12 | - **Dynamic Configuration**: Allows reading monitoring targets and parameters from a YAML configuration file, facilitating dynamic management of monitoring nodes. 13 | - **Asynchronous Collection**: Uses a thread pool for asynchronous data collection, enhancing data collection efficiency. 14 | - **Error Handling and Retry Mechanism**: Provides an automatic retry mechanism for SSH connection failures, ensuring reliable data collection. 15 | - **Multi-language Environment Support**: Automatically adapts to the system language when parsing certain command outputs, supporting both Chinese and English environments. 16 | 17 | ## Usage 18 | 19 | ### 1. Configuration 20 | 21 | First, edit the `config.yml` file to configure the nodes and metrics to be monitored. For example: 22 | 23 | ```yaml 24 | nodes: 25 | - ip: 192.168.1.101 26 | port: 22 27 | username: 28 | password: 29 | - ip: 192.168.1.102 30 | port: 22 31 | username: 32 | password: 33 | 34 | metrics: 35 | - ssh_cpu_utilization 36 | - ssh_cpu_utilization_user 37 | - ssh_cpu_utilization_system 38 | - ssh_cpu_utilization_top5 39 | - ssh_cpu_percentage_wait 40 | - ssh_cpu_percentage_idle 41 | - ssh_cpu_count 42 | - ssh_memory_utilization 43 | - ssh_memory_utilization_top5 44 | - ssh_memory_utilization_swap 45 | - ssh_memory_available_bytes 46 | - ssh_memory_available_swap_bytes 47 | - ssh_disk_utilization 48 | - ssh_disk_used_bytes 49 | - ssh_disk_available_bytes 50 | - ssh_disk_read_bytes_total 51 | - ssh_disk_write_bytes_total 52 | - ssh_network_receive_bytes_total 53 | - ssh_network_transmit_bytes_total 54 | ``` 55 | 56 | ### 2. Running 57 | 58 | Run the `ssh_exporter.py` script to start the SSH Exporter service. The service listens on the default port 9122, waiting for scraping requests from the Prometheus Server. 59 | 60 | ```bash 61 | python3 ssh_exporter.py 62 | ``` 63 | 64 | > Supported Python versions: python>=3.8 65 | 66 | ### 3. Prometheus Configuration 67 | 68 | Add a new job to the Prometheus configuration file, specifying the SSH Exporter's address so that Prometheus can scrape the data. 69 | 70 | ```yaml 71 | scrape_configs: 72 | - job_name: 'ssh-exporter' 73 | static_configs: 74 | - targets: ['localhost:9122'] 75 | ``` 76 | 77 | ## Notes 78 | 79 | - **Security**: Ensure the security of SSH credentials (username and password) to avoid leakage. 80 | - **Network Configuration**: Ensure that the Prometheus Server can access the server running SSH Exporter. 81 | - **Performance Impact**: Frequent SSH connections and data collection may have some performance impact on remote servers. Adjust the data collection frequency according to actual needs. 82 | 83 | ## Development and Maintenance 84 | 85 | - **Feedback**: Please submit issues in the GitHub repository. 86 | - **Contributing Code**: Contributions via pull requests are welcome to jointly improve SSH Exporter. 87 | 88 | ## License 89 | 90 | SSH Exporter is released under the LGPL license. Please refer to the LICENSE file for details. 91 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | # SSH Exporter 2 | [English](README.md) | 中文 3 | 4 | ## 介绍 5 | 6 | SSH Exporter 是一个基于 Prometheus 规范的监控工具,通过 SSH 协议远程收集目标服务器的系统性能数据,如 CPU 使用率、内存使用情况、磁盘和网络 I/O 等,并将这些数据暴露为 Prometheus 格式的 metrics,以便被 Prometheus Server 抓取和存储。 7 | 8 | ## 功能特性 9 | 10 | - **远程监控**:通过 SSH 协议连接到远程服务器,无需在被监控服务器上安装额外的 agent。 11 | - **全面的系统监控**:支持监控 CPU、内存、磁盘和网络等多个方面的性能指标。 12 | - **动态配置**:支持从 YAML 配置文件中读取监控目标和参数,便于动态管理监控节点。 13 | - **异步收集**:使用线程池异步收集数据,提高数据收集效率。 14 | - **错误处理与重试机制**:对于 SSH 连接失败的情况,提供自动重试机制,确保数据收集的可靠性。 15 | - **多语言环境支持**:在解析某些命令输出时,根据系统语言自动适配,支持中文和英文环境。 16 | 17 | ## 使用方法 18 | 19 | ### 1. 配置 20 | 21 | 首先,需要编辑 `config.yml` 文件,配置需要监控的节点和监控指标。例如: 22 | 23 | ```yaml 24 | nodes: 25 | - ip: 192.168.1.101 26 | port: 22 27 | username: 28 | password: 29 | - ip: 192.168.1.102 30 | port: 22 31 | username: 32 | password: 33 | 34 | metrics: 35 | - ssh_cpu_utilization 36 | - ssh_cpu_utilization_user 37 | - ssh_cpu_utilization_system 38 | - ssh_cpu_utilization_top5 39 | - ssh_cpu_percentage_wait 40 | - ssh_cpu_percentage_idle 41 | - ssh_cpu_count 42 | - ssh_memory_utilization 43 | - ssh_memory_utilization_top5 44 | - ssh_memory_utilization_swap 45 | - ssh_memory_available_bytes 46 | - ssh_memory_available_swap_bytes 47 | - ssh_disk_utilization 48 | - ssh_disk_used_bytes 49 | - ssh_disk_available_bytes 50 | - ssh_disk_read_bytes_total 51 | - ssh_disk_write_bytes_total 52 | - ssh_network_receive_bytes_total 53 | - ssh_network_transmit_bytes_total 54 | ``` 55 | 56 | ### 2. 运行 57 | 58 | 直接运行 `ssh_exporter.py` 脚本即可启动 SSH Exporter 服务。服务将监听默认的 9122 端口,等待 Prometheus Server 的抓取请求。 59 | 60 | ```bash 61 | python3 ssh_exporter.py 62 | ``` 63 | 64 | > 支持的Python版本:python>=3.8 65 | 66 | ### 3. Prometheus 配置 67 | 68 | 在 Prometheus 的配置文件中添加一个新的 job,指定 SSH Exporter 的地址,以便 Prometheus 可以抓取数据。 69 | 70 | ```yaml 71 | scrape_configs: 72 | - job_name: 'ssh-exporter' 73 | static_configs: 74 | - targets: ['localhost:9122'] 75 | ``` 76 | 77 | ## 注意事项 78 | 79 | - **安全性**:请确保 SSH 凭证(用户名和密码)的安全,避免泄露。 80 | - **网络配置**:确保 Prometheus Server 可以访问运行 SSH Exporter 的服务器。 81 | - **性能影响**:频繁的 SSH 连接和数据收集可能会对远程服务器造成一定的性能影响,请根据实际需求调整数据收集频率。 82 | 83 | ## 开发与维护 84 | 85 | - **问题反馈**:请在 GitHub 仓库中提交 issues。 86 | - **贡献代码**:欢迎提交 PR,共同完善 SSH Exporter。 87 | 88 | ## 许可证 89 | 90 | SSH Exporter 采用 LGPL 许可证发布,详情请参阅 LICENSE 文件。 91 | -------------------------------------------------------------------------------- /config.yml: -------------------------------------------------------------------------------- 1 | nodes: 2 | - ip: 192.168.1.101 3 | port: 22 4 | username: 5 | password: 6 | - ip: 192.168.1.102 7 | port: 22 8 | username: 9 | password: 10 | 11 | # General metrics configuration; the listed indexes will be collected; the following are all supported indexes. 12 | # 通用指标配置;列出的指标将被采集;下面列出的是支持的所有指标。 13 | metrics: 14 | - ssh_cpu_utilization 15 | - ssh_cpu_utilization_user 16 | - ssh_cpu_utilization_system 17 | - ssh_cpu_utilization_top5 18 | - ssh_cpu_percentage_wait 19 | - ssh_cpu_percentage_idle 20 | - ssh_cpu_count 21 | - ssh_memory_utilization 22 | - ssh_memory_utilization_top5 23 | - ssh_memory_utilization_swap 24 | - ssh_memory_available_bytes 25 | - ssh_memory_available_swap_bytes 26 | - ssh_disk_utilization 27 | - ssh_disk_used_bytes 28 | - ssh_disk_available_bytes 29 | - ssh_disk_read_bytes_total 30 | - ssh_disk_write_bytes_total 31 | - ssh_network_receive_bytes_total 32 | - ssh_network_transmit_bytes_total 33 | 34 | log: 35 | level: INFO 36 | output: [file,stream] 37 | logfile: /var/log/ssh_exporter.log 38 | datefmt: '%F %T' 39 | logfmt: '[%(asctime)s] [%(funcName)s.line%(lineno)d] [%(levelname)s] %(message)s' 40 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | prometheus_client>=0.20.0,<1.0 2 | gqylpy_ssh >=1.2.6, <2.0 3 | gqylpy_datastruct>=3.0, <4.0 4 | gqylpy_dict >=1.2.6, <2.0 5 | gqylpy_exception >=2.1, <3.0 6 | gqylpy_log >=1.2, <2.0 7 | systempath >=1.1.1, <2.0 8 | funccache >=2.0.1, <3.0 9 | PyYAML >=6.0, <7.0 10 | -------------------------------------------------------------------------------- /ssh_exporter.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is part of ssh-exporter. 3 | 4 | ssh-exporter is free software: you can redistribute it and/or modify it under the 5 | terms of the GNU Lesser General Public License as published by the Free Software 6 | Foundation, either version 3 of the License, or (at your option) any later 7 | version. 8 | 9 | ssh-exporter is distributed in the hope that it will be useful, but WITHOUT ANY 10 | WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 11 | PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. 12 | 13 | You should have received a copy of the GNU Lesser General Public License along 14 | with ssh-exporter. If not, see . 15 | """ 16 | import os 17 | import re 18 | import sys 19 | import time 20 | import socket 21 | import select 22 | import inspect 23 | import threading 24 | 25 | from socket import AF_INET 26 | from socket import SOCK_STREAM 27 | from socket import SOL_SOCKET 28 | from socket import SO_REUSEADDR 29 | 30 | from concurrent.futures import ThreadPoolExecutor 31 | 32 | import yaml 33 | import prometheus_client 34 | import funccache 35 | import gqylpy_log as glog 36 | 37 | from gqylpy_datastruct import DataStruct 38 | from gqylpy_dict import gdict 39 | from gqylpy_ssh import GqylpySSH 40 | from gqylpy_ssh import SSHException 41 | from gqylpy_ssh import NoValidConnectionsError 42 | from systempath import File 43 | from systempath import Directory 44 | from prometheus_client import generate_latest 45 | from prometheus_client.metrics import MetricWrapperBase 46 | from prometheus_client.metrics import Gauge 47 | 48 | from typing import Final, Union, Generator, Callable, Any 49 | 50 | basedir: Final[Directory] = File(__file__, strict=True).dirname 51 | 52 | god: Final = gdict( 53 | yaml.safe_load(basedir['config.yml'].open.rb()), basedir=basedir 54 | ) 55 | 56 | metrics: Final = gdict( 57 | ssh_cpu_utilization={ 58 | 'type': 'Gauge', 59 | 'documentation': 'utilization of cpu used', 60 | 'labelnames': ('hostname', 'hostuuid', 'ip') 61 | }, 62 | ssh_cpu_utilization_user={ 63 | 'type': 'Gauge', 64 | 'documentation': 'utilization of cpu used by user', 65 | 'labelnames': ('hostname', 'hostuuid', 'ip') 66 | }, 67 | ssh_cpu_utilization_system={ 68 | 'type': 'Gauge', 69 | 'documentation': 'utilization of cpu used by system', 70 | 'labelnames': ('hostname', 'hostuuid', 'ip') 71 | }, 72 | ssh_cpu_utilization_top5={ 73 | 'type': 'Gauge', 74 | 'documentation': 'utilization top 5 of cpu used by process', 75 | 'labelnames': ('hostname', 'hostuuid', 'ip', 'pid', 'command', 'args') 76 | }, 77 | ssh_cpu_percentage_wait={ 78 | 'type': 'Gauge', 79 | 'documentation': 'percentage of cpu wait', 80 | 'labelnames': ('hostname', 'hostuuid', 'ip') 81 | }, 82 | ssh_cpu_percentage_idle={ 83 | 'type': 'Gauge', 84 | 'documentation': 'percentage of cpu idle', 85 | 'labelnames': ('hostname', 'hostuuid', 'ip') 86 | }, 87 | ssh_cpu_count={ 88 | 'type': 'Gauge', 89 | 'documentation': 'number of cpu', 90 | 'labelnames': ('hostname', 'hostuuid', 'ip') 91 | }, 92 | ssh_memory_utilization={ 93 | 'type': 'Gauge', 94 | 'documentation': 'utilization of memory used', 95 | 'labelnames': ('hostname', 'hostuuid', 'ip') 96 | }, 97 | ssh_memory_utilization_top5={ 98 | 'type': 'Gauge', 99 | 'documentation': 'utilization top 5 of memory used by process', 100 | 'labelnames': ('hostname', 'hostuuid', 'ip', 'pid', 'command', 'args') 101 | }, 102 | ssh_memory_utilization_swap={ 103 | 'type': 'Gauge', 104 | 'documentation': 'utilization of swap memory used', 105 | 'labelnames': ('hostname', 'hostuuid', 'ip') 106 | }, 107 | ssh_memory_available_bytes={ 108 | 'type': 'Gauge', 109 | 'documentation': 'available of memory in bytes', 110 | 'labelnames': ('hostname', 'hostuuid', 'ip') 111 | }, 112 | ssh_memory_available_swap_bytes={ 113 | 'type': 'Gauge', 114 | 'documentation': 'available of swap memory in bytes', 115 | 'labelnames': ('hostname', 'hostuuid', 'ip') 116 | }, 117 | ssh_disk_utilization={ 118 | 'type': 'Gauge', 119 | 'documentation': 'utilization of mount point', 120 | 'labelnames': ( 121 | 'hostname', 'hostuuid', 'ip', 'device', 'fstype', 'mountpoint' 122 | ) 123 | }, 124 | ssh_disk_used_bytes={ 125 | 'type': 'Gauge', 126 | 'documentation': 'used of mount point in bytes', 127 | 'labelnames': ( 128 | 'hostname', 'hostuuid', 'ip', 'device', 'fstype', 'mountpoint' 129 | ) 130 | }, 131 | ssh_disk_available_bytes={ 132 | 'type': 'Gauge', 133 | 'documentation': 'available of mount point in bytes', 134 | 'labelnames': ( 135 | 'hostname', 'hostuuid', 'ip', 'device', 'fstype', 'mountpoint' 136 | ) 137 | }, 138 | ssh_disk_read_bytes_total={ 139 | 'type': 'Gauge', 140 | 'documentation': 'total disk read size in bytes', 141 | 'labelnames': ('hostname', 'hostuuid', 'ip', 'device') 142 | }, 143 | ssh_disk_write_bytes_total={ 144 | 'type': 'Gauge', 145 | 'documentation': 'total disk write size in bytes', 146 | 'labelnames': ('hostname', 'hostuuid', 'ip', 'device') 147 | }, 148 | ssh_network_receive_bytes_total={ 149 | 'type': 'Gauge', 150 | 'documentation': 'total interface receive in bytes', 151 | 'labelnames': ('hostname', 'hostuuid', 'ip', 'device') 152 | }, 153 | ssh_network_transmit_bytes_total={ 154 | 'type': 'Gauge', 155 | 'documentation': 'total interface transmit in bytes', 156 | 'labelnames': ('hostname', 'hostuuid', 'ip', 'device') 157 | } 158 | ) 159 | 160 | 161 | class Time2Second( 162 | metaclass=type('', (type,), {'__call__': lambda *a: type.__call__(*a)()}) 163 | ): 164 | matcher = re.compile(r'''^ 165 | (?:(\d+(?:\.\d+)?)y)? 166 | (?:(\d+(?:\.\d+)?)d)? 167 | (?:(\d+(?:\.\d+)?)h)? 168 | (?:(\d+(?:\.\d+)?)m)? 169 | (?:(\d+(?:\.\d+)?)s)? 170 | $''', flags=re.X) 171 | 172 | m = 60 173 | h = 60 * m 174 | d = 24 * h 175 | y = 365 * d 176 | 177 | def __init__(self, unit_time: str, /): 178 | self.unit_time = unit_time 179 | 180 | def __call__(self) -> Union[int, float]: 181 | if isinstance(self.unit_time, (int, float)): 182 | return self.unit_time 183 | elif self.unit_time.isdigit(): 184 | return float(self.unit_time) 185 | y, d, h, m, s = self.matcher.findall(self.unit_time.lower())[0] 186 | y, d, h, m, s = self.g(y), self.g(d), self.g(h), self.g(m), self.g(s) 187 | return self.y * y + self.d * d + self.h * h + self.m * m + s 188 | 189 | @staticmethod 190 | def g(x: str) -> Union[int, float]: 191 | return 0 if not x else int(x) if x.isdigit() else float(x) 192 | 193 | 194 | def init_socket(config: gdict) -> socket.socket: 195 | host, port = config.host, config.port 196 | 197 | skt = socket.socket(family=AF_INET, type=SOCK_STREAM) 198 | skt.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) 199 | skt.setblocking(False) 200 | 201 | skt.bind((host, port)) 202 | skt.settimeout(config.timeout) 203 | skt.listen() 204 | 205 | glog.info(f'bind http://{host}:{port}') 206 | 207 | return skt 208 | 209 | 210 | def init_ssh_connection(nodes: list) -> list: 211 | node_number: int = len(nodes) 212 | 213 | if node_number > 1: 214 | with ThreadPoolExecutor(node_number, 'InitSSHConnection') as pool: 215 | pool.map(init_ssh_connection_each, nodes) 216 | else: 217 | init_ssh_connection_each(nodes[0]) 218 | 219 | return nodes 220 | 221 | 222 | def init_ssh_connection_each(node: gdict): 223 | ip: str = node.pop('ip') 224 | 225 | not_ssh_params = dict((param, node.pop(param)) for param in set(node) - { 226 | *inspect.signature(GqylpySSH.connect).parameters, 227 | 'command_timeout', 'auto_sudo', 'reconnect' 228 | }) 229 | 230 | retry: bool = sys._getframe(1).f_code.co_name == 'init_ssh_connection_retry' 231 | 232 | try: 233 | ssh = GqylpySSH(ip, **node) 234 | ssh.cmd('echo Hi, SSH Exporter') 235 | 236 | node.hostname = ssh.cmd('hostname').output_else_raise() 237 | node.hostuuid = ssh.cmd( 238 | "dmidecode -t 1 | grep 'UUID: ' | awk '{print $NF}'" 239 | ).output_else_raise() 240 | 241 | node.system_lang = ssh.cmd('echo $LANG').output_else_raise()[:5].lower() 242 | except (SSHException, NoValidConnectionsError, OSError, EOFError) as e: 243 | node.ip = ip 244 | node.update(not_ssh_params) 245 | 246 | if retry: 247 | raise e 248 | 249 | glog.warning( 250 | f'SSH connection to "{ip}" failed, ' 251 | 'will switch to asynchronous try until succeed.' 252 | ) 253 | 254 | init_ssh_connection_again(node) 255 | else: 256 | node.ssh = ssh 257 | node.ip = ip 258 | node.update(not_ssh_params) 259 | 260 | if not retry: 261 | glog.info(f'SSH connection to "{ip}" has been established.') 262 | 263 | 264 | def init_ssh_connection_again(node: gdict, *, __nodes__=[]) -> None: 265 | __nodes__.append(node) 266 | 267 | if 'InitSSHConnectionAgain' in (x.name for x in threading.enumerate()): 268 | return 269 | 270 | def init_ssh_connection_retry(): 271 | time.sleep(10) 272 | i = -1 273 | while __nodes__: 274 | try: 275 | n: gdict = __nodes__[i] 276 | except IndexError: 277 | time.sleep(10) 278 | i = -1 279 | n: gdict = __nodes__[i] 280 | try: 281 | init_ssh_connection_each(n) 282 | except (SSHException, NoValidConnectionsError, OSError, EOFError): 283 | glog.warning(f'try SSH connection to "{n.ip}" failed once.') 284 | i -= 1 285 | else: 286 | glog.info( 287 | f'try SSH connection to "{n.ip}" has been established.' 288 | ) 289 | __nodes__.remove(n) 290 | 291 | threading.Thread( 292 | target=init_ssh_connection_retry, 293 | name='InitSSHConnectionAgain', 294 | daemon=True 295 | ).start() 296 | 297 | 298 | def init_collector_ignore_fstype(ignore_fstype: Union[list, str]) -> str: 299 | if not ignore_fstype: 300 | return '' 301 | 302 | if ignore_fstype.__class__ is list: 303 | x: str = ' -x '.join(ignore_fstype) 304 | else: 305 | x: str = ' -x '.join(i.strip() for i in ignore_fstype.split(',')) 306 | 307 | return '-x ' + x 308 | 309 | 310 | def init_metrics_wrapper(metric_list: list) -> list: 311 | for i, metric in enumerate(metric_list): 312 | config: gdict = metrics[metric] 313 | 314 | if config.__class__ is gdict: 315 | wrapper: MetricWrapperBase = getattr( 316 | prometheus_client.metrics, config.pop('type') 317 | )(metric, **config) 318 | metrics[metric] = metric_list[i] = wrapper 319 | else: 320 | metric_list[i]: MetricWrapperBase = config 321 | 322 | return metric_list 323 | 324 | 325 | def delete_unused_metrics(metric_list: list) -> list: 326 | for metric, wrapper in metrics.copy().items(): 327 | if wrapper.__class__ is gdict: 328 | del metrics[metric] 329 | return metric_list 330 | 331 | 332 | branch = 'branch' 333 | items = 'items' 334 | coerce = 'coerce' 335 | default = 'default' 336 | env = 'env' 337 | option = 'option' 338 | enum = 'enum' 339 | verify = 'verify' 340 | params = 'params' 341 | optional = 'optional' 342 | delete_empty = 'delete_empty' 343 | ignore_if_in = 'ignore_if_in' 344 | callback = 'callback' 345 | 346 | re_ip = r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$' 347 | re_domain = r'^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$' 348 | 349 | DataStruct({ 350 | 'log': { 351 | branch: { 352 | 'level': { 353 | type: str, 354 | default: 'INFO', 355 | env: 'LOG_LEVEL', 356 | option: '--log-level', 357 | enum: ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'FATAL'), 358 | params: [delete_empty] 359 | }, 360 | 'output': { 361 | type: (str, list), 362 | default: 'stream', 363 | set: ('stream', 'file'), 364 | params: [delete_empty], 365 | callback: lambda x: ','.join(x) 366 | }, 367 | 'logfile': { 368 | type: str, 369 | params: [optional, delete_empty] 370 | }, 371 | 'datefmt': { 372 | type: str, 373 | default: '%F %T', 374 | params: [delete_empty] 375 | }, 376 | 'logfmt': { 377 | type: str, 378 | default: '[%(asctime)s] [%(funcName)s.line%(lineno)d] ' 379 | '[%(levelname)s] %(message)s', 380 | params: [delete_empty] 381 | } 382 | }, 383 | default: {}, 384 | params: [delete_empty], 385 | callback: lambda x: glog.__init__(__name__, **x, gname=__name__) and x 386 | }, 387 | 'nodes': { 388 | items: { 389 | branch: { 390 | 'ip': { 391 | type: str, 392 | verify: [re_ip, re_domain] 393 | }, 394 | 'port': { 395 | type: (int, str), 396 | coerce: int, 397 | default: 22, 398 | verify: lambda x: -1 < x < 1 << 16, 399 | params: [delete_empty] 400 | }, 401 | 'username': { 402 | type: str, 403 | default: 'ssh_exporter', 404 | params: [delete_empty], 405 | callback: lambda x: x.strip() 406 | }, 407 | 'password': { 408 | type: str, 409 | params: [optional, delete_empty] 410 | }, 411 | 'key_filename': { 412 | type: str, 413 | params: [optional, delete_empty] 414 | }, 415 | 'key_password': { 416 | type: str, 417 | params: [optional, delete_empty] 418 | }, 419 | 'timeout': { 420 | type: (int, str), 421 | default: 30, 422 | env: 'SSH_CONNECT_TIMEOUT', 423 | option: '--ssh-connect-timeout', 424 | params: [delete_empty], 425 | callback: Time2Second 426 | }, 427 | 'command_timeout': { 428 | type: (int, str), 429 | default: 10, 430 | env: 'SSH_COMMAND_TIMEOUT', 431 | option: '--ssh-command-timeout', 432 | params: [delete_empty], 433 | callback: Time2Second 434 | }, 435 | 'allow_agent': { 436 | type: bool, 437 | default: False, 438 | params: [delete_empty] 439 | }, 440 | 'auto_sudo': { 441 | type: bool, 442 | default: True, 443 | params: [optional, delete_empty] 444 | }, 445 | 'reconnect': { 446 | type: bool, 447 | default: False, 448 | params: [delete_empty] 449 | }, 450 | 'metrics': { 451 | type: list, 452 | set: tuple(metrics), 453 | params: [optional, delete_empty], 454 | callback: init_metrics_wrapper 455 | }, 456 | 'collector': { 457 | branch: { 458 | 'ignore_fstype': { 459 | type: list, 460 | params: [optional], 461 | callback: init_collector_ignore_fstype 462 | } 463 | }, 464 | params: [optional, delete_empty] 465 | } 466 | } 467 | }, 468 | callback: lambda x: init_ssh_connection(x), 469 | ignore_if_in: [[]] 470 | }, 471 | 'collector': { 472 | branch: { 473 | 'ignore_fstype': { 474 | type: (list, str), 475 | default: ['tmpfs', 'devtmpfs', 'overlay'], 476 | env: 'COLLECTOR_IGNORE_FSTYPE', 477 | option: '--collector-ignore-fstype', 478 | params: [delete_empty], 479 | callback: init_collector_ignore_fstype 480 | } 481 | }, 482 | default: {} 483 | }, 484 | 'metrics': { 485 | type: list, 486 | default: list(metrics), 487 | set: tuple(metrics), 488 | params: [delete_empty], 489 | callback: lambda x: delete_unused_metrics(init_metrics_wrapper(x)) 490 | }, 491 | 'server': { 492 | branch: { 493 | 'host': { 494 | type: str, 495 | default: '0.0.0.0', 496 | env: 'HOST', 497 | option: '--host', 498 | verify: [re_ip, re_domain, lambda x: x == 'localhost'], 499 | params: [delete_empty] 500 | }, 501 | 'port': { 502 | type: (int, str), 503 | coerce: int, 504 | default: 9122, 505 | env: 'PORT', 506 | option: '--port', 507 | verify: lambda x: -1 < x < 1 << 16, 508 | params: [delete_empty] 509 | }, 510 | 'timeout': { 511 | type: (int, str), 512 | default: '1m', 513 | env: 'SERVER_TIMEOUT', 514 | option: '--server-timeout', 515 | params: [delete_empty], 516 | callback: Time2Second 517 | } 518 | }, 519 | default: {}, 520 | callback: init_socket 521 | } 522 | }, etitle='Config', eraise=True, ignore_undefined_data=True).verify(god) 523 | 524 | 525 | class Collector(metaclass=funccache): 526 | __shared_instance_cache__ = False 527 | 528 | def __init__(self, node: gdict, /, *, config: gdict): 529 | self.ssh: GqylpySSH = node.ssh 530 | self.system_lang: str = node.system_lang 531 | self.config = config 532 | 533 | @staticmethod 534 | def output2dict_for_utilization_top5(output: str, /) -> Generator: 535 | lines = ([ 536 | column.strip() for column in line.split() 537 | ] for line in output.splitlines()) 538 | 539 | title: list = next(lines) 540 | point: int = len(title) 541 | title.append('ARGS') 542 | 543 | for line in lines: 544 | front = line[:point] 545 | front.append(' '.join(line[point:])) 546 | yield dict(zip(title, front)) 547 | 548 | 549 | class CPUCollector(Collector): 550 | matcher = re.compile( 551 | r'(?P[\d.]+) us, *' 552 | r'(?P[\d.]+) sy, *' 553 | r'(?P[\d.]+) ni, *' 554 | r'(?P[\d.]+) id, *' 555 | r'(?P[\d.]+) wa, *' 556 | r'(?P[\d.]+) hi, *' 557 | r'(?P[\d.]+) si, *' 558 | r'(?P[\d.]+) st' 559 | ) 560 | 561 | @property 562 | def utilization(self) -> float: 563 | return float(self.info['us']) + float(self.info['sy']) 564 | 565 | @property 566 | def utilization_user(self) -> str: 567 | return self.info['us'] 568 | 569 | @property 570 | def utilization_system(self) -> str: 571 | return self.info['sy'] 572 | 573 | @property 574 | def utilization_top5(self) -> Generator: 575 | top5_processes: str = self.ssh.cmd(''' 576 | ps aux --sort -pcpu | head -6 | 577 | awk '{$1=$4=$5=$6=$7=$8=$9=$10=""; print $0}' 578 | ''').output_else_raise() 579 | return self.output2dict_for_utilization_top5(top5_processes) 580 | 581 | @property 582 | def count(self) -> str: 583 | return self.ssh.cmd(''' 584 | grep "^processor" /proc/cpuinfo | sort | uniq | wc -l 585 | ''').output_else_raise() 586 | 587 | @property 588 | def percentage_idle(self) -> str: 589 | return self.info['id'] 590 | 591 | @property 592 | def percentage_wait(self) -> str: 593 | return self.info['wa'] 594 | 595 | @property 596 | def info(self) -> dict: 597 | info: str = self.ssh.cmd( 598 | 'top -b -p0 -n1 | grep "^%Cpu(s):"' 599 | ).output_else_raise() 600 | return self.matcher.search(info, pos=8).groupdict() 601 | 602 | 603 | class MemoryCollector(Collector): 604 | 605 | @property 606 | def utilization(self) -> float: 607 | return 100 - self.available / self.total * 100 608 | 609 | @property 610 | def utilization_top5(self) -> Generator: 611 | top5_processes: str = self.ssh.cmd(''' 612 | ps aux --sort -pmem | head -6 | 613 | awk '{$1=$3=$5=$6=$7=$8=$9=$10=""; print $0}' 614 | ''').output_else_raise() 615 | return self.output2dict_for_utilization_top5(top5_processes) 616 | 617 | @property 618 | def utilization_swap(self) -> Union[float, int]: 619 | try: 620 | return 1 - (self.swap_free / self.swap_total) 621 | except ZeroDivisionError: 622 | return 0 623 | 624 | @property 625 | def available_bytes(self) -> int: 626 | return (self.total - self.available) * 1024 627 | 628 | @property 629 | def available_swap_bytes(self) -> int: 630 | return self.swap_free * 1024 631 | 632 | @property 633 | def available(self) -> int: 634 | return self.free + self.buffers + self.cached 635 | 636 | @property 637 | def total(self) -> int: 638 | return int(self.info['MemTotal']) 639 | 640 | @property 641 | def free(self) -> int: 642 | return int(self.info['MemFree']) 643 | 644 | @property 645 | def buffers(self) -> int: 646 | return int(self.info['Buffers']) 647 | 648 | @property 649 | def cached(self) -> int: 650 | return int(self.info['Cached']) 651 | 652 | @property 653 | def swap_total(self) -> int: 654 | return int(self.info['SwapTotal']) 655 | 656 | @property 657 | def swap_free(self) -> int: 658 | return int(self.info['SwapFree']) 659 | 660 | @property 661 | def info(self) -> dict: 662 | info: str = self.ssh.cmd(''' 663 | grep -E "^(MemTotal|MemFree|Buffers|Cached|SwapTotal|SwapFree)" \ 664 | /proc/meminfo | 665 | awk '{print $1, $2}' 666 | ''').output_else_raise() 667 | return dict(line.split(': ') for line in info.splitlines()) 668 | 669 | 670 | class DiskCollector(Collector): 671 | system_lang_mapping = { 672 | 'utilization_of_mountpoint': {'zh_cn': '已用%', 'en_us': 'Use%'}, 673 | 'used_bytes_of_mountpoint': {'zh_cn': '已用', 'en_us': 'Used'}, 674 | 'available_bytes_of_mountpoint': {'zh_cn': '可用', 'en_us': 'Available'}, 675 | 'filesystems': {'zh_cn': '文件系统', 'en_us': 'Filesystem'}, 676 | 'filesystem_types': {'zh_cn': '类型', 'en_us': 'Type'}, 677 | 'mountpoints': {'zh_cn': '挂载点', 'en_us': 'Mounted'} 678 | } 679 | 680 | def system_lang_selector(func) -> Callable[['DiskCollector'], Callable]: 681 | def inner(self: 'DiskCollector') -> Any: 682 | mapping: dict = self.system_lang_mapping[func.__name__] 683 | title: str = mapping.get(self.system_lang, mapping['en_us']) 684 | return func(self, title=title) 685 | 686 | return inner 687 | 688 | @property 689 | @system_lang_selector 690 | def utilization_of_mountpoint(self, *, title) -> Generator: 691 | return (info[title][:-1] for info in self.info_of_mountpoint) 692 | 693 | @property 694 | @system_lang_selector 695 | def used_bytes_of_mountpoint(self, *, title) -> Generator: 696 | return (info[title] for info in self.info_of_mountpoint) 697 | 698 | @property 699 | @system_lang_selector 700 | def available_bytes_of_mountpoint(self, *, title) -> Generator: 701 | return (info[title] for info in self.info_of_mountpoint) 702 | 703 | @property 704 | def read_bytes_total(self) -> Generator: 705 | return (int(info[1]) / 2 * 1024 for info in self.info_of_disk) 706 | 707 | @property 708 | def write_bytes_total(self) -> Generator: 709 | return (int(info[2]) / 2 * 1024 for info in self.info_of_disk) 710 | 711 | @property 712 | @system_lang_selector 713 | def filesystems(self, *, title) -> list: 714 | return [info[title] for info in self.info_of_mountpoint] 715 | 716 | @property 717 | @system_lang_selector 718 | def filesystem_types(self, *, title) -> list: 719 | return [info[title] for info in self.info_of_mountpoint] 720 | 721 | @property 722 | @system_lang_selector 723 | def mountpoints(self, *, title) -> list: 724 | return [info[title] for info in self.info_of_mountpoint] 725 | 726 | @property 727 | def disks(self) -> list: 728 | return [info[0] for info in self.info_of_disk] 729 | 730 | @property 731 | def info_of_mountpoint(self) -> list: 732 | return list(self.ssh.cmd(''' 733 | df -T --block-size=1 %s | awk '{$3=""; print $0}' 734 | ''' % self.config.ignore_fstype).table2dict()) 735 | 736 | @property 737 | def info_of_disk(self) -> list: 738 | disks: list = self.ssh.cmd(''' 739 | lsblk -d -o name,type | grep " disk$" | awk '{print $1}' 740 | ''').output_else_raise().splitlines() 741 | 742 | disk_performance: Generator = self.ssh.cmd(''' 743 | vmstat -d | grep -vE "^(disk| +?total)" | awk '{print $1, $4, $8}' 744 | ''').line2list() 745 | 746 | return [disk for disk in disk_performance if disk[0] in disks] 747 | 748 | 749 | class NetworkCollector(Collector): 750 | 751 | @property 752 | def receive_bytes_total(self) -> Generator: 753 | return (info[1] for info in self.info) 754 | 755 | @property 756 | def transmit_bytes_total(self) -> Generator: 757 | return (info[2] for info in self.info) 758 | 759 | @property 760 | def interfaces(self) -> list: 761 | return [info[0][:-1] for info in self.info] 762 | 763 | @property 764 | def info(self) -> list: 765 | return list(self.ssh.cmd(''' 766 | grep -vE "^(Inter-| face)" /proc/net/dev | awk '{print $1, $2, $10}' 767 | ''').line2list()) 768 | 769 | 770 | class MetricsHandler: 771 | 772 | @classmethod 773 | def get(cls) -> Generator: 774 | nodes = [node for node in god.nodes if 'ssh' in node] 775 | 776 | pool = ThreadPoolExecutor( 777 | max_workers=min(len(nodes) * len(metrics), os.cpu_count() * 5), 778 | thread_name_prefix='Collector' 779 | ) 780 | 781 | for node in nodes: 782 | collector_config: gdict = node.get('collector', god.collector) 783 | 784 | cpu = CPUCollector(node, config=collector_config) 785 | memory = MemoryCollector(node, config=collector_config) 786 | disk = DiskCollector(node, config=collector_config) 787 | network = NetworkCollector(node, config=collector_config) 788 | 789 | for wrapper in node.get('metrics', god.metrics): 790 | pool.submit( 791 | cls.get_metric, wrapper, node, 792 | cpu=cpu, memory=memory, disk=disk, network=network 793 | ) 794 | 795 | pool.shutdown() 796 | 797 | for w in metrics.values(): 798 | try: 799 | yield generate_latest(w) 800 | except Exception as e: 801 | wrappers = list(metrics.values()) 802 | for ww in wrappers[wrappers.index(w):]: 803 | ww.clear() 804 | raise e 805 | w.clear() 806 | 807 | @classmethod 808 | def get_metric(cls, wrapper: Gauge, node: gdict, **collectors) -> None: 809 | try: 810 | getattr(cls, f'get_metric__{wrapper._name}')( 811 | wrapper, node, **collectors 812 | ) 813 | except (SSHException, OSError, EOFError): 814 | del node.ssh, node.hostname, node.hostuuid 815 | glog.warning( 816 | f'SSH connection to "{node.ip}" is break, will try ' 817 | 're-establish until succeed, always skip this node ' 818 | 'during this period.' 819 | ) 820 | init_ssh_connection_again(node) 821 | except Exception as e: 822 | glog.error({ 823 | 'msg': 'get metric error.', 824 | 'metric': wrapper._name, 825 | 'node': node.ip, 826 | 'e': e 827 | }) 828 | 829 | @staticmethod 830 | def get_metric__ssh_cpu_utilization( 831 | wrapper: Gauge, 832 | node: gdict, 833 | *, 834 | cpu: CPUCollector, 835 | **other_collectors 836 | ) -> None: 837 | v: float = cpu.utilization 838 | wrapper.labels( 839 | hostname=node.hostname, 840 | hostuuid=node.hostuuid, 841 | ip=node.ip 842 | ).set(v) 843 | 844 | @staticmethod 845 | def get_metric__ssh_cpu_utilization_user( 846 | wrapper: Gauge, 847 | node: gdict, 848 | *, 849 | cpu: CPUCollector, 850 | **other_collectors 851 | ) -> None: 852 | v: str = cpu.utilization_user 853 | wrapper.labels( 854 | hostname=node.hostname, 855 | hostuuid=node.hostuuid, 856 | ip=node.ip 857 | ).set(v) 858 | 859 | @staticmethod 860 | def get_metric__ssh_cpu_utilization_system( 861 | wrapper: Gauge, 862 | node: gdict, 863 | *, 864 | cpu: CPUCollector, 865 | **other_collectors 866 | ) -> None: 867 | v: str = cpu.utilization_system 868 | wrapper.labels( 869 | hostname=node.hostname, 870 | hostuuid=node.hostuuid, 871 | ip=node.ip 872 | ).set(v) 873 | 874 | @staticmethod 875 | def get_metric__ssh_cpu_utilization_top5( 876 | wrapper: Gauge, 877 | node: gdict, 878 | *, 879 | cpu: CPUCollector, 880 | **other_collectors 881 | ) -> None: 882 | for top in cpu.utilization_top5: 883 | wrapper.labels( 884 | hostname=node.hostname, 885 | hostuuid=node.hostuuid, 886 | ip=node.ip, 887 | pid=top['PID'], 888 | command=top['COMMAND'], 889 | args=top['ARGS'] 890 | ).set(top['%CPU']) 891 | 892 | @staticmethod 893 | def get_metric__ssh_cpu_percentage_idle( 894 | wrapper: Gauge, 895 | node: gdict, 896 | *, 897 | cpu: CPUCollector, 898 | **other_collectors 899 | ) -> None: 900 | v: str = cpu.percentage_idle 901 | wrapper.labels( 902 | hostname=node.hostname, 903 | hostuuid=node.hostuuid, 904 | ip=node.ip 905 | ).set(v) 906 | 907 | @staticmethod 908 | def get_metric__ssh_cpu_percentage_wait( 909 | wrapper: Gauge, 910 | node: gdict, 911 | *, 912 | cpu: CPUCollector, 913 | **other_collectors, 914 | ) -> None: 915 | v: str = cpu.percentage_wait 916 | wrapper.labels( 917 | hostname=node.hostname, 918 | hostuuid=node.hostuuid, 919 | ip=node.ip 920 | ).set(v) 921 | 922 | @staticmethod 923 | def get_metric__ssh_cpu_count( 924 | wrapper: Gauge, 925 | node: gdict, 926 | *, 927 | cpu: CPUCollector, 928 | **other_collectors, 929 | ) -> None: 930 | v: str = cpu.count 931 | wrapper.labels( 932 | hostname=node.hostname, 933 | hostuuid=node.hostuuid, 934 | ip=node.ip 935 | ).set(v) 936 | 937 | @staticmethod 938 | def get_metric__ssh_memory_utilization( 939 | wrapper: Gauge, 940 | node: gdict, 941 | *, 942 | memory: MemoryCollector, 943 | **other_collectors 944 | ) -> None: 945 | v: float = memory.utilization 946 | wrapper.labels( 947 | hostname=node.hostname, 948 | hostuuid=node.hostuuid, 949 | ip=node.ip 950 | ).set(v) 951 | 952 | @staticmethod 953 | def get_metric__ssh_memory_utilization_top5( 954 | wrapper: Gauge, 955 | node: gdict, 956 | *, 957 | memory: MemoryCollector, 958 | **other_collectors 959 | ) -> None: 960 | for top in memory.utilization_top5: 961 | wrapper.labels( 962 | hostname=node.hostname, 963 | hostuuid=node.hostuuid, 964 | ip=node.ip, 965 | pid=top['PID'], 966 | command=top['COMMAND'], 967 | args=top['ARGS'] 968 | ).set(top['%MEM']) 969 | 970 | @staticmethod 971 | def get_metric__ssh_memory_utilization_swap( 972 | wrapper: Gauge, 973 | node: gdict, 974 | *, 975 | memory: MemoryCollector, 976 | **other_collectors 977 | ) -> None: 978 | v: float = memory.utilization_swap 979 | wrapper.labels( 980 | hostname=node.hostname, 981 | hostuuid=node.hostuuid, 982 | ip=node.ip 983 | ).set(v) 984 | 985 | @staticmethod 986 | def get_metric__ssh_memory_available_bytes( 987 | wrapper: Gauge, 988 | node: gdict, 989 | *, 990 | memory: MemoryCollector, 991 | **other_collectors 992 | ) -> None: 993 | v: int = memory.available_bytes 994 | wrapper.labels( 995 | hostname=node.hostname, 996 | hostuuid=node.hostuuid, 997 | ip=node.ip 998 | ).set(v) 999 | 1000 | @staticmethod 1001 | def get_metric__ssh_memory_available_swap_bytes( 1002 | wrapper: Gauge, 1003 | node: gdict, 1004 | *, 1005 | memory: MemoryCollector, 1006 | **other_collectors 1007 | ) -> None: 1008 | v: int = memory.available_swap_bytes 1009 | wrapper.labels( 1010 | hostname=node.hostname, 1011 | hostuuid=node.hostuuid, 1012 | ip=node.ip 1013 | ).set(v) 1014 | 1015 | @staticmethod 1016 | def get_metric__ssh_disk_utilization( 1017 | wrapper: Gauge, 1018 | node: gdict, 1019 | *, 1020 | disk: DiskCollector, 1021 | **other_collectors 1022 | ) -> None: 1023 | for i, v in enumerate(disk.utilization_of_mountpoint): 1024 | wrapper.labels( 1025 | hostname=node.hostname, 1026 | hostuuid=node.hostuuid, 1027 | ip=node.ip, 1028 | device=disk.filesystems[i], 1029 | fstype=disk.filesystem_types[i], 1030 | mountpoint=disk.mountpoints[i] 1031 | ).set(v) 1032 | 1033 | @staticmethod 1034 | def get_metric__ssh_disk_used_bytes( 1035 | wrapper: Gauge, 1036 | node: gdict, 1037 | *, 1038 | disk: DiskCollector, 1039 | **other_collectors 1040 | ) -> None: 1041 | for i, v in enumerate(disk.used_bytes_of_mountpoint): 1042 | wrapper.labels( 1043 | hostname=node.hostname, 1044 | hostuuid=node.hostuuid, 1045 | ip=node.ip, 1046 | device=disk.filesystems[i], 1047 | fstype=disk.filesystem_types[i], 1048 | mountpoint=disk.mountpoints[i] 1049 | ).set(v) 1050 | 1051 | @staticmethod 1052 | def get_metric__ssh_disk_available_bytes( 1053 | wrapper: Gauge, 1054 | node: gdict, 1055 | *, 1056 | disk: DiskCollector, 1057 | **other_collectors 1058 | ) -> None: 1059 | for i, v in enumerate(disk.available_bytes_of_mountpoint): 1060 | wrapper.labels( 1061 | hostname=node.hostname, 1062 | hostuuid=node.hostuuid, 1063 | ip=node.ip, 1064 | device=disk.filesystems[i], 1065 | fstype=disk.filesystem_types[i], 1066 | mountpoint=disk.mountpoints[i] 1067 | ).set(v) 1068 | 1069 | @staticmethod 1070 | def get_metric__ssh_disk_read_bytes_total( 1071 | wrapper: Gauge, 1072 | node: gdict, 1073 | *, 1074 | disk: DiskCollector, 1075 | **other_collectors 1076 | ) -> None: 1077 | for i, v in enumerate(disk.read_bytes_total): 1078 | wrapper.labels( 1079 | hostname=node.hostname, 1080 | hostuuid=node.hostuuid, 1081 | ip=node.ip, 1082 | device=disk.disks[i] 1083 | ).set(v) 1084 | 1085 | @staticmethod 1086 | def get_metric__ssh_disk_write_bytes_total( 1087 | wrapper: Gauge, 1088 | node: gdict, 1089 | *, 1090 | disk: DiskCollector, 1091 | **other_collectors 1092 | ) -> None: 1093 | for i, v in enumerate(disk.write_bytes_total): 1094 | wrapper.labels( 1095 | hostname=node.hostname, 1096 | hostuuid=node.hostuuid, 1097 | ip=node.ip, 1098 | device=disk.disks[i] 1099 | ).set(v) 1100 | 1101 | @staticmethod 1102 | def get_metric__ssh_network_receive_bytes_total( 1103 | wrapper: Gauge, 1104 | node: gdict, 1105 | *, 1106 | network: NetworkCollector, 1107 | **other_collectors 1108 | ) -> None: 1109 | for i, v in enumerate(network.receive_bytes_total): 1110 | wrapper.labels( 1111 | hostname=node.hostname, 1112 | hostuuid=node.hostuuid, 1113 | ip=node.ip, 1114 | device=network.interfaces[i] 1115 | ).set(v) 1116 | 1117 | @staticmethod 1118 | def get_metric__ssh_network_transmit_bytes_total( 1119 | wrapper: Gauge, 1120 | node: gdict, 1121 | *, 1122 | network: NetworkCollector, 1123 | **other_collectors 1124 | ) -> None: 1125 | for i, v in enumerate(network.transmit_bytes_total): 1126 | wrapper.labels( 1127 | hostname=node.hostname, 1128 | hostuuid=node.hostuuid, 1129 | ip=node.ip, 1130 | device=network.interfaces[i] 1131 | ).set(v) 1132 | 1133 | 1134 | if __name__ == '__main__': 1135 | index = b''' 1136 | 1137 | 1138 | 1139 | 1140 | SSH Exporter 1141 | 1142 | 1143 |

SSH Exporter

1144 | metrics 1145 | 1146 | 1147 | ''' 1148 | 1149 | server: socket.socket = god.server 1150 | http_timeout: int = server.timeout 1151 | next_collect_time = 0 1152 | 1153 | rlist = [server] 1154 | 1155 | while True: 1156 | for read_event in select.select(rlist, [], [])[0]: 1157 | if read_event is server: 1158 | fd, addr = server.accept() 1159 | rlist.append(fd) 1160 | glog.debug(f'establish connection, remote address: {addr}') 1161 | continue 1162 | try: 1163 | read_event.settimeout(http_timeout) 1164 | body: bytes = read_event.recv(8192) 1165 | if body[:21] == b'GET /metrics HTTP/1.1': 1166 | start = time.monotonic() 1167 | if start > next_collect_time: 1168 | metrics_response: bytes = b''.join(MetricsHandler.get()) 1169 | response: bytes = metrics_response 1170 | end = time.monotonic() 1171 | glog.info(f'GET /metrics 200 (runtime:{end - start:.2f}s)') 1172 | next_collect_time = end + .01 1173 | else: 1174 | response: bytes = index 1175 | glog.info('GET / 200') 1176 | read_event.sendall(b'HTTP/1.1 200 OK\r\n\r\n' + response) 1177 | except Exception as ee: 1178 | glog.error(f'server error: {repr(ee)}, clients: {rlist[1:]}') 1179 | finally: 1180 | rlist.remove(read_event) 1181 | read_event.close() 1182 | --------------------------------------------------------------------------------