├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .golangci.yml ├── LICENSE ├── Makefile ├── README.md ├── _example ├── memcached.go └── memcached_test.go ├── go.mod ├── go.sum ├── mackerel-plugin.go └── mackerel-plugin_test.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | jobs: 9 | lint: 10 | uses: mackerelio/workflows/.github/workflows/go-lint.yml@v1.2.0 11 | test: 12 | uses: mackerelio/workflows/.github/workflows/go-test.yml@v1.2.0 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters-settings: 2 | # There was a naming convention mismatch in Example*. 3 | # It depends on the output test. Thus, we decide to disable govet.tests, govet.fieldalignment in this package. 4 | govet: 5 | enable-all: true 6 | disable: 7 | - fieldalignment 8 | - tests 9 | 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | test: 3 | go test -v 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | go-mackerel-plugin-helper [![Build Status](https://github.com/mackerelio/go-mackerel-plugin-helper/workflows/Build/badge.svg?branch=master)](https://github.com/mackerelio/go-mackerel-plugin-helper/actions?workflow=Build) 2 | ================== 3 | 4 | This package provides helper methods to create mackerel agent plugin easily. 5 | 6 | Recommend to use go-mackerel-plugin 7 | =================================== 8 | 9 | We recommend to use [go-mackerel-plugin](https://github.com/mackerelio/go-mackerel-plugin) instead of go-mackerel-plugin-helper to create mackerel agent plugin. It is because you can create a plugin more simply by go-mackerel-plugin than by go-mackerel-plugin-helper. 10 | 11 | How to use 12 | ========== 13 | 14 | ## Graph Definition 15 | 16 | A plugin can specify `Graphs` and `Metrics`. 17 | `Graphs` represents one graph and includes some `Metrics`s which represent each line. 18 | 19 | `Graphs` includes followings: 20 | 21 | - `Label`: Label for the graph 22 | - `Unit`: Unit for lines, `integer`, `float` can be specified. 23 | - `Metrics`: Array of `Metrics` which represents each line. 24 | 25 | `Metics` includes followings: 26 | 27 | - `Name`: Key of the line 28 | - `Label`: Label of the line 29 | - `Diff`: If `Diff` is true, differential is used as value. 30 | - `Type`: 'float64', 'uint64' or 'uint32' can be specified. Default is `float64` 31 | - `Stacked`: If `Stacked` is true, the line is stacked. 32 | - `Scale`: Each value is multiplied by `Scale`. 33 | 34 | ```go 35 | var graphdef = map[string](mackerelplugin.Graphs){ 36 | "memcached.cmd": { 37 | Label: "Memcached Command", 38 | Unit: "integer", 39 | Metrics: [](mackerelplugin.Metrics){ 40 | {Name: "cmd_get", Label: "Get", Diff: true, Type: "uint64"}, 41 | {Name: "cmd_set", Label: "Set", Diff: true, Type: "uint64"}, 42 | {Name: "cmd_flush", Label: "Flush", Diff: true, Type: "uint64"}, 43 | {Name: "cmd_touch", Label: "Touch", Diff: true, Type: "uint64"}, 44 | }, 45 | }, 46 | } 47 | ``` 48 | 49 | ### Calculate Differential of Counter 50 | 51 | Many status values of popular middle-wares are provided as counter. 52 | But current Mackerel API can accept only absolute values, so differential values must be calculated beside agent plugins. 53 | 54 | `Diff` of `Metrics` is a flag whether values must be treated as counter or not. 55 | If this flag is set, this package calculate differential values automatically with current values and previous values, which are saved to a temporally file. 56 | 57 | ### Adjust Scale Value 58 | 59 | Some status values such as `jstat` memory usage are provided as scaled values. 60 | For example, `OGC` value are provided KB scale. 61 | 62 | `Scale` of `Metrics` is a multiplier for adjustment of the scale values. 63 | 64 | ```go 65 | var graphdef = map[string](mackerelplugin.Graphs){ 66 | "jvm.old_space": { 67 | Label: "JVM Old Space memory", 68 | Unit: "float", 69 | Metrics: [](mackerelplugin.Metrics){ 70 | {Name: "OGCMX", Label: "Old max", Diff: false, Scale: 1024}, 71 | {Name: "OGC", Label: "Old current", Diff: false, Scale: 1024}, 72 | {Name: "OU", Label: "Old used", Diff: false, Scale: 1024}, 73 | }, 74 | }, 75 | } 76 | ``` 77 | 78 | ### Deal with counter overflow 79 | 80 | If `Type` of metrics is `uint64` or `uint32` and `Diff` is true, the helper check counter overflow. 81 | When differential value is negative, overflow or counter reset may be occurred. 82 | If the differential value is ten-times above last value, the helper judge this is counter reset, not counter overflow, then the helper set value is unknown. If not, the helper recognizes counter overflow occurred. 83 | 84 | ## Tempfile 85 | 86 | `MackerelPlugin` interface has `Tempfile` field. The Tempfile is used to calculate differences in metrics with `Diff: true`. 87 | If this field is omitted, the filename of the temporaty file is automatically generated from plugin filename. 88 | 89 | ### Default value of Tempfile 90 | 91 | mackerel-agent's plugins should place its Tempfile under `os.Getenv("MACKEREL_PLUGIN_WORKDIR")` unless specified explicitly. 92 | Since this helper handles the environmental value, it's recommended not to set default Tempfile path. 93 | But if a plugin wants to set default Tempfile filename by itself, use `MackerelPlugin.SetTempfileByBasename()`, which sets Tempfile path considering the environmental value. 94 | 95 | ```go 96 | helper.Tempfile = *optTempfile 97 | if optTempfile == nil { 98 | helper.SetTempfileByBasename("YOUR_DEFAULT_FILENAME") 99 | } 100 | ``` 101 | 102 | ## Method 103 | 104 | A plugin must implement this interface and the `main` method. 105 | 106 | ```go 107 | type PluginWithPrefix interface { 108 | FetchMetrics() (map[string]interface{}, error) 109 | GraphDefinition() map[string]Graphs 110 | MetricKeyPrefix() string 111 | } 112 | ``` 113 | 114 | ```go 115 | func main() { 116 | optHost := flag.String("host", "localhost", "Hostname") 117 | optPort := flag.String("port", "11211", "Port") 118 | optTempfile := flag.String("tempfile", "", "Temp file name") 119 | optMetricKeyPrefix := flag.String("metric-key-prefix", "memcached", "Metric Key Prefix") 120 | flag.Parse() 121 | 122 | var memcached MemcachedPlugin 123 | 124 | memcached.Target = fmt.Sprintf("%s:%s", *optHost, *optPort) 125 | memcached.prefix = *optMetricKeyPrefix 126 | helper := mackerelplugin.NewMackerelPlugin(memcached) 127 | helper.Tempfile = *optTempfile 128 | 129 | helper.Run() 130 | } 131 | ``` 132 | 133 | ### old `Plugin` interface 134 | 135 | `Plugin` interface is old one. `PluginWithPrefix` interface is recommended now. 136 | -------------------------------------------------------------------------------- /_example/memcached.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "net" 9 | "strings" 10 | 11 | mp "github.com/mackerelio/go-mackerel-plugin-helper" 12 | ) 13 | 14 | // https://github.com/memcached/memcached/blob/master/doc/protocol.txt 15 | var graphdef map[string]mp.Graphs = map[string]mp.Graphs{ 16 | "memcached.connections": { 17 | Label: "Memcached Connections", 18 | Unit: "integer", 19 | Metrics: []mp.Metrics{ 20 | {Name: "curr_connections", Label: "Connections", Diff: false}, 21 | }, 22 | }, 23 | "memcached.cmd": { 24 | Label: "Memcached Command", 25 | Unit: "integer", 26 | Metrics: []mp.Metrics{ 27 | {Name: "cmd_get", Label: "Get", Diff: true, Type: "uint64"}, 28 | {Name: "cmd_set", Label: "Set", Diff: true, Type: "uint64"}, 29 | {Name: "cmd_flush", Label: "Flush", Diff: true, Type: "uint64"}, 30 | {Name: "cmd_touch", Label: "Touch", Diff: true, Type: "uint64"}, 31 | }, 32 | }, 33 | "memcached.hitmiss": { 34 | Label: "Memcached Hits/Misses", 35 | Unit: "integer", 36 | Metrics: []mp.Metrics{ 37 | {Name: "get_hits", Label: "Get Hits", Diff: true, Type: "uint64"}, 38 | {Name: "get_misses", Label: "Get Misses", Diff: true, Type: "uint64"}, 39 | {Name: "delete_hits", Label: "Delete Hits", Diff: true, Type: "uint64"}, 40 | {Name: "delete_misses", Label: "Delete Misses", Diff: true, Type: "uint64"}, 41 | {Name: "incr_hits", Label: "Incr Hits", Diff: true, Type: "uint64"}, 42 | {Name: "incr_misses", Label: "Incr Misses", Diff: true, Type: "uint64"}, 43 | {Name: "cas_hits", Label: "Cas Hits", Diff: true, Type: "uint64"}, 44 | {Name: "cas_misses", Label: "Cas Misses", Diff: true, Type: "uint64"}, 45 | {Name: "touch_hits", Label: "Touch Hits", Diff: true, Type: "uint64"}, 46 | {Name: "touch_misses", Label: "Touch Misses", Diff: true, Type: "uint64"}, 47 | }, 48 | }, 49 | "memcached.evictions": { 50 | Label: "Memcached Evictions", 51 | Unit: "integer", 52 | Metrics: []mp.Metrics{ 53 | {Name: "evictions", Label: "Evictions", Diff: true, Type: "uint64"}, 54 | }, 55 | }, 56 | "memcached.unfetched": { 57 | Label: "Memcached Unfetched", 58 | Unit: "integer", 59 | Metrics: []mp.Metrics{ 60 | {Name: "expired_unfetched", Label: "Expired unfetched", Diff: true, Type: "uint64"}, 61 | {Name: "evicted_unfetched", Label: "Evicted unfetched", Diff: true, Type: "uint64"}, 62 | }, 63 | }, 64 | "memcached.rusage": { 65 | Label: "Memcached Resouce Usage", 66 | Unit: "float", 67 | Metrics: []mp.Metrics{ 68 | {Name: "rusage_user", Label: "User", Diff: true}, 69 | {Name: "rusage_system", Label: "System", Diff: true}, 70 | }, 71 | }, 72 | "memcached.bytes": { 73 | Label: "Memcached Traffics", 74 | Unit: "bytes", 75 | Metrics: []mp.Metrics{ 76 | {Name: "bytes_read", Label: "Read", Diff: true, Type: "uint64"}, 77 | {Name: "bytes_written", Label: "Write", Diff: true, Type: "uint64"}, 78 | }, 79 | }, 80 | } 81 | 82 | type MemcachedPlugin struct { 83 | Target string 84 | Tempfile string 85 | } 86 | 87 | func (m MemcachedPlugin) FetchMetrics() (map[string]interface{}, error) { 88 | conn, err := net.Dial("tcp", m.Target) 89 | if err != nil { 90 | return nil, err 91 | } 92 | fmt.Fprintln(conn, "stats") 93 | return m.ParseStats(conn) 94 | } 95 | 96 | func (m MemcachedPlugin) ParseStats(conn io.Reader) (map[string]interface{}, error) { 97 | scanner := bufio.NewScanner(conn) 98 | stat := make(map[string]interface{}) 99 | 100 | for scanner.Scan() { 101 | s := scanner.Text() 102 | if s == "END" { 103 | return stat, nil 104 | } 105 | 106 | res := strings.Split(s, " ") 107 | if res[0] == "STAT" { 108 | stat[res[1]] = res[2] 109 | } 110 | } 111 | if err := scanner.Err(); err != nil { 112 | return stat, err 113 | } 114 | return nil, nil 115 | } 116 | 117 | func (m MemcachedPlugin) GraphDefinition() map[string]mp.Graphs { 118 | return graphdef 119 | } 120 | 121 | func main() { 122 | optHost := flag.String("host", "localhost", "Hostname") 123 | optPort := flag.String("port", "11211", "Port") 124 | optTempfile := flag.String("tempfile", "", "Temp file name") 125 | flag.Parse() 126 | 127 | var memcached MemcachedPlugin 128 | 129 | memcached.Target = fmt.Sprintf("%s:%s", *optHost, *optPort) 130 | helper := mp.NewMackerelPlugin(memcached) 131 | helper.Tempfile = *optTempfile 132 | 133 | helper.Run() 134 | } 135 | -------------------------------------------------------------------------------- /_example/memcached_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "reflect" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestGraphDefinition(t *testing.T) { 13 | var memcached MemcachedPlugin 14 | 15 | graphdef := memcached.GraphDefinition() 16 | if len(graphdef) != 7 { 17 | t.Errorf("GetTempfilename: %d should be 3", len(graphdef)) 18 | } 19 | } 20 | 21 | func TestParse(t *testing.T) { 22 | var memcached MemcachedPlugin 23 | stub := `STAT pid 1994 24 | STAT uptime 92066123 25 | STAT time 1436890963 26 | STAT version 1.4.0 27 | STAT pointer_size 64 28 | STAT rusage_user 1393.803107 29 | STAT rusage_system 2947.180187 30 | STAT curr_connections 1003 31 | STAT total_connections 965032539 32 | STAT connection_structures 16388 33 | STAT cmd_get 4306259844 34 | STAT cmd_set 2423543841 35 | STAT cmd_flush 0 36 | STAT get_hits 2769383483 37 | STAT get_misses 1536876361 38 | STAT delete_misses 244469885 39 | STAT delete_hits 14456835 40 | STAT incr_misses 0 41 | STAT incr_hits 0 42 | STAT decr_misses 0 43 | STAT decr_hits 0 44 | STAT cas_misses 0 45 | STAT cas_hits 0 46 | STAT cas_badval 0 47 | STAT bytes_read 8328670869009 48 | STAT bytes_written 9151962263382 49 | STAT limit_maxbytes 2147483648 50 | STAT accepting_conns 1 51 | STAT listen_disabled_num 0 52 | STAT threads 5 53 | STAT conn_yields 1487476 54 | STAT bytes 621371972 55 | STAT curr_items 955652 56 | STAT total_items 2423543841 57 | STAT evictions 236677775 58 | END 59 | ` 60 | 61 | memcachedStats := bytes.NewBufferString(stub) 62 | 63 | stat, err := memcached.ParseStats(memcachedStats) 64 | fmt.Println(stat) 65 | assert.Nil(t, err) 66 | // Memcached Stats 67 | assert.EqualValues(t, reflect.TypeOf(stat["get_hits"]).String(), "string") 68 | assert.EqualValues(t, stat["get_hits"].(string), "2769383483") 69 | } 70 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | // Deprecated: go-mackerel-plugin-helper is still maintained, but we recommend using go-mackerel-plugin instead. 2 | module github.com/mackerelio/go-mackerel-plugin-helper 3 | 4 | go 1.18 5 | 6 | require github.com/mackerelio/golib v1.2.1 7 | 8 | require golang.org/x/text v0.17.0 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/mackerelio/golib v1.2.1 h1:SDcDn6Jw3p9bi1N0bg1Z/ilG5qcBB23qL8xNwrU0gg4= 2 | github.com/mackerelio/golib v1.2.1/go.mod h1:b8ZaapsHGH1FlEJlCqfD98CqafLeyMevyATDlID2BsM= 3 | golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= 4 | golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 5 | -------------------------------------------------------------------------------- /mackerel-plugin.go: -------------------------------------------------------------------------------- 1 | package mackerelplugin 2 | 3 | import ( 4 | "crypto/sha1" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "log" 10 | "math" 11 | "os" 12 | "path/filepath" 13 | "regexp" 14 | "strconv" 15 | "strings" 16 | "time" 17 | 18 | "golang.org/x/text/cases" 19 | "golang.org/x/text/language" 20 | 21 | "github.com/mackerelio/golib/pluginutil" 22 | ) 23 | 24 | // Metrics represents definition of a metric 25 | type Metrics struct { 26 | Name string `json:"name"` 27 | Label string `json:"label"` 28 | Diff bool `json:"-"` 29 | Type string `json:"-"` 30 | Stacked bool `json:"stacked"` 31 | Scale float64 `json:"-"` 32 | AbsoluteName bool `json:"-"` 33 | } 34 | 35 | // Graphs represents definition of a graph 36 | type Graphs struct { 37 | Label string `json:"label"` 38 | Unit string `json:"unit"` 39 | Metrics []Metrics `json:"metrics"` 40 | } 41 | 42 | // MetricValues represents a collection of metric values and its timestamp 43 | type MetricValues struct { 44 | Values map[string]interface{} 45 | Timestamp time.Time 46 | } 47 | 48 | // Plugin is old interface of mackerel-plugin 49 | type Plugin interface { 50 | FetchMetrics() (map[string]interface{}, error) 51 | GraphDefinition() map[string]Graphs 52 | } 53 | 54 | // PluginWithPrefix is recommended interface 55 | type PluginWithPrefix interface { 56 | Plugin 57 | MetricKeyPrefix() string 58 | } 59 | 60 | // MackerelPlugin is for mackerel-agent-plugin 61 | type MackerelPlugin struct { 62 | Plugin 63 | Tempfile string 64 | diff *bool 65 | } 66 | 67 | // NewMackerelPlugin returns new MackerelPlugin struct 68 | func NewMackerelPlugin(plugin Plugin) MackerelPlugin { 69 | mp := MackerelPlugin{Plugin: plugin} 70 | return mp 71 | } 72 | 73 | func (h *MackerelPlugin) hasDiff() bool { 74 | if h.diff == nil { 75 | diff := false 76 | h.diff = &diff 77 | DiffCheck: 78 | for _, graph := range h.GraphDefinition() { 79 | for _, metric := range graph.Metrics { 80 | if metric.Diff { 81 | *h.diff = true 82 | break DiffCheck 83 | } 84 | } 85 | } 86 | } 87 | return *h.diff 88 | } 89 | 90 | func (h *MackerelPlugin) printValue(w io.Writer, key string, value interface{}, now time.Time) { 91 | switch v := value.(type) { 92 | case uint32: 93 | fmt.Fprintf(w, "%s\t%d\t%d\n", key, v, now.Unix()) 94 | case uint64: 95 | fmt.Fprintf(w, "%s\t%d\t%d\n", key, v, now.Unix()) 96 | case float64: 97 | if math.IsNaN(value.(float64)) || math.IsInf(v, 0) { 98 | log.Printf("Invalid value: key = %s, value = %f\n", key, value) 99 | } else { 100 | fmt.Fprintf(w, "%s\t%f\t%d\n", key, v, now.Unix()) 101 | } 102 | } 103 | } 104 | 105 | // FetchLastValues retrieves the last recorded metric value 106 | // if there is the graph-def that is set Diff to true in the result of h.GraphDefinition(). 107 | func (h *MackerelPlugin) FetchLastValues() (metricValues MetricValues, err error) { 108 | if !h.hasDiff() { 109 | return 110 | } 111 | 112 | f, err := os.Open(h.tempfilename()) 113 | if err != nil { 114 | if os.IsNotExist(err) { 115 | return metricValues, nil 116 | } 117 | return 118 | } 119 | defer f.Close() 120 | 121 | decoder := json.NewDecoder(f) 122 | err = decoder.Decode(&metricValues.Values) 123 | if err != nil { 124 | return 125 | } 126 | switch v := metricValues.Values["_lastTime"].(type) { 127 | case float64: 128 | metricValues.Timestamp = time.Unix(int64(v), 0) 129 | case int64: 130 | metricValues.Timestamp = time.Unix(v, 0) 131 | } 132 | return 133 | } 134 | 135 | var errStateUpdated = errors.New("state was recently updated") 136 | 137 | func (h *MackerelPlugin) fetchLastValuesSafe(now time.Time) (metricValues MetricValues, err error) { 138 | m, err := h.FetchLastValues() 139 | if err != nil { 140 | return m, err 141 | } 142 | if now.Sub(m.Timestamp) < time.Second { 143 | return m, errStateUpdated 144 | } 145 | return m, nil 146 | } 147 | 148 | func (h *MackerelPlugin) saveValues(metricValues MetricValues) error { 149 | if !h.hasDiff() { 150 | return nil 151 | } 152 | fname := h.tempfilename() 153 | f, err := os.Create(fname) 154 | if err != nil { 155 | return err 156 | } 157 | defer f.Close() 158 | 159 | // Since Go 1.15 strconv.ParseFloat returns +Inf if it couldn't parse a string. 160 | // But JSON does not accept invalid numbers, such as +Inf, -Inf or NaN. 161 | // We perhaps have some plugins that is affected above change, 162 | // so saveState should clear invalid numbers in the values before saving it. 163 | for k, v := range metricValues.Values { 164 | f, ok := v.(float64) 165 | if !ok { 166 | continue 167 | } 168 | if math.IsInf(f, 0) || math.IsNaN(f) { 169 | delete(metricValues.Values, k) 170 | } 171 | } 172 | 173 | metricValues.Values["_lastTime"] = metricValues.Timestamp.Unix() 174 | encoder := json.NewEncoder(f) 175 | err = encoder.Encode(metricValues.Values) 176 | if err != nil { 177 | return err 178 | } 179 | 180 | return nil 181 | } 182 | 183 | func (h *MackerelPlugin) calcDiff(value float64, now time.Time, lastValue float64, lastTime time.Time) (float64, error) { 184 | diffTime := now.Unix() - lastTime.Unix() 185 | if diffTime > 600 { 186 | return 0, errors.New("too long duration") 187 | } 188 | 189 | diff := (value - lastValue) * 60 / float64(diffTime) 190 | 191 | if lastValue <= value { 192 | return diff, nil 193 | } 194 | return 0.0, errors.New("counter seems to be reset") 195 | } 196 | 197 | func (h *MackerelPlugin) calcDiffUint32(value uint32, now time.Time, lastValue uint32, lastTime time.Time, lastDiff float64) (float64, error) { 198 | diffTime := now.Unix() - lastTime.Unix() 199 | if diffTime > 600 { 200 | return 0, errors.New("too long duration") 201 | } 202 | 203 | diff := float64((value-lastValue)*60) / float64(diffTime) 204 | 205 | if lastValue <= value || diff < lastDiff*10 { 206 | return diff, nil 207 | } 208 | return 0.0, errors.New("counter seems to be reset") 209 | 210 | } 211 | 212 | func (h *MackerelPlugin) calcDiffUint64(value uint64, now time.Time, lastValue uint64, lastTime time.Time, lastDiff float64) (float64, error) { 213 | diffTime := now.Unix() - lastTime.Unix() 214 | if diffTime > 600 { 215 | return 0, errors.New("too long duration") 216 | } 217 | 218 | diff := float64((value-lastValue)*60) / float64(diffTime) 219 | 220 | if lastValue <= value || diff < lastDiff*10 { 221 | return diff, nil 222 | } 223 | return 0.0, errors.New("counter seems to be reset") 224 | } 225 | 226 | func (h *MackerelPlugin) tempfilename() string { 227 | if h.Tempfile == "" { 228 | h.Tempfile = h.generateTempfilePath(os.Args) 229 | } 230 | return h.Tempfile 231 | } 232 | 233 | var tempfileSanitizeReg = regexp.MustCompile(`[^A-Za-z0-9_.-]`) 234 | 235 | // SetTempfileByBasename sets Tempfile under proper directory with specified basename. 236 | func (h *MackerelPlugin) SetTempfileByBasename(base string) { 237 | h.Tempfile = filepath.Join(pluginutil.PluginWorkDir(), base) 238 | } 239 | 240 | func (h *MackerelPlugin) generateTempfilePath(args []string) string { 241 | commandPath := args[0] 242 | var prefix string 243 | if p, ok := h.Plugin.(PluginWithPrefix); ok { 244 | prefix = p.MetricKeyPrefix() 245 | } else { 246 | name := filepath.Base(commandPath) 247 | prefix = strings.TrimPrefix(tempfileSanitizeReg.ReplaceAllString(name, "_"), "mackerel-plugin-") 248 | } 249 | filename := fmt.Sprintf( 250 | "mackerel-plugin-%s-%x", 251 | prefix, 252 | // When command-line options are different, mostly different metrics. 253 | // e.g. `-host` and `-port` options for mackerel-plugin-mysql 254 | sha1.Sum([]byte(strings.Join(args[1:], " "))), 255 | ) 256 | return filepath.Join(pluginutil.PluginWorkDir(), filename) 257 | } 258 | 259 | const ( 260 | metricTypeUint32 = "uint32" 261 | metricTypeUint64 = "uint64" 262 | // metricTypeFloat = "float64" 263 | ) 264 | 265 | func (h *MackerelPlugin) formatValues(prefix string, metric Metrics, metricValues MetricValues, lastMetricValues MetricValues) { 266 | name := metric.Name 267 | if metric.AbsoluteName && len(prefix) > 0 { 268 | name = prefix + "." + name 269 | } 270 | value, ok := metricValues.Values[name] 271 | if !ok || value == nil { 272 | return 273 | } 274 | 275 | var err error 276 | if v, ok := value.(string); ok { 277 | switch metric.Type { 278 | case metricTypeUint32: 279 | value, err = strconv.ParseUint(v, 10, 32) 280 | case metricTypeUint64: 281 | value, err = strconv.ParseUint(v, 10, 64) 282 | default: 283 | value, err = strconv.ParseFloat(v, 64) 284 | } 285 | } 286 | if err != nil { 287 | // For keeping compatibility, if each above statement occurred the error, 288 | // then the value is set to 0 and continue. 289 | log.Println("Parsing a value: ", err) 290 | } 291 | 292 | if metric.Diff { 293 | _, ok := lastMetricValues.Values[name] 294 | if ok { 295 | var lastDiff float64 296 | if lastMetricValues.Values[".last_diff."+name] != nil { 297 | lastDiff = toFloat64(lastMetricValues.Values[".last_diff."+name]) 298 | } 299 | var err error 300 | switch metric.Type { 301 | case metricTypeUint32: 302 | value, err = h.calcDiffUint32(toUint32(value), metricValues.Timestamp, toUint32(lastMetricValues.Values[name]), lastMetricValues.Timestamp, lastDiff) 303 | case metricTypeUint64: 304 | value, err = h.calcDiffUint64(toUint64(value), metricValues.Timestamp, toUint64(lastMetricValues.Values[name]), lastMetricValues.Timestamp, lastDiff) 305 | default: 306 | value, err = h.calcDiff(toFloat64(value), metricValues.Timestamp, toFloat64(lastMetricValues.Values[name]), lastMetricValues.Timestamp) 307 | } 308 | if err != nil { 309 | log.Println("OutputValues: ", err) 310 | return 311 | } 312 | metricValues.Values[".last_diff."+name] = value 313 | } else { 314 | log.Printf("%s does not exist at last fetch\n", name) 315 | return 316 | } 317 | } 318 | 319 | if metric.Scale != 0 { 320 | switch metric.Type { 321 | case metricTypeUint32: 322 | value = toUint32(value) * uint32(metric.Scale) 323 | case metricTypeUint64: 324 | value = toUint64(value) * uint64(metric.Scale) 325 | default: 326 | value = toFloat64(value) * metric.Scale 327 | } 328 | } 329 | 330 | metricNames := []string{} 331 | if p, ok := h.Plugin.(PluginWithPrefix); ok { 332 | metricNames = append(metricNames, p.MetricKeyPrefix()) 333 | } 334 | if len(prefix) > 0 { 335 | metricNames = append(metricNames, prefix) 336 | } 337 | metricNames = append(metricNames, metric.Name) 338 | h.printValue(os.Stdout, strings.Join(metricNames, "."), value, metricValues.Timestamp) 339 | } 340 | 341 | func (h *MackerelPlugin) formatValuesWithWildcard(prefix string, metric Metrics, metricValues MetricValues, lastMetricValues MetricValues) { 342 | regexpStr := `\A` + prefix + "." + metric.Name 343 | regexpStr = strings.Replace(regexpStr, ".", "\\.", -1) 344 | regexpStr = strings.Replace(regexpStr, "*", "[-a-zA-Z0-9_]+", -1) 345 | regexpStr = strings.Replace(regexpStr, "#", "[-a-zA-Z0-9_]+", -1) 346 | re, err := regexp.Compile(regexpStr) 347 | if err != nil { 348 | log.Fatalln("Failed to compile regexp: ", err) 349 | } 350 | for k := range metricValues.Values { 351 | if re.MatchString(k) { 352 | metricEach := metric 353 | metricEach.Name = k 354 | h.formatValues("", metricEach, metricValues, lastMetricValues) 355 | } 356 | } 357 | } 358 | 359 | // Run the plugin 360 | func (h *MackerelPlugin) Run() { 361 | if os.Getenv("MACKEREL_AGENT_PLUGIN_META") != "" { 362 | h.OutputDefinitions() 363 | } else { 364 | h.OutputValues() 365 | } 366 | } 367 | 368 | // OutputValues output the metrics 369 | func (h *MackerelPlugin) OutputValues() { 370 | stat, err := h.FetchMetrics() 371 | if err != nil { 372 | log.Fatalln("OutputValues: ", err) 373 | } 374 | metricValues := MetricValues{Values: stat, Timestamp: time.Now()} 375 | 376 | lastMetricValues, err := h.fetchLastValuesSafe(metricValues.Timestamp) 377 | if err != nil { 378 | if err == errStateUpdated { 379 | log.Println("OutputValues: ", err) 380 | return 381 | } 382 | log.Println("FetchLastValues (ignore):", err) 383 | } 384 | 385 | for key, graph := range h.GraphDefinition() { 386 | for _, metric := range graph.Metrics { 387 | if strings.ContainsAny(key+metric.Name, "*#") { 388 | h.formatValuesWithWildcard(key, metric, metricValues, lastMetricValues) 389 | } else { 390 | h.formatValues(key, metric, metricValues, lastMetricValues) 391 | } 392 | } 393 | } 394 | 395 | err = h.saveValues(metricValues) 396 | if err != nil { 397 | log.Fatalln("saveValues: ", err) 398 | } 399 | } 400 | 401 | // GraphDef represents graph definitions 402 | type GraphDef struct { 403 | Graphs map[string]Graphs `json:"graphs"` 404 | } 405 | 406 | func title(s string) string { 407 | r := strings.NewReplacer(".", " ", "_", " ") 408 | return cases.Title(language.Und, cases.NoLower).String(r.Replace(s)) 409 | } 410 | 411 | // OutputDefinitions outputs graph definitions 412 | func (h *MackerelPlugin) OutputDefinitions() { 413 | fmt.Println("# mackerel-agent-plugin") 414 | graphs := make(map[string]Graphs) 415 | for key, graph := range h.GraphDefinition() { 416 | g := graph 417 | k := key 418 | if p, ok := h.Plugin.(PluginWithPrefix); ok { 419 | prefix := p.MetricKeyPrefix() 420 | if k == "" { 421 | k = prefix 422 | } else { 423 | k = prefix + "." + k 424 | } 425 | } 426 | if g.Label == "" { 427 | g.Label = title(k) 428 | } 429 | metrics := []Metrics{} 430 | for _, v := range g.Metrics { 431 | if v.Label == "" { 432 | v.Label = title(v.Name) 433 | } 434 | metrics = append(metrics, v) 435 | } 436 | g.Metrics = metrics 437 | graphs[k] = g 438 | } 439 | var graphdef GraphDef 440 | graphdef.Graphs = graphs 441 | b, err := json.Marshal(graphdef) 442 | if err != nil { 443 | log.Fatalln("OutputDefinitions: ", err) 444 | } 445 | fmt.Println(string(b)) 446 | } 447 | 448 | func toUint32(value interface{}) uint32 { 449 | switch v := value.(type) { 450 | case uint32: 451 | return v 452 | case uint64: 453 | return uint32(v) 454 | case float64: 455 | return uint32(v) 456 | case string: 457 | n, err := strconv.ParseUint(v, 10, 32) 458 | if err != nil { 459 | return 0 460 | } 461 | return uint32(n) 462 | default: 463 | return 0 464 | } 465 | } 466 | 467 | func toUint64(value interface{}) uint64 { 468 | switch v := value.(type) { 469 | case uint32: 470 | return uint64(v) 471 | case uint64: 472 | return v 473 | case float64: 474 | return uint64(v) 475 | case string: 476 | n, err := strconv.ParseUint(v, 10, 64) 477 | if err != nil { 478 | return 0 479 | } 480 | return n 481 | default: 482 | return 0 483 | } 484 | } 485 | 486 | func toFloat64(value interface{}) float64 { 487 | switch v := value.(type) { 488 | case uint32: 489 | return float64(v) 490 | case uint64: 491 | return float64(v) 492 | case float64: 493 | return v 494 | case string: 495 | n, err := strconv.ParseFloat(v, 64) 496 | if err != nil { 497 | return 0 498 | } 499 | return n 500 | default: 501 | return 0 502 | } 503 | } 504 | -------------------------------------------------------------------------------- /mackerel-plugin_test.go: -------------------------------------------------------------------------------- 1 | package mackerelplugin 2 | 3 | import ( 4 | "bytes" 5 | "crypto/sha1" 6 | "fmt" 7 | "math" 8 | "os" 9 | "path/filepath" 10 | "reflect" 11 | "strings" 12 | "testing" 13 | "time" 14 | ) 15 | 16 | func TestCalcDiff(t *testing.T) { 17 | var mp MackerelPlugin 18 | 19 | val1 := 10.0 20 | val2 := 0.0 21 | now := time.Now() 22 | last := time.Unix(now.Unix()-10, 0) 23 | 24 | diff, err := mp.calcDiff(val1, now, val2, last) 25 | if diff != 60 { 26 | t.Errorf("calcDiff: %f should be %f", diff, 60.0) 27 | } 28 | if err != nil { 29 | t.Error("calcDiff causes an error") 30 | } 31 | } 32 | 33 | func TestCalcDiffWithReset(t *testing.T) { 34 | var mp MackerelPlugin 35 | 36 | val := 10.0 37 | now := time.Now() 38 | lastval := 12345.0 39 | last := time.Unix(now.Unix()-60, 0) 40 | 41 | diff, err := mp.calcDiff(val, now, lastval, last) 42 | if err == nil { 43 | t.Errorf("calcDiffUint32 with counter reset should cause an error: %f", diff) 44 | } 45 | } 46 | 47 | func TestCalcDiffWithUInt32WithReset(t *testing.T) { 48 | var mp MackerelPlugin 49 | 50 | val := uint32(10) 51 | now := time.Now() 52 | lastval := uint32(12345) 53 | last := time.Unix(now.Unix()-60, 0) 54 | 55 | diff, err := mp.calcDiffUint32(val, now, lastval, last, 10) 56 | if err != nil { 57 | } else { 58 | t.Errorf("calcDiffUint32 with counter reset should cause an error: %f", diff) 59 | } 60 | } 61 | 62 | func TestCalcDiffWithUInt32Overflow(t *testing.T) { 63 | var mp MackerelPlugin 64 | 65 | val := uint32(10) 66 | now := time.Now() 67 | lastval := math.MaxUint32 - uint32(10) 68 | last := time.Unix(now.Unix()-60, 0) 69 | 70 | diff, err := mp.calcDiffUint32(val, now, lastval, last, 10) 71 | if diff != 21.0 { 72 | t.Errorf("calcDiff: last: %d, now: %d, %f should be %f", val, lastval, diff, 21.0) 73 | } 74 | if err != nil { 75 | t.Error("calcDiff causes an error") 76 | } 77 | } 78 | 79 | func TestCalcDiffWithUInt64WithReset(t *testing.T) { 80 | var mp MackerelPlugin 81 | 82 | val := uint64(10) 83 | now := time.Now() 84 | lastval := uint64(12345) 85 | last := time.Unix(now.Unix()-60, 0) 86 | 87 | diff, err := mp.calcDiffUint64(val, now, lastval, last, 10) 88 | if err != nil { 89 | } else { 90 | t.Errorf("calcDiffUint64 with counter reset should cause an error: %f", diff) 91 | } 92 | } 93 | 94 | func TestCalcDiffWithUInt64Overflow(t *testing.T) { 95 | var mp MackerelPlugin 96 | 97 | val := uint64(10) 98 | now := time.Now() 99 | lastval := math.MaxUint64 - uint64(10) 100 | last := time.Unix(now.Unix()-60, 0) 101 | 102 | diff, err := mp.calcDiffUint64(val, now, lastval, last, 10) 103 | if diff != 21.0 { 104 | t.Errorf("calcDiff: last: %d, now: %d, %f should be %f", val, lastval, diff, 21.0) 105 | } 106 | if err != nil { 107 | t.Error("calcDiff causes an error") 108 | } 109 | } 110 | 111 | func TestPrintValueUint32(t *testing.T) { 112 | var mp MackerelPlugin 113 | s := new(bytes.Buffer) 114 | var now = time.Unix(1437227240, 0) 115 | mp.printValue(s, "test", uint32(10), now) 116 | 117 | expected := []byte("test\t10\t1437227240\n") 118 | 119 | if !bytes.Equal(expected, s.Bytes()) { 120 | t.Fatalf("not matched, expected: %s, got: %s", expected, s) 121 | } 122 | } 123 | 124 | func TestPrintValueUint64(t *testing.T) { 125 | var mp MackerelPlugin 126 | s := new(bytes.Buffer) 127 | var now = time.Unix(1437227240, 0) 128 | mp.printValue(s, "test", uint64(10), now) 129 | 130 | expected := []byte("test\t10\t1437227240\n") 131 | 132 | if !bytes.Equal(expected, s.Bytes()) { 133 | t.Fatalf("not matched, expected: %s, got: %s", expected, s) 134 | } 135 | } 136 | 137 | func TestPrintValueFloat64(t *testing.T) { 138 | var mp MackerelPlugin 139 | s := new(bytes.Buffer) 140 | var now = time.Unix(1437227240, 0) 141 | mp.printValue(s, "test", float64(10.0), now) 142 | 143 | expected := []byte("test\t10.000000\t1437227240\n") 144 | 145 | if !bytes.Equal(expected, s.Bytes()) { 146 | t.Fatalf("not matched, expected: %s, got: %s", expected, s) 147 | } 148 | } 149 | 150 | type emptyPlugin struct { 151 | } 152 | 153 | func (*emptyPlugin) FetchMetrics() (map[string]interface{}, error) { 154 | return nil, nil 155 | } 156 | 157 | func (*emptyPlugin) GraphDefinition() map[string]Graphs { 158 | return nil 159 | } 160 | 161 | func boolPtr(b bool) *bool { 162 | return &b 163 | } 164 | 165 | func TestFetchLastValues_stateFileNotFound(t *testing.T) { 166 | var mp MackerelPlugin 167 | mp.Plugin = &emptyPlugin{} 168 | mp.Tempfile = "state_file_should_not_exist.json" 169 | mp.diff = boolPtr(true) 170 | m, err := mp.FetchLastValues() 171 | if err != nil { 172 | t.Fatalf("FetchLastValues: %v", err) 173 | } 174 | if !m.Timestamp.IsZero() { 175 | t.Errorf("Timestamp = %v; want 0001-01-01", m.Timestamp) 176 | } 177 | } 178 | 179 | func TestFetchLastValues_readStateSameTime(t *testing.T) { 180 | var mp MackerelPlugin 181 | mp.Plugin = &emptyPlugin{} 182 | f, err := os.CreateTemp("", "mackerel-plugin-helper.") 183 | if err != nil { 184 | t.Fatal(err) 185 | } 186 | file := f.Name() 187 | defer os.Remove(file) 188 | mp.Tempfile = file 189 | mp.diff = boolPtr(true) 190 | metricValues := MetricValues{ 191 | Values: make(map[string]interface{}), 192 | Timestamp: time.Now(), 193 | } 194 | err = mp.saveValues(metricValues) 195 | if err != nil { 196 | t.Fatal(err) 197 | } 198 | 199 | _, err = mp.fetchLastValuesSafe(metricValues.Timestamp) 200 | if err != errStateUpdated { 201 | t.Errorf("FetchLastValues: %v; want %v", err, errStateUpdated) 202 | } 203 | } 204 | 205 | func ExampleFormatValues() { 206 | var mp MackerelPlugin 207 | prefix := "foo" 208 | metric := Metrics{Name: "cmd_get", Label: "Get", Diff: true, Type: "uint64"} 209 | now := time.Unix(1437227240, 0) 210 | metricValues := MetricValues{ 211 | Values: map[string]interface{}{"cmd_get": uint64(1000)}, 212 | Timestamp: now, 213 | } 214 | lastMetricValues := MetricValues{ 215 | Values: map[string]interface{}{"cmd_get": uint64(500), ".last_diff.cmd_get": 300.0}, 216 | Timestamp: now.Add(-time.Duration(60) * time.Second), 217 | } 218 | mp.formatValues(prefix, metric, metricValues, lastMetricValues) 219 | 220 | // Output: 221 | // foo.cmd_get 500.000000 1437227240 222 | } 223 | 224 | func ExampleFormatValuesAbsoluteName() { 225 | var mp MackerelPlugin 226 | prefixA := "foo" 227 | metricA := Metrics{Name: "cmd_get", Label: "Get", Diff: true, Type: "uint64", AbsoluteName: true} 228 | prefixB := "bar" 229 | metricB := Metrics{Name: "cmd_get", Label: "Get", Diff: true, Type: "uint64", AbsoluteName: true} 230 | now := time.Unix(1437227240, 0) 231 | metricValues := MetricValues{ 232 | Values: map[string]interface{}{"foo.cmd_get": uint64(1000), "bar.cmd_get": uint64(1234)}, 233 | Timestamp: now, 234 | } 235 | lastMetricValues := MetricValues{ 236 | Values: map[string]interface{}{"foo.cmd_get": uint64(500), ".last_diff.foo.cmd_get": 300.0, "bar.cmd_get": uint64(600), ".last_diff.bar.cmd_get": 400.0}, 237 | Timestamp: now.Add(-time.Duration(60) * time.Second), 238 | } 239 | mp.formatValues(prefixA, metricA, metricValues, lastMetricValues) 240 | mp.formatValues(prefixB, metricB, metricValues, lastMetricValues) 241 | 242 | // Output: 243 | // foo.cmd_get 500.000000 1437227240 244 | // bar.cmd_get 634.000000 1437227240 245 | } 246 | 247 | func ExampleFormatValuesAbsoluteNameButNoPrefix() { 248 | var mp MackerelPlugin 249 | prefix := "" 250 | metric := Metrics{Name: "cmd_get", Label: "Get", Diff: true, Type: "uint64", AbsoluteName: true} 251 | now := time.Unix(1437227240, 0) 252 | metricValues := MetricValues{ 253 | Values: map[string]interface{}{"cmd_get": uint64(1000)}, 254 | Timestamp: now, 255 | } 256 | lastMetricValues := MetricValues{ 257 | Values: map[string]interface{}{"cmd_get": uint64(500), ".last_diff.cmd_get": 300.0}, 258 | Timestamp: now.Add(-time.Duration(60) * time.Second), 259 | } 260 | mp.formatValues(prefix, metric, metricValues, lastMetricValues) 261 | 262 | // Output: 263 | // cmd_get 500.000000 1437227240 264 | } 265 | 266 | func ExampleFormatValuesWithCounterReset() { 267 | var mp MackerelPlugin 268 | prefix := "foo" 269 | metric := Metrics{Name: "cmd_get", Label: "Get", Diff: true, Type: "uint64"} 270 | now := time.Unix(1437227240, 0) 271 | metricValues := MetricValues{ 272 | Values: map[string]interface{}{"cmd_get": uint64(10)}, 273 | Timestamp: now, 274 | } 275 | lastMetricValues := MetricValues{ 276 | Values: map[string]interface{}{"cmd_get": uint64(500), ".last_diff.cmd_get": 300.0}, 277 | Timestamp: now.Add(-time.Duration(60) * time.Second), 278 | } 279 | mp.formatValues(prefix, metric, metricValues, lastMetricValues) 280 | 281 | // Output: 282 | } 283 | 284 | func ExampleFormatFloatValuesWithCounterReset() { 285 | var mp MackerelPlugin 286 | prefix := "foo" 287 | metric := Metrics{Name: "cmd_get", Label: "Get", Diff: true, Type: "float"} 288 | now := time.Unix(1437227240, 0) 289 | metricValues := MetricValues{ 290 | Values: map[string]interface{}{"cmd_get": 10.0}, 291 | Timestamp: now, 292 | } 293 | lastMetricValues := MetricValues{ 294 | Values: map[string]interface{}{"cmd_get": 500.0, ".last_diff.cmd_get": 300.0}, 295 | Timestamp: now.Add(-time.Duration(60) * time.Second), 296 | } 297 | mp.formatValues(prefix, metric, metricValues, lastMetricValues) 298 | 299 | // Output: 300 | } 301 | 302 | func ExampleFormatValuesWithOverflow() { 303 | var mp MackerelPlugin 304 | prefix := "foo" 305 | metric := Metrics{Name: "cmd_get", Label: "Get", Diff: true, Type: "uint64"} 306 | now := time.Unix(1437227240, 0) 307 | metricValues := MetricValues{ 308 | Values: map[string]interface{}{"cmd_get": uint64(500)}, 309 | Timestamp: now, 310 | } 311 | lastMetricValues := MetricValues{ 312 | Values: map[string]interface{}{"cmd_get": uint64(math.MaxUint64 - 100), ".last_diff.cmd_get": float64(100.0)}, 313 | Timestamp: now.Add(-time.Duration(60) * time.Second), 314 | } 315 | mp.formatValues(prefix, metric, metricValues, lastMetricValues) 316 | 317 | // Output: 318 | // foo.cmd_get 601.000000 1437227240 319 | } 320 | 321 | func ExampleFormatValuesWithOverflowAndTooHighDifference() { 322 | var mp MackerelPlugin 323 | prefix := "foo" 324 | metric := Metrics{Name: "cmd_get", Label: "Get", Diff: true, Type: "uint64"} 325 | now := time.Unix(1437227240, 0) 326 | metricValues := MetricValues{ 327 | Values: map[string]interface{}{"cmd_get": uint64(500)}, 328 | Timestamp: now, 329 | } 330 | lastMetricValues := MetricValues{ 331 | Values: map[string]interface{}{"cmd_get": uint64(math.MaxUint64 - 100), ".last_diff.cmd_get": float64(10.0)}, 332 | Timestamp: now.Add(-time.Duration(60) * time.Second), 333 | } 334 | mp.formatValues(prefix, metric, metricValues, lastMetricValues) 335 | 336 | // Output: 337 | } 338 | 339 | func ExampleFormatValuesWithOverflowAndNoLastDiff() { 340 | var mp MackerelPlugin 341 | prefix := "foo" 342 | metric := Metrics{Name: "cmd_get", Label: "Get", Diff: true, Type: "uint64"} 343 | now := time.Unix(1437227240, 0) 344 | metricValues := MetricValues{ 345 | Values: map[string]interface{}{"cmd_get": uint64(500)}, 346 | Timestamp: now, 347 | } 348 | lastMetricValues := MetricValues{ 349 | Values: map[string]interface{}{"cmd_get": uint64(math.MaxUint64 - 100)}, 350 | Timestamp: now.Add(-time.Duration(60) * time.Second), 351 | } 352 | mp.formatValues(prefix, metric, metricValues, lastMetricValues) 353 | 354 | // Output: 355 | } 356 | 357 | func ExampleFormatValuesWithWildcard() { 358 | var mp MackerelPlugin 359 | prefix := "foo.#" 360 | metric := Metrics{Name: "bar", Label: "Get", Diff: true, Type: "uint64"} 361 | now := time.Unix(1437227240, 0) 362 | metricValues := MetricValues{ 363 | Values: map[string]interface{}{"foo.1.bar": uint64(1000), "foo.2.bar": uint64(2000)}, 364 | Timestamp: now, 365 | } 366 | lastMetricValues := MetricValues{ 367 | Values: map[string]interface{}{"foo.1.bar": uint64(500), ".last_diff.foo.1.bar": float64(2.0)}, 368 | Timestamp: now.Add(-time.Duration(60) * time.Second), 369 | } 370 | mp.formatValuesWithWildcard(prefix, metric, metricValues, lastMetricValues) 371 | 372 | // Output: 373 | // foo.1.bar 500.000000 1437227240 374 | } 375 | 376 | func ExampleFormatValuesWithWildcardAndAbsoluteName() { 377 | // AbsoluteName should be ignored with WildCard 378 | var mp MackerelPlugin 379 | prefix := "foo.#" 380 | metric := Metrics{Name: "bar", Label: "Get", Diff: true, Type: "uint64", AbsoluteName: true} 381 | now := time.Unix(1437227240, 0) 382 | metricValues := MetricValues{ 383 | Values: map[string]interface{}{"foo.1.bar": uint64(1000), "foo.2.bar": uint64(2000)}, 384 | Timestamp: now, 385 | } 386 | lastMetricValues := MetricValues{ 387 | Values: map[string]interface{}{"foo.1.bar": uint64(500), ".last_diff.foo.1.bar": float64(2.0)}, 388 | Timestamp: now.Add(-time.Duration(60) * time.Second), 389 | } 390 | mp.formatValuesWithWildcard(prefix, metric, metricValues, lastMetricValues) 391 | 392 | // Output: 393 | // foo.1.bar 500.000000 1437227240 394 | } 395 | 396 | func ExampleFormatValuesWithWildcardAndNoDiff() { 397 | var mp MackerelPlugin 398 | prefix := "foo.#" 399 | metric := Metrics{Name: "bar", Label: "Get", Diff: false} 400 | now := time.Unix(1437227240, 0) 401 | metricValues := MetricValues{ 402 | Values: map[string]interface{}{"foo.1.bar": float64(1000)}, 403 | Timestamp: now, 404 | } 405 | lastMetricValues := MetricValues{ 406 | Values: map[string]interface{}{"foo.1.bar": float64(500), ".last_diff.foo.1.bar": float64(2.0)}, 407 | Timestamp: now.Add(-time.Duration(60) * time.Second), 408 | } 409 | mp.formatValuesWithWildcard(prefix, metric, metricValues, lastMetricValues) 410 | 411 | // Output: 412 | // foo.1.bar 1000.000000 1437227240 413 | } 414 | 415 | func ExampleFormatValuesWithWildcardAstarisk() { 416 | var mp MackerelPlugin 417 | prefix := "foo" 418 | metric := Metrics{Name: "*", Label: "Get", Diff: true, Type: "uint64"} 419 | now := time.Unix(1437227240, 0) 420 | metricValues := MetricValues{ 421 | Values: map[string]interface{}{"foo.1": uint64(1000), "foo.2": uint64(2000)}, 422 | Timestamp: now, 423 | } 424 | lastMetricValues := MetricValues{ 425 | Values: map[string]interface{}{"foo.1": uint64(500), ".last_diff.foo.1": float64(2.0)}, 426 | Timestamp: now.Add(-time.Duration(60) * time.Second), 427 | } 428 | mp.formatValuesWithWildcard(prefix, metric, metricValues, lastMetricValues) 429 | 430 | // Output: 431 | // foo.1 500.000000 1437227240 432 | } 433 | 434 | // an example implementation 435 | type MemcachedPlugin struct { 436 | } 437 | 438 | var graphdef = map[string]Graphs{ 439 | "memcached.cmd": { 440 | Label: "Memcached Command", 441 | Unit: "integer", 442 | Metrics: []Metrics{ 443 | {Name: "cmd_get", Label: "Get", Diff: true, Type: "uint64"}, 444 | }, 445 | }, 446 | } 447 | 448 | func (m MemcachedPlugin) GraphDefinition() map[string]Graphs { 449 | return graphdef 450 | } 451 | 452 | func (m MemcachedPlugin) FetchMetrics() (map[string]interface{}, error) { 453 | var stat map[string]interface{} 454 | return stat, nil 455 | } 456 | 457 | func ExampleOutputDefinitions() { 458 | var mp MemcachedPlugin 459 | helper := NewMackerelPlugin(mp) 460 | helper.OutputDefinitions() 461 | 462 | // Output: 463 | // # mackerel-agent-plugin 464 | // {"graphs":{"memcached.cmd":{"label":"Memcached Command","unit":"integer","metrics":[{"name":"cmd_get","label":"Get","stacked":false}]}}} 465 | } 466 | 467 | func TestToUint32(t *testing.T) { 468 | if ret := toUint32(uint32(100)); ret != uint32(100) { 469 | t.Errorf("toUint32(uint32) returns incorrect value: %v expected to be %v", ret, uint32(100)) 470 | } 471 | 472 | if ret := toUint32(uint64(100)); ret != uint32(100) { 473 | t.Errorf("toUint32(uint64) returns incorrect value: %v expected to be %v", ret, uint32(100)) 474 | } 475 | 476 | if ret := toUint32(float64(100)); ret != uint32(100) { 477 | t.Errorf("toUint32(float64) returns incorrect value: %v expected to be %v", ret, uint32(100)) 478 | } 479 | 480 | if ret := toUint32("100"); ret != uint32(100) { 481 | t.Errorf("toUint32(string) returns incorrect value: %v expected to be %v", ret, uint32(100)) 482 | } 483 | } 484 | 485 | func TestToUint64(t *testing.T) { 486 | if ret := toUint64(uint32(100)); ret != uint64(100) { 487 | t.Errorf("toUint64(uint32) returns incorrect value: %v expected to be %v", ret, uint64(100)) 488 | } 489 | 490 | if ret := toUint64(uint64(100)); ret != uint64(100) { 491 | t.Errorf("toUint64(uint64) returns incorrect value: %v expected to be %v", ret, uint64(100)) 492 | } 493 | 494 | if ret := toUint64(float64(100)); ret != uint64(100) { 495 | t.Errorf("toUint64(float64) returns incorrect value: %v expected to be %v", ret, uint64(100)) 496 | } 497 | 498 | if ret := toUint64("100"); ret != uint64(100) { 499 | t.Errorf("toUint64(string) returns incorrect value: %v expected to be %v", ret, uint64(100)) 500 | } 501 | } 502 | 503 | func TestToFloat64(t *testing.T) { 504 | if ret := toFloat64(uint32(100)); ret != float64(100) { 505 | t.Errorf("toFloat64(uint32) returns incorrect value: %v expected to be %v", ret, float64(100)) 506 | } 507 | 508 | if ret := toFloat64(uint64(100)); ret != float64(100) { 509 | t.Errorf("toFloat64(uint64) returns incorrect value: %v expected to be %v", ret, float64(100)) 510 | } 511 | 512 | if ret := toFloat64(float64(100)); ret != float64(100) { 513 | t.Errorf("toFloat64(float64) returns incorrect value: %v expected to be %v", ret, float64(100)) 514 | } 515 | 516 | if ret := toFloat64("100"); ret != float64(100) { 517 | t.Errorf("toFloat64(string) returns incorrect value: %v expected to be %v", ret, float64(100)) 518 | } 519 | } 520 | 521 | type testP struct{} 522 | 523 | func (t testP) FetchMetrics() (map[string]interface{}, error) { 524 | ret := make(map[string]interface{}) 525 | ret["bar"] = 15.0 526 | ret["baz"] = 18.0 527 | return ret, nil 528 | } 529 | 530 | func (t testP) GraphDefinition() map[string]Graphs { 531 | return map[string]Graphs{ 532 | "": { 533 | Unit: "integer", 534 | Metrics: []Metrics{ 535 | {Name: "bar"}, 536 | }, 537 | }, 538 | "fuga": { 539 | Unit: "float", 540 | Metrics: []Metrics{ 541 | {Name: "baz"}, 542 | }, 543 | }, 544 | } 545 | } 546 | 547 | func (t testP) MetricKeyPrefix() string { 548 | return "testP" 549 | } 550 | 551 | func TestDefaultTempfile(t *testing.T) { 552 | mp := &MackerelPlugin{} 553 | filename := filepath.Base(os.Args[0]) 554 | expect := filepath.Join(os.TempDir(), fmt.Sprintf( 555 | "mackerel-plugin-%s-%x", 556 | filename, 557 | sha1.Sum([]byte(strings.Join(os.Args[1:], " "))), 558 | )) 559 | if mp.tempfilename() != expect { 560 | t.Errorf("mp.tempfilename() should be %s, but: %s", expect, mp.tempfilename()) 561 | } 562 | 563 | pPrefix := NewMackerelPlugin(testP{}) 564 | expectForPrefix := filepath.Join(os.TempDir(), fmt.Sprintf( 565 | "mackerel-plugin-testP-%x", 566 | sha1.Sum([]byte(strings.Join(os.Args[1:], " "))), 567 | )) 568 | if pPrefix.tempfilename() != expectForPrefix { 569 | t.Errorf("pPrefix.tempfilename() should be %s, but: %s", expectForPrefix, pPrefix.tempfilename()) 570 | } 571 | } 572 | 573 | func TestTempfilenameFromExecutableFilePath(t *testing.T) { 574 | mp := &MackerelPlugin{} 575 | 576 | wd, _ := os.Getwd() 577 | // not PluginWithPrefix, regular filename 578 | expect1 := filepath.Join(os.TempDir(), "mackerel-plugin-foobar-da39a3ee5e6b4b0d3255bfef95601890afd80709") 579 | filename1 := mp.generateTempfilePath([]string{filepath.Join(wd, "foobar")}) 580 | if filename1 != expect1 { 581 | t.Errorf("p.generateTempfilePath() should be %s, but: %s", expect1, filename1) 582 | } 583 | 584 | // not PluginWithPrefix, contains some characters to be sanitized 585 | expect2 := filepath.Join(os.TempDir(), "mackerel-plugin-some_sanitized_name_1.2-da39a3ee5e6b4b0d3255bfef95601890afd80709") 586 | filename2 := mp.generateTempfilePath([]string{filepath.Join(wd, "some sanitized:name+1.2")}) 587 | if filename2 != expect2 { 588 | t.Errorf("p.generateTempfilePath() should be %s, but: %s", expect2, filename2) 589 | } 590 | 591 | // not PluginWithPrefix, begins with "mackerel-plugin-" 592 | expect3 := filepath.Join(os.TempDir(), "mackerel-plugin-trimmed-da39a3ee5e6b4b0d3255bfef95601890afd80709") 593 | filename3 := mp.generateTempfilePath([]string{filepath.Join(wd, "mackerel-plugin-trimmed")}) 594 | if filename3 != expect3 { 595 | t.Errorf("p.generateTempfilePath() should be %s, but: %s", expect3, filename3) 596 | } 597 | 598 | // PluginWithPrefix ignores current filename 599 | pPrefix := NewMackerelPlugin(testP{}) 600 | expectForPrefix := filepath.Join(os.TempDir(), "mackerel-plugin-testP-da39a3ee5e6b4b0d3255bfef95601890afd80709") 601 | filenameForPrefix := pPrefix.generateTempfilePath([]string{filepath.Join(wd, "foo")}) 602 | if filenameForPrefix != expectForPrefix { 603 | t.Errorf("pPrefix.generateTempfilePath() should be %s, but: %s", expectForPrefix, filenameForPrefix) 604 | } 605 | 606 | // Generate sha1 using command-line options, and use it for filename 607 | expect5 := filepath.Join(os.TempDir(), "mackerel-plugin-mysql-9045504f8fadd7ddcc8962ec1d9fc70e3f7ba627") 608 | filename5 := mp.generateTempfilePath([]string{filepath.Join(wd, "mackerel-plugin-mysql"), "-host", "hostname1", "-port", "3306"}) 609 | if filename5 != expect5 { 610 | t.Errorf("p.generateTempfilePath() should be %s, but: %s", expect5, filename5) 611 | } 612 | } 613 | 614 | func TestSetTempfileWithBasename(t *testing.T) { 615 | var p MackerelPlugin 616 | 617 | expect1 := filepath.Join(os.TempDir(), "my-super-tempfile") 618 | p.SetTempfileByBasename("my-super-tempfile") 619 | if p.Tempfile != expect1 { 620 | t.Errorf("p.SetTempfileByBasename() should set %s, but: %s", expect1, p.Tempfile) 621 | } 622 | 623 | origDir := os.Getenv("MACKEREL_PLUGIN_WORKDIR") 624 | os.Setenv("MACKEREL_PLUGIN_WORKDIR", "/tmp/somewhere") 625 | defer os.Setenv("MACKEREL_PLUGIN_WORKDIR", origDir) 626 | 627 | expect2 := filepath.FromSlash("/tmp/somewhere/my-great-tempfile") 628 | p.SetTempfileByBasename("my-great-tempfile") 629 | if p.Tempfile != expect2 { 630 | t.Errorf("p.SetTempfileByBasename() should set %s, but: %s", expect2, p.Tempfile) 631 | } 632 | } 633 | 634 | func ExamplePluginWithPrefixOutputDefinitions() { 635 | helper := NewMackerelPlugin(testP{}) 636 | helper.OutputDefinitions() 637 | 638 | // Output: 639 | // # mackerel-agent-plugin 640 | // {"graphs":{"testP":{"label":"TestP","unit":"integer","metrics":[{"name":"bar","label":"Bar","stacked":false}]},"testP.fuga":{"label":"TestP Fuga","unit":"float","metrics":[{"name":"baz","label":"Baz","stacked":false}]}}} 641 | } 642 | 643 | func ExamplePluginWithPrefixOutputValues() { 644 | helper := NewMackerelPlugin(testP{}) 645 | stat, _ := helper.FetchMetrics() 646 | key := "" 647 | metric := helper.GraphDefinition()[key].Metrics[0] 648 | var lastStat map[string]interface{} 649 | now := time.Unix(1437227240, 0) 650 | lastTime := time.Unix(0, 0) 651 | helper.formatValues(key, metric, MetricValues{Values: stat, Timestamp: now}, MetricValues{Values: lastStat, Timestamp: lastTime}) 652 | 653 | // Output: 654 | // testP.bar 15.000000 1437227240 655 | } 656 | 657 | func ExamplePluginWithPrefixOutputValues2() { 658 | helper := NewMackerelPlugin(testP{}) 659 | stat, _ := helper.FetchMetrics() 660 | key := "fuga" 661 | metric := helper.GraphDefinition()[key].Metrics[0] 662 | var lastStat map[string]interface{} 663 | now := time.Unix(1437227240, 0) 664 | lastTime := time.Unix(0, 0) 665 | helper.formatValues(key, metric, MetricValues{Values: stat, Timestamp: now}, MetricValues{Values: lastStat, Timestamp: lastTime}) 666 | 667 | // Output: 668 | // testP.fuga.baz 18.000000 1437227240 669 | } 670 | 671 | type testPHasDiff struct{} 672 | 673 | func (t testPHasDiff) FetchMetrics() (map[string]interface{}, error) { 674 | return nil, nil 675 | } 676 | 677 | func (t testPHasDiff) GraphDefinition() map[string]Graphs { 678 | return map[string]Graphs{ 679 | "hoge": { 680 | Metrics: []Metrics{ 681 | {Name: "hoge1", Label: "hoge1", Diff: true}, 682 | }, 683 | }, 684 | } 685 | } 686 | 687 | type testPHasntDiff struct{} 688 | 689 | func (t testPHasntDiff) FetchMetrics() (map[string]interface{}, error) { 690 | return nil, nil 691 | } 692 | 693 | func (t testPHasntDiff) GraphDefinition() map[string]Graphs { 694 | return map[string]Graphs{ 695 | "hoge": { 696 | Metrics: []Metrics{ 697 | {Name: "hoge1", Label: "hoge1"}, 698 | }, 699 | }, 700 | } 701 | } 702 | 703 | func TestPluginHasDiff(t *testing.T) { 704 | pHasDiff := NewMackerelPlugin(testPHasDiff{}) 705 | if !pHasDiff.hasDiff() { 706 | t.Errorf("something went wrong") 707 | } 708 | 709 | pHasntDiff := NewMackerelPlugin(testPHasntDiff{}) 710 | if pHasntDiff.hasDiff() { 711 | t.Errorf("something went wrong") 712 | } 713 | } 714 | 715 | func TestSaveStateIfContainsInvalidNumbers(t *testing.T) { 716 | p := NewMackerelPlugin(testPHasDiff{}) 717 | f := createTempState(t) 718 | defer f.Close() 719 | p.Tempfile = f.Name() 720 | 721 | stats := map[string]interface{}{ 722 | "key1": 3.0, 723 | "key2": math.Inf(1), 724 | "key3": math.Inf(-1), 725 | "key4": math.NaN(), 726 | } 727 | const lastTime = 1624848982 728 | 729 | now := time.Unix(lastTime, 0) 730 | values := MetricValues{ 731 | Values: stats, 732 | Timestamp: now, 733 | } 734 | if err := p.saveValues(values); err != nil { 735 | t.Errorf("saveValues: %v", err) 736 | } 737 | values, err := p.FetchLastValues() 738 | if err != nil { 739 | t.Fatal("FetchLastValues:", err) 740 | } 741 | want := MetricValues{ 742 | Values: map[string]interface{}{ 743 | "_lastTime": float64(lastTime), 744 | "key1": 3.0, 745 | }, 746 | Timestamp: now, 747 | } 748 | if !reflect.DeepEqual(values, want) { 749 | t.Errorf("saveValues stores only valid numbers: got %v; want %v", values, want) 750 | } 751 | } 752 | 753 | func createTempState(t testing.TB) *os.File { 754 | t.Helper() 755 | f, err := os.CreateTemp("", "mackerel-plugin.") 756 | if err != nil { 757 | t.Fatal(err) 758 | } 759 | t.Cleanup(func() { 760 | if err := os.Remove(f.Name()); err != nil { 761 | t.Fatal(err) 762 | } 763 | }) 764 | return f 765 | } 766 | --------------------------------------------------------------------------------