├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── config.json ├── eventrouter.go ├── go.mod ├── go.sum ├── main.go ├── sinks ├── eventdata.go ├── eventhub.go ├── glogsink.go ├── httpsink.go ├── httpsink_test.go ├── influxdb.go ├── interfaces.go ├── kafkasink.go ├── rocksetsink.go ├── s3sink.go ├── samplehttpsink │ ├── Dockerfile │ └── server.go └── stdoutsink.go ├── tests ├── README.md ├── kafka │ ├── README.md │ ├── docker-compose.yml │ └── main.go └── rockset │ └── main.go └── yaml ├── eventrouter-azure.yaml └── eventrouter.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | eventrouter 27 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | language: go 3 | services: 4 | - docker 5 | install: true 6 | 7 | script: make container test vet 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## DCO Sign off 2 | 3 | All authors to the project retain copyright to their work. However, to ensure 4 | that they are only submitting work that they have rights to, we are requiring 5 | everyone to acknowldge this by signing their work. 6 | 7 | Any copyright notices in this repos should specify the authors as "the contributors". 8 | 9 | To sign your work, just add a line like this at the end of your commit message: 10 | 11 | ``` 12 | Signed-off-by: Joe Beda 13 | ``` 14 | 15 | This can easily be done with the `--signoff` option to `git commit`. 16 | 17 | By doing this you state that you can certify the following (from https://developercertificate.org/): 18 | 19 | ``` 20 | Developer Certificate of Origin 21 | Version 1.1 22 | 23 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 24 | 1 Letterman Drive 25 | Suite D4700 26 | San Francisco, CA, 94129 27 | 28 | Everyone is permitted to copy and distribute verbatim copies of this 29 | license document, but changing it is not allowed. 30 | 31 | 32 | Developer's Certificate of Origin 1.1 33 | 34 | By making a contribution to this project, I certify that: 35 | 36 | (a) The contribution was created in whole or in part by me and I 37 | have the right to submit it under the open source license 38 | indicated in the file; or 39 | 40 | (b) The contribution is based upon previous work that, to the best 41 | of my knowledge, is covered under an appropriate open source 42 | license and I have the right under that license to submit that 43 | work with modifications, whether created in whole or in part 44 | by me, under the same open source license (unless I am 45 | permitted to submit under a different license), as indicated 46 | in the file; or 47 | 48 | (c) The contribution was provided directly to me by some other 49 | person who certified (a), (b) or (c) and I have not modified 50 | it. 51 | 52 | (d) I understand and agree that this project and the contribution 53 | are public and that a record of the contribution (including all 54 | personal information I submit with it, including my sign-off) is 55 | maintained indefinitely and may be redistributed consistent with 56 | this project or the open source license(s) involved. 57 | ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Heptio Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | FROM alpine:3.9 16 | MAINTAINER Timothy St. Clair "tstclair@heptio.com" 17 | 18 | WORKDIR /app 19 | RUN apk update --no-cache && apk add ca-certificates 20 | ADD eventrouter /app/ 21 | USER nobody:nobody 22 | 23 | CMD ["/bin/sh", "-c", "/app/eventrouter -v 3 -logtostderr"] 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Heptio Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | TARGET = eventrouter 16 | GOTARGET = github.com/heptiolabs/$(TARGET) 17 | BUILDMNT = /src/ 18 | REGISTRY ?= gcr.io/heptio-images 19 | VERSION ?= v0.3 20 | IMAGE = $(REGISTRY)/$(BIN) 21 | BUILD_IMAGE ?= golang:1.12.9 22 | DOCKER ?= docker 23 | DIR := ${CURDIR} 24 | 25 | ifneq ($(VERBOSE),) 26 | VERBOSE_FLAG = -v 27 | endif 28 | TESTARGS ?= $(VERBOSE_FLAG) -timeout 60s 29 | TEST_PKGS ?= $(GOTARGET)/sinks/... 30 | TEST = go test $(TEST_PKGS) $(TESTARGS) 31 | VET_PKGS ?= $(GOTARGET)/... 32 | VET = go vet $(VET_PKGS) 33 | 34 | DOCKER_BUILD ?= $(DOCKER) run --rm -v $(DIR):$(BUILDMNT) -w $(BUILDMNT) $(BUILD_IMAGE) /bin/sh -c 35 | 36 | all: container 37 | 38 | container: 39 | $(DOCKER_BUILD) 'CGO_ENABLED=0 go build' 40 | $(DOCKER) build -t $(REGISTRY)/$(TARGET):latest -t $(REGISTRY)/$(TARGET):$(VERSION) . 41 | 42 | push: 43 | $(DOCKER) push $(REGISTRY)/$(TARGET):latest 44 | if git describe --tags --exact-match >/dev/null 2>&1; \ 45 | then \ 46 | $(DOCKER) push $(REGISTRY)/$(TARGET):$(VERSION); \ 47 | fi 48 | 49 | test: 50 | $(DOCKER_BUILD) '$(TEST)' 51 | 52 | vet: 53 | $(DOCKER_BUILD) '$(VET)' 54 | 55 | .PHONY: all local container push 56 | 57 | clean: 58 | rm -f $(TARGET) 59 | $(DOCKER) rmi $(REGISTRY)/$(TARGET):latest 60 | $(DOCKER) rmi $(REGISTRY)/$(TARGET):$(VERSION) 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Eventrouter 2 | 3 | This repository contains a simple event router for the [Kubernetes][kubernetes] project. The event router serves as an active watcher of _event_ resource in the kubernetes system, which takes those events and _pushes_ them to a user specified _sink_. This is useful for a number of different purposes, but most notably long term behavioral analysis of your 4 | workloads running on your kubernetes cluster. 5 | 6 | ## Goals 7 | 8 | This project has several objectives, which include: 9 | 10 | * Persist events for longer period of time to allow for system debugging 11 | * Allows operators to forward events to other system(s) for archiving/ML/introspection/etc. 12 | * It should be relatively low overhead 13 | * Support for multiple _sinks_ should be configurable 14 | 15 | ### NOTE: 16 | 17 | By default, eventrouter is configured to leverage existing EFK stacks by outputting wrapped json object which are easy to index in elastic search. 18 | 19 | ## Non-Goals: 20 | 21 | * This service does not provide a querable extension, that is a responsibility of the 22 | _sink_ 23 | * This service does not serve as a storage layer, that is also the responsibility of the _sink_ 24 | 25 | ## Running Eventrouter 26 | Standup: 27 | ``` 28 | $ kubectl create -f https://raw.githubusercontent.com/heptiolabs/eventrouter/master/yaml/eventrouter.yaml 29 | ``` 30 | Teardown: 31 | ``` 32 | $ kubectl delete -f https://raw.githubusercontent.com/heptiolabs/eventrouter/master/yaml/eventrouter.yaml 33 | ``` 34 | 35 | ### Inspecting the output 36 | ``` 37 | $ kubectl logs -f deployment/eventrouter -n kube-system 38 | ``` 39 | 40 | Watch events roll through the system and hopefully stream into your ES cluster for mining, Hooray! 41 | 42 | [kubernetes]: https://github.com/kubernetes/kubernetes/ "Kubernetes" 43 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "kubeconfig": "/var/run/kubernetes/admin.kubeconfig", 3 | "sink": "glog", 4 | "kafkaBrokers": "kafka:9092", 5 | "kafkaTopic": "topic", 6 | "kafkaSaslUser": "user", 7 | "kafkaSaslPwd": "password" 8 | "httpSinkUrl": "http://localhost:8080", 9 | "httpSinkBufferSize": 1500, 10 | "httpSinkDiscardMessages": true, 11 | "rocksetAPIKey": "", 12 | "rocksetCollectionName": "", 13 | "rocksetWorkspaceName": "", 14 | "s3SinkAccessKeyID": "", 15 | "s3SinkSecretAccessKey": "", 16 | "s3SinkRegion": "ap-south-1", 17 | "s3SinkBucket": "", 18 | "s3SinkBucketDir": "", 19 | "s3SinkBufferSize": 1500, 20 | "s3SinkDiscardMessages": true, 21 | "s3SinkOutputFormat": "flatjson", 22 | "s3SinkUploadInterval": 120 23 | } 24 | -------------------------------------------------------------------------------- /eventrouter.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Heptio Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "fmt" 21 | 22 | "github.com/golang/glog" 23 | "github.com/heptiolabs/eventrouter/sinks" 24 | "github.com/prometheus/client_golang/prometheus" 25 | "github.com/spf13/viper" 26 | 27 | v1 "k8s.io/api/core/v1" 28 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 29 | coreinformers "k8s.io/client-go/informers/core/v1" 30 | "k8s.io/client-go/kubernetes" 31 | corelisters "k8s.io/client-go/listers/core/v1" 32 | "k8s.io/client-go/tools/cache" 33 | ) 34 | 35 | var ( 36 | kubernetesWarningEventCounterVec = prometheus.NewCounterVec(prometheus.CounterOpts{ 37 | Name: "heptio_eventrouter_warnings_total", 38 | Help: "Total number of warning events in the kubernetes cluster", 39 | }, []string{ 40 | "involved_object_kind", 41 | "involved_object_name", 42 | "involved_object_namespace", 43 | "reason", 44 | "source", 45 | }) 46 | kubernetesNormalEventCounterVec = prometheus.NewCounterVec(prometheus.CounterOpts{ 47 | Name: "heptio_eventrouter_normal_total", 48 | Help: "Total number of normal events in the kubernetes cluster", 49 | }, []string{ 50 | "involved_object_kind", 51 | "involved_object_name", 52 | "involved_object_namespace", 53 | "reason", 54 | "source", 55 | }) 56 | kubernetesInfoEventCounterVec = prometheus.NewCounterVec(prometheus.CounterOpts{ 57 | Name: "heptio_eventrouter_info_total", 58 | Help: "Total number of info events in the kubernetes cluster", 59 | }, []string{ 60 | "involved_object_kind", 61 | "involved_object_name", 62 | "involved_object_namespace", 63 | "reason", 64 | "source", 65 | }) 66 | kubernetesUnknownEventCounterVec = prometheus.NewCounterVec(prometheus.CounterOpts{ 67 | Name: "heptio_eventrouter_unknown_total", 68 | Help: "Total number of events of unknown type in the kubernetes cluster", 69 | }, []string{ 70 | "involved_object_kind", 71 | "involved_object_name", 72 | "involved_object_namespace", 73 | "reason", 74 | "source", 75 | }) 76 | ) 77 | 78 | // EventRouter is responsible for maintaining a stream of kubernetes 79 | // system Events and pushing them to another channel for storage 80 | type EventRouter struct { 81 | // kubeclient is the main kubernetes interface 82 | kubeClient kubernetes.Interface 83 | 84 | // store of events populated by the shared informer 85 | eLister corelisters.EventLister 86 | 87 | // returns true if the event store has been synced 88 | eListerSynched cache.InformerSynced 89 | 90 | // event sink 91 | // TODO: Determine if we want to support multiple sinks. 92 | eSink sinks.EventSinkInterface 93 | } 94 | 95 | // NewEventRouter will create a new event router using the input params 96 | func NewEventRouter(kubeClient kubernetes.Interface, eventsInformer coreinformers.EventInformer) *EventRouter { 97 | if viper.GetBool("enable-prometheus") { 98 | prometheus.MustRegister(kubernetesWarningEventCounterVec) 99 | prometheus.MustRegister(kubernetesNormalEventCounterVec) 100 | prometheus.MustRegister(kubernetesInfoEventCounterVec) 101 | prometheus.MustRegister(kubernetesUnknownEventCounterVec) 102 | } 103 | 104 | er := &EventRouter{ 105 | kubeClient: kubeClient, 106 | eSink: sinks.ManufactureSink(), 107 | } 108 | eventsInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ 109 | AddFunc: er.addEvent, 110 | UpdateFunc: er.updateEvent, 111 | DeleteFunc: er.deleteEvent, 112 | }) 113 | er.eLister = eventsInformer.Lister() 114 | er.eListerSynched = eventsInformer.Informer().HasSynced 115 | return er 116 | } 117 | 118 | // Run starts the EventRouter/Controller. 119 | func (er *EventRouter) Run(stopCh <-chan struct{}) { 120 | defer utilruntime.HandleCrash() 121 | defer glog.Infof("Shutting down EventRouter") 122 | 123 | glog.Infof("Starting EventRouter") 124 | 125 | // here is where we kick the caches into gear 126 | if !cache.WaitForCacheSync(stopCh, er.eListerSynched) { 127 | utilruntime.HandleError(fmt.Errorf("timed out waiting for caches to sync")) 128 | return 129 | } 130 | <-stopCh 131 | } 132 | 133 | // addEvent is called when an event is created, or during the initial list 134 | func (er *EventRouter) addEvent(obj interface{}) { 135 | e := obj.(*v1.Event) 136 | prometheusEvent(e) 137 | er.eSink.UpdateEvents(e, nil) 138 | } 139 | 140 | // updateEvent is called any time there is an update to an existing event 141 | func (er *EventRouter) updateEvent(objOld interface{}, objNew interface{}) { 142 | eOld := objOld.(*v1.Event) 143 | eNew := objNew.(*v1.Event) 144 | prometheusEvent(eNew) 145 | er.eSink.UpdateEvents(eNew, eOld) 146 | } 147 | 148 | // prometheusEvent is called when an event is added or updated 149 | func prometheusEvent(event *v1.Event) { 150 | if !viper.GetBool("enable-prometheus") { 151 | return 152 | } 153 | var counter prometheus.Counter 154 | var err error 155 | 156 | switch event.Type { 157 | case "Normal": 158 | counter, err = kubernetesNormalEventCounterVec.GetMetricWithLabelValues( 159 | event.InvolvedObject.Kind, 160 | event.InvolvedObject.Name, 161 | event.InvolvedObject.Namespace, 162 | event.Reason, 163 | event.Source.Host, 164 | ) 165 | case "Warning": 166 | counter, err = kubernetesWarningEventCounterVec.GetMetricWithLabelValues( 167 | event.InvolvedObject.Kind, 168 | event.InvolvedObject.Name, 169 | event.InvolvedObject.Namespace, 170 | event.Reason, 171 | event.Source.Host, 172 | ) 173 | case "Info": 174 | counter, err = kubernetesInfoEventCounterVec.GetMetricWithLabelValues( 175 | event.InvolvedObject.Kind, 176 | event.InvolvedObject.Name, 177 | event.InvolvedObject.Namespace, 178 | event.Reason, 179 | event.Source.Host, 180 | ) 181 | default: 182 | counter, err = kubernetesUnknownEventCounterVec.GetMetricWithLabelValues( 183 | event.InvolvedObject.Kind, 184 | event.InvolvedObject.Name, 185 | event.InvolvedObject.Namespace, 186 | event.Reason, 187 | event.Source.Host, 188 | ) 189 | } 190 | 191 | if err != nil { 192 | // Not sure this is the right place to log this error? 193 | glog.Warning(err) 194 | } else { 195 | counter.Add(1) 196 | } 197 | } 198 | 199 | // deleteEvent should only occur when the system garbage collects events via TTL expiration 200 | func (er *EventRouter) deleteEvent(obj interface{}) { 201 | e := obj.(*v1.Event) 202 | // NOTE: This should *only* happen on TTL expiration there 203 | // is no reason to push this to a sink 204 | glog.V(5).Infof("Event Deleted from the system:\n%v", e) 205 | } 206 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/heptiolabs/eventrouter 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/Azure/azure-event-hubs-go/v2 v2.0.3 7 | github.com/Shopify/sarama v1.23.1 8 | github.com/aws/aws-sdk-go v1.23.2 9 | github.com/crewjam/rfc5424 v0.0.0-20180723152949-c25bdd3a0ba2 10 | github.com/eapache/channels v1.1.0 11 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b 12 | github.com/imdario/mergo v0.3.7 // indirect 13 | github.com/influxdata/influxdb v1.7.7 14 | github.com/json-iterator/go v1.1.7 15 | github.com/kelseyhightower/envconfig v1.4.0 16 | github.com/nytlabs/gojsonexplode v0.0.0-20160201065013-0f3fe6bb573f 17 | github.com/prometheus/client_golang v1.1.0 18 | github.com/rockset/rockset-go-client v0.6.0 19 | github.com/sethgrid/pester v0.0.0-20190127155807-68a33a018ad0 20 | github.com/spf13/viper v1.4.0 21 | gopkg.in/jcmturner/goidentity.v3 v3.0.0 // indirect 22 | k8s.io/api v0.0.0-20190814101207-0772a1bdf941 23 | k8s.io/apimachinery v0.0.0-20190814100815-533d101be9a6 24 | k8s.io/client-go v12.0.0+incompatible 25 | k8s.io/utils v0.0.0-20190809000727-6c36bc71fc4a // indirect 26 | ) 27 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | contrib.go.opencensus.io/exporter/ocagent v0.5.0 h1:TKXjQSRS0/cCDrP7KvkgU6SmILtF/yV2TOs/02K/WZQ= 4 | contrib.go.opencensus.io/exporter/ocagent v0.5.0/go.mod h1:ImxhfLRpxoYiSq891pBrLVhN+qmP8BTVvdH2YLs7Gl0= 5 | github.com/Azure/azure-amqp-common-go/v2 v2.1.0 h1:+QbFgmWCnPzdaRMfsI0Yb6GrRdBj5jVL8N3EXuEUcBQ= 6 | github.com/Azure/azure-amqp-common-go/v2 v2.1.0/go.mod h1:R8rea+gJRuJR6QxTir/XuEd+YuKoUiazDC/N96FiDEU= 7 | github.com/Azure/azure-event-hubs-go/v2 v2.0.3 h1:4ilH4G/DpTHAzSOH1HodoQC5PBrJ0t28fHbI3G2/49A= 8 | github.com/Azure/azure-event-hubs-go/v2 v2.0.3/go.mod h1:3lDFmasdadcfRHAK4fEgsroU9i3oK+pY+8hpoUCLppk= 9 | github.com/Azure/azure-pipeline-go v0.1.8/go.mod h1:XA1kFWRVhSK+KNFiOhfv83Fv8L9achrP7OxIzeTn1Yg= 10 | github.com/Azure/azure-pipeline-go v0.1.9/go.mod h1:XA1kFWRVhSK+KNFiOhfv83Fv8L9achrP7OxIzeTn1Yg= 11 | github.com/Azure/azure-sdk-for-go v29.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= 12 | github.com/Azure/azure-sdk-for-go v30.0.0+incompatible h1:6o1Yzl7wTBYg+xw0pY4qnalaPmEQolubEEdepo1/kmI= 13 | github.com/Azure/azure-sdk-for-go v30.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= 14 | github.com/Azure/azure-storage-blob-go v0.6.0/go.mod h1:oGfmITT1V6x//CswqY2gtAHND+xIP64/qL7a5QJix0Y= 15 | github.com/Azure/go-autorest v11.1.2+incompatible h1:viZ3tV5l4gE2Sw0xrasFHytCGtzYCrT+um/rrSQ1BfA= 16 | github.com/Azure/go-autorest v11.1.2+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= 17 | github.com/Azure/go-autorest v12.0.0+incompatible h1:N+VqClcomLGD/sHb3smbSYYtNMgKpVV3Cd5r5i8z6bQ= 18 | github.com/Azure/go-autorest v12.0.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= 19 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 20 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 21 | github.com/DataDog/zstd v1.3.6-0.20190409195224-796139022798 h1:2T/jmrHeTezcCM58lvEQXs0UpQJCo5SoGAcg+mbSTIg= 22 | github.com/DataDog/zstd v1.3.6-0.20190409195224-796139022798/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= 23 | github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= 24 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 25 | github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= 26 | github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= 27 | github.com/Shopify/sarama v1.23.1 h1:XxJBCZEoWJtoWjf/xRbmGUpAmTZGnuuF0ON0EvxxBrs= 28 | github.com/Shopify/sarama v1.23.1/go.mod h1:XLH1GYJnLVE0XCr6KdJGVJRTwY30moWNJ4sERjXX6fs= 29 | github.com/Shopify/toxiproxy v2.1.4+incompatible h1:TKdv8HiTLgE5wdJuEML90aBgNWsokNbMijUGhmcoBJc= 30 | github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= 31 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 32 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 33 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 34 | github.com/aws/aws-sdk-go v1.23.2 h1:QSdnxlC29v6b2+C6mkriHhElh02ZlsRBoPX15SOZ6jU= 35 | github.com/aws/aws-sdk-go v1.23.2/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= 36 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 37 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 38 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 39 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 40 | github.com/census-instrumentation/opencensus-proto v0.2.0 h1:LzQXZOgg4CQfE6bFvXGM30YZL1WW/M337pXml+GrcZ4= 41 | github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 42 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 43 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 44 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= 45 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 46 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 47 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 48 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 49 | github.com/crewjam/rfc5424 v0.0.0-20180723152949-c25bdd3a0ba2 h1:ikTypaS8gho3dBf1gySXxxv+NkB8vyYgqMPYv51LD4U= 50 | github.com/crewjam/rfc5424 v0.0.0-20180723152949-c25bdd3a0ba2/go.mod h1:+E6hJ4dnJi+OtRGvE3sIOIwMivXJTbRqZfQkWeANo6I= 51 | github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 52 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 53 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 54 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 55 | github.com/devigned/tab v0.1.1 h1:3mD6Kb1mUOYeLpJvTVSDwSg5ZsfSxfvxGRTxRsJsITA= 56 | github.com/devigned/tab v0.1.1/go.mod h1:XG9mPq0dFghrYvoBF3xdRrJzSTX1b7IQrvaL9mzjeJY= 57 | github.com/dgrijalva/jwt-go v0.0.0-20160705203006-01aeca54ebda/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 58 | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= 59 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 60 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= 61 | github.com/dimchansky/utfbom v1.1.0 h1:FcM3g+nofKgUteL8dm/UpdRXNC9KmADgTpLKsu0TRo4= 62 | github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8= 63 | github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= 64 | github.com/eapache/channels v1.1.0 h1:F1taHcn7/F0i8DYqKXJnyhJcVpp2kgFcNePxXtnyu4k= 65 | github.com/eapache/channels v1.1.0/go.mod h1:jMm2qB5Ubtg9zLd+inMZd2/NUvXgzmWXsDaLyQIGfH0= 66 | github.com/eapache/go-resiliency v1.1.0 h1:1NtRmCAqadE2FN4ZcN6g90TP3uk8cg9rn9eNK2197aU= 67 | github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= 68 | github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21 h1:YEetp8/yCZMuEPMUDHG0CW/brkkEp8mzqk2+ODEitlw= 69 | github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= 70 | github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= 71 | github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= 72 | github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= 73 | github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= 74 | github.com/evanphx/json-patch v0.0.0-20190203023257-5858425f7550/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= 75 | github.com/evanphx/json-patch v4.2.0+incompatible h1:fUDGZCv/7iAN7u0puUVhvKCcsR6vRfwrJatElLBEf0I= 76 | github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= 77 | github.com/fortytw2/leaktest v1.2.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= 78 | github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= 79 | github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= 80 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 81 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 82 | github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 83 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 84 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 85 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 86 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 87 | github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= 88 | github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= 89 | github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= 90 | github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= 91 | github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= 92 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 93 | github.com/gogo/protobuf v0.0.0-20171007142547-342cbe0a0415/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 94 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 95 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 96 | github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d h1:3PaI8p3seN09VjbTYC/QWlUZdZ1qS1zGjy7LH2Wt07I= 97 | github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= 98 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= 99 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 100 | github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 101 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef h1:veQD95Isof8w9/WXiA+pa3tz3fJXkt5B7QaRBrM62gk= 102 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 103 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 104 | github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 105 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 106 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 107 | github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= 108 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 109 | github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= 110 | github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 111 | github.com/google/btree v0.0.0-20160524151835-7d79101e329e/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 112 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 113 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 114 | github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= 115 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 116 | github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= 117 | github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= 118 | github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= 119 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 120 | github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 121 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 122 | github.com/googleapis/gnostic v0.0.0-20170426233943-68f4ded48ba9/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= 123 | github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d h1:7XGaL1e6bYS1yIonGp9761ExpPPV1ui0SAC59Yube9k= 124 | github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= 125 | github.com/gophercloud/gophercloud v0.0.0-20190126172459-c818fa66e4c8/go.mod h1:3WdhXV3rUYy9p6AUW8d94kr+HS62Y4VL9mBnFxsD8q4= 126 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 127 | github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= 128 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 129 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 130 | github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 131 | github.com/grpc-ecosystem/grpc-gateway v1.9.0 h1:bM6ZAFZmc/wPFaRDi0d5L7hGEZEx/2u+Tmr2evNHDiI= 132 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 133 | github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE= 134 | github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 135 | github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= 136 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 137 | github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= 138 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 139 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 140 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 141 | github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= 142 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 143 | github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= 144 | github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI= 145 | github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= 146 | github.com/influxdata/influxdb v1.7.7 h1:UvNzAPfBrKMENVbQ4mr4ccA9sW+W1Ihl0Yh1s0BiVAg= 147 | github.com/influxdata/influxdb v1.7.7/go.mod h1:qZna6X/4elxqT3yI9iZYdZrWWdeFOOprn86kgg4+IzY= 148 | github.com/jcmturner/gofork v0.0.0-20190328161633-dc7c13fece03 h1:FUwcHNlEqkqLjLBdCp5PRlCFijNjvcYANOZXzCfXwCM= 149 | github.com/jcmturner/gofork v0.0.0-20190328161633-dc7c13fece03/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o= 150 | github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= 151 | github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= 152 | github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= 153 | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= 154 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 155 | github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7 h1:K//n/AqR5HjG3qxbrBCL4vJPW0MVFSs9CPK1OOJdRME= 156 | github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0= 157 | github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 158 | github.com/json-iterator/go v0.0.0-20180701071628-ab8a2e0c74be/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 159 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 160 | github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo= 161 | github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 162 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 163 | github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= 164 | github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= 165 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 166 | github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= 167 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 168 | github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= 169 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 170 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 171 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 172 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 173 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 174 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 175 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 176 | github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= 177 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 178 | github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 179 | github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= 180 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 181 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 182 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 183 | github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= 184 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 185 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 186 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 187 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 188 | github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 189 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 190 | github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= 191 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 192 | github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 193 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 194 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= 195 | github.com/nytlabs/gojsonexplode v0.0.0-20160201065013-0f3fe6bb573f h1:QprIMH86OebshvSxWUmDHn7w8SKAhyXAQyts7ZuOyWo= 196 | github.com/nytlabs/gojsonexplode v0.0.0-20160201065013-0f3fe6bb573f/go.mod h1:oVmnO+LczepuilmxAKaD0a5ItmJLmELEVVDOdU5HQA0= 197 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 198 | github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 199 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 200 | github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w= 201 | github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 202 | github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= 203 | github.com/onsi/gomega v0.0.0-20190113212917-5533ce8a0da3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 204 | github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo= 205 | github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 206 | github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= 207 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 208 | github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= 209 | github.com/pierrec/lz4 v0.0.0-20190327172049-315a67e90e41 h1:GeinFsrjWz97fAxVUEd748aV0cYL+I6k44gFJTCVvpU= 210 | github.com/pierrec/lz4 v0.0.0-20190327172049-315a67e90e41/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= 211 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 212 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 213 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 214 | github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= 215 | github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 216 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 217 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 218 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 219 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= 220 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 221 | github.com/prometheus/client_golang v1.1.0 h1:BQ53HtBmfOitExawJ6LokA4x8ov/z0SYYb0+HxJfRI8= 222 | github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= 223 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 224 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE= 225 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 226 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 227 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 228 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 229 | github.com/prometheus/common v0.6.0 h1:kRhiuYSXR3+uv2IbVbZhUxK5zVD/2pp3Gd2PpvPkpEo= 230 | github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= 231 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 232 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 233 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 234 | github.com/prometheus/procfs v0.0.3 h1:CTwfnzjQ+8dS6MhHHu4YswVAD99sL2wjPqP+VkURmKE= 235 | github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= 236 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= 237 | github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a h1:9ZKAASQSHhDYGoxY8uLVpewe1GDZ2vu2Tr/vTdVAkFQ= 238 | github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= 239 | github.com/rockset/rockset-go-client v0.6.0 h1:4eUjbiYWJcIqf/4h1k9p3V/qFDAtt7iEVG/FfXtp5/s= 240 | github.com/rockset/rockset-go-client v0.6.0/go.mod h1:DmrX6LsI3HPTorJaYGM6BJwTe5HhEqF9btehMmYqMLk= 241 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 242 | github.com/sethgrid/pester v0.0.0-20190127155807-68a33a018ad0 h1:X9XMOYjxEfAYSy3xK1DzO5dMkkWhs9E9UCcS1IERx2k= 243 | github.com/sethgrid/pester v0.0.0-20190127155807-68a33a018ad0/go.mod h1:Ad7IjTpvzZO8Fl0vh9AzQ+j/jYZfyp2diGwI8m5q+ns= 244 | github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo= 245 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 246 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 247 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 248 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 249 | github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= 250 | github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= 251 | github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= 252 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 253 | github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= 254 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 255 | github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 256 | github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 257 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 258 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 259 | github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU= 260 | github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= 261 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 262 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 263 | github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 264 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 265 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 266 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 267 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 268 | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= 269 | github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= 270 | github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= 271 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 272 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 273 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 274 | go.opencensus.io v0.21.0 h1:mU6zScU4U1YAFPHEHYk+3JC4SY7JxgkqS10ZOSyksNg= 275 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 276 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 277 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 278 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 279 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 280 | golang.org/x/crypto v0.0.0-20181025213731-e84da0312774/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 281 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 282 | golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5 h1:bselrhR0Or1vomJZC8ZIjWtbDmn9OYFLX5Ik9alpJpE= 283 | golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= 284 | golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f h1:R423Cnkcp5JABoeemiGEPlt9tHXFfw5kvc0yqlxRPWo= 285 | golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 286 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 287 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 288 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 289 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 290 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 291 | golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 292 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 293 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 294 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 295 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 296 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 297 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 298 | golang.org/x/net v0.0.0-20190206173232-65e2d4e15006/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 299 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 300 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 301 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 302 | golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 303 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 304 | golang.org/x/net v0.0.0-20190812203447-cdfb69ac37fc h1:gkKoSkUmnU6bpS/VhkuO27bzQeSA51uaEfbOW5dNb68= 305 | golang.org/x/net v0.0.0-20190812203447-cdfb69ac37fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 306 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 307 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 308 | golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a h1:tImsplftrFpALCYumobsd0K86vlAs/eXGFms2txfJfA= 309 | golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 310 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 311 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 312 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 313 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 314 | golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= 315 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 316 | golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 317 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 318 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 319 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 320 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 321 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 322 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 323 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 324 | golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 325 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 326 | golang.org/x/sys v0.0.0-20190529164535-6a60838ec259/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 327 | golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 328 | golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3 h1:4y9KwBHBgBNwDbtu44R5o1fdOCQUEXhbk/P4A9WmJq0= 329 | golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 330 | golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 331 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 332 | golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 333 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 334 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 335 | golang.org/x/time v0.0.0-20161028155119-f51c12702a4d/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 336 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= 337 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 338 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 339 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 340 | golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 341 | golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 342 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 343 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 344 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 345 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 346 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 347 | google.golang.org/api v0.5.0 h1:lj9SyhMzyoa38fgFF0oO2T6pjs5IzkLPKfVtxpyCRMM= 348 | google.golang.org/api v0.5.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 349 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 350 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 351 | google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c= 352 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 353 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 354 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 355 | google.golang.org/genproto v0.0.0-20190522204451-c2c4e71fbf69 h1:4rNOqY4ULrKzS6twXa619uQgI7h9PaVd4ZhjFQ7C5zs= 356 | google.golang.org/genproto v0.0.0-20190522204451-c2c4e71fbf69/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= 357 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 358 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 359 | google.golang.org/grpc v1.21.0 h1:G+97AoqBnmZIT91cLG/EkCoK9NSelj64P8bOHHNmGn0= 360 | google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 361 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 362 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 363 | gopkg.in/check.v1 v1.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 364 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 365 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 366 | gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= 367 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 368 | gopkg.in/inf.v0 v0.9.0 h1:3zYtXIO92bvsdS3ggAdA8Gb4Azj0YU+TVY1uGYNFA8o= 369 | gopkg.in/inf.v0 v0.9.0/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 370 | gopkg.in/jcmturner/aescts.v1 v1.0.1 h1:cVVZBK2b1zY26haWB4vbBiZrfFQnfbTVrE3xZq6hrEw= 371 | gopkg.in/jcmturner/aescts.v1 v1.0.1/go.mod h1:nsR8qBOg+OucoIW+WMhB3GspUQXq9XorLnQb9XtvcOo= 372 | gopkg.in/jcmturner/dnsutils.v1 v1.0.1 h1:cIuC1OLRGZrld+16ZJvvZxVJeKPsvd5eUIvxfoN5hSM= 373 | gopkg.in/jcmturner/dnsutils.v1 v1.0.1/go.mod h1:m3v+5svpVOhtFAP/wSz+yzh4Mc0Fg7eRhxkJMWSIz9Q= 374 | gopkg.in/jcmturner/goidentity.v3 v3.0.0 h1:1duIyWiTaYvVx3YX2CYtpJbUFd7/UuPYCfgXtQ3VTbI= 375 | gopkg.in/jcmturner/goidentity.v3 v3.0.0/go.mod h1:oG2kH0IvSYNIu80dVAyu/yoefjq1mNfM5bm88whjWx4= 376 | gopkg.in/jcmturner/gokrb5.v7 v7.2.3 h1:hHMV/yKPwMnJhPuPx7pH2Uw/3Qyf+thJYlisUc44010= 377 | gopkg.in/jcmturner/gokrb5.v7 v7.2.3/go.mod h1:l8VISx+WGYp+Fp7KRbsiUuXTTOnxIc3Tuvyavf11/WM= 378 | gopkg.in/jcmturner/rpc.v1 v1.1.0 h1:QHIUxTX1ISuAv9dD2wJ9HWQVuWDX/Zc0PfeC2tjc4rU= 379 | gopkg.in/jcmturner/rpc.v1 v1.1.0/go.mod h1:YIdkC4XfD6GXbzje11McwsDuOlZQSb9W4vfLvuNnlv8= 380 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 381 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 382 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 383 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 384 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 385 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 386 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 387 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 388 | k8s.io/api v0.0.0-20190620084959-7cf5895f2711/go.mod h1:TBhBqb1AWbBQbW3XRusr7n7E4v2+5ZY8r8sAMnyFC5A= 389 | k8s.io/api v0.0.0-20190814101207-0772a1bdf941 h1:Y8yEkyPyJstRyZRD2qAVXeFfgilYKxdxB8zjO0cb/XY= 390 | k8s.io/api v0.0.0-20190814101207-0772a1bdf941/go.mod h1:PBHnH5pdKCvv/+J4D3mpFXQCHFqtBbB1JvlkIVrfqrc= 391 | k8s.io/apimachinery v0.0.0-20190612205821-1799e75a0719/go.mod h1:I4A+glKBHiTgiEjQiCCQfCAIcIMFGt291SmsvcrFzJA= 392 | k8s.io/apimachinery v0.0.0-20190814100815-533d101be9a6 h1:g3kHsVIF7tLDtdP1RPw/Kuy3ANzPG5QPVwQ52qYkI0U= 393 | k8s.io/apimachinery v0.0.0-20190814100815-533d101be9a6/go.mod h1:MAmngDqHkEif0Kxdsl08wStgNTmHhat4DuIUyb4LbCc= 394 | k8s.io/client-go v12.0.0+incompatible h1:YlJxncpeVUC98/WMZKC3JZGk/OXQWCZjAB4Xr3B17RY= 395 | k8s.io/client-go v12.0.0+incompatible/go.mod h1:E95RaSlHr79aHaX0aGSwcPNfygDiPKOVXdmivCIZT0k= 396 | k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= 397 | k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= 398 | k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= 399 | k8s.io/klog v0.3.1/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= 400 | k8s.io/klog v0.4.0 h1:lCJCxf/LIowc2IGS9TPjWDyXY4nOmdGdfcwwDQCOURQ= 401 | k8s.io/klog v0.4.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= 402 | k8s.io/kube-openapi v0.0.0-20190228160746-b3a7cee44a30/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc= 403 | k8s.io/kube-openapi v0.0.0-20190709113604-33be087ad058 h1:di3XCwddOR9cWBNpfgXaskhh6cgJuwcK54rvtwUaC10= 404 | k8s.io/kube-openapi v0.0.0-20190709113604-33be087ad058/go.mod h1:nfDlWeOsu3pUf4yWGL+ERqohP4YsZcBJXWMK+gkzOA4= 405 | k8s.io/utils v0.0.0-20190221042446-c2654d5206da/go.mod h1:8k8uAuAQ0rXslZKaEWd0c3oVhZz7sSzSiPnVZayjIX0= 406 | k8s.io/utils v0.0.0-20190809000727-6c36bc71fc4a h1:uy5HAgt4Ha5rEMbhZA+aM1j2cq5LmR6LQ71EYC2sVH4= 407 | k8s.io/utils v0.0.0-20190809000727-6c36bc71fc4a/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= 408 | pack.ag/amqp v0.11.2/go.mod h1:4/cbmt4EJXSKlG6LCfWHoqmN0uFdy5i/+YFz+fTfhV4= 409 | pack.ag/amqp v0.12.1 h1:Q/0cS8lXAT2CFu6ngcOs6Rr9j2nD67/4X3hUA3u9s0s= 410 | pack.ag/amqp v0.12.1/go.mod h1:4/cbmt4EJXSKlG6LCfWHoqmN0uFdy5i/+YFz+fTfhV4= 411 | sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= 412 | sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= 413 | sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= 414 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Heptio Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "flag" 21 | "net/http" 22 | "os" 23 | "os/signal" 24 | "sync" 25 | "syscall" 26 | "time" 27 | 28 | "github.com/golang/glog" 29 | "github.com/prometheus/client_golang/prometheus/promhttp" 30 | "github.com/spf13/viper" 31 | 32 | "k8s.io/client-go/informers" 33 | "k8s.io/client-go/kubernetes" 34 | "k8s.io/client-go/rest" 35 | "k8s.io/client-go/tools/clientcmd" 36 | ) 37 | 38 | // addr tells us what address to have the Prometheus metrics listen on. 39 | var addr = flag.String("listen-address", ":8080", "The address to listen on for HTTP requests.") 40 | 41 | // setup a signal hander to gracefully exit 42 | func sigHandler() <-chan struct{} { 43 | stop := make(chan struct{}) 44 | go func() { 45 | c := make(chan os.Signal, 1) 46 | signal.Notify(c, 47 | syscall.SIGINT, // Ctrl+C 48 | syscall.SIGTERM, // Termination Request 49 | syscall.SIGSEGV, // FullDerp 50 | syscall.SIGABRT, // Abnormal termination 51 | syscall.SIGILL, // illegal instruction 52 | syscall.SIGFPE) // floating point - this is why we can't have nice things 53 | sig := <-c 54 | glog.Warningf("Signal (%v) Detected, Shutting Down", sig) 55 | close(stop) 56 | }() 57 | return stop 58 | } 59 | 60 | // loadConfig will parse input + config file and return a clientset 61 | func loadConfig() kubernetes.Interface { 62 | var config *rest.Config 63 | var err error 64 | 65 | flag.Parse() 66 | 67 | // leverages a file|(ConfigMap) 68 | // to be located at /etc/eventrouter/config 69 | viper.SetConfigType("json") 70 | viper.SetConfigName("config") 71 | viper.AddConfigPath("/etc/eventrouter/") 72 | viper.AddConfigPath(".") 73 | viper.SetDefault("kubeconfig", "") 74 | viper.SetDefault("sink", "glog") 75 | viper.SetDefault("resync-interval", time.Minute*30) 76 | viper.SetDefault("enable-prometheus", true) 77 | if err = viper.ReadInConfig(); err != nil { 78 | panic(err.Error()) 79 | } 80 | 81 | viper.BindEnv("kubeconfig") // Allows the KUBECONFIG env var to override where the kubeconfig is 82 | 83 | // Allow specifying a custom config file via the EVENTROUTER_CONFIG env var 84 | if forceCfg := os.Getenv("EVENTROUTER_CONFIG"); forceCfg != "" { 85 | viper.SetConfigFile(forceCfg) 86 | } 87 | kubeconfig := viper.GetString("kubeconfig") 88 | if len(kubeconfig) > 0 { 89 | config, err = clientcmd.BuildConfigFromFlags("", kubeconfig) 90 | } else { 91 | config, err = rest.InClusterConfig() 92 | } 93 | if err != nil { 94 | panic(err.Error()) 95 | } 96 | 97 | // creates the clientset from kubeconfig 98 | clientset, err := kubernetes.NewForConfig(config) 99 | if err != nil { 100 | panic(err.Error()) 101 | } 102 | return clientset 103 | } 104 | 105 | // main entry point of the program 106 | func main() { 107 | var wg sync.WaitGroup 108 | 109 | clientset := loadConfig() 110 | sharedInformers := informers.NewSharedInformerFactory(clientset, viper.GetDuration("resync-interval")) 111 | eventsInformer := sharedInformers.Core().V1().Events() 112 | 113 | // TODO: Support locking for HA https://github.com/kubernetes/kubernetes/pull/42666 114 | eventRouter := NewEventRouter(clientset, eventsInformer) 115 | stop := sigHandler() 116 | 117 | // Startup the http listener for Prometheus Metrics endpoint. 118 | if viper.GetBool("enable-prometheus") { 119 | go func() { 120 | glog.Info("Starting prometheus metrics.") 121 | http.Handle("/metrics", promhttp.Handler()) 122 | glog.Warning(http.ListenAndServe(*addr, nil)) 123 | }() 124 | } 125 | 126 | // Startup the EventRouter 127 | wg.Add(1) 128 | go func() { 129 | defer wg.Done() 130 | eventRouter.Run(stop) 131 | }() 132 | 133 | // Startup the Informer(s) 134 | glog.Infof("Starting shared Informer(s)") 135 | sharedInformers.Start(stop) 136 | wg.Wait() 137 | glog.Warningf("Exiting main()") 138 | os.Exit(1) 139 | } 140 | -------------------------------------------------------------------------------- /sinks/eventdata.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Heptio Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package sinks 18 | 19 | import ( 20 | "encoding/json" 21 | "fmt" 22 | "io" 23 | 24 | "github.com/crewjam/rfc5424" 25 | "github.com/json-iterator/go" 26 | "github.com/json-iterator/go/extra" 27 | "k8s.io/api/core/v1" 28 | 29 | "github.com/nytlabs/gojsonexplode" 30 | ) 31 | 32 | // EventData encodes an eventrouter event and previous event, with a verb for 33 | // whether the event is created or updated. 34 | type EventData struct { 35 | Verb string `json:"verb"` 36 | Event *v1.Event `json:"event"` 37 | OldEvent *v1.Event `json:"old_event,omitempty"` 38 | } 39 | 40 | // NewEventData constructs an EventData struct from an old and new event, 41 | // setting the verb accordingly 42 | func NewEventData(eNew *v1.Event, eOld *v1.Event) EventData { 43 | var eData EventData 44 | if eOld == nil { 45 | eData = EventData{ 46 | Verb: "ADDED", 47 | Event: eNew, 48 | } 49 | } else { 50 | eData = EventData{ 51 | Verb: "UPDATED", 52 | Event: eNew, 53 | OldEvent: eOld, 54 | } 55 | } 56 | 57 | return eData 58 | } 59 | 60 | // WriteRFC5424 writes the current event data to the given io.Writer using 61 | // RFC5424 (syslog over TCP) syntax. 62 | func (e *EventData) WriteRFC5424(w io.Writer) (int64, error) { 63 | var eJSONBytes []byte 64 | var err error 65 | if eJSONBytes, err = json.Marshal(e); err != nil { 66 | return 0, fmt.Errorf("failed to json serialize event: %v", err) 67 | } 68 | 69 | // Each message should look like an RFC5424 syslog message: 70 | // 71 | // 72 | // Note: There are some restrictions on length and character space for 73 | // Hostname and AppName, see 74 | // https://github.com/crewjam/rfc5424/blob/master/marshal.go#L90. There's no 75 | // attempt at trying to clean them up here because hostnames and component 76 | // names already adhere to this convention in practice. 77 | msg := rfc5424.Message{ 78 | Priority: rfc5424.Daemon, 79 | Timestamp: e.Event.LastTimestamp.Time, 80 | Hostname: e.Event.Source.Host, 81 | AppName: e.Event.Source.Component, 82 | Message: eJSONBytes, 83 | } 84 | 85 | return msg.WriteTo(w) 86 | } 87 | 88 | // WriteFlattenedJSON writes the json to the file in the below format 89 | // 1) Flattens the json into a not nested key:value 90 | // 2) Convert the json into snake format 91 | // Eg: {"event_involved_object_kind":"pod", "event_metadata_namespace":"kube-system"} 92 | func (e *EventData) WriteFlattenedJSON(w io.Writer) (int64, error) { 93 | var eJSONBytes []byte 94 | var err error 95 | extra.SetNamingStrategy(extra.LowerCaseWithUnderscores) 96 | if eJSONBytes, err = jsoniter.Marshal(e); err != nil { 97 | return 0, fmt.Errorf("failed to json serialize event: %v", err) 98 | } 99 | 100 | result, err := gojsonexplode.Explodejsonstr(string(eJSONBytes), "_") 101 | if err != nil { 102 | return 0, fmt.Errorf("failed to flatten json: %v", err) 103 | } 104 | 105 | written, err := w.Write([]byte(result)) 106 | return int64(written), err 107 | } 108 | -------------------------------------------------------------------------------- /sinks/eventhub.go: -------------------------------------------------------------------------------- 1 | package sinks 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | 7 | eventhub "github.com/Azure/azure-event-hubs-go/v2" 8 | "github.com/eapache/channels" 9 | "github.com/golang/glog" 10 | v1 "k8s.io/api/core/v1" 11 | ) 12 | 13 | const maxMessageSize = 1046528 14 | 15 | // EventHubSink sends events to an Azure Event Hub. 16 | type EventHubSink struct { 17 | hub *eventhub.Hub 18 | eventCh channels.Channel 19 | } 20 | 21 | // NewEventHubSink constructs a new EventHubSink given a event hub connection string 22 | // and buffering options. 23 | // 24 | // ``` 25 | // export EVENTHUB_RESOURCE_GROUP=eventrouter 26 | // export EVENTHUB_NAMESPACE=eventrouter-ns <<< 27 | // export EVENTHUB_NAME=eventrouter 28 | // export EVENTHUB_REGION=westus2 29 | // export EVENTHUB_RULE_NAME=eventrouter-send 30 | // 31 | // az group create -g ${EVENTHUB_RESOURCE_GROUP} -l ${EVENTHUB_REGION} 32 | // az eventhubs namespace create -g ${EVENTHUB_RESOURCE_GROUP} -n ${EVENTHUB_NAMESPACE} -l ${EVENTHUB_REGION} 33 | // az eventhubs eventhub create -g ${EVENTHUB_RESOURCE_GROUP} --namespace-name ${EVENTHUB_NAMESPACE} -n ${EVENTHUB_NAME} 34 | // az eventhubs eventhub authorization-rule create -g ${EVENTHUB_RESOURCE_GROUP} --namespace-name ${EVENTHUB_NAMESPACE} --eventhub-name ${EVENTHUB_NAME} -n ${EVENTHUB_RULE_NAME} --rights Send 35 | // export EVENTHUB_CONNECTION_STRING=$(az eventhubs eventhub authorization-rule keys list -g ${EVENTHUB_RESOURCE_GROUP} --namespace-name ${EVENTHUB_NAMESPACE} --eventhub-name ${EVENTHUB_NAME} -n ${EVENTHUB_RULE_NAME} | jq -r '.primaryConnectionString') 36 | // 37 | // cat yaml/eventrouter-azure.yaml | envsubst | kubectl apply -f 38 | // ``` 39 | // 40 | // connString expects the Azure Event Hub connection string format: 41 | // `Endpoint=sb://YOUR_ENDPOINT.servicebus.windows.net/;SharedAccessKeyName=YOUR_ACCESS_KEY_NAME;SharedAccessKey=YOUR_ACCESS_KEY;EntityPath=YOUR_EVENT_HUB_NAME` 42 | func NewEventHubSink(connString string, overflow bool, bufferSize int) (*EventHubSink, error) { 43 | hub, err := eventhub.NewHubFromConnectionString(connString) 44 | if err != nil { 45 | return nil, err 46 | } 47 | var eventCh channels.Channel 48 | if overflow { 49 | eventCh = channels.NewOverflowingChannel(channels.BufferCap(bufferSize)) 50 | } else { 51 | eventCh = channels.NewNativeChannel(channels.BufferCap(bufferSize)) 52 | } 53 | 54 | return &EventHubSink{hub: hub, eventCh: eventCh}, nil 55 | } 56 | 57 | // UpdateEvents implements the EventSinkInterface. It really just writes the 58 | // event data to the event OverflowingChannel, which should never block. 59 | // Messages that are buffered beyond the bufferSize specified for this EventHubSink 60 | // are discarded. 61 | func (h *EventHubSink) UpdateEvents(eNew *v1.Event, eOld *v1.Event) { 62 | h.eventCh.In() <- NewEventData(eNew, eOld) 63 | } 64 | 65 | // Run sits in a loop, waiting for data to come in through h.eventCh, 66 | // and forwarding them to the event hub sink. If multiple events have happened 67 | // between loop iterations, it puts all of them in one request instead of 68 | // making a single request per event. 69 | func (h *EventHubSink) Run(stopCh <-chan bool) { 70 | loop: 71 | for { 72 | select { 73 | case e := <-h.eventCh.Out(): 74 | var evt EventData 75 | var ok bool 76 | evt, ok = e.(EventData) 77 | if !ok { 78 | glog.Warningf("Invalid type sent through event channel: %T", e) 79 | continue loop 80 | } 81 | 82 | // Start with just this event... 83 | arr := []EventData{evt} 84 | 85 | // Consume all buffered events into an array, in case more have been written 86 | // since we last forwarded them 87 | numEvents := h.eventCh.Len() 88 | for i := 0; i < numEvents; i++ { 89 | e := <-h.eventCh.Out() 90 | if evt, ok = e.(EventData); ok { 91 | arr = append(arr, evt) 92 | } else { 93 | glog.Warningf("Invalid type sent through event channel: %T", e) 94 | } 95 | } 96 | 97 | h.drainEvents(arr) 98 | case <-stopCh: 99 | break loop 100 | } 101 | } 102 | } 103 | 104 | // drainEvents takes an array of event data and sends it to the receiving event hub. 105 | func (h *EventHubSink) drainEvents(events []EventData) { 106 | var messageSize int 107 | var evts []*eventhub.Event 108 | for _, evt := range events { 109 | eJSONBytes, err := json.Marshal(evt) 110 | if err != nil { 111 | glog.Warningf("Failed to flatten json: %v", err) 112 | return 113 | } 114 | glog.V(4).Infof("%s", string(eJSONBytes)) 115 | messageSize += len(eJSONBytes) 116 | if messageSize > maxMessageSize { 117 | h.sendBatch(evts) 118 | evts = nil 119 | messageSize = 0 120 | } 121 | evts = append(evts, eventhub.NewEvent(eJSONBytes)) 122 | } 123 | h.sendBatch(evts) 124 | } 125 | 126 | func (h *EventHubSink) sendBatch(evts []*eventhub.Event) { 127 | if err := h.hub.SendBatch(context.Background(), eventhub.NewEventBatchIterator(evts...)); err != nil { 128 | glog.Errorf("Failed to send batch of %d: %v", len(evts), err) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /sinks/glogsink.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Heptio Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package sinks 18 | 19 | import ( 20 | "encoding/json" 21 | 22 | "github.com/golang/glog" 23 | "k8s.io/api/core/v1" 24 | ) 25 | 26 | // GlogSink is the most basic sink 27 | // Useful when you already have ELK/EFK Stack 28 | type GlogSink struct { 29 | // TODO: create a channel and buffer for scaling 30 | } 31 | 32 | // NewGlogSink will create a new 33 | func NewGlogSink() EventSinkInterface { 34 | return &GlogSink{} 35 | } 36 | 37 | // UpdateEvents implements the EventSinkInterface 38 | func (gs *GlogSink) UpdateEvents(eNew *v1.Event, eOld *v1.Event) { 39 | eData := NewEventData(eNew, eOld) 40 | 41 | if eJSONBytes, err := json.Marshal(eData); err == nil { 42 | glog.Info(string(eJSONBytes)) 43 | } else { 44 | glog.Warningf("Failed to json serialize event: %v", err) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /sinks/httpsink.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Heptio Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package sinks 18 | 19 | import ( 20 | "bytes" 21 | "net/http" 22 | 23 | "github.com/eapache/channels" 24 | "github.com/golang/glog" 25 | "github.com/sethgrid/pester" 26 | 27 | "k8s.io/api/core/v1" 28 | ) 29 | 30 | /* 31 | The HTTP sink is a sink that sends events over HTTP using RFC5424 (syslog) 32 | compatible messages. It establishes an HTTP connection with the remote 33 | endpoint, sending messages as individual lines with the RFC5424 syntax: 34 | 35 | 36 | 37 | This is compatible with the protocol used by Heroku's Logplex: 38 | 39 | https://github.com/heroku/logplex/blob/master/doc/README.http_drains.md 40 | 41 | Many events may be coalesced into one request if they happen faster than we 42 | can send them, if not, a single HTTP request is made for each event. 43 | (Hopefully in a single keep-alive http connection, which is go's default.) 44 | 45 | But with the payload of the messages being a serialized JSON object 46 | containing the kubernetes v1.Event. 47 | */ 48 | 49 | // HTTPSink wraps an HTTP endpoint that messages should be sent to. 50 | type HTTPSink struct { 51 | SinkURL string 52 | 53 | eventCh channels.Channel 54 | httpClient *pester.Client 55 | bodyBuf *bytes.Buffer 56 | } 57 | 58 | // NewHTTPSink constructs a new HTTPSink given a sink URL and buffer size 59 | func NewHTTPSink(sinkURL string, overflow bool, bufferSize int) *HTTPSink { 60 | h := &HTTPSink{ 61 | SinkURL: sinkURL, 62 | } 63 | 64 | if overflow { 65 | h.eventCh = channels.NewOverflowingChannel(channels.BufferCap(bufferSize)) 66 | } else { 67 | h.eventCh = channels.NewNativeChannel(channels.BufferCap(bufferSize)) 68 | } 69 | 70 | h.httpClient = pester.New() 71 | h.httpClient.Backoff = pester.ExponentialJitterBackoff 72 | h.httpClient.MaxRetries = 10 73 | // Let the body buffer be 4096 bytes at the start. It will be grown if 74 | // necessary. 75 | h.bodyBuf = bytes.NewBuffer(make([]byte, 0, 4096)) 76 | 77 | return h 78 | } 79 | 80 | // UpdateEvents implements the EventSinkInterface. It really just writes the 81 | // event data to the event OverflowingChannel, which should never block. 82 | // Messages that are buffered beyond the bufferSize specified for this HTTPSink 83 | // are discarded. 84 | func (h *HTTPSink) UpdateEvents(eNew *v1.Event, eOld *v1.Event) { 85 | h.eventCh.In() <- NewEventData(eNew, eOld) 86 | } 87 | 88 | // Run sits in a loop, waiting for data to come in through h.eventCh, 89 | // and forwarding them to the HTTP sink. If multiple events have happened 90 | // between loop iterations, it puts all of them in one request instead of 91 | // making a single request per event. 92 | func (h *HTTPSink) Run(stopCh <-chan bool) { 93 | loop: 94 | for { 95 | select { 96 | case e := <-h.eventCh.Out(): 97 | var evt EventData 98 | var ok bool 99 | if evt, ok = e.(EventData); !ok { 100 | glog.Warningf("Invalid type sent through event channel: %T", e) 101 | continue loop 102 | } 103 | 104 | // Start with just this event... 105 | arr := []EventData{evt} 106 | 107 | // Consume all buffered events into an array, in case more have been written 108 | // since we last forwarded them 109 | numEvents := h.eventCh.Len() 110 | for i := 0; i < numEvents; i++ { 111 | e := <-h.eventCh.Out() 112 | if evt, ok = e.(EventData); ok { 113 | arr = append(arr, evt) 114 | } else { 115 | glog.Warningf("Invalid type sent through event channel: %T", e) 116 | } 117 | } 118 | 119 | h.drainEvents(arr) 120 | case <-stopCh: 121 | break loop 122 | } 123 | } 124 | } 125 | 126 | // drainEvents takes an array of event data and sends it to the receiving HTTP 127 | // server. This function is *NOT* re-entrant: it re-uses the same body buffer 128 | // for each call, truncating it each time to avoid extra memory allocations. 129 | func (h *HTTPSink) drainEvents(events []EventData) { 130 | // Reuse the body buffer for each request 131 | h.bodyBuf.Truncate(0) 132 | 133 | var written int64 134 | for _, evt := range events { 135 | w, err := evt.WriteRFC5424(h.bodyBuf) 136 | written += w 137 | if err != nil { 138 | glog.Warningf("Could not write to event request body (wrote %v) bytes: %v", written, err) 139 | return 140 | } 141 | 142 | h.bodyBuf.Write([]byte{'\n'}) 143 | written++ 144 | } 145 | 146 | req, err := http.NewRequest("POST", h.SinkURL, h.bodyBuf) 147 | if err != nil { 148 | glog.Warningf(err.Error()) 149 | return 150 | } 151 | 152 | resp, err := h.httpClient.Do(req) 153 | if err != nil { 154 | glog.Warningf(err.Error()) 155 | return 156 | } 157 | 158 | if resp.StatusCode < 200 || resp.StatusCode > 299 { 159 | glog.Warningf("Got HTTP code %v from %v", resp.StatusCode, h.SinkURL) 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /sinks/httpsink_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Heptio Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package sinks 18 | 19 | import ( 20 | "bytes" 21 | "fmt" 22 | "io" 23 | "net/http" 24 | "net/http/httptest" 25 | "strconv" 26 | "strings" 27 | "testing" 28 | "time" 29 | 30 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 31 | "k8s.io/client-go/kubernetes/scheme" 32 | ref "k8s.io/client-go/tools/reference" 33 | 34 | "k8s.io/api/core/v1" 35 | ) 36 | 37 | func TestUpdateEvents(t *testing.T) { 38 | stopCh := make(chan bool, 1) 39 | doneCh := make(chan bool, 1) 40 | 41 | got := bytes.NewBuffer(nil) 42 | seenRequests := make([]*http.Request, 0) 43 | mockStatus := http.StatusOK 44 | 45 | // Make a test server to send stuff too... it just copies its input to the 46 | // `got` buffer and records the request. 47 | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 48 | seenRequests = append(seenRequests, r) 49 | io.Copy(got, r.Body) 50 | w.WriteHeader(mockStatus) 51 | })) 52 | defer srv.Close() 53 | 54 | testPod := &v1.Pod{ 55 | TypeMeta: metav1.TypeMeta{ 56 | Kind: "Pod", 57 | }, 58 | ObjectMeta: metav1.ObjectMeta{ 59 | SelfLink: "/api/version/pods/foo", 60 | Name: "foo", 61 | Namespace: "baz", 62 | UID: "bar", 63 | }, 64 | Spec: v1.PodSpec{}, 65 | } 66 | podRef, err := ref.GetReference(scheme.Scheme, testPod) 67 | if err != nil { 68 | t.Fatalf(err.Error()) 69 | } 70 | 71 | evt := makeFakeEvent(podRef, v1.EventTypeWarning, "CreateInCluster", "Fake pod creation event") 72 | 73 | // 1. Try with a synchronous channel 74 | sink := NewHTTPSink(srv.URL, false, 0) 75 | go func() { 76 | sink.Run(stopCh) 77 | doneCh <- true 78 | }() 79 | 80 | // Send the event 81 | sink.UpdateEvents(evt, nil) 82 | stopCh <- true 83 | <-doneCh 84 | 85 | if got.Len() == 0 { 86 | t.Errorf("Sent logs but didn't read any back") 87 | } 88 | 89 | // 2. Try with the server returning 500's, test retries 90 | got.Truncate(0) 91 | seenRequests = make([]*http.Request, 0) 92 | sink = NewHTTPSink(srv.URL, false, 10) 93 | 94 | go func() { 95 | sink.Run(stopCh) 96 | doneCh <- true 97 | }() 98 | 99 | // Send the event, sleep to ensure the request is attempted 100 | mockStatus = http.StatusInternalServerError 101 | sink.UpdateEvents(evt, nil) 102 | // TODO(SLEEP): this can result in flakes if the events aren't sent yet. 103 | time.Sleep(100 * time.Millisecond) 104 | mockStatus = http.StatusOK 105 | 106 | // Start the server, then send the stop chan. Since it's synchronous, the HTTP 107 | // client should still be trying to retry, so it won't read from the stop chan 108 | // again until it's finished retrying 109 | stopCh <- true 110 | <-doneCh 111 | 112 | if got.Len() == 0 { 113 | t.Errorf("Sent logs but didn't read any back. HTTP error log: %v", sink.httpClient.ErrLog) 114 | } 115 | if len(seenRequests) < 2 { 116 | t.Errorf("Tried to simulate server errors for retry, more than one request should have been sent") 117 | } 118 | 119 | // 3. Try with an overflowing channel, write a bunch of events out, only 10 120 | // should be consumed (the rest discarded, since we're not running the 121 | // processing loop yet.) 122 | numExpected := 10 123 | got.Truncate(0) 124 | seenRequests = make([]*http.Request, 0) 125 | sink = NewHTTPSink(srv.URL, true, numExpected) 126 | 127 | for i := 0; i < 1000; i++ { 128 | evt.Message = "msg " + strconv.Itoa(i) 129 | sink.UpdateEvents(evt, nil) 130 | } 131 | 132 | go func() { 133 | sink.Run(stopCh) 134 | doneCh <- true 135 | }() 136 | 137 | // TODO(SLEEP): Let the events go through (yes, sleeping is lame but there's 138 | // no easy way to synchronize this since the code is supposed to be 139 | // non-blocking.) 140 | time.Sleep(100 * time.Millisecond) 141 | 142 | stopCh <- true 143 | <-doneCh 144 | 145 | newlines := strings.Count(got.String(), "\n") 146 | if newlines != numExpected { 147 | t.Errorf("Got wrong number of lines back (got %v, expected %v)", newlines, numExpected) 148 | } 149 | if len(seenRequests) > 1 { 150 | t.Errorf("Pending logs should have been coalesced into one request, but got %v requests", len(seenRequests)) 151 | } 152 | } 153 | 154 | func makeFakeEvent(ref *v1.ObjectReference, eventtype, reason, message string) *v1.Event { 155 | tm := metav1.Time{ 156 | Time: time.Now(), 157 | } 158 | return &v1.Event{ 159 | ObjectMeta: metav1.ObjectMeta{ 160 | Name: fmt.Sprintf("%v.%x", ref.Name, tm.UnixNano()), 161 | Namespace: ref.Namespace, 162 | }, 163 | InvolvedObject: *ref, 164 | Reason: reason, 165 | Message: message, 166 | FirstTimestamp: tm, 167 | LastTimestamp: tm, 168 | Count: 1, 169 | Type: eventtype, 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /sinks/influxdb.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 The Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package sinks 18 | 19 | import ( 20 | "encoding/json" 21 | "fmt" 22 | "net/url" 23 | "strings" 24 | "sync" 25 | "time" 26 | 27 | "github.com/golang/glog" 28 | influxdb "github.com/influxdata/influxdb/client" 29 | 30 | "k8s.io/api/core/v1" 31 | ) 32 | 33 | var ( 34 | LabelPodId = LabelDescriptor{ 35 | Key: "pod_id", 36 | Description: "The unique ID of the pod", 37 | } 38 | 39 | LabelPodName = LabelDescriptor{ 40 | Key: "pod_name", 41 | Description: "The name of the pod", 42 | } 43 | 44 | LabelNamespaceName = LabelDescriptor{ 45 | Key: "namespace_name", 46 | Description: "The name of the namespace", 47 | } 48 | 49 | LabelHostname = LabelDescriptor{ 50 | Key: "hostname", 51 | Description: "Hostname where the container ran", 52 | } 53 | ) 54 | 55 | const ( 56 | eventMeasurementName = "k8s_events" 57 | // Event special tags 58 | eventUID = "uid" 59 | // Value Field name 60 | valueField = "value" 61 | // Event special tags 62 | dbNotFoundError = "database not found" 63 | ) 64 | 65 | type LabelDescriptor struct { 66 | // Key to use for the label. 67 | Key string `json:"key,omitempty"` 68 | 69 | // Description of the label. 70 | Description string `json:"description,omitempty"` 71 | } 72 | 73 | type InfluxDBSink struct { 74 | config InfluxdbConfig 75 | client *influxdb.Client 76 | sync.RWMutex 77 | dbExists bool 78 | } 79 | 80 | type InfluxdbConfig struct { 81 | User string 82 | Password string 83 | Secure bool 84 | Host string 85 | DbName string 86 | WithFields bool 87 | InsecureSsl bool 88 | RetentionPolicy string 89 | ClusterName string 90 | DisableCounterMetrics bool 91 | Concurrency int 92 | } 93 | 94 | // Returns a thread-safe implementation of EventSinkInterface for InfluxDB. 95 | func NewInfuxdbSink(cfg InfluxdbConfig) (EventSinkInterface, error) { 96 | client, err := newClient(cfg) 97 | if err != nil { 98 | return nil, err 99 | } 100 | 101 | return &InfluxDBSink{ 102 | config: cfg, 103 | client: client, 104 | dbExists: false, 105 | }, nil 106 | } 107 | 108 | func newClient(c InfluxdbConfig) (*influxdb.Client, error) { 109 | url := &url.URL{ 110 | Scheme: "http", 111 | Host: c.Host, 112 | } 113 | 114 | if c.Secure { 115 | url.Scheme = "https" 116 | } 117 | 118 | iConfig := &influxdb.Config{ 119 | URL: *url, 120 | Username: c.User, 121 | Password: c.Password, 122 | UnsafeSsl: c.InsecureSsl, 123 | } 124 | 125 | client, err := influxdb.NewClient(*iConfig) 126 | if err != nil { 127 | return nil, err 128 | } 129 | 130 | if _, _, err := client.Ping(); err != nil { 131 | return nil, fmt.Errorf("failed to ping influxDB server at %q - %v", c.Host, err) 132 | } 133 | 134 | return client, nil 135 | } 136 | 137 | func (sink *InfluxDBSink) UpdateEvents(eNew *v1.Event, eOld *v1.Event) { 138 | sink.Lock() 139 | defer sink.Unlock() 140 | 141 | var point *influxdb.Point 142 | var err error 143 | if sink.config.WithFields { 144 | point, err = eventToPointWithFields(eNew) 145 | } else { 146 | point, err = eventToPoint(eNew) 147 | } 148 | if err != nil { 149 | glog.Warningf("Failed to convert event to point: %v", err) 150 | } 151 | 152 | point.Tags["cluster_name"] = sink.config.ClusterName 153 | 154 | dataPoints := make([]influxdb.Point, 0, 10) 155 | dataPoints = append(dataPoints, *point) 156 | sink.sendData(dataPoints) 157 | } 158 | 159 | // Generate point value for event 160 | func getEventValue(event *v1.Event) (string, error) { 161 | bytes, err := json.MarshalIndent(event, "", " ") 162 | if err != nil { 163 | return "", err 164 | } 165 | return string(bytes), nil 166 | } 167 | 168 | func eventToPointWithFields(event *v1.Event) (*influxdb.Point, error) { 169 | point := influxdb.Point{ 170 | Measurement: "events", 171 | Time: event.LastTimestamp.Time.UTC(), 172 | Fields: map[string]interface{}{ 173 | "message": event.Message, 174 | }, 175 | Tags: map[string]string{ 176 | eventUID: string(event.UID), 177 | }, 178 | } 179 | if event.InvolvedObject.Kind == "Pod" { 180 | point.Tags[LabelPodId.Key] = string(event.InvolvedObject.UID) 181 | } 182 | point.Tags["object_name"] = event.InvolvedObject.Name 183 | point.Tags["type"] = event.Type 184 | point.Tags["kind"] = event.InvolvedObject.Kind 185 | point.Tags["component"] = event.Source.Component 186 | point.Tags["reason"] = event.Reason 187 | point.Tags[LabelNamespaceName.Key] = event.Namespace 188 | point.Tags[LabelHostname.Key] = event.Source.Host 189 | return &point, nil 190 | } 191 | 192 | func eventToPoint(event *v1.Event) (*influxdb.Point, error) { 193 | value, err := getEventValue(event) 194 | if err != nil { 195 | return nil, err 196 | } 197 | 198 | point := influxdb.Point{ 199 | Measurement: eventMeasurementName, 200 | Time: event.LastTimestamp.Time.UTC(), 201 | Fields: map[string]interface{}{ 202 | valueField: value, 203 | }, 204 | Tags: map[string]string{ 205 | eventUID: string(event.UID), 206 | }, 207 | } 208 | if event.InvolvedObject.Kind == "Pod" { 209 | point.Tags[LabelPodId.Key] = string(event.InvolvedObject.UID) 210 | point.Tags[LabelPodName.Key] = event.InvolvedObject.Name 211 | } 212 | point.Tags[LabelHostname.Key] = event.Source.Host 213 | return &point, nil 214 | } 215 | 216 | func (sink *InfluxDBSink) sendData(dataPoints []influxdb.Point) { 217 | if err := sink.createDatabase(); err != nil { 218 | glog.Errorf("Failed to create influxdb: %v", err) 219 | return 220 | } 221 | bp := influxdb.BatchPoints{ 222 | Points: dataPoints, 223 | Database: sink.config.DbName, 224 | RetentionPolicy: "default", 225 | } 226 | 227 | start := time.Now() 228 | if _, err := sink.client.Write(bp); err != nil { 229 | glog.Errorf("InfluxDB write failed: %v", err) 230 | if strings.Contains(err.Error(), dbNotFoundError) { 231 | sink.resetConnection() 232 | } else if _, _, err := sink.client.Ping(); err != nil { 233 | glog.Errorf("InfluxDB ping failed: %v", err) 234 | sink.resetConnection() 235 | } 236 | } 237 | end := time.Now() 238 | glog.V(4).Infof("Exported %d data to influxDB in %s", len(dataPoints), end.Sub(start)) 239 | } 240 | 241 | func (sink *InfluxDBSink) resetConnection() { 242 | glog.Infof("Influxdb connection reset") 243 | sink.dbExists = false 244 | sink.client = nil 245 | sink.config = InfluxdbConfig{} 246 | } 247 | 248 | func (sink *InfluxDBSink) createDatabase() error { 249 | if sink.client == nil { 250 | client, err := newClient(sink.config) 251 | if err != nil { 252 | return err 253 | } 254 | sink.client = client 255 | } 256 | 257 | if sink.dbExists { 258 | return nil 259 | } 260 | 261 | q := influxdb.Query{ 262 | Command: fmt.Sprintf(`CREATE DATABASE %s WITH NAME "default"`, sink.config.DbName), 263 | } 264 | 265 | if resp, err := sink.client.Query(q); err != nil { 266 | // We want to return error only if it is not "already exists" error. 267 | if !(resp != nil && resp.Err != nil && strings.Contains(resp.Err.Error(), "existing policy")) { 268 | err := sink.createRetentionPolicy() 269 | if err != nil { 270 | return err 271 | } 272 | } 273 | } 274 | 275 | sink.dbExists = true 276 | glog.Infof("Created database %q on influxDB server at %q", sink.config.DbName, sink.config.Host) 277 | return nil 278 | } 279 | 280 | func (sink *InfluxDBSink) createRetentionPolicy() error { 281 | q := influxdb.Query{ 282 | Command: fmt.Sprintf(`CREATE RETENTION POLICY "default" ON %s DURATION 0d REPLICATION 1 DEFAULT`, sink.config.DbName), 283 | } 284 | 285 | if resp, err := sink.client.Query(q); err != nil { 286 | if !(resp != nil && resp.Err != nil && strings.Contains(resp.Err.Error(), "already exists")) { 287 | return fmt.Errorf("Retention policy creation failed: %v", err) 288 | } 289 | } 290 | 291 | glog.Infof("Created database %q on influxDB server at %q", sink.config.DbName, sink.config.Host) 292 | return nil 293 | } 294 | -------------------------------------------------------------------------------- /sinks/interfaces.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Heptio Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package sinks 18 | 19 | import ( 20 | "errors" 21 | 22 | "github.com/golang/glog" 23 | "github.com/spf13/viper" 24 | v1 "k8s.io/api/core/v1" 25 | ) 26 | 27 | // EventSinkInterface is the interface used to shunt events 28 | type EventSinkInterface interface { 29 | UpdateEvents(eNew *v1.Event, eOld *v1.Event) 30 | } 31 | 32 | // ManufactureSink will manufacture a sink according to viper configs 33 | // TODO: Determine if it should return an array of sinks 34 | func ManufactureSink() (e EventSinkInterface) { 35 | s := viper.GetString("sink") 36 | glog.Infof("Sink is [%v]", s) 37 | switch s { 38 | case "glog": 39 | e = NewGlogSink() 40 | case "stdout": 41 | viper.SetDefault("stdoutJSONNamespace", "") 42 | stdoutNamespace := viper.GetString("stdoutJSONNamespace") 43 | e = NewStdoutSink(stdoutNamespace) 44 | case "http": 45 | url := viper.GetString("httpSinkUrl") 46 | if url == "" { 47 | panic("http sink specified but no httpSinkUrl") 48 | } 49 | 50 | // By default we buffer up to 1500 events, and drop messages if more than 51 | // 1500 have come in without getting consumed 52 | viper.SetDefault("httpSinkBufferSize", 1500) 53 | viper.SetDefault("httpSinkDiscardMessages", true) 54 | 55 | bufferSize := viper.GetInt("httpSinkBufferSize") 56 | overflow := viper.GetBool("httpSinkDiscardMessages") 57 | 58 | h := NewHTTPSink(url, overflow, bufferSize) 59 | go h.Run(make(chan bool)) 60 | return h 61 | case "kafka": 62 | viper.SetDefault("kafkaBrokers", []string{"kafka:9092"}) 63 | viper.SetDefault("kafkaTopic", "eventrouter") 64 | viper.SetDefault("kafkaAsync", true) 65 | viper.SetDefault("kafkaRetryMax", 5) 66 | viper.SetDefault("kafkaSaslUser", "") 67 | viper.SetDefault("kafkaSaslPwd", "") 68 | 69 | brokers := viper.GetStringSlice("kafkaBrokers") 70 | topic := viper.GetString("kafkaTopic") 71 | async := viper.GetBool("kakfkaAsync") 72 | retryMax := viper.GetInt("kafkaRetryMax") 73 | saslUser := viper.GetString("kafkaSaslUser") 74 | saslPwd := viper.GetString("kafkaSaslPwd") 75 | 76 | e, err := NewKafkaSink(brokers, topic, async, retryMax, saslUser, saslPwd) 77 | if err != nil { 78 | panic(err.Error()) 79 | } 80 | return e 81 | case "s3sink": 82 | accessKeyID := viper.GetString("s3SinkAccessKeyID") 83 | if accessKeyID == "" { 84 | panic("s3 sink specified but s3SinkAccessKeyID not specified") 85 | } 86 | 87 | secretAccessKey := viper.GetString("s3SinkSecretAccessKey") 88 | if secretAccessKey == "" { 89 | panic("s3 sink specified but s3SinkSecretAccessKey not specified") 90 | } 91 | 92 | region := viper.GetString("s3SinkRegion") 93 | if region == "" { 94 | panic("s3 sink specified but s3SinkRegion not specified") 95 | } 96 | 97 | bucket := viper.GetString("s3SinkBucket") 98 | if bucket == "" { 99 | panic("s3 sink specified but s3SinkBucket not specified") 100 | } 101 | 102 | bucketDir := viper.GetString("s3SinkBucketDir") 103 | if bucketDir == "" { 104 | panic("s3 sink specified but s3SinkBucketDir not specified") 105 | } 106 | 107 | // By default the json is pushed to s3 in not flatenned rfc5424 write format 108 | // The option to write to s3 is in the flattened json format which will help in 109 | // using the data in redshift with least effort 110 | viper.SetDefault("s3SinkOutputFormat", "rfc5424") 111 | outputFormat := viper.GetString("s3SinkOutputFormat") 112 | if outputFormat != "rfc5424" && outputFormat != "flatjson" { 113 | panic("s3 sink specified, but incorrect s3SinkOutputFormat specifed. Supported formats are: rfc5424 (default) and flatjson") 114 | } 115 | 116 | // By default we buffer up to 1500 events, and drop messages if more than 117 | // 1500 have come in without getting consumed 118 | viper.SetDefault("s3SinkBufferSize", 1500) 119 | viper.SetDefault("s3SinkDiscardMessages", true) 120 | 121 | viper.SetDefault("s3SinkUploadInterval", 120) 122 | uploadInterval := viper.GetInt("s3SinkUploadInterval") 123 | 124 | bufferSize := viper.GetInt("s3SinkBufferSize") 125 | overflow := viper.GetBool("s3SinkDiscardMessages") 126 | 127 | s, err := NewS3Sink(accessKeyID, secretAccessKey, region, bucket, bucketDir, uploadInterval, overflow, bufferSize, outputFormat) 128 | if err != nil { 129 | panic(err.Error()) 130 | } 131 | 132 | go s.Run(make(chan bool)) 133 | return s 134 | case "influxdb": 135 | host := viper.GetString("influxdbHost") 136 | if host == "" { 137 | panic("influxdb sink specified but influxdbHost not specified") 138 | } 139 | 140 | username := viper.GetString("influxdbUsername") 141 | if username == "" { 142 | panic("influxdb sink specified but influxdbUsername not specified") 143 | } 144 | 145 | password := viper.GetString("influxdbPassword") 146 | if password == "" { 147 | panic("influxdb sink specified but influxdbPassword not specified") 148 | } 149 | 150 | viper.SetDefault("influxdbName", "k8s") 151 | viper.SetDefault("influxdbSecure", false) 152 | viper.SetDefault("influxdbWithFields", false) 153 | viper.SetDefault("influxdbInsecureSsl", false) 154 | viper.SetDefault("influxdbRetentionPolicy", "0") 155 | viper.SetDefault("influxdbClusterName", "default") 156 | viper.SetDefault("influxdbDisableCounterMetrics", false) 157 | viper.SetDefault("influxdbConcurrency", 1) 158 | 159 | dbName := viper.GetString("influxdbName") 160 | secure := viper.GetBool("influxdbSecure") 161 | withFields := viper.GetBool("influxdbWithFields") 162 | insecureSsl := viper.GetBool("influxdbInsecureSsl") 163 | retentionPolicy := viper.GetString("influxdbRetentionPolicy") 164 | cluterName := viper.GetString("influxdbClusterName") 165 | disableCounterMetrics := viper.GetBool("influxdbDisableCounterMetrics") 166 | concurrency := viper.GetInt("influxdbConcurrency") 167 | 168 | cfg := InfluxdbConfig{ 169 | User: username, 170 | Password: password, 171 | Secure: secure, 172 | Host: host, 173 | DbName: dbName, 174 | WithFields: withFields, 175 | InsecureSsl: insecureSsl, 176 | RetentionPolicy: retentionPolicy, 177 | ClusterName: cluterName, 178 | DisableCounterMetrics: disableCounterMetrics, 179 | Concurrency: concurrency, 180 | } 181 | 182 | influx, err := NewInfuxdbSink(cfg) 183 | if err != nil { 184 | panic(err.Error()) 185 | } 186 | return influx 187 | case "rockset": 188 | rocksetAPIKey := viper.GetString("rocksetAPIKey") 189 | if rocksetAPIKey == "" { 190 | panic("Rockset sink specified but rocksetAPIKey not specified") 191 | } 192 | 193 | rocksetCollectionName := viper.GetString("rocksetCollectionName") 194 | if rocksetCollectionName == "" { 195 | panic("Rockset sink specified but rocksetCollectionName not specified") 196 | } 197 | rocksetWorkspaceName := viper.GetString("rocksetWorkspaceName") 198 | if rocksetCollectionName == "" { 199 | panic("Rockset sink specified but rocksetWorkspaceName not specified") 200 | } 201 | e = NewRocksetSink(rocksetAPIKey, rocksetCollectionName, rocksetWorkspaceName) 202 | case "eventhub": 203 | connString := viper.GetString("eventHubConnectionString") 204 | if connString == "" { 205 | panic("eventhub sink specified but eventHubConnectionString not specified") 206 | } 207 | // By default we buffer up to 1500 events, and drop messages if more than 208 | // 1500 have come in without getting consumed 209 | viper.SetDefault("eventHubSinkBufferSize", 1500) 210 | viper.SetDefault("eventHubSinkDiscardMessages", true) 211 | 212 | bufferSize := viper.GetInt("eventHubSinkBufferSize") 213 | overflow := viper.GetBool("eventHubSinkDiscardMessages") 214 | eh, err := NewEventHubSink(connString, overflow, bufferSize) 215 | if err != nil { 216 | panic(err.Error()) 217 | } 218 | go eh.Run(make(chan bool)) 219 | return eh 220 | // case "logfile" 221 | default: 222 | err := errors.New("Invalid Sink Specified") 223 | panic(err.Error()) 224 | } 225 | return e 226 | } 227 | -------------------------------------------------------------------------------- /sinks/kafkasink.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 The Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package sinks 18 | 19 | import ( 20 | "encoding/json" 21 | "github.com/Shopify/sarama" 22 | "github.com/golang/glog" 23 | "k8s.io/api/core/v1" 24 | ) 25 | 26 | // KafkaSink implements the EventSinkInterface 27 | type KafkaSink struct { 28 | Topic string 29 | producer interface{} 30 | } 31 | 32 | // NewKafkaSinkSink will create a new KafkaSink with default options, returned as an EventSinkInterface 33 | func NewKafkaSink(brokers []string, topic string, async bool, retryMax int, saslUser string, saslPwd string) (EventSinkInterface, error) { 34 | 35 | p, err := sinkFactory(brokers, async, retryMax, saslUser, saslPwd) 36 | 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | return &KafkaSink{ 42 | Topic: topic, 43 | producer: p, 44 | }, err 45 | } 46 | 47 | func sinkFactory(brokers []string, async bool, retryMax int, saslUser string, saslPwd string) (interface{}, error) { 48 | config := sarama.NewConfig() 49 | config.Producer.Retry.Max = retryMax 50 | config.Producer.RequiredAcks = sarama.WaitForAll 51 | 52 | if saslUser != "" && saslPwd != "" { 53 | config.Net.SASL.Enable = true 54 | config.Net.SASL.User = saslUser 55 | config.Net.SASL.Password = saslPwd 56 | } 57 | 58 | if async { 59 | return sarama.NewAsyncProducer(brokers, config) 60 | } 61 | 62 | config.Producer.Return.Successes = true 63 | return sarama.NewSyncProducer(brokers, config) 64 | 65 | } 66 | 67 | // UpdateEvents implements EventSinkInterface.UpdateEvents 68 | func (ks *KafkaSink) UpdateEvents(eNew *v1.Event, eOld *v1.Event) { 69 | 70 | eData := NewEventData(eNew, eOld) 71 | 72 | eJSONBytes, err := json.Marshal(eData) 73 | if err != nil { 74 | glog.Errorf("Failed to json serialize event: %v", err) 75 | return 76 | } 77 | msg := &sarama.ProducerMessage{ 78 | Topic: ks.Topic, 79 | Key: sarama.StringEncoder(eNew.InvolvedObject.Name), 80 | Value: sarama.ByteEncoder(eJSONBytes), 81 | } 82 | 83 | switch p := ks.producer.(type) { 84 | case sarama.SyncProducer: 85 | partition, offset, err := p.SendMessage(msg) 86 | if err != nil { 87 | glog.Errorf("Failed to send to: topic(%s)/partition(%d)/offset(%d)\n", 88 | ks.Topic, partition, offset) 89 | } 90 | 91 | case sarama.AsyncProducer: 92 | select { 93 | case p.Input() <- msg: 94 | case err := <-p.Errors(): 95 | glog.Errorf("Failed to produce message: %v", err) 96 | } 97 | 98 | default: 99 | glog.Errorf("Unhandled producer type: %s", p) 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /sinks/rocksetsink.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Contributors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package sinks 18 | 19 | import ( 20 | "encoding/json" 21 | "fmt" 22 | "os" 23 | 24 | apiclient "github.com/rockset/rockset-go-client" 25 | models "github.com/rockset/rockset-go-client/lib/go" 26 | v1 "k8s.io/api/core/v1" 27 | ) 28 | 29 | /* 30 | RocksetSink is a sink that uploads the kubernetes events as json object 31 | and converts them to documents inside of a Rockset collection. 32 | 33 | Rockset can later be used with 34 | many different connectors such as Tableau or Redash to use this data. 35 | */ 36 | type RocksetSink struct { 37 | client *apiclient.RockClient 38 | rocksetCollectionName string 39 | rocksetWorkspaceName string 40 | } 41 | 42 | // NewRocksetSink will create a new RocksetSink with default options, returned as 43 | // an EventSinkInterface 44 | func NewRocksetSink(rocksetAPIKey string, rocksetCollectionName string, rocksetWorkspaceName string) EventSinkInterface { 45 | client := apiclient.Client(rocksetAPIKey, "https://api.rs2.usw2.rockset.com") 46 | return &RocksetSink{ 47 | client: client, 48 | rocksetCollectionName: rocksetCollectionName, 49 | rocksetWorkspaceName: rocksetWorkspaceName, 50 | } 51 | } 52 | 53 | // UpdateEvents implements the EventSinkInterface 54 | func (rs *RocksetSink) UpdateEvents(eNew *v1.Event, eOld *v1.Event) { 55 | eData := NewEventData(eNew, eOld) 56 | 57 | if eJSONBytes, err := json.Marshal(eData); err == nil { 58 | var m map[string]interface{} 59 | json.Unmarshal(eJSONBytes, &m) 60 | docs := []interface{}{ 61 | m, 62 | } 63 | dinfo := models.AddDocumentsRequest{ 64 | Data: docs, 65 | } 66 | rs.client.Documents.Add(rs.rocksetWorkspaceName, rs.rocksetCollectionName, dinfo) 67 | } else { 68 | fmt.Fprintf(os.Stderr, "Failed to json serialize event: %v", err) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /sinks/s3sink.go: -------------------------------------------------------------------------------- 1 | package sinks 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "time" 8 | 9 | "k8s.io/api/core/v1" 10 | 11 | "github.com/aws/aws-sdk-go/aws" 12 | "github.com/aws/aws-sdk-go/aws/credentials" 13 | "github.com/aws/aws-sdk-go/aws/session" 14 | "github.com/aws/aws-sdk-go/service/s3/s3manager" 15 | "github.com/eapache/channels" 16 | "github.com/golang/glog" 17 | ) 18 | 19 | /* 20 | S3Sink is the sink that uploads the kubernetes events as json object stored in a file. 21 | The sinker uploads it to s3 if any of the below criteria gets fullfilled 22 | 1) Time(uploadInterval): If the specfied time has passed since the last upload it uploads 23 | 2) [TODO] Data size: If the total data getting uploaded becomes greater than N bytes 24 | 25 | S3 is cheap and the sink can be used to store events data. S3 can later then be used with 26 | Redshift and other visualization tools to use this data. 27 | 28 | */ 29 | type S3Sink struct { 30 | // uploader is the uploader client from aws which makes the API call to aws for upload 31 | uploader *s3manager.Uploader 32 | 33 | // bucket is the s3 bucket name where the events data would be stored 34 | bucket string 35 | 36 | // bucketDir is the first level directory in the bucket where the events would be stored 37 | bucketDir string 38 | 39 | // outPutFormat is the format in which the data is stored in the s3 file 40 | outputFormat string 41 | 42 | // lastUploadTimestamp stores the timestamp when the last upload to s3 happened 43 | lastUploadTimestamp int64 44 | 45 | // uploadInterval tells after how many seconds the next upload can happen 46 | // sink waits till this time is passed before next upload can happen 47 | uploadInterval time.Duration 48 | 49 | // eventCh is used to interact eventRouter and the sharedInformer 50 | eventCh channels.Channel 51 | 52 | // bodyBuf stores all the event captured data in a buffer before upload 53 | bodyBuf *bytes.Buffer 54 | } 55 | 56 | // NewS3Sink is the factory method constructing a new S3Sink 57 | func NewS3Sink(awsAccessKeyID string, s3SinkSecretAccessKey string, s3SinkRegion string, s3SinkBucket string, s3SinkBucketDir string, s3SinkUploadInterval int, overflow bool, bufferSize int, outputFormat string) (*S3Sink, error) { 58 | awsConfig := &aws.Config{ 59 | Region: aws.String(s3SinkRegion), 60 | Credentials: credentials.NewStaticCredentials(awsAccessKeyID, s3SinkSecretAccessKey, ""), 61 | } 62 | 63 | awsConfig = awsConfig.WithCredentialsChainVerboseErrors(true) 64 | sess, err := session.NewSession(awsConfig) 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | uploader := s3manager.NewUploader(sess) 70 | 71 | s := &S3Sink{ 72 | uploader: uploader, 73 | bucket: s3SinkBucket, 74 | bucketDir: s3SinkBucketDir, 75 | uploadInterval: time.Second * time.Duration(s3SinkUploadInterval), 76 | outputFormat: outputFormat, 77 | bodyBuf: bytes.NewBuffer(make([]byte, 0, 4096)), 78 | } 79 | 80 | if overflow { 81 | s.eventCh = channels.NewOverflowingChannel(channels.BufferCap(bufferSize)) 82 | } else { 83 | s.eventCh = channels.NewNativeChannel(channels.BufferCap(bufferSize)) 84 | } 85 | 86 | return s, nil 87 | } 88 | 89 | // UpdateEvents implements the EventSinkInterface. It really just writes the 90 | // event data to the event OverflowingChannel, which should never block. 91 | // Messages that are buffered beyond the bufferSize specified for this HTTPSink 92 | // are discarded. 93 | func (s *S3Sink) UpdateEvents(eNew *v1.Event, eOld *v1.Event) { 94 | s.eventCh.In() <- NewEventData(eNew, eOld) 95 | } 96 | 97 | // Run sits in a loop, waiting for data to come in through h.eventCh, 98 | // and forwarding them to the HTTP sink. If multiple events have happened 99 | // between loop iterations, it puts all of them in one request instead of 100 | // making a single request per event. 101 | func (s *S3Sink) Run(stopCh <-chan bool) { 102 | loop: 103 | for { 104 | select { 105 | case e := <-s.eventCh.Out(): 106 | var evt EventData 107 | var ok bool 108 | if evt, ok = e.(EventData); !ok { 109 | glog.Warningf("Invalid type sent through event channel: %T", e) 110 | continue loop 111 | } 112 | 113 | // Start with just this event... 114 | arr := []EventData{evt} 115 | 116 | // Consume all buffered events into an array, in case more have been written 117 | // since we last forwarded them 118 | numEvents := s.eventCh.Len() 119 | for i := 0; i < numEvents; i++ { 120 | e := <-s.eventCh.Out() 121 | if evt, ok = e.(EventData); ok { 122 | arr = append(arr, evt) 123 | } else { 124 | glog.Warningf("Invalid type sent through event channel: %T", e) 125 | } 126 | } 127 | 128 | s.drainEvents(arr) 129 | case <-stopCh: 130 | break loop 131 | } 132 | } 133 | } 134 | 135 | // drainEvents takes an array of event data and sends it to s3 136 | func (s *S3Sink) drainEvents(events []EventData) { 137 | var written int64 138 | for _, evt := range events { 139 | switch s.outputFormat { 140 | case "rfc5424": 141 | w, err := evt.WriteRFC5424(s.bodyBuf) 142 | written += w 143 | if err != nil { 144 | glog.Warningf("Could not write to event request body (wrote %v) bytes: %v", written, err) 145 | return 146 | } 147 | case "flatjson": 148 | w, err := evt.WriteFlattenedJSON(s.bodyBuf) 149 | written += w 150 | if err != nil { 151 | glog.Warningf("Could not write to event request body (wrote %v) bytes: %v", written, err) 152 | return 153 | } 154 | default: 155 | err := errors.New("Invalid Sink Output Format specified") 156 | panic(err.Error()) 157 | } 158 | s.bodyBuf.Write([]byte{'\n'}) 159 | written++ 160 | } 161 | 162 | if s.canUpload() == false { 163 | return 164 | } 165 | 166 | s.upload() 167 | } 168 | 169 | // canUpload verifies the conditions suitable for a new file upload and upload the data 170 | func (s *S3Sink) canUpload() bool { 171 | now := time.Now().UnixNano() 172 | if (s.lastUploadTimestamp + s.uploadInterval.Nanoseconds()) < now { 173 | return true 174 | } 175 | return false 176 | } 177 | 178 | // getNewKey gets the key name based on time 179 | func (s *S3Sink) getNewKey(t time.Time) string { 180 | return fmt.Sprintf("%s/%d/%d/%d/%d.txt", s.bucketDir, t.Year(), t.Month(), t.Day(), t.UnixNano()) 181 | } 182 | 183 | // upload uploads the events stored in buffer to s3 in the specified key 184 | // and clears the buffer 185 | func (s *S3Sink) upload() { 186 | now := time.Now() 187 | key := s.getNewKey(now) 188 | 189 | _, err := s.uploader.Upload(&s3manager.UploadInput{ 190 | Bucket: aws.String(s.bucket), 191 | Key: aws.String(key), 192 | Body: s.bodyBuf, 193 | }) 194 | if err != nil { 195 | glog.Errorf("Error uploading %s to s3, %v", key, err) 196 | } 197 | glog.Infof("Uploaded at %s", key) 198 | s.lastUploadTimestamp = now.UnixNano() 199 | 200 | s.bodyBuf.Truncate(0) 201 | } 202 | -------------------------------------------------------------------------------- /sinks/samplehttpsink/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.9-alpine 2 | 3 | RUN apk add --no-cache git 4 | COPY server.go . 5 | RUN go get -v -d ./... 6 | 7 | RUN go build -o httpsink 8 | ENTRYPOINT ./httpsink 9 | -------------------------------------------------------------------------------- /sinks/samplehttpsink/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "net/http" 7 | 8 | "github.com/crewjam/rfc5424" 9 | ) 10 | 11 | func handler(w http.ResponseWriter, r *http.Request) { 12 | log.Printf("request method=%s from=%s", r.Method, r.RemoteAddr) 13 | if r.Body == nil { 14 | return 15 | } 16 | defer r.Body.Close() 17 | 18 | m := new(rfc5424.Message) 19 | discardBuf := make([]byte, 1) 20 | for { 21 | _, err := m.ReadFrom(r.Body) 22 | if err == io.EOF { 23 | break 24 | } else if err != nil { 25 | log.Fatalf("Parsing rfc5424 message failed: %+v", err) 26 | } 27 | log.Printf("%s", m.Message) 28 | 29 | // read the extraneous \n at the end of the message and discard 30 | _, _ = io.ReadFull(r.Body, discardBuf) 31 | } 32 | } 33 | 34 | func main() { 35 | log.Println("starting httpsink server") 36 | http.HandleFunc("/", handler) 37 | log.Fatal(http.ListenAndServe(":8080", nil)) 38 | } 39 | -------------------------------------------------------------------------------- /sinks/stdoutsink.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Heptio Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package sinks 18 | 19 | import ( 20 | "encoding/json" 21 | "fmt" 22 | "os" 23 | 24 | "k8s.io/api/core/v1" 25 | ) 26 | 27 | // StdoutSink is the other basic sink 28 | // By default, Fluentd/ElasticSearch won't index glog formatted lines 29 | // By logging raw JSON to stdout, we will get automated indexing which 30 | // can be queried in Kibana. 31 | type StdoutSink struct { 32 | // TODO: create a channel and buffer for scaling 33 | namespace string 34 | } 35 | 36 | 37 | // NewStdoutSink will create a new StdoutSink with default options, returned as 38 | // an EventSinkInterface 39 | func NewStdoutSink(namespace string) EventSinkInterface { 40 | return &StdoutSink{ 41 | namespace: namespace} 42 | } 43 | 44 | // UpdateEvents implements the EventSinkInterface 45 | func (gs *StdoutSink) UpdateEvents(eNew *v1.Event, eOld *v1.Event) { 46 | eData := NewEventData(eNew, eOld) 47 | 48 | if len(gs.namespace) > 0 { 49 | namespacedData := map[string]interface{}{} 50 | namespacedData[gs.namespace] = eData 51 | if eJSONBytes, err := json.Marshal(namespacedData); err == nil { 52 | fmt.Println(string(eJSONBytes)) 53 | } else { 54 | fmt.Fprintf(os.Stderr, "Failed to json serialize event: %v", err) 55 | } 56 | } else { 57 | if eJSONBytes, err := json.Marshal(eData); err == nil { 58 | fmt.Println(string(eJSONBytes)) 59 | } else { 60 | fmt.Fprintf(os.Stderr, "Failed to json serialize event: %v", err) 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | eventrouter integration tests 2 | ============================= 3 | 4 | The integration tests should be run manually. 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tests/kafka/README.md: -------------------------------------------------------------------------------- 1 | # Kafka Sink Integration Test 2 | 3 | ### Start kafka 4 | ```bash 5 | docker-compose up -d kafka 6 | ``` 7 | 8 | ### Running the consumer 9 | ```bash 10 | # give kafka time to start before running the consumer 11 | docker-compose up consumer 12 | ``` 13 | 14 | ### Running the integration test 15 | ```bash 16 | # one time test 17 | docker-compose up app 18 | 19 | # repeated testing, much faster 20 | docker-compose run --entrypoint=/bin/sh app 21 | $ cd tests/kafka 22 | $ go install 23 | $ go run main.go 24 | 25 | ``` 26 | -------------------------------------------------------------------------------- /tests/kafka/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | app: 5 | image: golang:1.12.9 6 | volumes: 7 | - ../../:/src/ 8 | working_dir: /src/ 9 | command: ["go", "run", "tests/kafka/main.go"] 10 | links: 11 | - kafka 12 | environment: 13 | KAFKA_BROKERS: kafka:9092 14 | KAFKA_TOPIC: eventrouter 15 | KAFKA_RETRYMAX: 5 16 | KAFKA_ASYNC: "true" 17 | depends_on: 18 | - "kafka" 19 | 20 | kafka: 21 | image: spotify/kafka 22 | hostname: kafka 23 | environment: 24 | ADVERTISED_HOST: kafka 25 | ADVERTISED_PORT: 9092 26 | 27 | consumer: 28 | image: spotify/kafka 29 | links: 30 | - kafka 31 | command: [ 32 | "/opt/kafka_2.11-0.10.1.0/bin/kafka-console-consumer.sh", 33 | "--bootstrap-server", "kafka:9092", 34 | "--topic", "eventrouter" 35 | ] 36 | depends_on: 37 | - "kafka" 38 | -------------------------------------------------------------------------------- /tests/kafka/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 The Contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "fmt" 21 | "log" 22 | "time" 23 | 24 | "github.com/heptiolabs/eventrouter/sinks" 25 | "github.com/kelseyhightower/envconfig" 26 | "k8s.io/api/core/v1" 27 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 28 | "k8s.io/client-go/kubernetes/scheme" 29 | ref "k8s.io/client-go/tools/reference" 30 | ) 31 | 32 | type KafkaEnv struct { 33 | Brokers []string `required:"true"` 34 | Topic string `required:"true"` 35 | Async bool `default:"true"` 36 | RetryMax int `default:"5"` 37 | } 38 | 39 | func main() { 40 | var k KafkaEnv 41 | err := envconfig.Process("kafka", &k) 42 | if err != nil { 43 | log.Fatal(err) 44 | } 45 | 46 | kSink, err := sinks.NewKafkaSink(k.Brokers, k.Topic, k.Async, k.RetryMax, "user", "password") 47 | if err != nil { 48 | log.Fatal(err) 49 | } 50 | 51 | testPod := &v1.Pod{ 52 | TypeMeta: metav1.TypeMeta{ 53 | Kind: "Pod", 54 | }, 55 | ObjectMeta: metav1.ObjectMeta{ 56 | SelfLink: "/api/version/pods/somePod", 57 | Name: "somePod", 58 | Namespace: "someNameSpace", 59 | UID: "some-UID", 60 | }, 61 | Spec: v1.PodSpec{}, 62 | } 63 | 64 | podRef, err := ref.GetReference(scheme.Scheme, testPod) 65 | if err != nil { 66 | log.Fatal(err) 67 | } 68 | 69 | kvs := map[string]string{ 70 | "CreateInCluster": "Mock create event on Pod", 71 | "UpdateInCluster": "Mock update event on Pod", 72 | "DeleteInCluster": "Mock delete event on Pod", 73 | } 74 | 75 | var oldData, newData *v1.Event 76 | 77 | for k, v := range kvs { 78 | newData = newMockEvent(podRef, v1.EventTypeWarning, k, v) 79 | kSink.UpdateEvents(newData, oldData) 80 | oldData = newData 81 | time.Sleep(time.Second) 82 | } 83 | } 84 | 85 | // TODO: This function should be moved where it can be re-used... 86 | func newMockEvent(ref *v1.ObjectReference, eventtype, reason, message string) *v1.Event { 87 | tm := metav1.Time{ 88 | Time: time.Now(), 89 | } 90 | return &v1.Event{ 91 | ObjectMeta: metav1.ObjectMeta{ 92 | Name: fmt.Sprintf("%v.%x", ref.Name, tm.UnixNano()), 93 | Namespace: ref.Namespace, 94 | }, 95 | InvolvedObject: *ref, 96 | Reason: reason, 97 | Message: message, 98 | FirstTimestamp: tm, 99 | LastTimestamp: tm, 100 | Count: 1, 101 | Type: eventtype, 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /tests/rockset/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Heptio Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package main 15 | 16 | import ( 17 | "fmt" 18 | "github.com/heptiolabs/eventrouter/sinks" 19 | "time" 20 | 21 | v1 "k8s.io/api/core/v1" 22 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 23 | "k8s.io/client-go/kubernetes/scheme" 24 | 25 | ref "k8s.io/client-go/tools/reference" 26 | ) 27 | 28 | func main() { 29 | testPod := &v1.Pod{ 30 | TypeMeta: metav1.TypeMeta{ 31 | Kind: "Pod", 32 | }, 33 | ObjectMeta: metav1.ObjectMeta{ 34 | SelfLink: "/api/version/pods/foo", 35 | Name: "foo", 36 | Namespace: "baz", 37 | UID: "bar", 38 | }, 39 | Spec: v1.PodSpec{}, 40 | } 41 | podRef, err := ref.GetReference(scheme.Scheme, testPod) 42 | if err != nil { 43 | panic(err.Error()) 44 | } 45 | 46 | evt := makeFakeEvent(podRef, v1.EventTypeWarning, "CreateInCluster", "Fake pod creation event") 47 | 48 | sink := sinks.NewRocksetSink("key", "collection", "commons") 49 | 50 | sink.UpdateEvents(evt, nil) 51 | } 52 | 53 | func makeFakeEvent(ref *v1.ObjectReference, eventtype, reason, message string) *v1.Event { 54 | tm := metav1.Time{ 55 | Time: time.Now(), 56 | } 57 | return &v1.Event{ 58 | ObjectMeta: metav1.ObjectMeta{ 59 | Name: fmt.Sprintf("%v.%x", ref.Name, tm.UnixNano()), 60 | Namespace: ref.Namespace, 61 | }, 62 | InvolvedObject: *ref, 63 | Reason: reason, 64 | Message: message, 65 | FirstTimestamp: tm, 66 | LastTimestamp: tm, 67 | Count: 1, 68 | Type: eventtype, 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /yaml/eventrouter-azure.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Heptio Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: v1 16 | kind: ServiceAccount 17 | metadata: 18 | name: eventrouter 19 | namespace: kube-system 20 | --- 21 | apiVersion: rbac.authorization.k8s.io/v1beta1 22 | kind: ClusterRole 23 | metadata: 24 | name: eventrouter 25 | rules: 26 | - apiGroups: [""] 27 | resources: ["events"] 28 | verbs: ["get", "watch", "list"] 29 | --- 30 | apiVersion: rbac.authorization.k8s.io/v1beta1 31 | kind: ClusterRoleBinding 32 | metadata: 33 | name: eventrouter 34 | roleRef: 35 | apiGroup: rbac.authorization.k8s.io 36 | kind: ClusterRole 37 | name: eventrouter 38 | subjects: 39 | - kind: ServiceAccount 40 | name: eventrouter 41 | namespace: kube-system 42 | --- 43 | apiVersion: v1 44 | data: 45 | config.json: |- 46 | { 47 | "sink": "eventhub", 48 | "eventHubConnectionString": "${EVENTHUB_CONNECTION_STRING}" 49 | } 50 | kind: ConfigMap 51 | metadata: 52 | name: eventrouter-cm 53 | namespace: kube-system 54 | --- 55 | apiVersion: apps/v1 56 | kind: Deployment 57 | metadata: 58 | name: eventrouter 59 | namespace: kube-system 60 | labels: 61 | app: eventrouter 62 | spec: 63 | replicas: 1 64 | selector: 65 | matchLabels: 66 | app: eventrouter 67 | template: 68 | metadata: 69 | labels: 70 | app: eventrouter 71 | tier: control-plane-addons 72 | spec: 73 | containers: 74 | - name: kube-eventrouter 75 | image: gcr.io/heptio-images/eventrouter:latest 76 | imagePullPolicy: Always 77 | volumeMounts: 78 | - name: config-volume 79 | mountPath: /etc/eventrouter 80 | serviceAccount: eventrouter 81 | volumes: 82 | - name: config-volume 83 | configMap: 84 | name: eventrouter-cm 85 | -------------------------------------------------------------------------------- /yaml/eventrouter.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Heptio Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: v1 16 | kind: ServiceAccount 17 | metadata: 18 | name: eventrouter 19 | namespace: kube-system 20 | --- 21 | apiVersion: rbac.authorization.k8s.io/v1beta1 22 | kind: ClusterRole 23 | metadata: 24 | name: eventrouter 25 | rules: 26 | - apiGroups: [""] 27 | resources: ["events"] 28 | verbs: ["get", "watch", "list"] 29 | --- 30 | apiVersion: rbac.authorization.k8s.io/v1beta1 31 | kind: ClusterRoleBinding 32 | metadata: 33 | name: eventrouter 34 | roleRef: 35 | apiGroup: rbac.authorization.k8s.io 36 | kind: ClusterRole 37 | name: eventrouter 38 | subjects: 39 | - kind: ServiceAccount 40 | name: eventrouter 41 | namespace: kube-system 42 | --- 43 | apiVersion: v1 44 | data: 45 | config.json: |- 46 | { 47 | "sink": "glog" 48 | } 49 | kind: ConfigMap 50 | metadata: 51 | name: eventrouter-cm 52 | namespace: kube-system 53 | --- 54 | apiVersion: apps/v1 55 | kind: Deployment 56 | metadata: 57 | name: eventrouter 58 | namespace: kube-system 59 | labels: 60 | app: eventrouter 61 | spec: 62 | replicas: 1 63 | selector: 64 | matchLabels: 65 | app: eventrouter 66 | template: 67 | metadata: 68 | labels: 69 | app: eventrouter 70 | tier: control-plane-addons 71 | spec: 72 | containers: 73 | - name: kube-eventrouter 74 | image: gcr.io/heptio-images/eventrouter:latest 75 | imagePullPolicy: IfNotPresent 76 | volumeMounts: 77 | - name: config-volume 78 | mountPath: /etc/eventrouter 79 | serviceAccount: eventrouter 80 | volumes: 81 | - name: config-volume 82 | configMap: 83 | name: eventrouter-cm 84 | --------------------------------------------------------------------------------