pax_global_header00006660000000000000000000000064135132716110014512gustar00rootroot0000000000000052 comment=2f068394615f73e460c2f3d2c158b0ad9321cadb golang-github-mwitkow-go-conntrack-0.0~git20190716.2f06839/000077500000000000000000000000001351327161100226365ustar00rootroot00000000000000golang-github-mwitkow-go-conntrack-0.0~git20190716.2f06839/.gitignore000066400000000000000000000044241351327161100246320ustar00rootroot00000000000000# Created by .ignore support plugin (hsz.mobi) ### JetBrains template # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 # User-specific stuff: .idea .idea/workspace.xml .idea/tasks.xml .idea/dictionaries .idea/vcs.xml .idea/jsLibraryMappings.xml # Sensitive or high-churn files: .idea/dataSources.ids .idea/dataSources.xml .idea/dataSources.local.xml .idea/sqlDataSources.xml .idea/dynamic.xml .idea/uiDesigner.xml # Gradle: .idea/gradle.xml .idea/libraries # Mongo Explorer plugin: .idea/mongoSettings.xml ## File-based project format: *.iws ## Plugin-specific files: # IntelliJ /out/ # mpeltonen/sbt-idea plugin .idea_modules/ # JIRA plugin atlassian-ide-plugin.xml # Crashlytics plugin (for Android Studio and IntelliJ) com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties fabric.properties ### Go template # Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a *.so # Folders _obj _test # Architecture specific extensions/prefixes *.[568vq] [568vq].out *.cgo1.go *.cgo2.c _cgo_defun.c _cgo_gotypes.go _cgo_export.* _testmain.go *.exe *.test *.prof ### Python template # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover .hypothesis/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # IPython Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # dotenv .env # virtualenv venv/ ENV/ # Spyder project settings .spyderproject # Rope project settings .ropeproject golang-github-mwitkow-go-conntrack-0.0~git20190716.2f06839/.travis.yml000066400000000000000000000004151351327161100247470ustar00rootroot00000000000000sudo: false language: go go: - "1.8" - "1.9" - "1.10" - "1.11" - "1.12" install: - go get github.com/stretchr/testify - go get github.com/prometheus/client_golang/prometheus - go get golang.org/x/net/context - go get golang.org/x/net/trace script: - go test -v ./... golang-github-mwitkow-go-conntrack-0.0~git20190716.2f06839/LICENSE000066400000000000000000000261351351327161100236520ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. golang-github-mwitkow-go-conntrack-0.0~git20190716.2f06839/README.md000066400000000000000000000111141351327161100241130ustar00rootroot00000000000000# Go tracing and monitoring (Prometheus) for `net.Conn` [![Travis Build](https://travis-ci.org/mwitkow/go-conntrack.svg)](https://travis-ci.org/mwitkow/go-conntrack) [![Go Report Card](https://goreportcard.com/badge/github.com/mwitkow/go-conntrack)](http://goreportcard.com/report/mwitkow/go-conntrack) [![GoDoc](http://img.shields.io/badge/GoDoc-Reference-blue.svg)](https://godoc.org/github.com/mwitkow/go-conntrack) [![Apache 2.0 License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE) [Prometheus](https://prometheus.io/) monitoring and [`x/net/trace`](https://godoc.org/golang.org/x/net/trace#EventLog) tracing wrappers `net.Conn`, both inbound (`net.Listener`) and outbound (`net.Dialer`). ## Why? Go standard library does a great job of doing "the right" things with your connections: `http.Transport` pools outbound ones, and `http.Server` sets good *Keep Alive* defaults. However, it is still easy to get it wrong, see the excellent [*The complete guide to Go net/http timeouts*](https://blog.cloudflare.com/the-complete-guide-to-golang-net-http-timeouts/). That's why you should be able to monitor (using Prometheus) how many connections your Go frontend servers have inbound, and how big are the connection pools to your backends. You should also be able to inspect your connection without `ssh` and `netstat`. ![Events page with connections](https://raw.githubusercontent.com/mwitkow/go-conntrack/images/events.png) ## How to use? All of these examples can be found in [`example/server.go`](example/server.go): ### Conntrack Dialer for HTTP DefaultClient Most often people use the default `http.DefaultClient` that uses `http.DefaultTransport`. The easiest way to make sure all your outbound connections monitored and trace is: ```go http.DefaultTransport.(*http.Transport).DialContext = conntrack.NewDialContextFunc( conntrack.DialWithTracing(), conntrack.DialWithDialer(&net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, }), ) ``` #### Dialer Name Tracked outbound connections are organised by *dialer name* (with `default` being default). The *dialer name* is used for monitoring (`dialer_name` label) and tracing (`net.ClientConn.` family). You can pass `conntrack.WithDialerName()` to `NewDialContextFunc` to set the name for the dialer. Moreover, you can set the *dialer name* per invocation of the dialer, by passing it in the `Context`. For example using the [`ctxhttp`](https://godoc.org/golang.org/x/net/context/ctxhttp) lib: ```go callCtx := conntrack.DialNameToContext(parentCtx, "google") ctxhttp.Get(callCtx, http.DefaultClient, "https://www.google.com") ``` ### Conntrack Listener for HTTP Server Tracked inbound connections are organised by *listener name* (with `default` being default). The *listener name* is used for monitoring (`listener_name` label) and tracing (`net.ServerConn.` family). For example, a simple `http.Server` can be instrumented like this: ```go listener, err := net.Listen("tcp", fmt.Sprintf(":%d", *port)) listener = conntrack.NewListener(listener, conntrack.TrackWithName("http"), conntrack.TrackWithTracing(), conntrack.TrackWithTcpKeepAlive(5 * time.Minutes)) httpServer.Serve(listener) ``` Note, the `TrackWithTcpKeepAlive`. The default `http.ListenAndServe` adds a tcp keep alive wrapper to inbound TCP connections. `conntrack.NewListener` allows you to do that without another layer of wrapping. #### TLS server example The standard lobrary `http.ListenAndServerTLS` does a lot to bootstrap TLS connections, including supporting HTTP2 negotiation. Unfortunately, that is hard to do if you want to provide your own `net.Listener`. That's why this repo comes with `connhelpers` package, which takes care of configuring `tls.Config` for that use case. Here's an example of use: ```go listener, err := net.Listen("tcp", fmt.Sprintf(":%d", *port)) listener = conntrack.NewListener(listener, conntrack.TrackWithName("https"), conntrack.TrackWithTracing(), conntrack.TrackWithTcpKeepAlive(5 * time.Minutes)) tlsConfig, err := connhelpers.TlsConfigForServerCerts(*tlsCertFilePath, *tlsKeyFilePath) tlsConfig, err = connhelpers.TlsConfigWithHttp2Enabled(tlsConfig) tlsListener := tls.NewListener(listener, tlsConfig) httpServer.Serve(listener) ``` # Status This code is used by Improbable's HTTP frontending and proxying stack for debuging and monitoring of established user connections. Additional tooling will be added if needed, and contributions are welcome. #License `go-conntrack` is released under the Apache 2.0 license. See the [LICENSE](LICENSE) file for details. golang-github-mwitkow-go-conntrack-0.0~git20190716.2f06839/connhelpers/000077500000000000000000000000001351327161100251565ustar00rootroot00000000000000golang-github-mwitkow-go-conntrack-0.0~git20190716.2f06839/connhelpers/tls.go000066400000000000000000000036071351327161100263150ustar00rootroot00000000000000// Copyright 2016 Michal Witkowski. All Rights Reserved. // See LICENSE for licensing terms. package connhelpers import ( "crypto/tls" "fmt" ) // TlsConfigForServerCerts is a returns a simple `tls.Config` with the given server cert loaded. // This is useful if you can't use `http.ListenAndServerTLS` when using a custom `net.Listener`. func TlsConfigForServerCerts(certFile string, keyFile string) (*tls.Config, error) { var err error config := new(tls.Config) config.Certificates = make([]tls.Certificate, 1) config.Certificates[0], err = tls.LoadX509KeyPair(certFile, keyFile) if err != nil { return nil, err } return config, nil } // TlsConfigWithHttp2Enabled makes it easy to configure the given `tls.Config` to prefer H2 connections. // This is useful if you can't use `http.ListenAndServerTLS` when using a custom `net.Listener`. func TlsConfigWithHttp2Enabled(config *tls.Config) (*tls.Config, error) { // mostly based on http2 code in the standards library. if config.CipherSuites != nil { // If they already provided a CipherSuite list, return // an error if it has a bad order or is missing // ECDHE_RSA_WITH_AES_128_GCM_SHA256. const requiredCipher = tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 haveRequired := false for _, cs := range config.CipherSuites { if cs == requiredCipher { haveRequired = true } } if !haveRequired { return nil, fmt.Errorf("http2: TLSConfig.CipherSuites is missing HTTP/2-required TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256") } } config.PreferServerCipherSuites = true haveNPN := false for _, p := range config.NextProtos { if p == "h2" { haveNPN = true break } } if !haveNPN { config.NextProtos = append(config.NextProtos, "h2") } config.NextProtos = append(config.NextProtos, "h2-14") // make sure http 1.1 is *after* all of the other ones. config.NextProtos = append(config.NextProtos, "http/1.1") return config, nil } golang-github-mwitkow-go-conntrack-0.0~git20190716.2f06839/dialer_reporter.go000066400000000000000000000066021351327161100263530ustar00rootroot00000000000000// Copyright 2016 Michal Witkowski. All Rights Reserved. // See LICENSE for licensing terms. package conntrack import ( "context" "net" "os" "syscall" prom "github.com/prometheus/client_golang/prometheus" ) type failureReason string const ( failedResolution = "resolution" failedConnRefused = "refused" failedTimeout = "timeout" failedUnknown = "unknown" ) var ( dialerAttemptedTotal = prom.NewCounterVec( prom.CounterOpts{ Namespace: "net", Subsystem: "conntrack", Name: "dialer_conn_attempted_total", Help: "Total number of connections attempted by the given dialer a given name.", }, []string{"dialer_name"}) dialerConnEstablishedTotal = prom.NewCounterVec( prom.CounterOpts{ Namespace: "net", Subsystem: "conntrack", Name: "dialer_conn_established_total", Help: "Total number of connections successfully established by the given dialer a given name.", }, []string{"dialer_name"}) dialerConnFailedTotal = prom.NewCounterVec( prom.CounterOpts{ Namespace: "net", Subsystem: "conntrack", Name: "dialer_conn_failed_total", Help: "Total number of connections failed to dial by the dialer a given name.", }, []string{"dialer_name", "reason"}) dialerConnClosedTotal = prom.NewCounterVec( prom.CounterOpts{ Namespace: "net", Subsystem: "conntrack", Name: "dialer_conn_closed_total", Help: "Total number of connections closed which originated from the dialer of a given name.", }, []string{"dialer_name"}) ) func init() { prom.MustRegister(dialerAttemptedTotal) prom.MustRegister(dialerConnEstablishedTotal) prom.MustRegister(dialerConnFailedTotal) prom.MustRegister(dialerConnClosedTotal) } // preRegisterDialerMetrics pre-populates Prometheus labels for the given dialer name, to avoid Prometheus missing labels issue. func PreRegisterDialerMetrics(dialerName string) { dialerAttemptedTotal.WithLabelValues(dialerName) dialerConnEstablishedTotal.WithLabelValues(dialerName) for _, reason := range []failureReason{failedTimeout, failedResolution, failedConnRefused, failedUnknown} { dialerConnFailedTotal.WithLabelValues(dialerName, string(reason)) } dialerConnClosedTotal.WithLabelValues(dialerName) } func reportDialerConnAttempt(dialerName string) { dialerAttemptedTotal.WithLabelValues(dialerName).Inc() } func reportDialerConnEstablished(dialerName string) { dialerConnEstablishedTotal.WithLabelValues(dialerName).Inc() } func reportDialerConnClosed(dialerName string) { dialerConnClosedTotal.WithLabelValues(dialerName).Inc() } func reportDialerConnFailed(dialerName string, err error) { if netErr, ok := err.(*net.OpError); ok { switch nestErr := netErr.Err.(type) { case *net.DNSError: dialerConnFailedTotal.WithLabelValues(dialerName, string(failedResolution)).Inc() return case *os.SyscallError: if nestErr.Err == syscall.ECONNREFUSED { dialerConnFailedTotal.WithLabelValues(dialerName, string(failedConnRefused)).Inc() } dialerConnFailedTotal.WithLabelValues(dialerName, string(failedUnknown)).Inc() return } if netErr.Timeout() { dialerConnFailedTotal.WithLabelValues(dialerName, string(failedTimeout)).Inc() } } else if err == context.Canceled || err == context.DeadlineExceeded { dialerConnFailedTotal.WithLabelValues(dialerName, string(failedTimeout)).Inc() return } dialerConnFailedTotal.WithLabelValues(dialerName, string(failedUnknown)).Inc() } golang-github-mwitkow-go-conntrack-0.0~git20190716.2f06839/dialer_test.go000066400000000000000000000244651351327161100254770ustar00rootroot00000000000000// Copyright 2016 Michal Witkowski. All Rights Reserved. // See LICENSE for licensing terms. package conntrack_test import ( "context" "net" "net/http" "testing" "time" "github.com/mwitkow/go-conntrack" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" ) func TestDialerWrapper(t *testing.T) { suite.Run(t, &DialerTestSuite{}) } type DialerTestSuite struct { suite.Suite serverListener net.Listener httpServer http.Server } func (s *DialerTestSuite) SetupSuite() { var err error s.serverListener, err = net.Listen("tcp", "127.0.0.1:0") require.NoError(s.T(), err, "must be able to allocate a port for serverListener") s.httpServer = http.Server{ Handler: http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { resp.WriteHeader(http.StatusOK) }), } go func() { s.httpServer.Serve(s.serverListener) }() } func (s *DialerTestSuite) TestDialerMetricsArePreregistered() { conntrack.NewDialContextFunc() // dialer name = default conntrack.NewDialContextFunc(conntrack.DialWithName("foobar")) conntrack.PreRegisterDialerMetrics("something_manual") for testId, testCase := range []struct { metricName string existingLabels []string }{ {"net_conntrack_dialer_conn_attempted_total", []string{"default"}}, {"net_conntrack_dialer_conn_attempted_total", []string{"foobar"}}, {"net_conntrack_dialer_conn_attempted_total", []string{"something_manual"}}, {"net_conntrack_dialer_conn_closed_total", []string{"default"}}, {"net_conntrack_dialer_conn_closed_total", []string{"foobar"}}, {"net_conntrack_dialer_conn_closed_total", []string{"something_manual"}}, {"net_conntrack_dialer_conn_established_total", []string{"default"}}, {"net_conntrack_dialer_conn_established_total", []string{"foobar"}}, {"net_conntrack_dialer_conn_established_total", []string{"something_manual"}}, {"net_conntrack_dialer_conn_failed_total", []string{"default", "resolution"}}, {"net_conntrack_dialer_conn_failed_total", []string{"default", "refused"}}, {"net_conntrack_dialer_conn_failed_total", []string{"default", "timeout"}}, {"net_conntrack_dialer_conn_failed_total", []string{"default", "unknown"}}, } { lineCount := len(fetchPrometheusLines(s.T(), testCase.metricName, testCase.existingLabels...)) assert.NotEqual(s.T(), 0, lineCount, "metrics must exist for test case %d", testId) } } func (s *DialerTestSuite) TestDialerMetricsAreNotPreregisteredWithMonitoringOff() { conntrack.NewDialContextFunc(conntrack.DialWithName("nomon"), conntrack.DialWithoutMonitoring()) for testId, testCase := range []struct { metricName string existingLabels []string }{ {"net_conntrack_dialer_conn_attempted_total", []string{"nomon"}}, {"net_conntrack_dialer_conn_closed_total", []string{"nomon"}}, {"net_conntrack_dialer_conn_established_total", []string{"nomon"}}, {"net_conntrack_dialer_conn_failed_total", []string{"nomon", "resolution"}}, {"net_conntrack_dialer_conn_failed_total", []string{"nomon", "refused"}}, {"net_conntrack_dialer_conn_failed_total", []string{"nomon", "timeout"}}, {"net_conntrack_dialer_conn_failed_total", []string{"nomon", "unknown"}}, } { lineCount := len(fetchPrometheusLines(s.T(), testCase.metricName, testCase.existingLabels...)) assert.Equal(s.T(), 0, lineCount, "metrics should not be registered exist for test case %d", testId) } } func (s *DialerTestSuite) TestDialerUnderNormalConnection() { dialFunc := conntrack.NewDialContextFunc(conntrack.DialWithName("normal_conn")) beforeAttempts := sumCountersForMetricAndLabels(s.T(), "net_conntrack_dialer_conn_attempted_total", "normal_conn") beforeEstablished := sumCountersForMetricAndLabels(s.T(), "net_conntrack_dialer_conn_established_total", "normal_conn") beforeClosed := sumCountersForMetricAndLabels(s.T(), "net_conntrack_dialer_conn_closed_total", "normal_conn") conn, err := dialFunc(context.TODO(), "tcp", s.serverListener.Addr().String()) require.NoError(s.T(), err, "NewDialContextFunc should successfully establish a conn here") assert.Equal(s.T(), beforeAttempts+1, sumCountersForMetricAndLabels(s.T(), "net_conntrack_dialer_conn_attempted_total", "normal_conn"), "the attempted conn counter must be incremented after connection was opened") assert.Equal(s.T(), beforeEstablished+1, sumCountersForMetricAndLabels(s.T(), "net_conntrack_dialer_conn_established_total", "normal_conn"), "the established conn counter must be incremented after connection was opened") assert.Equal(s.T(), beforeClosed, sumCountersForMetricAndLabels(s.T(), "net_conntrack_dialer_conn_closed_total", "normal_conn"), "the closed conn counter must not be incremented after connection was opened") conn.Close() assert.Equal(s.T(), beforeClosed+1, sumCountersForMetricAndLabels(s.T(), "net_conntrack_dialer_conn_established_total", "normal_conn"), "the closed conn counter must be incremented after connection was closed") } func (s *DialerTestSuite) TestDialerWithContextName() { dialFunc := conntrack.NewDialContextFunc() conntrack.PreRegisterDialerMetrics("ctx_conn") beforeAttempts := sumCountersForMetricAndLabels(s.T(), "net_conntrack_dialer_conn_attempted_total", "ctx_conn") beforeEstablished := sumCountersForMetricAndLabels(s.T(), "net_conntrack_dialer_conn_established_total", "ctx_conn") beforeClosed := sumCountersForMetricAndLabels(s.T(), "net_conntrack_dialer_conn_closed_total", "ctx_conn") conn, err := dialFunc(conntrack.DialNameToContext(context.TODO(), "ctx_conn"), "tcp", s.serverListener.Addr().String()) require.NoError(s.T(), err, "NewDialContextFunc should successfully establish a conn here") assert.Equal(s.T(), beforeAttempts+1, sumCountersForMetricAndLabels(s.T(), "net_conntrack_dialer_conn_attempted_total", "ctx_conn"), "the attempted conn counter must be incremented after connection was opened") assert.Equal(s.T(), beforeEstablished+1, sumCountersForMetricAndLabels(s.T(), "net_conntrack_dialer_conn_established_total", "ctx_conn"), "the established conn counter must be incremented after connection was opened") assert.Equal(s.T(), beforeClosed, sumCountersForMetricAndLabels(s.T(), "net_conntrack_dialer_conn_closed_total", "ctx_conn"), "the closed conn counter must not be incremented after connection was opened") conn.Close() assert.Equal(s.T(), beforeClosed+1, sumCountersForMetricAndLabels(s.T(), "net_conntrack_dialer_conn_established_total", "ctx_conn"), "the closed conn counter must be incremented after connection was closed") } func (s *ListenerTestSuite) TestDialerTracingCapturedInPage() { dialFunc := conntrack.NewDialContextFunc(conntrack.DialWithTracing()) dialerName := "some_dialer" conn, err := dialFunc(conntrack.DialNameToContext(context.TODO(), dialerName), "tcp", s.serverListener.Addr().String()) time.Sleep(5 * time.Millisecond) require.NoError(s.T(), err, "DialContext should successfully establish a conn here") assert.Contains(s.T(), fetchTraceEvents(s.T(), "net.ClientConn."+dialerName), conn.LocalAddr().String(), "the /debug/trace/events page must contain the live connection") time.Sleep(5 * time.Millisecond) conn.Close() } func (s *DialerTestSuite) TestDialerResolutionFailure() { dialFunc := conntrack.NewDialContextFunc(conntrack.DialWithName("res_err")) beforeAttempts := sumCountersForMetricAndLabels(s.T(), "net_conntrack_dialer_conn_attempted_total", "res_err") beforeEstablished := sumCountersForMetricAndLabels(s.T(), "net_conntrack_dialer_conn_established_total", "res_err") beforeResolutionErrors := sumCountersForMetricAndLabels(s.T(), "net_conntrack_dialer_conn_failed_total", "res_err", "resolution") beforeClosed := sumCountersForMetricAndLabels(s.T(), "net_conntrack_dialer_conn_closed_total", "res_err") _, err := dialFunc(context.TODO(), "tcp", "dialer.test.wrong.domain.wrong:443") require.Error(s.T(), err, "NewDialContextFunc should fail here") assert.Equal(s.T(), beforeAttempts+1, sumCountersForMetricAndLabels(s.T(), "net_conntrack_dialer_conn_attempted_total", "res_err"), "the attempted conn counter must be incremented after connection was opened") assert.Equal(s.T(), beforeEstablished, sumCountersForMetricAndLabels(s.T(), "net_conntrack_dialer_conn_established_total", "res_err"), "the established conn counter must not be incremented on a failure") assert.Equal(s.T(), beforeClosed, sumCountersForMetricAndLabels(s.T(), "net_conntrack_dialer_conn_closed_total", "res_err"), "the closed conn counter must not be incremented on a failure") assert.Equal(s.T(), beforeResolutionErrors+1, sumCountersForMetricAndLabels(s.T(), "net_conntrack_dialer_conn_failed_total", "res_err", "resolution"), "the failure counter for resolution error should be incremented") } func (s *DialerTestSuite) TestDialerRefusedFailure() { dialFunc := conntrack.NewDialContextFunc(conntrack.DialWithName("ref_err")) beforeAttempts := sumCountersForMetricAndLabels(s.T(), "net_conntrack_dialer_conn_attempted_total", "ref_err") beforeEstablished := sumCountersForMetricAndLabels(s.T(), "net_conntrack_dialer_conn_established_total", "ref_err") beforeResolutionErrors := sumCountersForMetricAndLabels(s.T(), "net_conntrack_dialer_conn_failed_total", "ref_err", "resolution") beforeClosed := sumCountersForMetricAndLabels(s.T(), "net_conntrack_dialer_conn_closed_total", "ref_err") _, err := dialFunc(context.TODO(), "tcp", "127.0.0.1:337") // 337 is a cool port, let's hope its unused. require.Error(s.T(), err, "NewDialContextFunc should fail here") assert.Equal(s.T(), beforeAttempts+1, sumCountersForMetricAndLabels(s.T(), "net_conntrack_dialer_conn_attempted_total", "ref_err"), "the attempted conn counter must be incremented after connection was opened") assert.Equal(s.T(), beforeEstablished, sumCountersForMetricAndLabels(s.T(), "net_conntrack_dialer_conn_established_total", "ref_err"), "the established conn counter must not be incremented on a failure") assert.Equal(s.T(), beforeClosed, sumCountersForMetricAndLabels(s.T(), "net_conntrack_dialer_conn_closed_total", "ref_err"), "the closed conn counter must not be incremented on a failure") assert.Equal(s.T(), beforeResolutionErrors+1, sumCountersForMetricAndLabels(s.T(), "net_conntrack_dialer_conn_failed_total", "ref_err", "refused"), "the failure counter for connection refused error should be incremented") } func (s *DialerTestSuite) TearDownSuite() { if s.serverListener != nil { s.T().Logf("stopped http.Server at: %v", s.serverListener.Addr().String()) s.serverListener.Close() } } golang-github-mwitkow-go-conntrack-0.0~git20190716.2f06839/dialer_wrapper.go000066400000000000000000000112241351327161100261650ustar00rootroot00000000000000// Copyright 2016 Michal Witkowski. All Rights Reserved. // See LICENSE for licensing terms. package conntrack import ( "context" "fmt" "net" "sync" "golang.org/x/net/trace" ) var ( dialerNameKey = "conntrackDialerKey" ) type dialerOpts struct { name string monitoring bool tracing bool parentDialContextFunc dialerContextFunc } type dialerOpt func(*dialerOpts) type dialerContextFunc func(context.Context, string, string) (net.Conn, error) // DialWithName sets the name of the dialer for tracking and monitoring. // This is the name for the dialer (default is `default`), but for `NewDialContextFunc` can be overwritten from the // Context using `DialNameToContext`. func DialWithName(name string) dialerOpt { return func(opts *dialerOpts) { opts.name = name } } // DialWithoutMonitoring turns *off* Prometheus monitoring for this dialer. func DialWithoutMonitoring() dialerOpt { return func(opts *dialerOpts) { opts.monitoring = false } } // DialWithTracing turns *on* the /debug/events tracing of the dial calls. func DialWithTracing() dialerOpt { return func(opts *dialerOpts) { opts.tracing = true } } // DialWithDialer allows you to override the `net.Dialer` instance used to actually conduct the dials. func DialWithDialer(parentDialer *net.Dialer) dialerOpt { return DialWithDialContextFunc(parentDialer.DialContext) } // DialWithDialContextFunc allows you to override func gets used for the actual dialing. The default is `net.Dialer.DialContext`. func DialWithDialContextFunc(parentDialerFunc dialerContextFunc) dialerOpt { return func(opts *dialerOpts) { opts.parentDialContextFunc = parentDialerFunc } } // DialNameFromContext returns the name of the dialer from the context of the DialContext func, if any. func DialNameFromContext(ctx context.Context) string { val, ok := ctx.Value(dialerNameKey).(string) if !ok { return "" } return val } // DialNameToContext returns a context that will contain a dialer name override. func DialNameToContext(ctx context.Context, dialerName string) context.Context { return context.WithValue(ctx, dialerNameKey, dialerName) } // NewDialContextFunc returns a `DialContext` function that tracks outbound connections. // The signature is compatible with `http.Tranport.DialContext` and is meant to be used there. func NewDialContextFunc(optFuncs ...dialerOpt) func(context.Context, string, string) (net.Conn, error) { opts := &dialerOpts{name: defaultName, monitoring: true, parentDialContextFunc: (&net.Dialer{}).DialContext} for _, f := range optFuncs { f(opts) } if opts.monitoring { PreRegisterDialerMetrics(opts.name) } return func(ctx context.Context, network string, addr string) (net.Conn, error) { name := opts.name if ctxName := DialNameFromContext(ctx); ctxName != "" { name = ctxName } return dialClientConnTracker(ctx, network, addr, name, opts) } } // NewDialFunc returns a `Dial` function that tracks outbound connections. // The signature is compatible with `http.Tranport.Dial` and is meant to be used there for Go < 1.7. func NewDialFunc(optFuncs ...dialerOpt) func(string, string) (net.Conn, error) { dialContextFunc := NewDialContextFunc(optFuncs...) return func(network string, addr string) (net.Conn, error) { return dialContextFunc(context.TODO(), network, addr) } } type clientConnTracker struct { net.Conn opts *dialerOpts dialerName string event trace.EventLog mu sync.Mutex } func dialClientConnTracker(ctx context.Context, network string, addr string, dialerName string, opts *dialerOpts) (net.Conn, error) { var event trace.EventLog if opts.tracing { event = trace.NewEventLog(fmt.Sprintf("net.ClientConn.%s", dialerName), fmt.Sprintf("%v", addr)) } if opts.monitoring { reportDialerConnAttempt(dialerName) } conn, err := opts.parentDialContextFunc(ctx, network, addr) if err != nil { if event != nil { event.Errorf("failed dialing: %v", err) event.Finish() } if opts.monitoring { reportDialerConnFailed(dialerName, err) } return nil, err } if event != nil { event.Printf("established: %s -> %s", conn.LocalAddr(), conn.RemoteAddr()) } if opts.monitoring { reportDialerConnEstablished(dialerName) } tracker := &clientConnTracker{ Conn: conn, opts: opts, dialerName: dialerName, event: event, } return tracker, nil } func (ct *clientConnTracker) Close() error { err := ct.Conn.Close() ct.mu.Lock() if ct.event != nil { if err != nil { ct.event.Errorf("failed closing: %v", err) } else { ct.event.Printf("closing") } ct.event.Finish() ct.event = nil } ct.mu.Unlock() if ct.opts.monitoring { reportDialerConnClosed(ct.dialerName) } return err } golang-github-mwitkow-go-conntrack-0.0~git20190716.2f06839/example/000077500000000000000000000000001351327161100242715ustar00rootroot00000000000000golang-github-mwitkow-go-conntrack-0.0~git20190716.2f06839/example/.gitignore000066400000000000000000000000071351327161100262560ustar00rootroot00000000000000examplegolang-github-mwitkow-go-conntrack-0.0~git20190716.2f06839/example/certs/000077500000000000000000000000001351327161100254115ustar00rootroot00000000000000golang-github-mwitkow-go-conntrack-0.0~git20190716.2f06839/example/certs/gen_cert.sh000066400000000000000000000002541351327161100275340ustar00rootroot00000000000000#!/bin/bash # Regenerate the self-signed certificate for local host. openssl req -x509 -sha256 -nodes -newkey rsa:2048 -days 365 -keyout localhost.key -out localhost.crt golang-github-mwitkow-go-conntrack-0.0~git20190716.2f06839/example/certs/localhost.crt000066400000000000000000000024011351327161100301100ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIDhTCCAm2gAwIBAgIJALjSEF7Y2tBMMA0GCSqGSIb3DQEBCwUAMFkxCzAJBgNV BAYTAlVLMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX aWRnaXRzIFB0eSBMdGQxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0xNjA4MTYwNzQz MTFaFw0xNzA4MTYwNzQzMTFaMFkxCzAJBgNVBAYTAlVLMRMwEQYDVQQIDApTb21l LVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxEjAQBgNV BAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANnu F3N5F3dqmshGs1jMDWI/FNWMNn5T9l9L+X/bw0FylPr6z46I6baFvuOR0CFnOVRj VnL4XGnHuf5BF00r3yaZTik7E/XAP3I2SHjwIRMO6v+dZqkFB1dqGSumBP2Vxh9F HdqVj5Q7No7HAdbef7QMlngW/K8+dZ4I5vR95ZedIhmrVzEGsndB83xqJeq8Jpnf Xwq2Pzmwa5zy0P2cdMFebYqzyOpu5nt3+IE1GFSbQ+xDSAAtejirIARGxIwFMVBg wpy43fbU5zQ/HeMjs5l3WrQ9KPVlR+mw8gtrv0JnpxgwBaPTA9W8jr6Z6Qdb0/BR L/3j9LjtLdB2U6cR2FUCAwEAAaNQME4wHQYDVR0OBBYEFFJ4XhqFff7nv/gLMObh DaIQ8MMUMB8GA1UdIwQYMBaAFFJ4XhqFff7nv/gLMObhDaIQ8MMUMAwGA1UdEwQF MAMBAf8wDQYJKoZIhvcNAQELBQADggEBAB0YmklAhFPY4X5/uVem53Gcr+SB02Gs VHDHKkMFlc74lFoRd/ZFnzI9y4oSBvK5ryjDiA+A7msyc5X2Ii+96wdfRUbyzeUi oYC66GlYitRHJhJIp/alvU8qHEkWXQD5s2rk5RuLNDZKKHEHcZRsQcK11p1otAQa r1L1V+O3OMZJmAdlWuehW0IN161vMlR0oCEpmL9VLrRNPcOL5lGV/GlNCMzLP2XM T9EWt4i6mtGELZ94hea6GxaYfK00K+3t0eVUsMs2IbpoJinLHcJ4fPQdAjzvaAud FCrJIOkJPk0rNeBIXnRRtBL87MV+/rhG1I/eLqLgz5QVjpNG9QDjx8A= -----END CERTIFICATE----- golang-github-mwitkow-go-conntrack-0.0~git20190716.2f06839/example/certs/localhost.key000066400000000000000000000032501351327161100301130ustar00rootroot00000000000000-----BEGIN PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDZ7hdzeRd3aprI RrNYzA1iPxTVjDZ+U/ZfS/l/28NBcpT6+s+OiOm2hb7jkdAhZzlUY1Zy+Fxpx7n+ QRdNK98mmU4pOxP1wD9yNkh48CETDur/nWapBQdXahkrpgT9lcYfRR3alY+UOzaO xwHW3n+0DJZ4FvyvPnWeCOb0feWXnSIZq1cxBrJ3QfN8aiXqvCaZ318Ktj85sGuc 8tD9nHTBXm2Ks8jqbuZ7d/iBNRhUm0PsQ0gALXo4qyAERsSMBTFQYMKcuN321Oc0 Px3jI7OZd1q0PSj1ZUfpsPILa79CZ6cYMAWj0wPVvI6+mekHW9PwUS/94/S47S3Q dlOnEdhVAgMBAAECggEAYurqJBS7rQ1rUiqdL1n3XTfKyh9JgM+1jY3boshqeSN8 Met0GHtWse1FNuAxe2fyIrawP6ExuSXZ62k6HWIjeM6vJpHVPn/TjJDNFm/QY2kr C3kzZtaMfYYABMrniv7XncvugA/QyvSRj/8Oe6wkhOINzlaIvTZ5hpD9283lT4pB vT6qthNrr+6tBX4qCqKD/xhBQbCXTdXmQHVMksesSTOELuSKtnK2QJiAp7L502Hw tWm0obt4T9AbK8QDkLWMUAUsYzSRgRhKB3FGSa4TT0uI2TE9UpNPiOeb47YcZodd eHjbmToL1NK2OXGRhxeYY7bDa24lkKT8rKSXkiTngQKBgQDxg0GajHqtKR2S1UMt UStHwQ+0mEYrNPtT4ipwEIbGbc5Z38E7xUcHimV5NcAgDno4oUvo/lolDYb3E64u Nie+YsK84np0RsqHxKKF+JumRB1HT7MYwdV1CsKJP3g0SnQYV0VYW/C56LOf4Z1I l4HGKrPGYRUyGjIo223pcOigtQKBgQDnALFJa6KLX6kdkYPvpsVr3eC+2iPsr4qj lMhr30UkWVSZG4Pfx3fZIUsUNa20PbtSTBKo+tHGTtf0aRPCwvGnyhaAaPy3w2w8 9ohULe4IPn7zal+UgGyiGWpyt29SGF8zSpdalfxSNuup+Lm3TBE9Rh31N0djqgrl jHxpf+I9IQKBgQCsYDqazFli7k2lV4Gy/pQdirZi96xdeltH68zOX31Sc10s2H9a 4dtojmcOtEaEmtCxSq6bha9hct45y1ousYh8YpELr7om87/qV3aImIC/ky4yj7gM m4x3FU70FtD8wYdLOD7OahDPID/UhXt1LG37us7FcNVoBTp33uX8EBJ5YQKBgAH8 64mqN3fjltz+R5hkYwaOnkSGNBDxYcwOl7r17O5nJmc66WOfn9RqiO7fl2MZtOb8 aJyzq+J9AzbDQLxIWTQMdS0dui8Kq3/Kz1mKG6ZOg2Es5S2t/UFX3qamFXsrYoZa efr5l3ZNqrGHxnFhYjSYyeE2XJLq/7UCBIAT7aqBAoGARyysROXvpI0VWd1m/x3X 8zTlZqy4jMxWm1XArDJWEYcLd8uWUsQ4mad+qopwBP8w2Irn9qFpokLp7qT82bKg Hwha/6qRG6aTD/JHl2Q636EPXHmSPPTUhEjVKAwDIeo1W32pbn10L+1fIPl5Ig7v 31zo/OuHi+06gzcM8Pb+Oh0= -----END PRIVATE KEY----- golang-github-mwitkow-go-conntrack-0.0~git20190716.2f06839/example/server.go000066400000000000000000000052071351327161100261320ustar00rootroot00000000000000// Copyright 2016 Michal Witkowski. All Rights Reserved. // See LICENSE for licensing terms. package main import ( "crypto/tls" "flag" "fmt" "log" "net" "net/http" "time" "github.com/mwitkow/go-conntrack" "github.com/mwitkow/go-conntrack/connhelpers" "github.com/prometheus/client_golang/prometheus/promhttp" "golang.org/x/net/context/ctxhttp" _ "golang.org/x/net/trace" ) var ( port = flag.Int("port", 9090, "whether to use tls or not") useTls = flag.Bool("tls", true, "Whether to use TLS and HTTP2.") tlsCertFilePath = flag.String("tls_cert_file", "certs/localhost.crt", "Path to the CRT/PEM file.") tlsKeyFilePath = flag.String("tls_key_file", "certs/localhost.key", "Path to the private key file.") ) func main() { flag.Parse() // Make sure all outbound connections use the wrapped dialer. http.DefaultTransport.(*http.Transport).DialContext = conntrack.NewDialContextFunc( conntrack.DialWithTracing(), conntrack.DialWithDialer(&net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, }), ) // Since we're using a dynamic name, let's preregister it with prometheus. conntrack.PreRegisterDialerMetrics("google") handler := func(resp http.ResponseWriter, req *http.Request) { resp.WriteHeader(http.StatusOK) resp.Header().Add("Content-Type", "application/json") resp.Write([]byte(`{"msg": "hello"}`)) callCtx := conntrack.DialNameToContext(req.Context(), "google") _, err := ctxhttp.Get(callCtx, http.DefaultClient, "https://www.google.comx") log.Printf("Google reached with err: %v", err) log.Printf("Got request: %v", req) } http.DefaultServeMux.Handle("/", http.HandlerFunc(handler)) http.DefaultServeMux.Handle("/metrics", promhttp.Handler()) httpServer := http.Server{ Handler: http.DefaultServeMux, } var httpListener net.Listener listener, err := net.Listen("tcp", fmt.Sprintf(":%d", *port)) if err != nil { log.Fatalf("Failed to listen: %v", err) } listener = conntrack.NewListener(listener, conntrack.TrackWithTracing()) if !*useTls { httpListener = listener } else { tlsConfig, err := connhelpers.TlsConfigForServerCerts(*tlsCertFilePath, *tlsKeyFilePath) if err != nil { log.Fatalf("Failed configuring TLS: %v", err) } tlsConfig, err = connhelpers.TlsConfigWithHttp2Enabled(tlsConfig) if err != nil { log.Fatalf("Failed configuring TLS: %v", err) } log.Printf("Listening with TLS") tlsListener := tls.NewListener(listener, tlsConfig) httpListener = tlsListener } //httpListener.Addr() log.Printf("Listening on: %s", listener.Addr().String()) if err := httpServer.Serve(httpListener); err != nil { log.Fatalf("Failed listning: %v", err) } } golang-github-mwitkow-go-conntrack-0.0~git20190716.2f06839/listener_reporter.go000066400000000000000000000025241351327161100267370ustar00rootroot00000000000000// Copyright 2016 Michal Witkowski. All Rights Reserved. // See LICENSE for licensing terms. package conntrack import prom "github.com/prometheus/client_golang/prometheus" var ( listenerAcceptedTotal = prom.NewCounterVec( prom.CounterOpts{ Namespace: "net", Subsystem: "conntrack", Name: "listener_conn_accepted_total", Help: "Total number of connections opened to the listener of a given name.", }, []string{"listener_name"}) listenerClosedTotal = prom.NewCounterVec( prom.CounterOpts{ Namespace: "net", Subsystem: "conntrack", Name: "listener_conn_closed_total", Help: "Total number of connections closed that were made to the listener of a given name.", }, []string{"listener_name"}) ) func init() { prom.MustRegister(listenerAcceptedTotal) prom.MustRegister(listenerClosedTotal) } // preRegisterListener pre-populates Prometheus labels for the given listener name, to avoid Prometheus missing labels issue. func preRegisterListenerMetrics(listenerName string) { listenerAcceptedTotal.WithLabelValues(listenerName) listenerClosedTotal.WithLabelValues(listenerName) } func reportListenerConnAccepted(listenerName string) { listenerAcceptedTotal.WithLabelValues(listenerName).Inc() } func reportListenerConnClosed(listenerName string) { listenerClosedTotal.WithLabelValues(listenerName).Inc() } golang-github-mwitkow-go-conntrack-0.0~git20190716.2f06839/listener_test.go000066400000000000000000000071221351327161100260530ustar00rootroot00000000000000// Copyright 2016 Michal Witkowski. All Rights Reserved. // See LICENSE for licensing terms. package conntrack_test import ( "net" "net/http" "testing" "context" "time" "github.com/mwitkow/go-conntrack" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" ) func TestListenerTestSuite(t *testing.T) { suite.Run(t, &ListenerTestSuite{}) } var ( listenerName = "some_name" ) type ListenerTestSuite struct { suite.Suite serverListener net.Listener httpServer http.Server } func (s *ListenerTestSuite) SetupSuite() { var err error s.serverListener, err = net.Listen("tcp", "127.0.0.1:0") require.NoError(s.T(), err, "must be able to allocate a port for serverListener") s.serverListener = conntrack.NewListener(s.serverListener, conntrack.TrackWithName(listenerName), conntrack.TrackWithTracing()) s.httpServer = http.Server{ Handler: http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { resp.WriteHeader(http.StatusOK) }), } go func() { s.httpServer.Serve(s.serverListener) }() } func (s *ListenerTestSuite) TestTrackingMetricsPreregistered() { // this will create the default listener, check if it is registered conntrack.NewListener(s.serverListener) for testId, testCase := range []struct { metricName string existingLabels []string }{ {"net_conntrack_listener_conn_accepted_total", []string{"default"}}, {"net_conntrack_listener_conn_closed_total", []string{"default"}}, {"net_conntrack_listener_conn_accepted_total", []string{listenerName}}, {"net_conntrack_listener_conn_closed_total", []string{listenerName}}, } { lineCount := len(fetchPrometheusLines(s.T(), testCase.metricName, testCase.existingLabels...)) assert.NotEqual(s.T(), 0, lineCount, "metrics must exist for test case %d", testId) } } func (s *ListenerTestSuite) TestMonitoringNormalConns() { beforeAccepted := sumCountersForMetricAndLabels(s.T(), "net_conntrack_listener_conn_accepted_total", listenerName) beforeClosed := sumCountersForMetricAndLabels(s.T(), "net_conntrack_listener_conn_closed_total", listenerName) conn, err := (&net.Dialer{}).DialContext(context.TODO(), "tcp", s.serverListener.Addr().String()) require.NoError(s.T(), err, "DialContext should successfully establish a conn here") assert.Equal(s.T(), beforeAccepted+1, sumCountersForMetricAndLabels(s.T(), "net_conntrack_listener_conn_accepted_total", listenerName), "the accepted conn counter must be incremented after connection was opened") assert.Equal(s.T(), beforeClosed, sumCountersForMetricAndLabels(s.T(), "net_conntrack_listener_conn_closed_total", listenerName), "the closed conn counter must not be incremented before the connection is closed") conn.Close() assert.Equal(s.T(), beforeClosed+1, sumCountersForMetricAndLabels(s.T(), "net_conntrack_listener_conn_closed_total", listenerName), "the closed conn counter must be incremented after connection was closed") } func (s *ListenerTestSuite) TestTracingNormalComms() { conn, err := (&net.Dialer{}).DialContext(context.TODO(), "tcp", s.serverListener.Addr().String()) require.NoError(s.T(), err, "DialContext should successfully establish a conn here") time.Sleep(5 * time.Millisecond) assert.Contains(s.T(), fetchTraceEvents(s.T(), "net.ServerConn."+listenerName), conn.LocalAddr().String(), "the /debug/trace/events page must contain the live connection") time.Sleep(5 * time.Millisecond) conn.Close() } func (s *ListenerTestSuite) TearDownSuite() { if s.serverListener != nil { s.T().Logf("stopped http.Server at: %v", s.serverListener.Addr().String()) s.serverListener.Close() } } golang-github-mwitkow-go-conntrack-0.0~git20190716.2f06839/listener_wrapper.go000066400000000000000000000073711351327161100265620ustar00rootroot00000000000000// Copyright 2016 Michal Witkowski. All Rights Reserved. // See LICENSE for licensing terms. package conntrack import ( "fmt" "net" "sync" "time" "github.com/jpillora/backoff" "golang.org/x/net/trace" ) const ( defaultName = "default" ) type listenerOpts struct { name string monitoring bool tracing bool tcpKeepAlive time.Duration retryBackoff *backoff.Backoff } type listenerOpt func(*listenerOpts) // TrackWithName sets the name of the Listener for use in tracking and monitoring. func TrackWithName(name string) listenerOpt { return func(opts *listenerOpts) { opts.name = name } } // TrackWithoutMonitoring turns *off* Prometheus monitoring for this listener. func TrackWithoutMonitoring() listenerOpt { return func(opts *listenerOpts) { opts.monitoring = false } } // TrackWithTracing turns *on* the /debug/events tracing of the live listener connections. func TrackWithTracing() listenerOpt { return func(opts *listenerOpts) { opts.tracing = true } } // TrackWithRetries enables retrying of temporary Accept() errors, with the given backoff between attempts. // Concurrent accept calls that receive temporary errors have independent backoff scaling. func TrackWithRetries(b backoff.Backoff) listenerOpt { return func(opts *listenerOpts) { opts.retryBackoff = &b } } // TrackWithTcpKeepAlive makes sure that any `net.TCPConn` that get accepted have a keep-alive. // This is useful for HTTP servers in order for, for example laptops, to not use up resources on the // server while they don't utilise their connection. // A value of 0 disables it. func TrackWithTcpKeepAlive(keepalive time.Duration) listenerOpt { return func(opts *listenerOpts) { opts.tcpKeepAlive = keepalive } } type connTrackListener struct { net.Listener opts *listenerOpts } // NewListener returns the given listener wrapped in connection tracking listener. func NewListener(inner net.Listener, optFuncs ...listenerOpt) net.Listener { opts := &listenerOpts{ name: defaultName, monitoring: true, tracing: false, } for _, f := range optFuncs { f(opts) } if opts.monitoring { preRegisterListenerMetrics(opts.name) } return &connTrackListener{ Listener: inner, opts: opts, } } func (ct *connTrackListener) Accept() (net.Conn, error) { // TODO(mwitkow): Add monitoring of failed accept. var ( conn net.Conn err error ) for attempt := 0; ; attempt++ { conn, err = ct.Listener.Accept() if err == nil || ct.opts.retryBackoff == nil { break } if t, ok := err.(interface{ Temporary() bool }); !ok || !t.Temporary() { break } time.Sleep(ct.opts.retryBackoff.ForAttempt(float64(attempt))) } if err != nil { return nil, err } if tcpConn, ok := conn.(*net.TCPConn); ok && ct.opts.tcpKeepAlive > 0 { tcpConn.SetKeepAlive(true) tcpConn.SetKeepAlivePeriod(ct.opts.tcpKeepAlive) } return newServerConnTracker(conn, ct.opts), nil } type serverConnTracker struct { net.Conn opts *listenerOpts event trace.EventLog mu sync.Mutex } func newServerConnTracker(inner net.Conn, opts *listenerOpts) net.Conn { tracker := &serverConnTracker{ Conn: inner, opts: opts, } if opts.tracing { tracker.event = trace.NewEventLog(fmt.Sprintf("net.ServerConn.%s", opts.name), fmt.Sprintf("%v", inner.RemoteAddr())) tracker.event.Printf("accepted: %v -> %v", inner.RemoteAddr(), inner.LocalAddr()) } if opts.monitoring { reportListenerConnAccepted(opts.name) } return tracker } func (ct *serverConnTracker) Close() error { err := ct.Conn.Close() ct.mu.Lock() if ct.event != nil { if err != nil { ct.event.Errorf("failed closing: %v", err) } else { ct.event.Printf("closing") } ct.event.Finish() ct.event = nil } ct.mu.Unlock() if ct.opts.monitoring { reportListenerConnClosed(ct.opts.name) } return err } golang-github-mwitkow-go-conntrack-0.0~git20190716.2f06839/promhelper_test.go000066400000000000000000000026211351327161100264020ustar00rootroot00000000000000package conntrack_test import ( "bufio" "io" "net/http" "net/http/httptest" "strconv" "strings" "testing" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/stretchr/testify/require" ) func fetchPrometheusLines(t *testing.T, metricName string, matchingLabelValues ...string) []string { resp := httptest.NewRecorder() req, err := http.NewRequest("GET", "/", nil) require.NoError(t, err, "failed creating request for Prometheus handler") promhttp.Handler().ServeHTTP(resp, req) reader := bufio.NewReader(resp.Body) ret := []string{} for { line, err := reader.ReadString('\n') if err == io.EOF { break } else { require.NoError(t, err, "error reading stuff") } if !strings.HasPrefix(line, metricName) { continue } matches := true for _, labelValue := range matchingLabelValues { if !strings.Contains(line, `"`+labelValue+`"`) { matches = false } } if matches { ret = append(ret, line) } } return ret } func sumCountersForMetricAndLabels(t *testing.T, metricName string, matchingLabelValues ...string) int { count := 0 for _, line := range fetchPrometheusLines(t, metricName, matchingLabelValues...) { valueString := line[strings.LastIndex(line, " ")+1 : len(line)-1] valueFloat, err := strconv.ParseFloat(valueString, 32) require.NoError(t, err, "failed parsing value for line: %v", line) count += int(valueFloat) } return count } golang-github-mwitkow-go-conntrack-0.0~git20190716.2f06839/tracehelper_test.go000066400000000000000000000011351351327161100265220ustar00rootroot00000000000000package conntrack_test import ( "fmt" "io/ioutil" "net/http" "net/http/httptest" "testing" "github.com/stretchr/testify/require" "golang.org/x/net/trace" ) func fetchTraceEvents(t *testing.T, familyName string) string { resp := httptest.NewRecorder() url := fmt.Sprintf("/debug/events?fam=%s&b=0&exp=1", familyName) req, err := http.NewRequest("GET", url, nil) require.NoError(t, err, "failed creating request for Prometheus handler") trace.RenderEvents(resp, req, true) out, err := ioutil.ReadAll(resp.Body) require.NoError(t, err, "failed reading the trace page") return string(out) }