├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── client.go ├── client_metrics.go ├── client_reporter.go ├── client_test.go ├── examples ├── grpc-server-with-prometheus │ ├── client │ │ └── client.go │ ├── prometheus │ │ └── prometheus.yaml │ ├── protobuf │ │ ├── service.pb.go │ │ └── service.proto │ └── server │ │ └── server.go └── testproto │ ├── Makefile │ ├── test.pb.go │ └── test.proto ├── go.mod ├── go.sum ├── makefile ├── metric_options.go ├── packages └── grpcstatus │ ├── grpcstatus.go │ ├── grpcstatus1.13+_test.go │ ├── grpcstatus_test.go │ ├── native_unwrap1.12-.go │ └── native_unwrap1.13+.go ├── scripts └── test_all.sh ├── server.go ├── server_metrics.go ├── server_reporter.go ├── server_test.go └── util.go /.gitignore: -------------------------------------------------------------------------------- 1 | #vendor 2 | vendor/ 3 | 4 | # Created by .ignore support plugin (hsz.mobi) 5 | coverage.txt 6 | ### Go template 7 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 8 | *.o 9 | *.a 10 | *.so 11 | 12 | # Folders 13 | _obj 14 | _test 15 | 16 | # Architecture specific extensions/prefixes 17 | *.[568vq] 18 | [568vq].out 19 | 20 | *.cgo1.go 21 | *.cgo2.c 22 | _cgo_defun.c 23 | _cgo_gotypes.go 24 | _cgo_export.* 25 | 26 | _testmain.go 27 | 28 | *.exe 29 | *.test 30 | *.prof 31 | ### Windows template 32 | # Windows image file caches 33 | Thumbs.db 34 | ehthumbs.db 35 | 36 | # Folder config file 37 | Desktop.ini 38 | 39 | # Recycle Bin used on file shares 40 | $RECYCLE.BIN/ 41 | 42 | # Windows Installer files 43 | *.cab 44 | *.msi 45 | *.msm 46 | *.msp 47 | 48 | # Windows shortcuts 49 | *.lnk 50 | ### Kate template 51 | # Swap Files # 52 | .*.kate-swp 53 | .swp.* 54 | ### SublimeText template 55 | # cache files for sublime text 56 | *.tmlanguage.cache 57 | *.tmPreferences.cache 58 | *.stTheme.cache 59 | 60 | # workspace files are user-specific 61 | *.sublime-workspace 62 | 63 | # project files should be checked into the repository, unless a significant 64 | # proportion of contributors will probably not be using SublimeText 65 | # *.sublime-project 66 | 67 | # sftp configuration file 68 | sftp-config.json 69 | ### Linux template 70 | *~ 71 | 72 | # temporary files which can be created if a process still has a handle open of a deleted file 73 | .fuse_hidden* 74 | 75 | # KDE directory preferences 76 | .directory 77 | 78 | # Linux trash folder which might appear on any partition or disk 79 | .Trash-* 80 | ### JetBrains template 81 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 82 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 83 | 84 | # User-specific stuff: 85 | .idea 86 | .idea/tasks.xml 87 | .idea/dictionaries 88 | .idea/vcs.xml 89 | .idea/jsLibraryMappings.xml 90 | 91 | # Sensitive or high-churn files: 92 | .idea/dataSources.ids 93 | .idea/dataSources.xml 94 | .idea/dataSources.local.xml 95 | .idea/sqlDataSources.xml 96 | .idea/dynamic.xml 97 | .idea/uiDesigner.xml 98 | 99 | # Gradle: 100 | .idea/gradle.xml 101 | .idea/libraries 102 | 103 | # Mongo Explorer plugin: 104 | .idea/mongoSettings.xml 105 | 106 | ## File-based project format: 107 | *.iws 108 | 109 | ## Plugin-specific files: 110 | 111 | # IntelliJ 112 | /out/ 113 | 114 | # mpeltonen/sbt-idea plugin 115 | .idea_modules/ 116 | 117 | # JIRA plugin 118 | atlassian-ide-plugin.xml 119 | 120 | # Crashlytics plugin (for Android Studio and IntelliJ) 121 | com_crashlytics_export_strings.xml 122 | crashlytics.properties 123 | crashlytics-build.properties 124 | fabric.properties 125 | ### Xcode template 126 | # Xcode 127 | # 128 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 129 | 130 | ## Build generated 131 | build/ 132 | DerivedData/ 133 | 134 | ## Various settings 135 | *.pbxuser 136 | !default.pbxuser 137 | *.mode1v3 138 | !default.mode1v3 139 | *.mode2v3 140 | !default.mode2v3 141 | *.perspectivev3 142 | !default.perspectivev3 143 | xcuserdata/ 144 | 145 | ## Other 146 | *.moved-aside 147 | *.xccheckout 148 | *.xcscmblueprint 149 | ### Eclipse template 150 | 151 | .metadata 152 | bin/ 153 | tmp/ 154 | *.tmp 155 | *.bak 156 | *.swp 157 | *~.nib 158 | local.properties 159 | .settings/ 160 | .loadpath 161 | .recommenders 162 | 163 | # Eclipse Core 164 | .project 165 | 166 | # External tool builders 167 | .externalToolBuilders/ 168 | 169 | # Locally stored "Eclipse launch configurations" 170 | *.launch 171 | 172 | # PyDev specific (Python IDE for Eclipse) 173 | *.pydevproject 174 | 175 | # CDT-specific (C/C++ Development Tooling) 176 | .cproject 177 | 178 | # JDT-specific (Eclipse Java Development Tools) 179 | .classpath 180 | 181 | # Java annotation processor (APT) 182 | .factorypath 183 | 184 | # PDT-specific (PHP Development Tools) 185 | .buildpath 186 | 187 | # sbteclipse plugin 188 | .target 189 | 190 | # Tern plugin 191 | .tern-project 192 | 193 | # TeXlipse plugin 194 | .texlipse 195 | 196 | # STS (Spring Tool Suite) 197 | .springBeans 198 | 199 | # Code Recommenders 200 | .recommenders/ 201 | 202 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: go 3 | go: 4 | - 1.9.x 5 | - 1.10.x 6 | - 1.11.x 7 | - 1.12.x 8 | - master 9 | 10 | env: 11 | - GO111MODULE=on 12 | 13 | install: 14 | # With Go modules (eg Go >= 1.11), it isn't necessary anymore to 'go get' the dependencies before running the tests. 15 | - if [[ $TRAVIS_GO_VERSION =~ ^1\.(7|8|9|10)\. ]]; then go get github.com/prometheus/client_golang/prometheus; fi 16 | - if [[ $TRAVIS_GO_VERSION =~ ^1\.(7|8|9|10)\. ]]; then go get google.golang.org/grpc; fi 17 | - if [[ $TRAVIS_GO_VERSION =~ ^1\.(7|8|9|10)\. ]]; then go get github.com/stretchr/testify; fi 18 | 19 | script: 20 | - make test 21 | 22 | after_success: 23 | - bash <(curl -s https://codecov.io/bash) 24 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ### Added 10 | * Support for error unwrapping. (Supported for `github.com/pkg/errors` and native wrapping added in go1.13) 11 | 12 | ## [1.2.0](https://github.com/grpc-ecosystem/go-grpc-prometheus/releases/tag/v1.2.0) - 2018-06-04 13 | 14 | ### Added 15 | 16 | * Require go 1.9 or later and test against these versions in CI. 17 | * Provide metrics object as `prometheus.Collector`, for conventional metric registration. 18 | * Support non-default/global Prometheus registry. 19 | * Allow configuring counters with `prometheus.CounterOpts`. 20 | 21 | ### Changed 22 | 23 | * Remove usage of deprecated `grpc.Code()`. 24 | * Remove usage of deprecated `grpc.Errorf` and replace with `status.Errorf`. 25 | 26 | --- 27 | 28 | This changelog was started with version `v1.2.0`, for earlier versions refer to the respective [GitHub releases](https://github.com/grpc-ecosystem/go-grpc-prometheus/releases). 29 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # (Deprecated) Go gRPC Interceptors for Prometheus monitoring 2 | 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/grpc-ecosystem/go-grpc-prometheus)](http://goreportcard.com/report/grpc-ecosystem/go-grpc-prometheus) 4 | [![GoDoc](http://img.shields.io/badge/GoDoc-Reference-blue.svg)](https://godoc.org/github.com/grpc-ecosystem/go-grpc-prometheus) 5 | [![SourceGraph](https://sourcegraph.com/github.com/grpc-ecosystem/go-grpc-prometheus/-/badge.svg)](https://sourcegraph.com/github.com/grpc-ecosystem/go-grpc-prometheus/?badge) 6 | [![codecov](https://codecov.io/gh/grpc-ecosystem/go-grpc-prometheus/branch/master/graph/badge.svg)](https://codecov.io/gh/grpc-ecosystem/go-grpc-prometheus) 7 | [![Slack](https://img.shields.io/badge/join%20slack-%23go--grpc--prometheus-brightgreen.svg)](https://join.slack.com/t/improbable-eng/shared_invite/enQtMzQ1ODcyMzQ5MjM4LWY5ZWZmNGM2ODc5MmViNmQ3ZTA3ZTY3NzQwOTBlMTkzZmIxZTIxODk0OWU3YjZhNWVlNDU3MDlkZGViZjhkMjc) 8 | [![Apache 2.0 License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE) 9 | 10 | > :warning: This project is depreacted and archived as the functionality moved to [go-grpc-middleware](https://github.com/grpc-ecosystem/go-grpc-middleware) repo since [provider/prometheus@v1.0.0-rc.0](https://github.com/grpc-ecosystem/go-grpc-middleware/releases/tag/providers%2Fprometheus%2Fv1.0.0-rc.0) release. You can pull it using `go get github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus`. The API is simplified and morernized, yet functionality is similar to what v1.2.0 offered. All questions and issues you can submit [here](https://github.com/grpc-ecosystem/go-grpc-middleware/issues). 11 | 12 | [Prometheus](https://prometheus.io/) monitoring for your [gRPC Go](https://github.com/grpc/grpc-go) servers and clients. 13 | 14 | A sister implementation for [gRPC Java](https://github.com/grpc/grpc-java) (same metrics, same semantics) is in [grpc-ecosystem/java-grpc-prometheus](https://github.com/grpc-ecosystem/java-grpc-prometheus). 15 | 16 | ## Interceptors 17 | 18 | [gRPC Go](https://github.com/grpc/grpc-go) recently acquired support for Interceptors, i.e. middleware that is executed 19 | by a gRPC Server before the request is passed onto the user's application logic. It is a perfect way to implement 20 | common patterns: auth, logging and... monitoring. 21 | 22 | To use Interceptors in chains, please see [`go-grpc-middleware`](https://github.com/mwitkow/go-grpc-middleware). 23 | 24 | This library requires Go 1.9 or later. 25 | 26 | ## Usage 27 | 28 | There are two types of interceptors: client-side and server-side. This package provides monitoring Interceptors for both. 29 | 30 | ### Server-side 31 | 32 | ```go 33 | import "github.com/grpc-ecosystem/go-grpc-prometheus" 34 | ... 35 | // Initialize your gRPC server's interceptor. 36 | myServer := grpc.NewServer( 37 | grpc.StreamInterceptor(grpc_prometheus.StreamServerInterceptor), 38 | grpc.UnaryInterceptor(grpc_prometheus.UnaryServerInterceptor), 39 | ) 40 | // Register your gRPC service implementations. 41 | myservice.RegisterMyServiceServer(s.server, &myServiceImpl{}) 42 | // After all your registrations, make sure all of the Prometheus metrics are initialized. 43 | grpc_prometheus.Register(myServer) 44 | // Register Prometheus metrics handler. 45 | http.Handle("/metrics", promhttp.Handler()) 46 | ... 47 | ``` 48 | 49 | ### Client-side 50 | 51 | ```go 52 | import "github.com/grpc-ecosystem/go-grpc-prometheus" 53 | ... 54 | clientConn, err = grpc.Dial( 55 | address, 56 | grpc.WithUnaryInterceptor(grpc_prometheus.UnaryClientInterceptor), 57 | grpc.WithStreamInterceptor(grpc_prometheus.StreamClientInterceptor) 58 | ) 59 | client = pb_testproto.NewTestServiceClient(clientConn) 60 | resp, err := client.PingEmpty(s.ctx, &myservice.Request{Msg: "hello"}) 61 | ... 62 | ``` 63 | 64 | # Metrics 65 | 66 | ## Labels 67 | 68 | All server-side metrics start with `grpc_server` as Prometheus subsystem name. All client-side metrics start with `grpc_client`. Both of them have mirror-concepts. Similarly all methods 69 | contain the same rich labels: 70 | 71 | * `grpc_service` - the [gRPC service](http://www.grpc.io/docs/#defining-a-service) name, which is the combination of protobuf `package` and 72 | the `grpc_service` section name. E.g. for `package = mwitkow.testproto` and 73 | `service TestService` the label will be `grpc_service="mwitkow.testproto.TestService"` 74 | * `grpc_method` - the name of the method called on the gRPC service. E.g. 75 | `grpc_method="Ping"` 76 | * `grpc_type` - the gRPC [type of request](http://www.grpc.io/docs/guides/concepts.html#rpc-life-cycle). 77 | Differentiating between the two is important especially for latency measurements. 78 | 79 | - `unary` is single request, single response RPC 80 | - `client_stream` is a multi-request, single response RPC 81 | - `server_stream` is a single request, multi-response RPC 82 | - `bidi_stream` is a multi-request, multi-response RPC 83 | 84 | 85 | Additionally for completed RPCs, the following labels are used: 86 | 87 | * `grpc_code` - the human-readable [gRPC status code](https://github.com/grpc/grpc-go/blob/master/codes/codes.go). 88 | The list of all statuses is to long, but here are some common ones: 89 | 90 | - `OK` - means the RPC was successful 91 | - `IllegalArgument` - RPC contained bad values 92 | - `Internal` - server-side error not disclosed to the clients 93 | 94 | ## Counters 95 | 96 | The counters and their up to date documentation is in [server_reporter.go](server_reporter.go) and [client_reporter.go](client_reporter.go) 97 | the respective Prometheus handler (usually `/metrics`). 98 | 99 | For the purpose of this documentation we will only discuss `grpc_server` metrics. The `grpc_client` ones contain mirror concepts. 100 | 101 | For simplicity, let's assume we're tracking a single server-side RPC call of [`mwitkow.testproto.TestService`](examples/testproto/test.proto), 102 | calling the method `PingList`. The call succeeds and returns 20 messages in the stream. 103 | 104 | First, immediately after the server receives the call it will increment the 105 | `grpc_server_started_total` and start the handling time clock (if histograms are enabled). 106 | 107 | ```jsoniq 108 | grpc_server_started_total{grpc_method="PingList",grpc_service="mwitkow.testproto.TestService",grpc_type="server_stream"} 1 109 | ``` 110 | 111 | Then the user logic gets invoked. It receives one message from the client containing the request 112 | (it's a `server_stream`): 113 | 114 | ```jsoniq 115 | grpc_server_msg_received_total{grpc_method="PingList",grpc_service="mwitkow.testproto.TestService",grpc_type="server_stream"} 1 116 | ``` 117 | 118 | The user logic may return an error, or send multiple messages back to the client. In this case, on 119 | each of the 20 messages sent back, a counter will be incremented: 120 | 121 | ```jsoniq 122 | grpc_server_msg_sent_total{grpc_method="PingList",grpc_service="mwitkow.testproto.TestService",grpc_type="server_stream"} 20 123 | ``` 124 | 125 | After the call completes, its status (`OK` or other [gRPC status code](https://github.com/grpc/grpc-go/blob/master/codes/codes.go)) 126 | and the relevant call labels increment the `grpc_server_handled_total` counter. 127 | 128 | ```jsoniq 129 | grpc_server_handled_total{grpc_code="OK",grpc_method="PingList",grpc_service="mwitkow.testproto.TestService",grpc_type="server_stream"} 1 130 | ``` 131 | 132 | ## Histograms 133 | 134 | [Prometheus histograms](https://prometheus.io/docs/concepts/metric_types/#histogram) are a great way 135 | to measure latency distributions of your RPCs. However, since it is bad practice to have metrics 136 | of [high cardinality](https://prometheus.io/docs/practices/instrumentation/#do-not-overuse-labels) 137 | the latency monitoring metrics are disabled by default. To enable them please call the following 138 | in your server initialization code: 139 | 140 | ```jsoniq 141 | grpc_prometheus.EnableHandlingTimeHistogram() 142 | ``` 143 | 144 | After the call completes, its handling time will be recorded in a [Prometheus histogram](https://prometheus.io/docs/concepts/metric_types/#histogram) 145 | variable `grpc_server_handling_seconds`. The histogram variable contains three sub-metrics: 146 | 147 | * `grpc_server_handling_seconds_count` - the count of all completed RPCs by status and method 148 | * `grpc_server_handling_seconds_sum` - cumulative time of RPCs by status and method, useful for 149 | calculating average handling times 150 | * `grpc_server_handling_seconds_bucket` - contains the counts of RPCs by status and method in respective 151 | handling-time buckets. These buckets can be used by Prometheus to estimate SLAs (see [here](https://prometheus.io/docs/practices/histograms/)) 152 | 153 | The counter values will look as follows: 154 | 155 | ```jsoniq 156 | grpc_server_handling_seconds_bucket{grpc_code="OK",grpc_method="PingList",grpc_service="mwitkow.testproto.TestService",grpc_type="server_stream",le="0.005"} 1 157 | grpc_server_handling_seconds_bucket{grpc_code="OK",grpc_method="PingList",grpc_service="mwitkow.testproto.TestService",grpc_type="server_stream",le="0.01"} 1 158 | grpc_server_handling_seconds_bucket{grpc_code="OK",grpc_method="PingList",grpc_service="mwitkow.testproto.TestService",grpc_type="server_stream",le="0.025"} 1 159 | grpc_server_handling_seconds_bucket{grpc_code="OK",grpc_method="PingList",grpc_service="mwitkow.testproto.TestService",grpc_type="server_stream",le="0.05"} 1 160 | grpc_server_handling_seconds_bucket{grpc_code="OK",grpc_method="PingList",grpc_service="mwitkow.testproto.TestService",grpc_type="server_stream",le="0.1"} 1 161 | grpc_server_handling_seconds_bucket{grpc_code="OK",grpc_method="PingList",grpc_service="mwitkow.testproto.TestService",grpc_type="server_stream",le="0.25"} 1 162 | grpc_server_handling_seconds_bucket{grpc_code="OK",grpc_method="PingList",grpc_service="mwitkow.testproto.TestService",grpc_type="server_stream",le="0.5"} 1 163 | grpc_server_handling_seconds_bucket{grpc_code="OK",grpc_method="PingList",grpc_service="mwitkow.testproto.TestService",grpc_type="server_stream",le="1"} 1 164 | grpc_server_handling_seconds_bucket{grpc_code="OK",grpc_method="PingList",grpc_service="mwitkow.testproto.TestService",grpc_type="server_stream",le="2.5"} 1 165 | grpc_server_handling_seconds_bucket{grpc_code="OK",grpc_method="PingList",grpc_service="mwitkow.testproto.TestService",grpc_type="server_stream",le="5"} 1 166 | grpc_server_handling_seconds_bucket{grpc_code="OK",grpc_method="PingList",grpc_service="mwitkow.testproto.TestService",grpc_type="server_stream",le="10"} 1 167 | grpc_server_handling_seconds_bucket{grpc_code="OK",grpc_method="PingList",grpc_service="mwitkow.testproto.TestService",grpc_type="server_stream",le="+Inf"} 1 168 | grpc_server_handling_seconds_sum{grpc_code="OK",grpc_method="PingList",grpc_service="mwitkow.testproto.TestService",grpc_type="server_stream"} 0.0003866430000000001 169 | grpc_server_handling_seconds_count{grpc_code="OK",grpc_method="PingList",grpc_service="mwitkow.testproto.TestService",grpc_type="server_stream"} 1 170 | ``` 171 | 172 | 173 | ## Useful query examples 174 | 175 | Prometheus philosophy is to provide raw metrics to the monitoring system, and 176 | let the aggregations be handled there. The verbosity of above metrics make it possible to have that 177 | flexibility. Here's a couple of useful monitoring queries: 178 | 179 | 180 | ### request inbound rate 181 | ```jsoniq 182 | sum(rate(grpc_server_started_total{job="foo"}[1m])) by (grpc_service) 183 | ``` 184 | For `job="foo"` (common label to differentiate between Prometheus monitoring targets), calculate the 185 | rate of requests per second (1 minute window) for each gRPC `grpc_service` that the job has. Please note 186 | how the `grpc_method` is being omitted here: all methods of a given gRPC service will be summed together. 187 | 188 | ### unary request error rate 189 | ```jsoniq 190 | sum(rate(grpc_server_handled_total{job="foo",grpc_type="unary",grpc_code!="OK"}[1m])) by (grpc_service) 191 | ``` 192 | For `job="foo"`, calculate the per-`grpc_service` rate of `unary` (1:1) RPCs that failed, i.e. the 193 | ones that didn't finish with `OK` code. 194 | 195 | ### unary request error percentage 196 | ```jsoniq 197 | sum(rate(grpc_server_handled_total{job="foo",grpc_type="unary",grpc_code!="OK"}[1m])) by (grpc_service) 198 | / 199 | sum(rate(grpc_server_started_total{job="foo",grpc_type="unary"}[1m])) by (grpc_service) 200 | * 100.0 201 | ``` 202 | For `job="foo"`, calculate the percentage of failed requests by service. It's easy to notice that 203 | this is a combination of the two above examples. This is an example of a query you would like to 204 | [alert on](https://prometheus.io/docs/alerting/rules/) in your system for SLA violations, e.g. 205 | "no more than 1% requests should fail". 206 | 207 | ### average response stream size 208 | ```jsoniq 209 | sum(rate(grpc_server_msg_sent_total{job="foo",grpc_type="server_stream"}[10m])) by (grpc_service) 210 | / 211 | sum(rate(grpc_server_started_total{job="foo",grpc_type="server_stream"}[10m])) by (grpc_service) 212 | ``` 213 | For `job="foo"` what is the `grpc_service`-wide `10m` average of messages returned for all ` 214 | server_stream` RPCs. This allows you to track the stream sizes returned by your system, e.g. allows 215 | you to track when clients started to send "wide" queries that ret 216 | Note the divisor is the number of started RPCs, in order to account for in-flight requests. 217 | 218 | ### 99%-tile latency of unary requests 219 | ```jsoniq 220 | histogram_quantile(0.99, 221 | sum(rate(grpc_server_handling_seconds_bucket{job="foo",grpc_type="unary"}[5m])) by (grpc_service,le) 222 | ) 223 | ``` 224 | For `job="foo"`, returns an 99%-tile [quantile estimation](https://prometheus.io/docs/practices/histograms/#quantiles) 225 | of the handling time of RPCs per service. Please note the `5m` rate, this means that the quantile 226 | estimation will take samples in a rolling `5m` window. When combined with other quantiles 227 | (e.g. 50%, 90%), this query gives you tremendous insight into the responsiveness of your system 228 | (e.g. impact of caching). 229 | 230 | ### percentage of slow unary queries (>250ms) 231 | ```jsoniq 232 | 100.0 - ( 233 | sum(rate(grpc_server_handling_seconds_bucket{job="foo",grpc_type="unary",le="0.25"}[5m])) by (grpc_service) 234 | / 235 | sum(rate(grpc_server_handling_seconds_count{job="foo",grpc_type="unary"}[5m])) by (grpc_service) 236 | ) * 100.0 237 | ``` 238 | For `job="foo"` calculate the by-`grpc_service` fraction of slow requests that took longer than `0.25` 239 | seconds. This query is relatively complex, since the Prometheus aggregations use `le` (less or equal) 240 | buckets, meaning that counting "fast" requests fractions is easier. However, simple maths helps. 241 | This is an example of a query you would like to alert on in your system for SLA violations, 242 | e.g. "less than 1% of requests are slower than 250ms". 243 | 244 | 245 | ## Status 246 | 247 | This code has been used since August 2015 as the basis for monitoring of *production* gRPC micro services at [Improbable](https://improbable.io). 248 | 249 | ## License 250 | 251 | `go-grpc-prometheus` is released under the Apache 2.0 license. See the [LICENSE](LICENSE) file for details. 252 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Michal Witkowski. All Rights Reserved. 2 | // See LICENSE for licensing terms. 3 | 4 | // gRPC Prometheus monitoring interceptors for client-side gRPC. 5 | 6 | package grpc_prometheus 7 | 8 | import ( 9 | prom "github.com/prometheus/client_golang/prometheus" 10 | ) 11 | 12 | var ( 13 | // DefaultClientMetrics is the default instance of ClientMetrics. It is 14 | // intended to be used in conjunction the default Prometheus metrics 15 | // registry. 16 | DefaultClientMetrics = NewClientMetrics() 17 | 18 | // UnaryClientInterceptor is a gRPC client-side interceptor that provides Prometheus monitoring for Unary RPCs. 19 | UnaryClientInterceptor = DefaultClientMetrics.UnaryClientInterceptor() 20 | 21 | // StreamClientInterceptor is a gRPC client-side interceptor that provides Prometheus monitoring for Streaming RPCs. 22 | StreamClientInterceptor = DefaultClientMetrics.StreamClientInterceptor() 23 | ) 24 | 25 | func init() { 26 | prom.MustRegister(DefaultClientMetrics.clientStartedCounter) 27 | prom.MustRegister(DefaultClientMetrics.clientHandledCounter) 28 | prom.MustRegister(DefaultClientMetrics.clientStreamMsgReceived) 29 | prom.MustRegister(DefaultClientMetrics.clientStreamMsgSent) 30 | } 31 | 32 | // EnableClientHandlingTimeHistogram turns on recording of handling time of 33 | // RPCs. Histogram metrics can be very expensive for Prometheus to retain and 34 | // query. This function acts on the DefaultClientMetrics variable and the 35 | // default Prometheus metrics registry. 36 | func EnableClientHandlingTimeHistogram(opts ...HistogramOption) { 37 | DefaultClientMetrics.EnableClientHandlingTimeHistogram(opts...) 38 | prom.Register(DefaultClientMetrics.clientHandledHistogram) 39 | } 40 | 41 | // EnableClientStreamReceiveTimeHistogram turns on recording of 42 | // single message receive time of streaming RPCs. 43 | // This function acts on the DefaultClientMetrics variable and the 44 | // default Prometheus metrics registry. 45 | func EnableClientStreamReceiveTimeHistogram(opts ...HistogramOption) { 46 | DefaultClientMetrics.EnableClientStreamReceiveTimeHistogram(opts...) 47 | prom.Register(DefaultClientMetrics.clientStreamRecvHistogram) 48 | } 49 | 50 | // EnableClientStreamSendTimeHistogram turns on recording of 51 | // single message send time of streaming RPCs. 52 | // This function acts on the DefaultClientMetrics variable and the 53 | // default Prometheus metrics registry. 54 | func EnableClientStreamSendTimeHistogram(opts ...HistogramOption) { 55 | DefaultClientMetrics.EnableClientStreamSendTimeHistogram(opts...) 56 | prom.Register(DefaultClientMetrics.clientStreamSendHistogram) 57 | } 58 | -------------------------------------------------------------------------------- /client_metrics.go: -------------------------------------------------------------------------------- 1 | package grpc_prometheus 2 | 3 | import ( 4 | "context" 5 | "io" 6 | 7 | prom "github.com/prometheus/client_golang/prometheus" 8 | "google.golang.org/grpc" 9 | "google.golang.org/grpc/codes" 10 | "google.golang.org/grpc/status" 11 | ) 12 | 13 | // ClientMetrics represents a collection of metrics to be registered on a 14 | // Prometheus metrics registry for a gRPC client. 15 | type ClientMetrics struct { 16 | clientStartedCounter *prom.CounterVec 17 | clientHandledCounter *prom.CounterVec 18 | clientStreamMsgReceived *prom.CounterVec 19 | clientStreamMsgSent *prom.CounterVec 20 | 21 | clientHandledHistogramEnabled bool 22 | clientHandledHistogramOpts prom.HistogramOpts 23 | clientHandledHistogram *prom.HistogramVec 24 | 25 | clientStreamRecvHistogramEnabled bool 26 | clientStreamRecvHistogramOpts prom.HistogramOpts 27 | clientStreamRecvHistogram *prom.HistogramVec 28 | 29 | clientStreamSendHistogramEnabled bool 30 | clientStreamSendHistogramOpts prom.HistogramOpts 31 | clientStreamSendHistogram *prom.HistogramVec 32 | } 33 | 34 | // NewClientMetrics returns a ClientMetrics object. Use a new instance of 35 | // ClientMetrics when not using the default Prometheus metrics registry, for 36 | // example when wanting to control which metrics are added to a registry as 37 | // opposed to automatically adding metrics via init functions. 38 | func NewClientMetrics(counterOpts ...CounterOption) *ClientMetrics { 39 | opts := counterOptions(counterOpts) 40 | return &ClientMetrics{ 41 | clientStartedCounter: prom.NewCounterVec( 42 | opts.apply(prom.CounterOpts{ 43 | Name: "grpc_client_started_total", 44 | Help: "Total number of RPCs started on the client.", 45 | }), []string{"grpc_type", "grpc_service", "grpc_method"}), 46 | 47 | clientHandledCounter: prom.NewCounterVec( 48 | opts.apply(prom.CounterOpts{ 49 | Name: "grpc_client_handled_total", 50 | Help: "Total number of RPCs completed by the client, regardless of success or failure.", 51 | }), []string{"grpc_type", "grpc_service", "grpc_method", "grpc_code"}), 52 | 53 | clientStreamMsgReceived: prom.NewCounterVec( 54 | opts.apply(prom.CounterOpts{ 55 | Name: "grpc_client_msg_received_total", 56 | Help: "Total number of RPC stream messages received by the client.", 57 | }), []string{"grpc_type", "grpc_service", "grpc_method"}), 58 | 59 | clientStreamMsgSent: prom.NewCounterVec( 60 | opts.apply(prom.CounterOpts{ 61 | Name: "grpc_client_msg_sent_total", 62 | Help: "Total number of gRPC stream messages sent by the client.", 63 | }), []string{"grpc_type", "grpc_service", "grpc_method"}), 64 | 65 | clientHandledHistogramEnabled: false, 66 | clientHandledHistogramOpts: prom.HistogramOpts{ 67 | Name: "grpc_client_handling_seconds", 68 | Help: "Histogram of response latency (seconds) of the gRPC until it is finished by the application.", 69 | Buckets: prom.DefBuckets, 70 | }, 71 | clientHandledHistogram: nil, 72 | clientStreamRecvHistogramEnabled: false, 73 | clientStreamRecvHistogramOpts: prom.HistogramOpts{ 74 | Name: "grpc_client_msg_recv_handling_seconds", 75 | Help: "Histogram of response latency (seconds) of the gRPC single message receive.", 76 | Buckets: prom.DefBuckets, 77 | }, 78 | clientStreamRecvHistogram: nil, 79 | clientStreamSendHistogramEnabled: false, 80 | clientStreamSendHistogramOpts: prom.HistogramOpts{ 81 | Name: "grpc_client_msg_send_handling_seconds", 82 | Help: "Histogram of response latency (seconds) of the gRPC single message send.", 83 | Buckets: prom.DefBuckets, 84 | }, 85 | clientStreamSendHistogram: nil, 86 | } 87 | } 88 | 89 | // Describe sends the super-set of all possible descriptors of metrics 90 | // collected by this Collector to the provided channel and returns once 91 | // the last descriptor has been sent. 92 | func (m *ClientMetrics) Describe(ch chan<- *prom.Desc) { 93 | m.clientStartedCounter.Describe(ch) 94 | m.clientHandledCounter.Describe(ch) 95 | m.clientStreamMsgReceived.Describe(ch) 96 | m.clientStreamMsgSent.Describe(ch) 97 | if m.clientHandledHistogramEnabled { 98 | m.clientHandledHistogram.Describe(ch) 99 | } 100 | if m.clientStreamRecvHistogramEnabled { 101 | m.clientStreamRecvHistogram.Describe(ch) 102 | } 103 | if m.clientStreamSendHistogramEnabled { 104 | m.clientStreamSendHistogram.Describe(ch) 105 | } 106 | } 107 | 108 | // Collect is called by the Prometheus registry when collecting 109 | // metrics. The implementation sends each collected metric via the 110 | // provided channel and returns once the last metric has been sent. 111 | func (m *ClientMetrics) Collect(ch chan<- prom.Metric) { 112 | m.clientStartedCounter.Collect(ch) 113 | m.clientHandledCounter.Collect(ch) 114 | m.clientStreamMsgReceived.Collect(ch) 115 | m.clientStreamMsgSent.Collect(ch) 116 | if m.clientHandledHistogramEnabled { 117 | m.clientHandledHistogram.Collect(ch) 118 | } 119 | if m.clientStreamRecvHistogramEnabled { 120 | m.clientStreamRecvHistogram.Collect(ch) 121 | } 122 | if m.clientStreamSendHistogramEnabled { 123 | m.clientStreamSendHistogram.Collect(ch) 124 | } 125 | } 126 | 127 | // EnableClientHandlingTimeHistogram turns on recording of handling time of RPCs. 128 | // Histogram metrics can be very expensive for Prometheus to retain and query. 129 | func (m *ClientMetrics) EnableClientHandlingTimeHistogram(opts ...HistogramOption) { 130 | for _, o := range opts { 131 | o(&m.clientHandledHistogramOpts) 132 | } 133 | if !m.clientHandledHistogramEnabled { 134 | m.clientHandledHistogram = prom.NewHistogramVec( 135 | m.clientHandledHistogramOpts, 136 | []string{"grpc_type", "grpc_service", "grpc_method"}, 137 | ) 138 | } 139 | m.clientHandledHistogramEnabled = true 140 | } 141 | 142 | // EnableClientStreamReceiveTimeHistogram turns on recording of single message receive time of streaming RPCs. 143 | // Histogram metrics can be very expensive for Prometheus to retain and query. 144 | func (m *ClientMetrics) EnableClientStreamReceiveTimeHistogram(opts ...HistogramOption) { 145 | for _, o := range opts { 146 | o(&m.clientStreamRecvHistogramOpts) 147 | } 148 | 149 | if !m.clientStreamRecvHistogramEnabled { 150 | m.clientStreamRecvHistogram = prom.NewHistogramVec( 151 | m.clientStreamRecvHistogramOpts, 152 | []string{"grpc_type", "grpc_service", "grpc_method"}, 153 | ) 154 | } 155 | 156 | m.clientStreamRecvHistogramEnabled = true 157 | } 158 | 159 | // EnableClientStreamSendTimeHistogram turns on recording of single message send time of streaming RPCs. 160 | // Histogram metrics can be very expensive for Prometheus to retain and query. 161 | func (m *ClientMetrics) EnableClientStreamSendTimeHistogram(opts ...HistogramOption) { 162 | for _, o := range opts { 163 | o(&m.clientStreamSendHistogramOpts) 164 | } 165 | 166 | if !m.clientStreamSendHistogramEnabled { 167 | m.clientStreamSendHistogram = prom.NewHistogramVec( 168 | m.clientStreamSendHistogramOpts, 169 | []string{"grpc_type", "grpc_service", "grpc_method"}, 170 | ) 171 | } 172 | 173 | m.clientStreamSendHistogramEnabled = true 174 | } 175 | 176 | // UnaryClientInterceptor is a gRPC client-side interceptor that provides Prometheus monitoring for Unary RPCs. 177 | func (m *ClientMetrics) UnaryClientInterceptor() func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { 178 | return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { 179 | monitor := newClientReporter(m, Unary, method) 180 | monitor.SentMessage() 181 | err := invoker(ctx, method, req, reply, cc, opts...) 182 | if err == nil { 183 | monitor.ReceivedMessage() 184 | } 185 | st, _ := status.FromError(err) 186 | monitor.Handled(st.Code()) 187 | return err 188 | } 189 | } 190 | 191 | // StreamClientInterceptor is a gRPC client-side interceptor that provides Prometheus monitoring for Streaming RPCs. 192 | func (m *ClientMetrics) StreamClientInterceptor() func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) { 193 | return func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) { 194 | monitor := newClientReporter(m, clientStreamType(desc), method) 195 | clientStream, err := streamer(ctx, desc, cc, method, opts...) 196 | if err != nil { 197 | st, _ := status.FromError(err) 198 | monitor.Handled(st.Code()) 199 | return nil, err 200 | } 201 | return &monitoredClientStream{clientStream, monitor}, nil 202 | } 203 | } 204 | 205 | func clientStreamType(desc *grpc.StreamDesc) grpcType { 206 | if desc.ClientStreams && !desc.ServerStreams { 207 | return ClientStream 208 | } else if !desc.ClientStreams && desc.ServerStreams { 209 | return ServerStream 210 | } 211 | return BidiStream 212 | } 213 | 214 | // monitoredClientStream wraps grpc.ClientStream allowing each Sent/Recv of message to increment counters. 215 | type monitoredClientStream struct { 216 | grpc.ClientStream 217 | monitor *clientReporter 218 | } 219 | 220 | func (s *monitoredClientStream) SendMsg(m interface{}) error { 221 | timer := s.monitor.SendMessageTimer() 222 | err := s.ClientStream.SendMsg(m) 223 | timer.ObserveDuration() 224 | if err == nil { 225 | s.monitor.SentMessage() 226 | } 227 | return err 228 | } 229 | 230 | func (s *monitoredClientStream) RecvMsg(m interface{}) error { 231 | timer := s.monitor.ReceiveMessageTimer() 232 | err := s.ClientStream.RecvMsg(m) 233 | timer.ObserveDuration() 234 | 235 | if err == nil { 236 | s.monitor.ReceivedMessage() 237 | } else if err == io.EOF { 238 | s.monitor.Handled(codes.OK) 239 | } else { 240 | st, _ := status.FromError(err) 241 | s.monitor.Handled(st.Code()) 242 | } 243 | return err 244 | } 245 | -------------------------------------------------------------------------------- /client_reporter.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Michal Witkowski. All Rights Reserved. 2 | // See LICENSE for licensing terms. 3 | 4 | package grpc_prometheus 5 | 6 | import ( 7 | "time" 8 | 9 | "github.com/prometheus/client_golang/prometheus" 10 | "google.golang.org/grpc/codes" 11 | ) 12 | 13 | type clientReporter struct { 14 | metrics *ClientMetrics 15 | rpcType grpcType 16 | serviceName string 17 | methodName string 18 | startTime time.Time 19 | } 20 | 21 | func newClientReporter(m *ClientMetrics, rpcType grpcType, fullMethod string) *clientReporter { 22 | r := &clientReporter{ 23 | metrics: m, 24 | rpcType: rpcType, 25 | } 26 | if r.metrics.clientHandledHistogramEnabled { 27 | r.startTime = time.Now() 28 | } 29 | r.serviceName, r.methodName = splitMethodName(fullMethod) 30 | r.metrics.clientStartedCounter.WithLabelValues(string(r.rpcType), r.serviceName, r.methodName).Inc() 31 | return r 32 | } 33 | 34 | // timer is a helper interface to time functions. 35 | type timer interface { 36 | ObserveDuration() time.Duration 37 | } 38 | 39 | type noOpTimer struct { 40 | } 41 | 42 | func (noOpTimer) ObserveDuration() time.Duration { 43 | return 0 44 | } 45 | 46 | var emptyTimer = noOpTimer{} 47 | 48 | func (r *clientReporter) ReceiveMessageTimer() timer { 49 | if r.metrics.clientStreamRecvHistogramEnabled { 50 | hist := r.metrics.clientStreamRecvHistogram.WithLabelValues(string(r.rpcType), r.serviceName, r.methodName) 51 | return prometheus.NewTimer(hist) 52 | } 53 | 54 | return emptyTimer 55 | } 56 | 57 | func (r *clientReporter) ReceivedMessage() { 58 | r.metrics.clientStreamMsgReceived.WithLabelValues(string(r.rpcType), r.serviceName, r.methodName).Inc() 59 | } 60 | 61 | func (r *clientReporter) SendMessageTimer() timer { 62 | if r.metrics.clientStreamSendHistogramEnabled { 63 | hist := r.metrics.clientStreamSendHistogram.WithLabelValues(string(r.rpcType), r.serviceName, r.methodName) 64 | return prometheus.NewTimer(hist) 65 | } 66 | 67 | return emptyTimer 68 | } 69 | 70 | func (r *clientReporter) SentMessage() { 71 | r.metrics.clientStreamMsgSent.WithLabelValues(string(r.rpcType), r.serviceName, r.methodName).Inc() 72 | } 73 | 74 | func (r *clientReporter) Handled(code codes.Code) { 75 | r.metrics.clientHandledCounter.WithLabelValues(string(r.rpcType), r.serviceName, r.methodName, code.String()).Inc() 76 | if r.metrics.clientHandledHistogramEnabled { 77 | r.metrics.clientHandledHistogram.WithLabelValues(string(r.rpcType), r.serviceName, r.methodName).Observe(time.Since(r.startTime).Seconds()) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /client_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Michal Witkowski. All Rights Reserved. 2 | // See LICENSE for licensing terms. 3 | 4 | package grpc_prometheus 5 | 6 | import ( 7 | "context" 8 | "io" 9 | "net" 10 | "testing" 11 | "time" 12 | 13 | pb_testproto "github.com/grpc-ecosystem/go-grpc-prometheus/examples/testproto" 14 | "github.com/prometheus/client_golang/prometheus" 15 | "github.com/stretchr/testify/require" 16 | "github.com/stretchr/testify/suite" 17 | "google.golang.org/grpc" 18 | "google.golang.org/grpc/codes" 19 | "google.golang.org/grpc/status" 20 | ) 21 | 22 | var ( 23 | // client metrics must satisfy the Collector interface 24 | _ prometheus.Collector = NewClientMetrics() 25 | ) 26 | 27 | func TestClientInterceptorSuite(t *testing.T) { 28 | suite.Run(t, &ClientInterceptorTestSuite{}) 29 | } 30 | 31 | type ClientInterceptorTestSuite struct { 32 | suite.Suite 33 | 34 | serverListener net.Listener 35 | server *grpc.Server 36 | clientConn *grpc.ClientConn 37 | testClient pb_testproto.TestServiceClient 38 | ctx context.Context 39 | cancel context.CancelFunc 40 | } 41 | 42 | func (s *ClientInterceptorTestSuite) SetupSuite() { 43 | var err error 44 | 45 | EnableClientHandlingTimeHistogram() 46 | 47 | s.serverListener, err = net.Listen("tcp", "127.0.0.1:0") 48 | require.NoError(s.T(), err, "must be able to allocate a port for serverListener") 49 | 50 | // This is the point where we hook up the interceptor 51 | s.server = grpc.NewServer() 52 | pb_testproto.RegisterTestServiceServer(s.server, &testService{t: s.T()}) 53 | 54 | go func() { 55 | s.server.Serve(s.serverListener) 56 | }() 57 | 58 | s.clientConn, err = grpc.Dial( 59 | s.serverListener.Addr().String(), 60 | grpc.WithInsecure(), 61 | grpc.WithBlock(), 62 | grpc.WithUnaryInterceptor(UnaryClientInterceptor), 63 | grpc.WithStreamInterceptor(StreamClientInterceptor), 64 | grpc.WithTimeout(2*time.Second)) 65 | require.NoError(s.T(), err, "must not error on client Dial") 66 | s.testClient = pb_testproto.NewTestServiceClient(s.clientConn) 67 | } 68 | 69 | func (s *ClientInterceptorTestSuite) SetupTest() { 70 | // Make all RPC calls last at most 2 sec, meaning all async issues or deadlock will not kill tests. 71 | s.ctx, s.cancel = context.WithTimeout(context.TODO(), 2*time.Second) 72 | 73 | // Make sure every test starts with same fresh, intialized metric state. 74 | DefaultClientMetrics.clientStartedCounter.Reset() 75 | DefaultClientMetrics.clientHandledCounter.Reset() 76 | DefaultClientMetrics.clientHandledHistogram.Reset() 77 | DefaultClientMetrics.clientStreamMsgReceived.Reset() 78 | DefaultClientMetrics.clientStreamMsgSent.Reset() 79 | } 80 | 81 | func (s *ClientInterceptorTestSuite) TearDownSuite() { 82 | if s.serverListener != nil { 83 | s.server.Stop() 84 | s.T().Logf("stopped grpc.Server at: %v", s.serverListener.Addr().String()) 85 | s.serverListener.Close() 86 | 87 | } 88 | if s.clientConn != nil { 89 | s.clientConn.Close() 90 | } 91 | } 92 | 93 | func (s *ClientInterceptorTestSuite) TearDownTest() { 94 | s.cancel() 95 | } 96 | 97 | func (s *ClientInterceptorTestSuite) TestUnaryIncrementsMetrics() { 98 | _, err := s.testClient.PingEmpty(s.ctx, &pb_testproto.Empty{}) // should return with code=OK 99 | require.NoError(s.T(), err) 100 | requireValue(s.T(), 1, DefaultClientMetrics.clientStartedCounter.WithLabelValues("unary", "mwitkow.testproto.TestService", "PingEmpty")) 101 | requireValue(s.T(), 1, DefaultClientMetrics.clientHandledCounter.WithLabelValues("unary", "mwitkow.testproto.TestService", "PingEmpty", "OK")) 102 | requireValueHistCount(s.T(), 1, DefaultClientMetrics.clientHandledHistogram.WithLabelValues("unary", "mwitkow.testproto.TestService", "PingEmpty")) 103 | 104 | _, err = s.testClient.PingError(s.ctx, &pb_testproto.PingRequest{ErrorCodeReturned: uint32(codes.FailedPrecondition)}) // should return with code=FailedPrecondition 105 | require.Error(s.T(), err) 106 | requireValue(s.T(), 1, DefaultClientMetrics.clientStartedCounter.WithLabelValues("unary", "mwitkow.testproto.TestService", "PingError")) 107 | requireValue(s.T(), 1, DefaultClientMetrics.clientHandledCounter.WithLabelValues("unary", "mwitkow.testproto.TestService", "PingError", "FailedPrecondition")) 108 | requireValueHistCount(s.T(), 1, DefaultClientMetrics.clientHandledHistogram.WithLabelValues("unary", "mwitkow.testproto.TestService", "PingError")) 109 | } 110 | 111 | func (s *ClientInterceptorTestSuite) TestStartedStreamingIncrementsStarted() { 112 | _, err := s.testClient.PingList(s.ctx, &pb_testproto.PingRequest{}) 113 | require.NoError(s.T(), err) 114 | requireValue(s.T(), 1, DefaultClientMetrics.clientStartedCounter.WithLabelValues("server_stream", "mwitkow.testproto.TestService", "PingList")) 115 | 116 | _, err = s.testClient.PingList(s.ctx, &pb_testproto.PingRequest{ErrorCodeReturned: uint32(codes.FailedPrecondition)}) // should return with code=FailedPrecondition 117 | require.NoError(s.T(), err, "PingList must not fail immediately") 118 | requireValue(s.T(), 2, DefaultClientMetrics.clientStartedCounter.WithLabelValues("server_stream", "mwitkow.testproto.TestService", "PingList")) 119 | } 120 | 121 | func (s *ClientInterceptorTestSuite) TestStreamingIncrementsMetrics() { 122 | ss, _ := s.testClient.PingList(s.ctx, &pb_testproto.PingRequest{}) // should return with code=OK 123 | // Do a read, just for kicks. 124 | count := 0 125 | for { 126 | _, err := ss.Recv() 127 | if err == io.EOF { 128 | break 129 | } 130 | require.NoError(s.T(), err, "reading pingList shouldn't fail") 131 | count++ 132 | } 133 | require.EqualValues(s.T(), countListResponses, count, "Number of received msg on the wire must match") 134 | 135 | requireValue(s.T(), 1, DefaultClientMetrics.clientStartedCounter.WithLabelValues("server_stream", "mwitkow.testproto.TestService", "PingList")) 136 | requireValue(s.T(), 1, DefaultClientMetrics.clientHandledCounter.WithLabelValues("server_stream", "mwitkow.testproto.TestService", "PingList", "OK")) 137 | requireValue(s.T(), countListResponses, DefaultClientMetrics.clientStreamMsgReceived.WithLabelValues("server_stream", "mwitkow.testproto.TestService", "PingList")) 138 | requireValue(s.T(), 1, DefaultClientMetrics.clientStreamMsgSent.WithLabelValues("server_stream", "mwitkow.testproto.TestService", "PingList")) 139 | requireValueHistCount(s.T(), 1, DefaultClientMetrics.clientHandledHistogram.WithLabelValues("server_stream", "mwitkow.testproto.TestService", "PingList")) 140 | 141 | ss, err := s.testClient.PingList(s.ctx, &pb_testproto.PingRequest{ErrorCodeReturned: uint32(codes.FailedPrecondition)}) // should return with code=FailedPrecondition 142 | require.NoError(s.T(), err, "PingList must not fail immediately") 143 | 144 | // Do a read, just to progate errors. 145 | _, err = ss.Recv() 146 | st, _ := status.FromError(err) 147 | require.Equal(s.T(), codes.FailedPrecondition, st.Code(), "Recv must return FailedPrecondition, otherwise the test is wrong") 148 | 149 | requireValue(s.T(), 2, DefaultClientMetrics.clientStartedCounter.WithLabelValues("server_stream", "mwitkow.testproto.TestService", "PingList")) 150 | requireValue(s.T(), 1, DefaultClientMetrics.clientHandledCounter.WithLabelValues("server_stream", "mwitkow.testproto.TestService", "PingList", "FailedPrecondition")) 151 | requireValueHistCount(s.T(), 2, DefaultClientMetrics.clientHandledHistogram.WithLabelValues("server_stream", "mwitkow.testproto.TestService", "PingList")) 152 | } 153 | -------------------------------------------------------------------------------- /examples/grpc-server-with-prometheus/client/client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "fmt" 7 | "log" 8 | "net/http" 9 | "os" 10 | "strings" 11 | "time" 12 | 13 | "google.golang.org/grpc" 14 | 15 | "github.com/grpc-ecosystem/go-grpc-prometheus" 16 | pb "github.com/grpc-ecosystem/go-grpc-prometheus/examples/grpc-server-with-prometheus/protobuf" 17 | "github.com/prometheus/client_golang/prometheus" 18 | "github.com/prometheus/client_golang/prometheus/promhttp" 19 | ) 20 | 21 | func main() { 22 | // Create a metrics registry. 23 | reg := prometheus.NewRegistry() 24 | // Create some standard client metrics. 25 | grpcMetrics := grpc_prometheus.NewClientMetrics() 26 | // Register client metrics to registry. 27 | reg.MustRegister(grpcMetrics) 28 | // Create a insecure gRPC channel to communicate with the server. 29 | conn, err := grpc.Dial( 30 | fmt.Sprintf("localhost:%v", 9093), 31 | grpc.WithUnaryInterceptor(grpcMetrics.UnaryClientInterceptor()), 32 | grpc.WithStreamInterceptor(grpcMetrics.StreamClientInterceptor()), 33 | grpc.WithInsecure(), 34 | ) 35 | if err != nil { 36 | log.Fatal(err) 37 | } 38 | 39 | defer conn.Close() 40 | 41 | // Create a HTTP server for prometheus. 42 | httpServer := &http.Server{Handler: promhttp.HandlerFor(reg, promhttp.HandlerOpts{}), Addr: fmt.Sprintf("0.0.0.0:%d", 9094)} 43 | 44 | // Start your http server for prometheus. 45 | go func() { 46 | if err := httpServer.ListenAndServe(); err != nil { 47 | log.Fatal("Unable to start a http server.") 48 | } 49 | }() 50 | 51 | // Create a gRPC server client. 52 | client := pb.NewDemoServiceClient(conn) 53 | fmt.Println("Start to call the method called SayHello every 3 seconds") 54 | go func() { 55 | for { 56 | // Call “SayHello” method and wait for response from gRPC Server. 57 | _, err := client.SayHello(context.Background(), &pb.HelloRequest{Name: "Test"}) 58 | if err != nil { 59 | log.Printf("Calling the SayHello method unsuccessfully. ErrorInfo: %+v", err) 60 | log.Printf("You should to stop the process") 61 | return 62 | } 63 | time.Sleep(3 * time.Second) 64 | } 65 | }() 66 | scanner := bufio.NewScanner(os.Stdin) 67 | fmt.Println("You can press n or N to stop the process of client") 68 | for scanner.Scan() { 69 | if strings.ToLower(scanner.Text()) == "n" { 70 | os.Exit(0) 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /examples/grpc-server-with-prometheus/prometheus/prometheus.yaml: -------------------------------------------------------------------------------- 1 | # my global config 2 | global: 3 | scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute. 4 | evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute. 5 | # scrape_timeout is set to the global default (10s). 6 | 7 | # Attach these labels to any time series or alerts when communicating with 8 | # external systems (federation, remote storage, Alertmanager). 9 | external_labels: 10 | monitor: 'kirk-grpc-service-monitor' 11 | 12 | # Load rules once and periodically evaluate them according to the global 'evaluation_interval'. 13 | rule_files: 14 | # - "first.rules" 15 | # - "second.rules" 16 | 17 | # A scrape configuration containing exactly one endpoint to scrape: 18 | # Here it's Prometheus itself. 19 | scrape_configs: 20 | # The job name is added as a label `job=` to any timeseries scraped from this config. 21 | 22 | # - job_name: 'prometheus' 23 | # scrape_interval: 5s 24 | # static_configs: 25 | # - targets: ['localhost:9090'] 26 | 27 | - job_name: 'grpcserver' 28 | scrape_interval: 1s 29 | static_configs: 30 | - targets: ['localhost:9092'] 31 | - job_name: 'grpcclient' 32 | scrape_interval: 1s 33 | static_configs: 34 | - targets: ['localhost:9094'] 35 | -------------------------------------------------------------------------------- /examples/grpc-server-with-prometheus/protobuf/service.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // source: service.proto 3 | 4 | package proto 5 | 6 | import proto "github.com/golang/protobuf/proto" 7 | import fmt "fmt" 8 | import math "math" 9 | 10 | import ( 11 | context "golang.org/x/net/context" 12 | grpc "google.golang.org/grpc" 13 | ) 14 | 15 | // Reference imports to suppress errors if they are not otherwise used. 16 | var _ = proto.Marshal 17 | var _ = fmt.Errorf 18 | var _ = math.Inf 19 | 20 | // This is a compile-time assertion to ensure that this generated file 21 | // is compatible with the proto package it is being compiled against. 22 | // A compilation error at this line likely means your copy of the 23 | // proto package needs to be updated. 24 | const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package 25 | 26 | type HelloRequest struct { 27 | Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` 28 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 29 | XXX_unrecognized []byte `json:"-"` 30 | XXX_sizecache int32 `json:"-"` 31 | } 32 | 33 | func (m *HelloRequest) Reset() { *m = HelloRequest{} } 34 | func (m *HelloRequest) String() string { return proto.CompactTextString(m) } 35 | func (*HelloRequest) ProtoMessage() {} 36 | func (*HelloRequest) Descriptor() ([]byte, []int) { 37 | return fileDescriptor_service_85197be0990351b4, []int{0} 38 | } 39 | func (m *HelloRequest) XXX_Unmarshal(b []byte) error { 40 | return xxx_messageInfo_HelloRequest.Unmarshal(m, b) 41 | } 42 | func (m *HelloRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 43 | return xxx_messageInfo_HelloRequest.Marshal(b, m, deterministic) 44 | } 45 | func (dst *HelloRequest) XXX_Merge(src proto.Message) { 46 | xxx_messageInfo_HelloRequest.Merge(dst, src) 47 | } 48 | func (m *HelloRequest) XXX_Size() int { 49 | return xxx_messageInfo_HelloRequest.Size(m) 50 | } 51 | func (m *HelloRequest) XXX_DiscardUnknown() { 52 | xxx_messageInfo_HelloRequest.DiscardUnknown(m) 53 | } 54 | 55 | var xxx_messageInfo_HelloRequest proto.InternalMessageInfo 56 | 57 | func (m *HelloRequest) GetName() string { 58 | if m != nil { 59 | return m.Name 60 | } 61 | return "" 62 | } 63 | 64 | type HelloResponse struct { 65 | Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` 66 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 67 | XXX_unrecognized []byte `json:"-"` 68 | XXX_sizecache int32 `json:"-"` 69 | } 70 | 71 | func (m *HelloResponse) Reset() { *m = HelloResponse{} } 72 | func (m *HelloResponse) String() string { return proto.CompactTextString(m) } 73 | func (*HelloResponse) ProtoMessage() {} 74 | func (*HelloResponse) Descriptor() ([]byte, []int) { 75 | return fileDescriptor_service_85197be0990351b4, []int{1} 76 | } 77 | func (m *HelloResponse) XXX_Unmarshal(b []byte) error { 78 | return xxx_messageInfo_HelloResponse.Unmarshal(m, b) 79 | } 80 | func (m *HelloResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 81 | return xxx_messageInfo_HelloResponse.Marshal(b, m, deterministic) 82 | } 83 | func (dst *HelloResponse) XXX_Merge(src proto.Message) { 84 | xxx_messageInfo_HelloResponse.Merge(dst, src) 85 | } 86 | func (m *HelloResponse) XXX_Size() int { 87 | return xxx_messageInfo_HelloResponse.Size(m) 88 | } 89 | func (m *HelloResponse) XXX_DiscardUnknown() { 90 | xxx_messageInfo_HelloResponse.DiscardUnknown(m) 91 | } 92 | 93 | var xxx_messageInfo_HelloResponse proto.InternalMessageInfo 94 | 95 | func (m *HelloResponse) GetMessage() string { 96 | if m != nil { 97 | return m.Message 98 | } 99 | return "" 100 | } 101 | 102 | func init() { 103 | proto.RegisterType((*HelloRequest)(nil), "proto.HelloRequest") 104 | proto.RegisterType((*HelloResponse)(nil), "proto.HelloResponse") 105 | } 106 | 107 | // Reference imports to suppress errors if they are not otherwise used. 108 | var _ context.Context 109 | var _ grpc.ClientConn 110 | 111 | // This is a compile-time assertion to ensure that this generated file 112 | // is compatible with the grpc package it is being compiled against. 113 | const _ = grpc.SupportPackageIsVersion4 114 | 115 | // DemoServiceClient is the client API for DemoService service. 116 | // 117 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. 118 | type DemoServiceClient interface { 119 | SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloResponse, error) 120 | } 121 | 122 | type demoServiceClient struct { 123 | cc *grpc.ClientConn 124 | } 125 | 126 | func NewDemoServiceClient(cc *grpc.ClientConn) DemoServiceClient { 127 | return &demoServiceClient{cc} 128 | } 129 | 130 | func (c *demoServiceClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloResponse, error) { 131 | out := new(HelloResponse) 132 | err := c.cc.Invoke(ctx, "/proto.DemoService/SayHello", in, out, opts...) 133 | if err != nil { 134 | return nil, err 135 | } 136 | return out, nil 137 | } 138 | 139 | // DemoServiceServer is the server API for DemoService service. 140 | type DemoServiceServer interface { 141 | SayHello(context.Context, *HelloRequest) (*HelloResponse, error) 142 | } 143 | 144 | func RegisterDemoServiceServer(s *grpc.Server, srv DemoServiceServer) { 145 | s.RegisterService(&_DemoService_serviceDesc, srv) 146 | } 147 | 148 | func _DemoService_SayHello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 149 | in := new(HelloRequest) 150 | if err := dec(in); err != nil { 151 | return nil, err 152 | } 153 | if interceptor == nil { 154 | return srv.(DemoServiceServer).SayHello(ctx, in) 155 | } 156 | info := &grpc.UnaryServerInfo{ 157 | Server: srv, 158 | FullMethod: "/proto.DemoService/SayHello", 159 | } 160 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 161 | return srv.(DemoServiceServer).SayHello(ctx, req.(*HelloRequest)) 162 | } 163 | return interceptor(ctx, in, info, handler) 164 | } 165 | 166 | var _DemoService_serviceDesc = grpc.ServiceDesc{ 167 | ServiceName: "proto.DemoService", 168 | HandlerType: (*DemoServiceServer)(nil), 169 | Methods: []grpc.MethodDesc{ 170 | { 171 | MethodName: "SayHello", 172 | Handler: _DemoService_SayHello_Handler, 173 | }, 174 | }, 175 | Streams: []grpc.StreamDesc{}, 176 | Metadata: "service.proto", 177 | } 178 | 179 | func init() { proto.RegisterFile("service.proto", fileDescriptor_service_85197be0990351b4) } 180 | 181 | var fileDescriptor_service_85197be0990351b4 = []byte{ 182 | // 142 bytes of a gzipped FileDescriptorProto 183 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x2d, 0x4e, 0x2d, 0x2a, 184 | 0xcb, 0x4c, 0x4e, 0xd5, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x62, 0x05, 0x53, 0x4a, 0x4a, 0x5c, 185 | 0x3c, 0x1e, 0xa9, 0x39, 0x39, 0xf9, 0x41, 0xa9, 0x85, 0xa5, 0xa9, 0xc5, 0x25, 0x42, 0x42, 0x5c, 186 | 0x2c, 0x79, 0x89, 0xb9, 0xa9, 0x12, 0x8c, 0x0a, 0x8c, 0x1a, 0x9c, 0x41, 0x60, 0xb6, 0x92, 0x26, 187 | 0x17, 0x2f, 0x54, 0x4d, 0x71, 0x41, 0x7e, 0x5e, 0x71, 0xaa, 0x90, 0x04, 0x17, 0x7b, 0x6e, 0x6a, 188 | 0x71, 0x71, 0x62, 0x3a, 0x4c, 0x1d, 0x8c, 0x6b, 0xe4, 0xc6, 0xc5, 0xed, 0x92, 0x9a, 0x9b, 0x1f, 189 | 0x0c, 0xb1, 0x4a, 0xc8, 0x9c, 0x8b, 0x23, 0x38, 0xb1, 0x12, 0xac, 0x59, 0x48, 0x18, 0x62, 0xb1, 190 | 0x1e, 0xb2, 0x75, 0x52, 0x22, 0xa8, 0x82, 0x10, 0xf3, 0x95, 0x18, 0x92, 0xd8, 0xc0, 0xc2, 0xc6, 191 | 0x80, 0x00, 0x00, 0x00, 0xff, 0xff, 0xf2, 0xf5, 0x90, 0x47, 0xb5, 0x00, 0x00, 0x00, 192 | } 193 | -------------------------------------------------------------------------------- /examples/grpc-server-with-prometheus/protobuf/service.proto: -------------------------------------------------------------------------------- 1 | syntax="proto3"; 2 | 3 | package proto; 4 | 5 | service DemoService { 6 | rpc SayHello(HelloRequest) returns (HelloResponse) {} 7 | } 8 | 9 | message HelloRequest { 10 | string name = 1; 11 | } 12 | 13 | message HelloResponse { 14 | string message = 1; 15 | } 16 | -------------------------------------------------------------------------------- /examples/grpc-server-with-prometheus/server/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net" 8 | "net/http" 9 | 10 | "google.golang.org/grpc" 11 | 12 | "github.com/grpc-ecosystem/go-grpc-prometheus" 13 | pb "github.com/grpc-ecosystem/go-grpc-prometheus/examples/grpc-server-with-prometheus/protobuf" 14 | "github.com/prometheus/client_golang/prometheus" 15 | "github.com/prometheus/client_golang/prometheus/promhttp" 16 | ) 17 | 18 | // DemoServiceServer defines a Server. 19 | type DemoServiceServer struct{} 20 | 21 | func newDemoServer() *DemoServiceServer { 22 | return &DemoServiceServer{} 23 | } 24 | 25 | // SayHello implements a interface defined by protobuf. 26 | func (s *DemoServiceServer) SayHello(ctx context.Context, request *pb.HelloRequest) (*pb.HelloResponse, error) { 27 | customizedCounterMetric.WithLabelValues(request.Name).Inc() 28 | return &pb.HelloResponse{Message: fmt.Sprintf("Hello %s", request.Name)}, nil 29 | } 30 | 31 | var ( 32 | // Create a metrics registry. 33 | reg = prometheus.NewRegistry() 34 | 35 | // Create some standard server metrics. 36 | grpcMetrics = grpc_prometheus.NewServerMetrics() 37 | 38 | // Create a customized counter metric. 39 | customizedCounterMetric = prometheus.NewCounterVec(prometheus.CounterOpts{ 40 | Name: "demo_server_say_hello_method_handle_count", 41 | Help: "Total number of RPCs handled on the server.", 42 | }, []string{"name"}) 43 | ) 44 | 45 | func init() { 46 | // Register standard server metrics and customized metrics to registry. 47 | reg.MustRegister(grpcMetrics, customizedCounterMetric) 48 | customizedCounterMetric.WithLabelValues("Test") 49 | } 50 | 51 | // NOTE: Graceful shutdown is missing. Don't use this demo in your production setup. 52 | func main() { 53 | // Listen an actual port. 54 | lis, err := net.Listen("tcp", fmt.Sprintf(":%d", 9093)) 55 | if err != nil { 56 | log.Fatalf("failed to listen: %v", err) 57 | } 58 | defer lis.Close() 59 | 60 | // Create a HTTP server for prometheus. 61 | httpServer := &http.Server{Handler: promhttp.HandlerFor(reg, promhttp.HandlerOpts{}), Addr: fmt.Sprintf("0.0.0.0:%d", 9092)} 62 | 63 | // Create a gRPC Server with gRPC interceptor. 64 | grpcServer := grpc.NewServer( 65 | grpc.StreamInterceptor(grpcMetrics.StreamServerInterceptor()), 66 | grpc.UnaryInterceptor(grpcMetrics.UnaryServerInterceptor()), 67 | ) 68 | 69 | // Create a new api server. 70 | demoServer := newDemoServer() 71 | 72 | // Register your service. 73 | pb.RegisterDemoServiceServer(grpcServer, demoServer) 74 | 75 | // Initialize all metrics. 76 | grpcMetrics.InitializeMetrics(grpcServer) 77 | 78 | // Start your http server for prometheus. 79 | go func() { 80 | if err := httpServer.ListenAndServe(); err != nil { 81 | log.Fatal("Unable to start a http server.") 82 | } 83 | }() 84 | 85 | // Start your gRPC server. 86 | log.Fatal(grpcServer.Serve(lis)) 87 | } 88 | -------------------------------------------------------------------------------- /examples/testproto/Makefile: -------------------------------------------------------------------------------- 1 | all: test_go 2 | 3 | test_go: test.proto 4 | PATH="${GOPATH}/bin:${PATH}" protoc \ 5 | -I. \ 6 | -I${GOPATH}/src \ 7 | --go_out=plugins=grpc:. \ 8 | test.proto 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/testproto/test.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // source: test.proto 3 | 4 | package mwitkow_testproto 5 | 6 | import proto "github.com/golang/protobuf/proto" 7 | import fmt "fmt" 8 | import math "math" 9 | 10 | import ( 11 | context "golang.org/x/net/context" 12 | grpc "google.golang.org/grpc" 13 | ) 14 | 15 | // Reference imports to suppress errors if they are not otherwise used. 16 | var _ = proto.Marshal 17 | var _ = fmt.Errorf 18 | var _ = math.Inf 19 | 20 | // This is a compile-time assertion to ensure that this generated file 21 | // is compatible with the proto package it is being compiled against. 22 | // A compilation error at this line likely means your copy of the 23 | // proto package needs to be updated. 24 | const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package 25 | 26 | type Empty struct { 27 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 28 | XXX_unrecognized []byte `json:"-"` 29 | XXX_sizecache int32 `json:"-"` 30 | } 31 | 32 | func (m *Empty) Reset() { *m = Empty{} } 33 | func (m *Empty) String() string { return proto.CompactTextString(m) } 34 | func (*Empty) ProtoMessage() {} 35 | func (*Empty) Descriptor() ([]byte, []int) { 36 | return fileDescriptor_test_61863aae6603e9b8, []int{0} 37 | } 38 | func (m *Empty) XXX_Unmarshal(b []byte) error { 39 | return xxx_messageInfo_Empty.Unmarshal(m, b) 40 | } 41 | func (m *Empty) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 42 | return xxx_messageInfo_Empty.Marshal(b, m, deterministic) 43 | } 44 | func (dst *Empty) XXX_Merge(src proto.Message) { 45 | xxx_messageInfo_Empty.Merge(dst, src) 46 | } 47 | func (m *Empty) XXX_Size() int { 48 | return xxx_messageInfo_Empty.Size(m) 49 | } 50 | func (m *Empty) XXX_DiscardUnknown() { 51 | xxx_messageInfo_Empty.DiscardUnknown(m) 52 | } 53 | 54 | var xxx_messageInfo_Empty proto.InternalMessageInfo 55 | 56 | type PingRequest struct { 57 | Value string `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"` 58 | SleepTimeMs int32 `protobuf:"varint,2,opt,name=sleep_time_ms,json=sleepTimeMs,proto3" json:"sleep_time_ms,omitempty"` 59 | ErrorCodeReturned uint32 `protobuf:"varint,3,opt,name=error_code_returned,json=errorCodeReturned,proto3" json:"error_code_returned,omitempty"` 60 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 61 | XXX_unrecognized []byte `json:"-"` 62 | XXX_sizecache int32 `json:"-"` 63 | } 64 | 65 | func (m *PingRequest) Reset() { *m = PingRequest{} } 66 | func (m *PingRequest) String() string { return proto.CompactTextString(m) } 67 | func (*PingRequest) ProtoMessage() {} 68 | func (*PingRequest) Descriptor() ([]byte, []int) { 69 | return fileDescriptor_test_61863aae6603e9b8, []int{1} 70 | } 71 | func (m *PingRequest) XXX_Unmarshal(b []byte) error { 72 | return xxx_messageInfo_PingRequest.Unmarshal(m, b) 73 | } 74 | func (m *PingRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 75 | return xxx_messageInfo_PingRequest.Marshal(b, m, deterministic) 76 | } 77 | func (dst *PingRequest) XXX_Merge(src proto.Message) { 78 | xxx_messageInfo_PingRequest.Merge(dst, src) 79 | } 80 | func (m *PingRequest) XXX_Size() int { 81 | return xxx_messageInfo_PingRequest.Size(m) 82 | } 83 | func (m *PingRequest) XXX_DiscardUnknown() { 84 | xxx_messageInfo_PingRequest.DiscardUnknown(m) 85 | } 86 | 87 | var xxx_messageInfo_PingRequest proto.InternalMessageInfo 88 | 89 | func (m *PingRequest) GetValue() string { 90 | if m != nil { 91 | return m.Value 92 | } 93 | return "" 94 | } 95 | 96 | func (m *PingRequest) GetSleepTimeMs() int32 { 97 | if m != nil { 98 | return m.SleepTimeMs 99 | } 100 | return 0 101 | } 102 | 103 | func (m *PingRequest) GetErrorCodeReturned() uint32 { 104 | if m != nil { 105 | return m.ErrorCodeReturned 106 | } 107 | return 0 108 | } 109 | 110 | type PingResponse struct { 111 | Value string `protobuf:"bytes,1,opt,name=Value,proto3" json:"Value,omitempty"` 112 | Counter int32 `protobuf:"varint,2,opt,name=counter,proto3" json:"counter,omitempty"` 113 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 114 | XXX_unrecognized []byte `json:"-"` 115 | XXX_sizecache int32 `json:"-"` 116 | } 117 | 118 | func (m *PingResponse) Reset() { *m = PingResponse{} } 119 | func (m *PingResponse) String() string { return proto.CompactTextString(m) } 120 | func (*PingResponse) ProtoMessage() {} 121 | func (*PingResponse) Descriptor() ([]byte, []int) { 122 | return fileDescriptor_test_61863aae6603e9b8, []int{2} 123 | } 124 | func (m *PingResponse) XXX_Unmarshal(b []byte) error { 125 | return xxx_messageInfo_PingResponse.Unmarshal(m, b) 126 | } 127 | func (m *PingResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 128 | return xxx_messageInfo_PingResponse.Marshal(b, m, deterministic) 129 | } 130 | func (dst *PingResponse) XXX_Merge(src proto.Message) { 131 | xxx_messageInfo_PingResponse.Merge(dst, src) 132 | } 133 | func (m *PingResponse) XXX_Size() int { 134 | return xxx_messageInfo_PingResponse.Size(m) 135 | } 136 | func (m *PingResponse) XXX_DiscardUnknown() { 137 | xxx_messageInfo_PingResponse.DiscardUnknown(m) 138 | } 139 | 140 | var xxx_messageInfo_PingResponse proto.InternalMessageInfo 141 | 142 | func (m *PingResponse) GetValue() string { 143 | if m != nil { 144 | return m.Value 145 | } 146 | return "" 147 | } 148 | 149 | func (m *PingResponse) GetCounter() int32 { 150 | if m != nil { 151 | return m.Counter 152 | } 153 | return 0 154 | } 155 | 156 | func init() { 157 | proto.RegisterType((*Empty)(nil), "mwitkow.testproto.Empty") 158 | proto.RegisterType((*PingRequest)(nil), "mwitkow.testproto.PingRequest") 159 | proto.RegisterType((*PingResponse)(nil), "mwitkow.testproto.PingResponse") 160 | } 161 | 162 | // Reference imports to suppress errors if they are not otherwise used. 163 | var _ context.Context 164 | var _ grpc.ClientConn 165 | 166 | // This is a compile-time assertion to ensure that this generated file 167 | // is compatible with the grpc package it is being compiled against. 168 | const _ = grpc.SupportPackageIsVersion4 169 | 170 | // TestServiceClient is the client API for TestService service. 171 | // 172 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. 173 | type TestServiceClient interface { 174 | PingEmpty(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*PingResponse, error) 175 | Ping(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*PingResponse, error) 176 | PingError(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*Empty, error) 177 | PingList(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (TestService_PingListClient, error) 178 | } 179 | 180 | type testServiceClient struct { 181 | cc *grpc.ClientConn 182 | } 183 | 184 | func NewTestServiceClient(cc *grpc.ClientConn) TestServiceClient { 185 | return &testServiceClient{cc} 186 | } 187 | 188 | func (c *testServiceClient) PingEmpty(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*PingResponse, error) { 189 | out := new(PingResponse) 190 | err := c.cc.Invoke(ctx, "/mwitkow.testproto.TestService/PingEmpty", in, out, opts...) 191 | if err != nil { 192 | return nil, err 193 | } 194 | return out, nil 195 | } 196 | 197 | func (c *testServiceClient) Ping(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*PingResponse, error) { 198 | out := new(PingResponse) 199 | err := c.cc.Invoke(ctx, "/mwitkow.testproto.TestService/Ping", in, out, opts...) 200 | if err != nil { 201 | return nil, err 202 | } 203 | return out, nil 204 | } 205 | 206 | func (c *testServiceClient) PingError(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*Empty, error) { 207 | out := new(Empty) 208 | err := c.cc.Invoke(ctx, "/mwitkow.testproto.TestService/PingError", in, out, opts...) 209 | if err != nil { 210 | return nil, err 211 | } 212 | return out, nil 213 | } 214 | 215 | func (c *testServiceClient) PingList(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (TestService_PingListClient, error) { 216 | stream, err := c.cc.NewStream(ctx, &_TestService_serviceDesc.Streams[0], "/mwitkow.testproto.TestService/PingList", opts...) 217 | if err != nil { 218 | return nil, err 219 | } 220 | x := &testServicePingListClient{stream} 221 | if err := x.ClientStream.SendMsg(in); err != nil { 222 | return nil, err 223 | } 224 | if err := x.ClientStream.CloseSend(); err != nil { 225 | return nil, err 226 | } 227 | return x, nil 228 | } 229 | 230 | type TestService_PingListClient interface { 231 | Recv() (*PingResponse, error) 232 | grpc.ClientStream 233 | } 234 | 235 | type testServicePingListClient struct { 236 | grpc.ClientStream 237 | } 238 | 239 | func (x *testServicePingListClient) Recv() (*PingResponse, error) { 240 | m := new(PingResponse) 241 | if err := x.ClientStream.RecvMsg(m); err != nil { 242 | return nil, err 243 | } 244 | return m, nil 245 | } 246 | 247 | // TestServiceServer is the server API for TestService service. 248 | type TestServiceServer interface { 249 | PingEmpty(context.Context, *Empty) (*PingResponse, error) 250 | Ping(context.Context, *PingRequest) (*PingResponse, error) 251 | PingError(context.Context, *PingRequest) (*Empty, error) 252 | PingList(*PingRequest, TestService_PingListServer) error 253 | } 254 | 255 | func RegisterTestServiceServer(s *grpc.Server, srv TestServiceServer) { 256 | s.RegisterService(&_TestService_serviceDesc, srv) 257 | } 258 | 259 | func _TestService_PingEmpty_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 260 | in := new(Empty) 261 | if err := dec(in); err != nil { 262 | return nil, err 263 | } 264 | if interceptor == nil { 265 | return srv.(TestServiceServer).PingEmpty(ctx, in) 266 | } 267 | info := &grpc.UnaryServerInfo{ 268 | Server: srv, 269 | FullMethod: "/mwitkow.testproto.TestService/PingEmpty", 270 | } 271 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 272 | return srv.(TestServiceServer).PingEmpty(ctx, req.(*Empty)) 273 | } 274 | return interceptor(ctx, in, info, handler) 275 | } 276 | 277 | func _TestService_Ping_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 278 | in := new(PingRequest) 279 | if err := dec(in); err != nil { 280 | return nil, err 281 | } 282 | if interceptor == nil { 283 | return srv.(TestServiceServer).Ping(ctx, in) 284 | } 285 | info := &grpc.UnaryServerInfo{ 286 | Server: srv, 287 | FullMethod: "/mwitkow.testproto.TestService/Ping", 288 | } 289 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 290 | return srv.(TestServiceServer).Ping(ctx, req.(*PingRequest)) 291 | } 292 | return interceptor(ctx, in, info, handler) 293 | } 294 | 295 | func _TestService_PingError_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 296 | in := new(PingRequest) 297 | if err := dec(in); err != nil { 298 | return nil, err 299 | } 300 | if interceptor == nil { 301 | return srv.(TestServiceServer).PingError(ctx, in) 302 | } 303 | info := &grpc.UnaryServerInfo{ 304 | Server: srv, 305 | FullMethod: "/mwitkow.testproto.TestService/PingError", 306 | } 307 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 308 | return srv.(TestServiceServer).PingError(ctx, req.(*PingRequest)) 309 | } 310 | return interceptor(ctx, in, info, handler) 311 | } 312 | 313 | func _TestService_PingList_Handler(srv interface{}, stream grpc.ServerStream) error { 314 | m := new(PingRequest) 315 | if err := stream.RecvMsg(m); err != nil { 316 | return err 317 | } 318 | return srv.(TestServiceServer).PingList(m, &testServicePingListServer{stream}) 319 | } 320 | 321 | type TestService_PingListServer interface { 322 | Send(*PingResponse) error 323 | grpc.ServerStream 324 | } 325 | 326 | type testServicePingListServer struct { 327 | grpc.ServerStream 328 | } 329 | 330 | func (x *testServicePingListServer) Send(m *PingResponse) error { 331 | return x.ServerStream.SendMsg(m) 332 | } 333 | 334 | var _TestService_serviceDesc = grpc.ServiceDesc{ 335 | ServiceName: "mwitkow.testproto.TestService", 336 | HandlerType: (*TestServiceServer)(nil), 337 | Methods: []grpc.MethodDesc{ 338 | { 339 | MethodName: "PingEmpty", 340 | Handler: _TestService_PingEmpty_Handler, 341 | }, 342 | { 343 | MethodName: "Ping", 344 | Handler: _TestService_Ping_Handler, 345 | }, 346 | { 347 | MethodName: "PingError", 348 | Handler: _TestService_PingError_Handler, 349 | }, 350 | }, 351 | Streams: []grpc.StreamDesc{ 352 | { 353 | StreamName: "PingList", 354 | Handler: _TestService_PingList_Handler, 355 | ServerStreams: true, 356 | }, 357 | }, 358 | Metadata: "test.proto", 359 | } 360 | 361 | func init() { proto.RegisterFile("test.proto", fileDescriptor_test_61863aae6603e9b8) } 362 | 363 | var fileDescriptor_test_61863aae6603e9b8 = []byte{ 364 | // 274 bytes of a gzipped FileDescriptorProto 365 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x50, 0x4d, 0x4b, 0xc3, 0x40, 366 | 0x10, 0x6d, 0xaa, 0xb1, 0x76, 0x62, 0x0f, 0x1d, 0x3d, 0x04, 0x0f, 0x1a, 0xf6, 0x94, 0x53, 0x10, 367 | 0xbd, 0x7b, 0x11, 0x51, 0x41, 0x51, 0x62, 0xf1, 0x1a, 0x34, 0x19, 0x64, 0xb1, 0x9b, 0x8d, 0xbb, 368 | 0x93, 0x06, 0xff, 0x9b, 0x3f, 0x4e, 0x76, 0x1b, 0xa1, 0xa0, 0x45, 0x0f, 0x3d, 0xbe, 0xf7, 0x86, 369 | 0xf7, 0x31, 0x00, 0x4c, 0x96, 0xb3, 0xc6, 0x68, 0xd6, 0x38, 0x55, 0x9d, 0xe4, 0x37, 0xdd, 0x65, 370 | 0x8e, 0xf3, 0x94, 0x18, 0x41, 0x78, 0xa9, 0x1a, 0xfe, 0x10, 0x1d, 0x44, 0x0f, 0xb2, 0x7e, 0xcd, 371 | 0xe9, 0xbd, 0x25, 0xcb, 0x78, 0x00, 0xe1, 0xe2, 0x79, 0xde, 0x52, 0x1c, 0x24, 0x41, 0x3a, 0xce, 372 | 0x97, 0x00, 0x05, 0x4c, 0xec, 0x9c, 0xa8, 0x29, 0x58, 0x2a, 0x2a, 0x94, 0x8d, 0x87, 0x49, 0x90, 373 | 0x86, 0x79, 0xe4, 0xc9, 0x99, 0x54, 0x74, 0x67, 0x31, 0x83, 0x7d, 0x32, 0x46, 0x9b, 0xa2, 0xd4, 374 | 0x15, 0x15, 0x86, 0xb8, 0x35, 0x35, 0x55, 0xf1, 0x56, 0x12, 0xa4, 0x93, 0x7c, 0xea, 0xa5, 0x0b, 375 | 0x5d, 0x51, 0xde, 0x0b, 0xe2, 0x1c, 0xf6, 0x96, 0xc1, 0xb6, 0xd1, 0xb5, 0x25, 0x97, 0xfc, 0xb4, 376 | 0x9a, 0xec, 0x01, 0xc6, 0x30, 0x2a, 0x75, 0x5b, 0x33, 0x99, 0x3e, 0xf3, 0x1b, 0x9e, 0x7e, 0x0e, 377 | 0x21, 0x9a, 0x91, 0xe5, 0x47, 0x32, 0x0b, 0x59, 0x12, 0x5e, 0xc3, 0xd8, 0xf9, 0xf9, 0x55, 0x18, 378 | 0x67, 0x3f, 0x26, 0x67, 0x5e, 0x39, 0x3c, 0xfe, 0x45, 0x59, 0xed, 0x21, 0x06, 0x78, 0x03, 0xdb, 379 | 0x8e, 0xc1, 0xa3, 0xb5, 0xa7, 0xfe, 0x57, 0xff, 0xb1, 0xba, 0xea, 0x4b, 0xb9, 0xf5, 0x7f, 0xfa, 380 | 0xad, 0x2d, 0x2d, 0x06, 0x78, 0x0f, 0xbb, 0xee, 0xf4, 0x56, 0x5a, 0xde, 0x40, 0xaf, 0x93, 0xe0, 381 | 0x65, 0xc7, 0xf3, 0x67, 0x5f, 0x01, 0x00, 0x00, 0xff, 0xff, 0xdb, 0x2f, 0xc7, 0x45, 0x28, 0x02, 382 | 0x00, 0x00, 383 | } 384 | -------------------------------------------------------------------------------- /examples/testproto/test.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package mwitkow.testproto; 4 | 5 | 6 | message Empty { 7 | } 8 | 9 | message PingRequest { 10 | string value = 1; 11 | int32 sleep_time_ms = 2; 12 | uint32 error_code_returned = 3; 13 | } 14 | 15 | message PingResponse { 16 | string Value = 1; 17 | int32 counter = 2; 18 | } 19 | 20 | service TestService { 21 | rpc PingEmpty(Empty) returns (PingResponse) {} 22 | 23 | rpc Ping(PingRequest) returns (PingResponse) {} 24 | 25 | rpc PingError(PingRequest) returns (Empty) {} 26 | 27 | rpc PingList(PingRequest) returns (stream PingResponse) {} 28 | } 29 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/grpc-ecosystem/go-grpc-prometheus 2 | 3 | require ( 4 | github.com/golang/protobuf v1.2.0 5 | github.com/prometheus/client_golang v0.9.2 6 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 7 | github.com/stretchr/testify v1.3.0 8 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd 9 | google.golang.org/grpc v1.18.0 10 | ) 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0= 3 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 4 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 5 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 6 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= 8 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 9 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 10 | github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= 11 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 12 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 13 | github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= 14 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 15 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 16 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 17 | github.com/prometheus/client_golang v0.9.2 h1:awm861/B8OKDd2I/6o1dy3ra4BamzKhYOiGItCeZ740= 18 | github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= 19 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 20 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE= 21 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 22 | github.com/prometheus/common v0.0.0-20181126121408-4724e9255275 h1:PnBWHBf+6L0jOqq0gIVUe6Yk0/QMZ640k6NvkxcBf+8= 23 | github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 24 | github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a h1:9a8MnZMP0X2nLJdBg+pBmGgkJlSaKC2KaQmTCk1XDtE= 25 | github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 26 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 27 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 28 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 29 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 30 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 31 | golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 32 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd h1:HuTn7WObtcDo9uEEU7rEqL0jYthdXAmZ6PP+meazmaU= 33 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 34 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 35 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 36 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ= 37 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 38 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522 h1:Ve1ORMCxvRmSXBwJK+t3Oy+V2vRW2OetUQBq4rJIkZE= 39 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 40 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 41 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 42 | golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 43 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 44 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc= 45 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 46 | google.golang.org/grpc v1.18.0 h1:IZl7mfBGfbhYx2p2rKRtYgDFw6SBz+kclmxYrCksPPA= 47 | google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= 48 | honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 49 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | GOFILES_NOVENDOR = $(shell go list ./... | grep -v /vendor/) 2 | 3 | all: vet fmt test 4 | 5 | fmt: 6 | go fmt $(GOFILES_NOVENDOR) 7 | 8 | vet: 9 | go vet $(GOFILES_NOVENDOR) 10 | 11 | test: vet 12 | ./scripts/test_all.sh 13 | 14 | .PHONY: all vet test 15 | -------------------------------------------------------------------------------- /metric_options.go: -------------------------------------------------------------------------------- 1 | package grpc_prometheus 2 | 3 | import ( 4 | prom "github.com/prometheus/client_golang/prometheus" 5 | ) 6 | 7 | // A CounterOption lets you add options to Counter metrics using With* funcs. 8 | type CounterOption func(*prom.CounterOpts) 9 | 10 | type counterOptions []CounterOption 11 | 12 | func (co counterOptions) apply(o prom.CounterOpts) prom.CounterOpts { 13 | for _, f := range co { 14 | f(&o) 15 | } 16 | return o 17 | } 18 | 19 | // WithConstLabels allows you to add ConstLabels to Counter metrics. 20 | func WithConstLabels(labels prom.Labels) CounterOption { 21 | return func(o *prom.CounterOpts) { 22 | o.ConstLabels = labels 23 | } 24 | } 25 | 26 | // A HistogramOption lets you add options to Histogram metrics using With* 27 | // funcs. 28 | type HistogramOption func(*prom.HistogramOpts) 29 | 30 | // WithHistogramBuckets allows you to specify custom bucket ranges for histograms if EnableHandlingTimeHistogram is on. 31 | func WithHistogramBuckets(buckets []float64) HistogramOption { 32 | return func(o *prom.HistogramOpts) { o.Buckets = buckets } 33 | } 34 | 35 | // WithHistogramConstLabels allows you to add custom ConstLabels to 36 | // histograms metrics. 37 | func WithHistogramConstLabels(labels prom.Labels) HistogramOption { 38 | return func(o *prom.HistogramOpts) { 39 | o.ConstLabels = labels 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/grpcstatus/grpcstatus.go: -------------------------------------------------------------------------------- 1 | package grpcstatus 2 | 3 | import ( 4 | "google.golang.org/grpc/codes" 5 | "google.golang.org/grpc/status" 6 | ) 7 | 8 | type gRPCStatus interface { 9 | GRPCStatus() *status.Status 10 | } 11 | 12 | func unwrapPkgErrorsGRPCStatus(err error) (*status.Status, bool) { 13 | type causer interface { 14 | Cause() error 15 | } 16 | 17 | // Unwrapping the github.com/pkg/errors causer interface, using `Cause` directly could miss some error implementing 18 | // the `GRPCStatus` function so we have to check it on our selves. 19 | unwrappedCauser := err 20 | for unwrappedCauser != nil { 21 | if s, ok := unwrappedCauser.(gRPCStatus); ok { 22 | return s.GRPCStatus(), true 23 | } 24 | cause, ok := unwrappedCauser.(causer) 25 | if !ok { 26 | break 27 | } 28 | unwrappedCauser = cause.Cause() 29 | } 30 | return nil, false 31 | } 32 | 33 | // Since error can be wrapped and the `FromError` function only checks for `GRPCStatus` function 34 | // and as a fallback uses the `Unknown` gRPC status we need to unwrap the error if possible to get the original status. 35 | // pkg/errors and Go native errors packages have two different approaches so we try to unwrap both types. 36 | // Eventually should be implemented in the go-grpc status function `FromError`. See https://github.com/grpc/grpc-go/issues/2934 37 | func FromError(err error) (s *status.Status, ok bool) { 38 | s, ok = status.FromError(err) 39 | if ok { 40 | return s, true 41 | } 42 | 43 | // Try to unwrap `github.com/pkg/errors` wrapped error 44 | s, ok = unwrapPkgErrorsGRPCStatus(err) 45 | if ok { 46 | return s, true 47 | } 48 | 49 | // Try to unwrap native wrapped errors using `fmt.Errorf` and `%w` 50 | s, ok = unwrapNativeWrappedGRPCStatus(err) 51 | if ok { 52 | return s, true 53 | } 54 | 55 | // We failed to unwrap any GRPSStatus so return default `Unknown` 56 | return status.New(codes.Unknown, err.Error()), false 57 | } 58 | -------------------------------------------------------------------------------- /packages/grpcstatus/grpcstatus1.13+_test.go: -------------------------------------------------------------------------------- 1 | // +build go1.13 2 | 3 | package grpcstatus 4 | 5 | import ( 6 | "fmt" 7 | "github.com/stretchr/testify/require" 8 | "google.golang.org/grpc/codes" 9 | "google.golang.org/grpc/status" 10 | "testing" 11 | ) 12 | 13 | func TestNativeErrorUnwrapping(t *testing.T) { 14 | gRPCCode := codes.FailedPrecondition 15 | gRPCError := status.Errorf(gRPCCode, "Userspace error.") 16 | expectedGRPCStatus, _ := status.FromError(gRPCError) 17 | testedErrors := []error{ 18 | fmt.Errorf("go native wrapped error: %w", gRPCError), 19 | } 20 | 21 | for _, e := range testedErrors { 22 | resultingStatus, ok := FromError(e) 23 | require.True(t, ok) 24 | require.Equal(t, expectedGRPCStatus, resultingStatus) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/grpcstatus/grpcstatus_test.go: -------------------------------------------------------------------------------- 1 | package grpcstatus 2 | 3 | import ( 4 | "github.com/stretchr/testify/require" 5 | "google.golang.org/grpc/codes" 6 | "google.golang.org/grpc/status" 7 | "testing" 8 | ) 9 | 10 | // Own implementation of pkg/errors withStack to avoid additional dependency 11 | type wrappedError struct { 12 | cause error 13 | msg string 14 | } 15 | 16 | func (w *wrappedError) Error() string { return w.msg + ": " + w.cause.Error() } 17 | 18 | func (w *wrappedError) Cause() error { return w.cause } 19 | 20 | func TestErrorUnwrapping(t *testing.T) { 21 | gRPCCode := codes.FailedPrecondition 22 | gRPCError := status.Errorf(gRPCCode, "Userspace error.") 23 | expectedGRPCStatus, _ := status.FromError(gRPCError) 24 | testedErrors := []error{ 25 | gRPCError, 26 | &wrappedError{cause: gRPCError, msg: "pkg/errors wrapped error: "}, 27 | } 28 | 29 | for _, e := range testedErrors { 30 | resultingStatus, ok := FromError(e) 31 | require.True(t, ok) 32 | require.Equal(t, expectedGRPCStatus, resultingStatus) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/grpcstatus/native_unwrap1.12-.go: -------------------------------------------------------------------------------- 1 | // +build !go1.13 2 | 3 | package grpcstatus 4 | 5 | import ( 6 | "google.golang.org/grpc/status" 7 | ) 8 | 9 | func unwrapNativeWrappedGRPCStatus(err error) (*status.Status, bool) { 10 | return nil, false 11 | } 12 | -------------------------------------------------------------------------------- /packages/grpcstatus/native_unwrap1.13+.go: -------------------------------------------------------------------------------- 1 | // +build go1.13 2 | 3 | package grpcstatus 4 | 5 | import ( 6 | "errors" 7 | "google.golang.org/grpc/status" 8 | ) 9 | 10 | func unwrapNativeWrappedGRPCStatus(err error) (*status.Status, bool) { 11 | // Unwrapping the native Go unwrap interface 12 | var unwrappedStatus gRPCStatus 13 | if ok := errors.As(err, &unwrappedStatus); ok { 14 | return unwrappedStatus.GRPCStatus(), true 15 | } 16 | return nil, false 17 | } 18 | -------------------------------------------------------------------------------- /scripts/test_all.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | echo "" > coverage.txt 5 | 6 | for d in $(go list ./... | grep -v vendor); do 7 | echo -e "TESTS FOR: for \033[0;35m${d}\033[0m" 8 | go test -race -v -coverprofile=profile.coverage.out -covermode=atomic $d 9 | if [ -f profile.coverage.out ]; then 10 | cat profile.coverage.out >> coverage.txt 11 | rm profile.coverage.out 12 | fi 13 | echo "" 14 | done 15 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Michal Witkowski. All Rights Reserved. 2 | // See LICENSE for licensing terms. 3 | 4 | // gRPC Prometheus monitoring interceptors for server-side gRPC. 5 | 6 | package grpc_prometheus 7 | 8 | import ( 9 | prom "github.com/prometheus/client_golang/prometheus" 10 | "google.golang.org/grpc" 11 | ) 12 | 13 | var ( 14 | // DefaultServerMetrics is the default instance of ServerMetrics. It is 15 | // intended to be used in conjunction the default Prometheus metrics 16 | // registry. 17 | DefaultServerMetrics = NewServerMetrics() 18 | 19 | // UnaryServerInterceptor is a gRPC server-side interceptor that provides Prometheus monitoring for Unary RPCs. 20 | UnaryServerInterceptor = DefaultServerMetrics.UnaryServerInterceptor() 21 | 22 | // StreamServerInterceptor is a gRPC server-side interceptor that provides Prometheus monitoring for Streaming RPCs. 23 | StreamServerInterceptor = DefaultServerMetrics.StreamServerInterceptor() 24 | ) 25 | 26 | func init() { 27 | prom.MustRegister(DefaultServerMetrics.serverStartedCounter) 28 | prom.MustRegister(DefaultServerMetrics.serverHandledCounter) 29 | prom.MustRegister(DefaultServerMetrics.serverStreamMsgReceived) 30 | prom.MustRegister(DefaultServerMetrics.serverStreamMsgSent) 31 | } 32 | 33 | // Register takes a gRPC server and pre-initializes all counters to 0. This 34 | // allows for easier monitoring in Prometheus (no missing metrics), and should 35 | // be called *after* all services have been registered with the server. This 36 | // function acts on the DefaultServerMetrics variable. 37 | func Register(server *grpc.Server) { 38 | DefaultServerMetrics.InitializeMetrics(server) 39 | } 40 | 41 | // EnableHandlingTimeHistogram turns on recording of handling time 42 | // of RPCs. Histogram metrics can be very expensive for Prometheus 43 | // to retain and query. This function acts on the DefaultServerMetrics 44 | // variable and the default Prometheus metrics registry. 45 | func EnableHandlingTimeHistogram(opts ...HistogramOption) { 46 | DefaultServerMetrics.EnableHandlingTimeHistogram(opts...) 47 | prom.Register(DefaultServerMetrics.serverHandledHistogram) 48 | } 49 | -------------------------------------------------------------------------------- /server_metrics.go: -------------------------------------------------------------------------------- 1 | package grpc_prometheus 2 | 3 | import ( 4 | "context" 5 | "github.com/grpc-ecosystem/go-grpc-prometheus/packages/grpcstatus" 6 | prom "github.com/prometheus/client_golang/prometheus" 7 | 8 | "google.golang.org/grpc" 9 | ) 10 | 11 | // ServerMetrics represents a collection of metrics to be registered on a 12 | // Prometheus metrics registry for a gRPC server. 13 | type ServerMetrics struct { 14 | serverStartedCounter *prom.CounterVec 15 | serverHandledCounter *prom.CounterVec 16 | serverStreamMsgReceived *prom.CounterVec 17 | serverStreamMsgSent *prom.CounterVec 18 | serverHandledHistogramEnabled bool 19 | serverHandledHistogramOpts prom.HistogramOpts 20 | serverHandledHistogram *prom.HistogramVec 21 | } 22 | 23 | // NewServerMetrics returns a ServerMetrics object. Use a new instance of 24 | // ServerMetrics when not using the default Prometheus metrics registry, for 25 | // example when wanting to control which metrics are added to a registry as 26 | // opposed to automatically adding metrics via init functions. 27 | func NewServerMetrics(counterOpts ...CounterOption) *ServerMetrics { 28 | opts := counterOptions(counterOpts) 29 | return &ServerMetrics{ 30 | serverStartedCounter: prom.NewCounterVec( 31 | opts.apply(prom.CounterOpts{ 32 | Name: "grpc_server_started_total", 33 | Help: "Total number of RPCs started on the server.", 34 | }), []string{"grpc_type", "grpc_service", "grpc_method"}), 35 | serverHandledCounter: prom.NewCounterVec( 36 | opts.apply(prom.CounterOpts{ 37 | Name: "grpc_server_handled_total", 38 | Help: "Total number of RPCs completed on the server, regardless of success or failure.", 39 | }), []string{"grpc_type", "grpc_service", "grpc_method", "grpc_code"}), 40 | serverStreamMsgReceived: prom.NewCounterVec( 41 | opts.apply(prom.CounterOpts{ 42 | Name: "grpc_server_msg_received_total", 43 | Help: "Total number of RPC stream messages received on the server.", 44 | }), []string{"grpc_type", "grpc_service", "grpc_method"}), 45 | serverStreamMsgSent: prom.NewCounterVec( 46 | opts.apply(prom.CounterOpts{ 47 | Name: "grpc_server_msg_sent_total", 48 | Help: "Total number of gRPC stream messages sent by the server.", 49 | }), []string{"grpc_type", "grpc_service", "grpc_method"}), 50 | serverHandledHistogramEnabled: false, 51 | serverHandledHistogramOpts: prom.HistogramOpts{ 52 | Name: "grpc_server_handling_seconds", 53 | Help: "Histogram of response latency (seconds) of gRPC that had been application-level handled by the server.", 54 | Buckets: prom.DefBuckets, 55 | }, 56 | serverHandledHistogram: nil, 57 | } 58 | } 59 | 60 | // EnableHandlingTimeHistogram enables histograms being registered when 61 | // registering the ServerMetrics on a Prometheus registry. Histograms can be 62 | // expensive on Prometheus servers. It takes options to configure histogram 63 | // options such as the defined buckets. 64 | func (m *ServerMetrics) EnableHandlingTimeHistogram(opts ...HistogramOption) { 65 | for _, o := range opts { 66 | o(&m.serverHandledHistogramOpts) 67 | } 68 | if !m.serverHandledHistogramEnabled { 69 | m.serverHandledHistogram = prom.NewHistogramVec( 70 | m.serverHandledHistogramOpts, 71 | []string{"grpc_type", "grpc_service", "grpc_method"}, 72 | ) 73 | } 74 | m.serverHandledHistogramEnabled = true 75 | } 76 | 77 | // Describe sends the super-set of all possible descriptors of metrics 78 | // collected by this Collector to the provided channel and returns once 79 | // the last descriptor has been sent. 80 | func (m *ServerMetrics) Describe(ch chan<- *prom.Desc) { 81 | m.serverStartedCounter.Describe(ch) 82 | m.serverHandledCounter.Describe(ch) 83 | m.serverStreamMsgReceived.Describe(ch) 84 | m.serverStreamMsgSent.Describe(ch) 85 | if m.serverHandledHistogramEnabled { 86 | m.serverHandledHistogram.Describe(ch) 87 | } 88 | } 89 | 90 | // Collect is called by the Prometheus registry when collecting 91 | // metrics. The implementation sends each collected metric via the 92 | // provided channel and returns once the last metric has been sent. 93 | func (m *ServerMetrics) Collect(ch chan<- prom.Metric) { 94 | m.serverStartedCounter.Collect(ch) 95 | m.serverHandledCounter.Collect(ch) 96 | m.serverStreamMsgReceived.Collect(ch) 97 | m.serverStreamMsgSent.Collect(ch) 98 | if m.serverHandledHistogramEnabled { 99 | m.serverHandledHistogram.Collect(ch) 100 | } 101 | } 102 | 103 | // UnaryServerInterceptor is a gRPC server-side interceptor that provides Prometheus monitoring for Unary RPCs. 104 | func (m *ServerMetrics) UnaryServerInterceptor() func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { 105 | return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { 106 | monitor := newServerReporter(m, Unary, info.FullMethod) 107 | monitor.ReceivedMessage() 108 | resp, err := handler(ctx, req) 109 | st, _ := grpcstatus.FromError(err) 110 | monitor.Handled(st.Code()) 111 | if err == nil { 112 | monitor.SentMessage() 113 | } 114 | return resp, err 115 | } 116 | } 117 | 118 | // StreamServerInterceptor is a gRPC server-side interceptor that provides Prometheus monitoring for Streaming RPCs. 119 | func (m *ServerMetrics) StreamServerInterceptor() func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { 120 | return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { 121 | monitor := newServerReporter(m, streamRPCType(info), info.FullMethod) 122 | err := handler(srv, &monitoredServerStream{ss, monitor}) 123 | st, _ := grpcstatus.FromError(err) 124 | monitor.Handled(st.Code()) 125 | return err 126 | } 127 | } 128 | 129 | // InitializeMetrics initializes all metrics, with their appropriate null 130 | // value, for all gRPC methods registered on a gRPC server. This is useful, to 131 | // ensure that all metrics exist when collecting and querying. 132 | func (m *ServerMetrics) InitializeMetrics(server *grpc.Server) { 133 | serviceInfo := server.GetServiceInfo() 134 | for serviceName, info := range serviceInfo { 135 | for _, mInfo := range info.Methods { 136 | preRegisterMethod(m, serviceName, &mInfo) 137 | } 138 | } 139 | } 140 | 141 | func streamRPCType(info *grpc.StreamServerInfo) grpcType { 142 | if info.IsClientStream && !info.IsServerStream { 143 | return ClientStream 144 | } else if !info.IsClientStream && info.IsServerStream { 145 | return ServerStream 146 | } 147 | return BidiStream 148 | } 149 | 150 | // monitoredStream wraps grpc.ServerStream allowing each Sent/Recv of message to increment counters. 151 | type monitoredServerStream struct { 152 | grpc.ServerStream 153 | monitor *serverReporter 154 | } 155 | 156 | func (s *monitoredServerStream) SendMsg(m interface{}) error { 157 | err := s.ServerStream.SendMsg(m) 158 | if err == nil { 159 | s.monitor.SentMessage() 160 | } 161 | return err 162 | } 163 | 164 | func (s *monitoredServerStream) RecvMsg(m interface{}) error { 165 | err := s.ServerStream.RecvMsg(m) 166 | if err == nil { 167 | s.monitor.ReceivedMessage() 168 | } 169 | return err 170 | } 171 | 172 | // preRegisterMethod is invoked on Register of a Server, allowing all gRPC services labels to be pre-populated. 173 | func preRegisterMethod(metrics *ServerMetrics, serviceName string, mInfo *grpc.MethodInfo) { 174 | methodName := mInfo.Name 175 | methodType := string(typeFromMethodInfo(mInfo)) 176 | // These are just references (no increments), as just referencing will create the labels but not set values. 177 | metrics.serverStartedCounter.GetMetricWithLabelValues(methodType, serviceName, methodName) 178 | metrics.serverStreamMsgReceived.GetMetricWithLabelValues(methodType, serviceName, methodName) 179 | metrics.serverStreamMsgSent.GetMetricWithLabelValues(methodType, serviceName, methodName) 180 | if metrics.serverHandledHistogramEnabled { 181 | metrics.serverHandledHistogram.GetMetricWithLabelValues(methodType, serviceName, methodName) 182 | } 183 | for _, code := range allCodes { 184 | metrics.serverHandledCounter.GetMetricWithLabelValues(methodType, serviceName, methodName, code.String()) 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /server_reporter.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Michal Witkowski. All Rights Reserved. 2 | // See LICENSE for licensing terms. 3 | 4 | package grpc_prometheus 5 | 6 | import ( 7 | "time" 8 | 9 | "google.golang.org/grpc/codes" 10 | ) 11 | 12 | type serverReporter struct { 13 | metrics *ServerMetrics 14 | rpcType grpcType 15 | serviceName string 16 | methodName string 17 | startTime time.Time 18 | } 19 | 20 | func newServerReporter(m *ServerMetrics, rpcType grpcType, fullMethod string) *serverReporter { 21 | r := &serverReporter{ 22 | metrics: m, 23 | rpcType: rpcType, 24 | } 25 | if r.metrics.serverHandledHistogramEnabled { 26 | r.startTime = time.Now() 27 | } 28 | r.serviceName, r.methodName = splitMethodName(fullMethod) 29 | r.metrics.serverStartedCounter.WithLabelValues(string(r.rpcType), r.serviceName, r.methodName).Inc() 30 | return r 31 | } 32 | 33 | func (r *serverReporter) ReceivedMessage() { 34 | r.metrics.serverStreamMsgReceived.WithLabelValues(string(r.rpcType), r.serviceName, r.methodName).Inc() 35 | } 36 | 37 | func (r *serverReporter) SentMessage() { 38 | r.metrics.serverStreamMsgSent.WithLabelValues(string(r.rpcType), r.serviceName, r.methodName).Inc() 39 | } 40 | 41 | func (r *serverReporter) Handled(code codes.Code) { 42 | r.metrics.serverHandledCounter.WithLabelValues(string(r.rpcType), r.serviceName, r.methodName, code.String()).Inc() 43 | if r.metrics.serverHandledHistogramEnabled { 44 | r.metrics.serverHandledHistogram.WithLabelValues(string(r.rpcType), r.serviceName, r.methodName).Observe(time.Since(r.startTime).Seconds()) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /server_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Michal Witkowski. All Rights Reserved. 2 | // See LICENSE for licensing terms. 3 | 4 | package grpc_prometheus 5 | 6 | import ( 7 | "bufio" 8 | "context" 9 | "fmt" 10 | "io" 11 | "net" 12 | "net/http" 13 | "net/http/httptest" 14 | "reflect" 15 | "strings" 16 | "testing" 17 | "time" 18 | 19 | "github.com/prometheus/client_golang/prometheus/promhttp" 20 | "github.com/prometheus/client_golang/prometheus/testutil" 21 | 22 | pb_testproto "github.com/grpc-ecosystem/go-grpc-prometheus/examples/testproto" 23 | "github.com/prometheus/client_golang/prometheus" 24 | dto "github.com/prometheus/client_model/go" 25 | "github.com/stretchr/testify/assert" 26 | "github.com/stretchr/testify/require" 27 | "github.com/stretchr/testify/suite" 28 | "google.golang.org/grpc" 29 | "google.golang.org/grpc/codes" 30 | "google.golang.org/grpc/status" 31 | ) 32 | 33 | var ( 34 | // server metrics must satisfy the Collector interface 35 | _ prometheus.Collector = NewServerMetrics() 36 | ) 37 | 38 | const ( 39 | pingDefaultValue = "I like kittens." 40 | countListResponses = 20 41 | ) 42 | 43 | func TestServerInterceptorSuite(t *testing.T) { 44 | suite.Run(t, &ServerInterceptorTestSuite{}) 45 | } 46 | 47 | type ServerInterceptorTestSuite struct { 48 | suite.Suite 49 | 50 | serverListener net.Listener 51 | server *grpc.Server 52 | clientConn *grpc.ClientConn 53 | testClient pb_testproto.TestServiceClient 54 | ctx context.Context 55 | cancel context.CancelFunc 56 | } 57 | 58 | func (s *ServerInterceptorTestSuite) SetupSuite() { 59 | var err error 60 | 61 | EnableHandlingTimeHistogram() 62 | 63 | s.serverListener, err = net.Listen("tcp", "127.0.0.1:0") 64 | require.NoError(s.T(), err, "must be able to allocate a port for serverListener") 65 | 66 | // This is the point where we hook up the interceptor 67 | s.server = grpc.NewServer( 68 | grpc.StreamInterceptor(StreamServerInterceptor), 69 | grpc.UnaryInterceptor(UnaryServerInterceptor), 70 | ) 71 | pb_testproto.RegisterTestServiceServer(s.server, &testService{t: s.T()}) 72 | 73 | go func() { 74 | s.server.Serve(s.serverListener) 75 | }() 76 | 77 | s.clientConn, err = grpc.Dial(s.serverListener.Addr().String(), grpc.WithInsecure(), grpc.WithBlock(), grpc.WithTimeout(2*time.Second)) 78 | require.NoError(s.T(), err, "must not error on client Dial") 79 | s.testClient = pb_testproto.NewTestServiceClient(s.clientConn) 80 | } 81 | 82 | func (s *ServerInterceptorTestSuite) SetupTest() { 83 | // Make all RPC calls last at most 2 sec, meaning all async issues or deadlock will not kill tests. 84 | s.ctx, s.cancel = context.WithTimeout(context.TODO(), 2*time.Second) 85 | 86 | // Make sure every test starts with same fresh, intialized metric state. 87 | DefaultServerMetrics.serverStartedCounter.Reset() 88 | DefaultServerMetrics.serverHandledCounter.Reset() 89 | DefaultServerMetrics.serverHandledHistogram.Reset() 90 | DefaultServerMetrics.serverStreamMsgReceived.Reset() 91 | DefaultServerMetrics.serverStreamMsgSent.Reset() 92 | Register(s.server) 93 | } 94 | 95 | func (s *ServerInterceptorTestSuite) TearDownSuite() { 96 | if s.serverListener != nil { 97 | s.server.Stop() 98 | s.T().Logf("stopped grpc.Server at: %v", s.serverListener.Addr().String()) 99 | s.serverListener.Close() 100 | 101 | } 102 | if s.clientConn != nil { 103 | s.clientConn.Close() 104 | } 105 | } 106 | 107 | func (s *ServerInterceptorTestSuite) TearDownTest() { 108 | s.cancel() 109 | } 110 | 111 | func (s *ServerInterceptorTestSuite) TestRegisterPresetsStuff() { 112 | for testID, testCase := range []struct { 113 | metricName string 114 | existingLabels []string 115 | }{ 116 | // Order of label is irrelevant. 117 | {"grpc_server_started_total", []string{"mwitkow.testproto.TestService", "PingEmpty", "unary"}}, 118 | {"grpc_server_started_total", []string{"mwitkow.testproto.TestService", "PingList", "server_stream"}}, 119 | {"grpc_server_msg_received_total", []string{"mwitkow.testproto.TestService", "PingList", "server_stream"}}, 120 | {"grpc_server_msg_sent_total", []string{"mwitkow.testproto.TestService", "PingEmpty", "unary"}}, 121 | {"grpc_server_handling_seconds_sum", []string{"mwitkow.testproto.TestService", "PingEmpty", "unary"}}, 122 | {"grpc_server_handling_seconds_count", []string{"mwitkow.testproto.TestService", "PingList", "server_stream"}}, 123 | {"grpc_server_handled_total", []string{"mwitkow.testproto.TestService", "PingList", "server_stream", "OutOfRange"}}, 124 | {"grpc_server_handled_total", []string{"mwitkow.testproto.TestService", "PingList", "server_stream", "Aborted"}}, 125 | {"grpc_server_handled_total", []string{"mwitkow.testproto.TestService", "PingEmpty", "unary", "FailedPrecondition"}}, 126 | {"grpc_server_handled_total", []string{"mwitkow.testproto.TestService", "PingEmpty", "unary", "ResourceExhausted"}}, 127 | } { 128 | lineCount := len(fetchPrometheusLines(s.T(), testCase.metricName, testCase.existingLabels...)) 129 | assert.NotEqual(s.T(), 0, lineCount, "metrics must exist for test case %d", testID) 130 | } 131 | } 132 | 133 | func (s *ServerInterceptorTestSuite) TestUnaryIncrementsMetrics() { 134 | _, err := s.testClient.PingEmpty(s.ctx, &pb_testproto.Empty{}) // should return with code=OK 135 | require.NoError(s.T(), err) 136 | requireValue(s.T(), 1, DefaultServerMetrics.serverStartedCounter.WithLabelValues("unary", "mwitkow.testproto.TestService", "PingEmpty")) 137 | requireValue(s.T(), 1, DefaultServerMetrics.serverHandledCounter.WithLabelValues("unary", "mwitkow.testproto.TestService", "PingEmpty", "OK")) 138 | requireValueHistCount(s.T(), 1, DefaultServerMetrics.serverHandledHistogram.WithLabelValues("unary", "mwitkow.testproto.TestService", "PingEmpty")) 139 | 140 | _, err = s.testClient.PingError(s.ctx, &pb_testproto.PingRequest{ErrorCodeReturned: uint32(codes.FailedPrecondition)}) // should return with code=FailedPrecondition 141 | require.Error(s.T(), err) 142 | requireValue(s.T(), 1, DefaultServerMetrics.serverStartedCounter.WithLabelValues("unary", "mwitkow.testproto.TestService", "PingError")) 143 | requireValue(s.T(), 1, DefaultServerMetrics.serverHandledCounter.WithLabelValues("unary", "mwitkow.testproto.TestService", "PingError", "FailedPrecondition")) 144 | requireValueHistCount(s.T(), 1, DefaultServerMetrics.serverHandledHistogram.WithLabelValues("unary", "mwitkow.testproto.TestService", "PingError")) 145 | } 146 | 147 | func (s *ServerInterceptorTestSuite) TestStartedStreamingIncrementsStarted() { 148 | _, err := s.testClient.PingList(s.ctx, &pb_testproto.PingRequest{}) 149 | require.NoError(s.T(), err) 150 | requireValueWithRetry(s.ctx, s.T(), 1, 151 | DefaultServerMetrics.serverStartedCounter.WithLabelValues("server_stream", "mwitkow.testproto.TestService", "PingList")) 152 | 153 | _, err = s.testClient.PingList(s.ctx, &pb_testproto.PingRequest{ErrorCodeReturned: uint32(codes.FailedPrecondition)}) // should return with code=FailedPrecondition 154 | require.NoError(s.T(), err, "PingList must not fail immediately") 155 | requireValueWithRetry(s.ctx, s.T(), 2, 156 | DefaultServerMetrics.serverStartedCounter.WithLabelValues("server_stream", "mwitkow.testproto.TestService", "PingList")) 157 | } 158 | 159 | func (s *ServerInterceptorTestSuite) TestStreamingIncrementsMetrics() { 160 | ss, _ := s.testClient.PingList(s.ctx, &pb_testproto.PingRequest{}) // should return with code=OK 161 | // Do a read, just for kicks. 162 | count := 0 163 | for { 164 | _, err := ss.Recv() 165 | if err == io.EOF { 166 | break 167 | } 168 | require.NoError(s.T(), err, "reading pingList shouldn't fail") 169 | count++ 170 | } 171 | require.EqualValues(s.T(), countListResponses, count, "Number of received msg on the wire must match") 172 | 173 | requireValueWithRetry(s.ctx, s.T(), 1, 174 | DefaultServerMetrics.serverStartedCounter.WithLabelValues("server_stream", "mwitkow.testproto.TestService", "PingList")) 175 | requireValueWithRetry(s.ctx, s.T(), 1, 176 | DefaultServerMetrics.serverHandledCounter.WithLabelValues("server_stream", "mwitkow.testproto.TestService", "PingList", "OK")) 177 | requireValueWithRetry(s.ctx, s.T(), countListResponses, 178 | DefaultServerMetrics.serverStreamMsgSent.WithLabelValues("server_stream", "mwitkow.testproto.TestService", "PingList")) 179 | requireValueWithRetry(s.ctx, s.T(), 1, 180 | DefaultServerMetrics.serverStreamMsgReceived.WithLabelValues("server_stream", "mwitkow.testproto.TestService", "PingList")) 181 | requireValueWithRetryHistCount(s.ctx, s.T(), 1, 182 | DefaultServerMetrics.serverHandledHistogram.WithLabelValues("server_stream", "mwitkow.testproto.TestService", "PingList")) 183 | 184 | _, err := s.testClient.PingList(s.ctx, &pb_testproto.PingRequest{ErrorCodeReturned: uint32(codes.FailedPrecondition)}) // should return with code=FailedPrecondition 185 | require.NoError(s.T(), err, "PingList must not fail immediately") 186 | 187 | requireValueWithRetry(s.ctx, s.T(), 2, 188 | DefaultServerMetrics.serverStartedCounter.WithLabelValues("server_stream", "mwitkow.testproto.TestService", "PingList")) 189 | requireValueWithRetry(s.ctx, s.T(), 1, 190 | DefaultServerMetrics.serverHandledCounter.WithLabelValues("server_stream", "mwitkow.testproto.TestService", "PingList", "FailedPrecondition")) 191 | requireValueWithRetryHistCount(s.ctx, s.T(), 2, 192 | DefaultServerMetrics.serverHandledHistogram.WithLabelValues("server_stream", "mwitkow.testproto.TestService", "PingList")) 193 | } 194 | 195 | // fetchPrometheusLines does mocked HTTP GET request against real prometheus handler to get the same view that Prometheus 196 | // would have while scraping this endpoint. 197 | // Order of matching label vales does not matter. 198 | func fetchPrometheusLines(t *testing.T, metricName string, matchingLabelValues ...string) []string { 199 | resp := httptest.NewRecorder() 200 | req, err := http.NewRequest("GET", "/", nil) 201 | require.NoError(t, err, "failed creating request for Prometheus handler") 202 | 203 | promhttp.Handler().ServeHTTP(resp, req) 204 | reader := bufio.NewReader(resp.Body) 205 | 206 | var ret []string 207 | for { 208 | line, err := reader.ReadString('\n') 209 | if err == io.EOF { 210 | break 211 | } else { 212 | require.NoError(t, err, "error reading stuff") 213 | } 214 | if !strings.HasPrefix(line, metricName) { 215 | continue 216 | } 217 | matches := true 218 | for _, labelValue := range matchingLabelValues { 219 | if !strings.Contains(line, `"`+labelValue+`"`) { 220 | matches = false 221 | } 222 | } 223 | if matches { 224 | ret = append(ret, line) 225 | } 226 | 227 | } 228 | return ret 229 | } 230 | 231 | type testService struct { 232 | t *testing.T 233 | } 234 | 235 | func (s *testService) PingEmpty(ctx context.Context, _ *pb_testproto.Empty) (*pb_testproto.PingResponse, error) { 236 | return &pb_testproto.PingResponse{Value: pingDefaultValue, Counter: 42}, nil 237 | } 238 | 239 | func (s *testService) Ping(ctx context.Context, ping *pb_testproto.PingRequest) (*pb_testproto.PingResponse, error) { 240 | // Send user trailers and headers. 241 | return &pb_testproto.PingResponse{Value: ping.Value, Counter: 42}, nil 242 | } 243 | 244 | func (s *testService) PingError(ctx context.Context, ping *pb_testproto.PingRequest) (*pb_testproto.Empty, error) { 245 | code := codes.Code(ping.ErrorCodeReturned) 246 | return nil, status.Errorf(code, "Userspace error.") 247 | } 248 | 249 | func (s *testService) PingList(ping *pb_testproto.PingRequest, stream pb_testproto.TestService_PingListServer) error { 250 | if ping.ErrorCodeReturned != 0 { 251 | return status.Errorf(codes.Code(ping.ErrorCodeReturned), "foobar") 252 | } 253 | // Send user trailers and headers. 254 | for i := 0; i < countListResponses; i++ { 255 | stream.Send(&pb_testproto.PingResponse{Value: ping.Value, Counter: int32(i)}) 256 | } 257 | return nil 258 | } 259 | 260 | // toFloat64HistCount does the same thing as prometheus go client testutil.ToFloat64, but for histograms. 261 | // TODO(bwplotka): Upstream this function to prometheus client. 262 | func toFloat64HistCount(h prometheus.Observer) uint64 { 263 | var ( 264 | m prometheus.Metric 265 | mCount int 266 | mChan = make(chan prometheus.Metric) 267 | done = make(chan struct{}) 268 | ) 269 | 270 | go func() { 271 | for m = range mChan { 272 | mCount++ 273 | } 274 | close(done) 275 | }() 276 | 277 | c, ok := h.(prometheus.Collector) 278 | if !ok { 279 | panic(fmt.Errorf("observer is not a collector; got: %T", h)) 280 | } 281 | 282 | c.Collect(mChan) 283 | close(mChan) 284 | <-done 285 | 286 | if mCount != 1 { 287 | panic(fmt.Errorf("collected %d metrics instead of exactly 1", mCount)) 288 | } 289 | 290 | pb := &dto.Metric{} 291 | m.Write(pb) 292 | if pb.Histogram != nil { 293 | return pb.Histogram.GetSampleCount() 294 | } 295 | panic(fmt.Errorf("collected a non-histogram metric: %s", pb)) 296 | } 297 | 298 | func requireValue(t *testing.T, expect int, c prometheus.Collector) { 299 | v := int(testutil.ToFloat64(c)) 300 | if v == expect { 301 | return 302 | } 303 | 304 | metricFullName := reflect.ValueOf(*c.(prometheus.Metric).Desc()).FieldByName("fqName").String() 305 | t.Errorf("expected %d %s value; got %d; ", expect, metricFullName, v) 306 | t.Fail() 307 | } 308 | 309 | func requireValueHistCount(t *testing.T, expect int, o prometheus.Observer) { 310 | v := int(toFloat64HistCount(o)) 311 | if v == expect { 312 | return 313 | } 314 | 315 | metricFullName := reflect.ValueOf(*o.(prometheus.Metric).Desc()).FieldByName("fqName").String() 316 | t.Errorf("expected %d %s value; got %d; ", expect, metricFullName, v) 317 | t.Fail() 318 | } 319 | 320 | func requireValueWithRetry(ctx context.Context, t *testing.T, expect int, c prometheus.Collector) { 321 | for { 322 | v := int(testutil.ToFloat64(c)) 323 | if v == expect { 324 | return 325 | } 326 | 327 | select { 328 | case <-ctx.Done(): 329 | metricFullName := reflect.ValueOf(*c.(prometheus.Metric).Desc()).FieldByName("fqName").String() 330 | t.Errorf("timeout while expecting %d %s value; got %d; ", expect, metricFullName, v) 331 | t.Fail() 332 | return 333 | case <-time.After(100 * time.Millisecond): 334 | } 335 | } 336 | } 337 | 338 | func requireValueWithRetryHistCount(ctx context.Context, t *testing.T, expect int, o prometheus.Observer) { 339 | for { 340 | v := int(toFloat64HistCount(o)) 341 | if v == expect { 342 | return 343 | } 344 | 345 | select { 346 | case <-ctx.Done(): 347 | metricFullName := reflect.ValueOf(*o.(prometheus.Metric).Desc()).FieldByName("fqName").String() 348 | t.Errorf("timeout while expecting %d %s histogram count value; got %d; ", expect, metricFullName, v) 349 | t.Fail() 350 | return 351 | case <-time.After(100 * time.Millisecond): 352 | } 353 | } 354 | } 355 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Michal Witkowski. All Rights Reserved. 2 | // See LICENSE for licensing terms. 3 | 4 | package grpc_prometheus 5 | 6 | import ( 7 | "strings" 8 | 9 | "google.golang.org/grpc" 10 | "google.golang.org/grpc/codes" 11 | ) 12 | 13 | type grpcType string 14 | 15 | const ( 16 | Unary grpcType = "unary" 17 | ClientStream grpcType = "client_stream" 18 | ServerStream grpcType = "server_stream" 19 | BidiStream grpcType = "bidi_stream" 20 | ) 21 | 22 | var ( 23 | allCodes = []codes.Code{ 24 | codes.OK, codes.Canceled, codes.Unknown, codes.InvalidArgument, codes.DeadlineExceeded, codes.NotFound, 25 | codes.AlreadyExists, codes.PermissionDenied, codes.Unauthenticated, codes.ResourceExhausted, 26 | codes.FailedPrecondition, codes.Aborted, codes.OutOfRange, codes.Unimplemented, codes.Internal, 27 | codes.Unavailable, codes.DataLoss, 28 | } 29 | ) 30 | 31 | func splitMethodName(fullMethodName string) (string, string) { 32 | fullMethodName = strings.TrimPrefix(fullMethodName, "/") // remove leading slash 33 | if i := strings.Index(fullMethodName, "/"); i >= 0 { 34 | return fullMethodName[:i], fullMethodName[i+1:] 35 | } 36 | return "unknown", "unknown" 37 | } 38 | 39 | func typeFromMethodInfo(mInfo *grpc.MethodInfo) grpcType { 40 | if !mInfo.IsClientStream && !mInfo.IsServerStream { 41 | return Unary 42 | } 43 | if mInfo.IsClientStream && !mInfo.IsServerStream { 44 | return ClientStream 45 | } 46 | if !mInfo.IsClientStream && mInfo.IsServerStream { 47 | return ServerStream 48 | } 49 | return BidiStream 50 | } 51 | --------------------------------------------------------------------------------