├── .github └── workflows │ └── main.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── build ├── repos │ └── opstools.repo └── test-framework │ ├── pre-commit │ └── run_tests.sh ├── cmd ├── main.go └── main_test.go ├── configs ├── events │ └── smartgateway_config.json ├── metrics │ └── smartgateway_config.json └── prometheus.rules.yml ├── docs ├── sa_smart_agent.png ├── scale_as_group.png ├── smartagentsetup.png └── smartgateway_setup.md ├── go.mod ├── go.sum ├── internal └── pkg │ ├── amqp10 │ ├── receiver.go │ └── sender.go │ ├── api │ └── handler.go │ ├── cacheutil │ ├── cacheserver.go │ └── processcache.go │ ├── events │ ├── events.go │ ├── handlers.go │ └── incoming │ │ ├── ceilometer.go │ │ ├── collectd.go │ │ └── formats.go │ ├── metrics │ ├── incoming │ │ ├── ceilometer.go │ │ ├── collectd.go │ │ └── formats.go │ └── metrics.go │ ├── saconfig │ └── config.go │ ├── saelastic │ └── client.go │ └── tsdb │ └── prometheus.go └── tests └── internal_pkg ├── amqp10_test.go ├── cacheutil_test.go ├── elasticsearch_test.go ├── events_incoming_test.go ├── messages ├── formatCeilometerMessage.py └── metric-tests.json ├── metrics_incoming_test.go ├── saconfig_test.go └── tsdb_test.go /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | env: 3 | ELASTIC_VERSION: 7.5.1 4 | QDROUTERD_VERSION: 1.8.0 5 | COVERALLS_TOKEN: ${{ secrets.COVERALLS_TOKEN }} 6 | on: push 7 | 8 | jobs: 9 | test-framework: 10 | name: Base testing 11 | runs-on: ubuntu-20.04 12 | 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v2 16 | 17 | - name: Start ElasticSearch 18 | run: | 19 | docker run -p 9200:9200 --name elastic -p 9300:9300 -e "discovery.type=single-node" -d docker.elastic.co/elasticsearch/elasticsearch:$ELASTIC_VERSION 20 | 21 | - name: Start QDR 22 | run: | 23 | docker run -p 5672:5672 -d quay.io/interconnectedcloud/qdrouterd:$QDROUTERD_VERSION 24 | 25 | - name: Run unit testing and code coverage 26 | run: | 27 | docker run -eCOVERALLS_TOKEN -uroot --network host -i --volume $GITHUB_WORKSPACE:/go/src/github.com/infrawatch/smart-gateway:z --workdir /go/src/github.com/infrawatch/smart-gateway registry.access.redhat.com/ubi8 /bin/sh -c 'sh ./build/test-framework/run_tests.sh' 28 | 29 | - name: Verify image builds 30 | run: | 31 | docker build --tag infrawatch/smart_gateway:latest . 32 | 33 | - name: List images 34 | run: | 35 | docker images 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/* 2 | events_consumer 3 | metrics_consumer 4 | coverage.out 5 | coverfragment.out 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # --- build smart gateway --- 2 | FROM registry.access.redhat.com/ubi8 AS builder 3 | ENV GOPATH=/go 4 | ENV D=/go/src/github.com/infrawatch/smart-gateway 5 | 6 | WORKDIR $D 7 | COPY . $D/ 8 | COPY build/repos/opstools.repo /etc/yum.repos.d/opstools.repo 9 | 10 | RUN dnf install qpid-proton-c-devel git golang --setopt=tsflags=nodocs -y && \ 11 | go build -o smart_gateway cmd/main.go && \ 12 | mv smart_gateway /tmp/ 13 | 14 | # --- end build, create smart gateway layer --- 15 | FROM registry.access.redhat.com/ubi8 16 | 17 | COPY build/repos/opstools.repo /etc/yum.repos.d/opstools.repo 18 | 19 | RUN dnf install qpid-proton-c --setopt=tsflags=nodocs -y && \ 20 | dnf clean all && \ 21 | rm -rf /var/cache/yum 22 | 23 | COPY --from=builder /tmp/smart_gateway / 24 | 25 | ENTRYPOINT ["/smart_gateway"] 26 | 27 | LABEL io.k8s.display-name="Smart Gateway" \ 28 | io.k8s.description="A component of the Service Telemetry Framework on the server side that ingests data from AMQP 1.x and provides a metrics scrape endpoint for Prometheus, and forwards events to ElasticSearch" \ 29 | maintainer="Leif Madsen " 30 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # smart-gateway ![build status](https://travis-ci.org/infrawatch/smart-gateway.svg?branch=master) [![Go Report Card](https://goreportcard.com/badge/github.com/infrawatch/smart-gateway)](https://goreportcard.com/report/github.com/infrawatch/smart-gateway) [![Coverage Status](https://coveralls.io/repos/github/infrawatch/smart-gateway/badge.svg)](https://coveralls.io/github/infrawatch/smart-gateway) [![Docker Repository on Quay](https://quay.io/repository/infrawatch/smart-gateway/status "Docker Repository on Quay")](https://quay.io/repository/infrawatch/smart-gateway) 2 | 3 | Smart Gateway for Service Telemetry Framework. Includes applications for both 4 | metrics and events gathering. 5 | 6 | > **WARNING** 7 | > 8 | > This repository contains the legacy Smart Gateway, no longer in use as of STF 1.3 and will no longer be released once STF 1.4 is available and STF 1.2 moves to EOL. If this code base looks interesting or useful for your project, you are strongly encouraged to use sg-bridge and sg-core which have significantly better performance and integration capabilities. See https://github.com/infrawatch/sg-bridge and https://github.com/infrawatch/sg-core 9 | 10 | Provides middleware that connects to an AMQP 1.0 message bus, pulling data off 11 | the bus and exposing it as a scrape target for Prometheus. Metrics are provided 12 | via the OPNFV Barometer project (collectd) and Ceilometer (OpenStack). Events 13 | are provided by the various event plugins for collectd, including connectivity, 14 | procevent and sysevent, and Ceilometer. 15 | 16 | # Dependencies 17 | 18 | Dependencies are managed using golang modules. Clone this project, then obtain 19 | the dependencies with the following commands. Example below is built on CentOS 20 | 8. 21 | 22 | ``` 23 | go get -u github.com/infrawatch/smart-gateway 24 | sudo curl -L https://trunk.rdoproject.org/centos8-master/delorean-deps.repo -o /etc/yum.repos.d/delorean-deps.repo 25 | dnf install -y golang qpid-proton-c-devel iproute 26 | cd $GOPATH/src/github.com/infrawatch/smart-gateway 27 | ``` 28 | 29 | # Building Smart Gateway 30 | 31 | ## Building with Golang 32 | 33 | Build the `smart_gateway` with Golang using the following command. 34 | 35 | ``` 36 | cd $GOPATH/src/github.com/infrawatch/smart-gateway 37 | go build -o smart_gateway cmd/main.go 38 | ``` 39 | 40 | # Building with Docker 41 | 42 | Building the `smart-gateway` with docker using the following commands. 43 | 44 | ``` 45 | git clone --depth=1 --branch=master https://github.com/infrawatch/smart-gateway.git smart-gateway; rm -rf ./smart-gateway/.git 46 | cd smart-gateway 47 | docker build -t smart-gateway . 48 | ``` 49 | 50 | # Testing 51 | 52 | The Smart Gateway ships with various unit tests located in the `tests/` 53 | directory. To execute these unit tests, run the following command from the 54 | top-level directory. 55 | 56 | ``` 57 | go test -v ./... 58 | ``` 59 | 60 | > **A note about test layout in Smart Gateway** 61 | > 62 | > Generally tests are shipped in Golang directly within the packages as 63 | > `_test.go`, for example, `alerts.go` will have a 64 | > corresponding `alerts_test.go` within the `alerts` package. 65 | > 66 | > In the Smart Gateway, we've purposely taken a separate approach by moving the 67 | > tests into their own package, located within the `tests/` subdirectory. The 68 | > reason for this is two-fold: 69 | > 70 | > 1. It is the recommended workaround for avoiding falling into cyclic 71 | > dependencies / ciclical import problems. 72 | > 1. Test implementer loses access to private logic, so has to think about the 73 | > tested API more in depth. Also when the test of package A breaks, we can 74 | > be sure that other packages are broken too. On the other side, when you 75 | > have access to private logic, you can unintentionally workaround issues 76 | > and hit the bugs in production deployments. 77 | -------------------------------------------------------------------------------- /build/repos/opstools.repo: -------------------------------------------------------------------------------- 1 | [centos-opstools] 2 | name=opstools 3 | baseurl=http://mirror.centos.org/centos/8/opstools/$basearch/collectd-5/ 4 | gpgcheck=0 5 | enabled=1 6 | module_hotfixes=1 7 | -------------------------------------------------------------------------------- /build/test-framework/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # original source: https://raw.githubusercontent.com/streadway/amqp/master/pre-commit 3 | 4 | GOFMT_OK=1 5 | GOLINT_OK=1 6 | GOVET_OK=1 7 | STATICCHECK_OK=1 8 | DEPCHECK_OK=1 9 | 10 | # TODO: uncomment each of these tests during future pull-requests with 11 | # resolving code changes 12 | main() { 13 | mv_vendor_out 14 | run_gofmt 15 | run_golint 16 | mv_vendor_in 17 | run_govet 18 | run_staticcheck 19 | check_results 20 | } 21 | 22 | log_error() { 23 | echo "$*" 1>&2 24 | } 25 | 26 | mv_vendor_out() { 27 | mv ./vendor/ /tmp/ 28 | } 29 | 30 | mv_vendor_in() { 31 | mv /tmp/vendor ./ 32 | } 33 | 34 | check_results() { 35 | if [[ "$GOFMT_OK" == "1" && "$GOLINT_OK" == "1" && "$GOVET_OK" == "1" && "$STATICCHECK_OK" == "1" && "$DEPCHECK_OK" == "1" ]] 36 | then 37 | echo "Formatting, Linting, Vetting, and Static Checks all returned OK." 38 | exit 0 39 | else 40 | echo "One of Formatting, Linting, Vetting, or Static Checks, or vendor check is invalid. Check logs for more information." 41 | exit 1 42 | fi 43 | } 44 | 45 | run_gofmt() { 46 | GOFMT_FILES=$(gofmt -l .) 47 | if [ -n "$GOFMT_FILES" ] 48 | then 49 | log_error "gofmt failed for the following files: 50 | $GOFMT_FILES 51 | 52 | please run 'gofmt -w .' on your changes before committing." 53 | GOFMT_OK=0 54 | fi 55 | } 56 | 57 | run_golint() { 58 | GOLINT_ERRORS=$(golint ./... | grep -v "Id should be") 59 | if [ -n "$GOLINT_ERRORS" ] 60 | then 61 | log_error "golint failed for the following reasons: 62 | $GOLINT_ERRORS 63 | 64 | please run 'golint ./...' on your changes before committing." 65 | GOLINT_OK=0 66 | fi 67 | } 68 | 69 | run_staticcheck() { 70 | STATIC_ERRORS=$(staticcheck ./...) 71 | if [ -n "${STATIC_ERRORS}" ] 72 | then 73 | log_error "staticcheck failed: 74 | $STATIC_ERRORS 75 | 76 | please run 'staticcheck ./...' on your changes before committing." 77 | STATICCHECK_OK=0 78 | fi 79 | } 80 | 81 | run_govet() { 82 | go mod download # run this to avoid stdout causing invalid failure 83 | GOVET_ERRORS=$(go vet ./... 2>&1) 84 | if [ -n "$GOVET_ERRORS" ] 85 | then 86 | log_error "go vet failed for the following reasons: 87 | $GOVET_ERRORS 88 | 89 | please run 'go vet ./...' on your changes before committing." 90 | GOVET_OK=0 91 | fi 92 | } 93 | 94 | run_dep_check() { 95 | DEPCHECK_ERRORS=$(dep check) 96 | if [ -n "$DEPCHECK_ERRORS" ] 97 | then 98 | log_error "dep check failed for the following reasons: 99 | $DEPCHECK_ERRORS 100 | 101 | please run 'dep check' on your changes before committing." 102 | DEPCHECK_OK=0 103 | fi 104 | } 105 | 106 | main 107 | # vim: set ft=sh:ts=2:sw=2:noai: 108 | -------------------------------------------------------------------------------- /build/test-framework/run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ex 3 | 4 | 5 | # bootstrap 6 | mkdir -p /go/bin /go/src /go/pkg 7 | export GOPATH=/go 8 | export PATH=$PATH:$GOPATH/bin 9 | 10 | # get dependencies 11 | sed -i '/^tsflags=.*/a ip_resolve=4' /etc/yum.conf 12 | cp build/repos/opstools.repo /etc/yum.repos.d/opstools.repo 13 | dnf install -y git golang qpid-proton-c-devel iproute 14 | go get -u golang.org/x/tools/cmd/cover 15 | GO111MODULE=off go get -u github.com/mattn/goveralls 16 | go get -u golang.org/x/lint/golint 17 | go get -u honnef.co/go/tools/cmd/staticcheck 18 | 19 | # get vendor code 20 | go mod vendor 21 | 22 | # run code validation tools 23 | echo " *** Running pre-commit code validation" 24 | ./build/test-framework/pre-commit 25 | 26 | # run unit tests 27 | echo " *** Running test suite" 28 | # TODO: re-enable the test suite once supporting changes result in tests to pass 29 | go test -v ./... 30 | 31 | # check test coverage 32 | echo " *** Running code coverage tooling" 33 | go test ./... -race -covermode=atomic -coverprofile=coverage.txt 34 | 35 | # upload coverage report 36 | echo " *** Uploading coverage report to coveralls" 37 | goveralls -coverprofile=coverage.txt 38 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "strings" 7 | 8 | "github.com/infrawatch/smart-gateway/internal/pkg/events" 9 | "github.com/infrawatch/smart-gateway/internal/pkg/metrics" 10 | ) 11 | 12 | var metricType = "metrics" 13 | var eventType = "events" 14 | 15 | func main() { 16 | 17 | serviceType := getServiceType() 18 | if serviceType == metricType { 19 | metrics.StartMetrics() 20 | } else if serviceType == eventType { 21 | events.StartEvents() 22 | } else { 23 | log.Printf("Unknow command line argument 'servicetype' valid values are '%s' or '%s'.", metricType, eventType) 24 | } 25 | 26 | } 27 | 28 | //getServiceType ... checks for servicetype parameter to be either events or metrics. 29 | func getServiceType() string { 30 | lenOfArgs := len(os.Args) - 1 31 | var servicetype = metricType 32 | for index, arg := range os.Args { 33 | pattern := "-servicetype=" 34 | pposition := strings.Index(arg, pattern) 35 | if pposition > -1 { 36 | if pposition+len(pattern) < len(arg) { 37 | servicetype = arg[pposition+len(pattern):] 38 | return servicetype 39 | } 40 | log.Printf("Command line argument 'servicetype' %s is not valid.", arg) 41 | return "" 42 | } 43 | if arg == "-servicetype" || arg == "--servicetype" { 44 | if lenOfArgs >= index+1 { 45 | servicetype = os.Args[index+1] 46 | return servicetype 47 | } 48 | log.Printf("Command line argument 'servicetype' %s is not valid.", arg) 49 | return "" 50 | 51 | } 52 | } 53 | log.Printf("Command line argument 'servicetype' is not set, using default '%s' type.", servicetype) 54 | return servicetype 55 | } 56 | -------------------------------------------------------------------------------- /cmd/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "testing" 7 | ) 8 | 9 | func TestServiceTypeExists(t *testing.T) { 10 | var serviceType string 11 | os.Args = []string{"cmd", "-servicetype=bla"} 12 | serviceType = getServiceType() 13 | fmt.Println(serviceType) 14 | if serviceType != "bla" { 15 | t.Error("failed : command line argument 'servicetype' was not found") 16 | } 17 | 18 | } 19 | 20 | func TestServiceTypeDoesntExists(t *testing.T) { 21 | var serviceType string 22 | os.Args = []string{"cmd", "-servicenottype="} 23 | serviceType = getServiceType() 24 | //if valid not passed it will return defualt 25 | if serviceType == "" { 26 | t.Error("Failed for known command line argument 'servicenottype'.") 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /configs/events/smartgateway_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "AMQP1EventURL": "localhost:5672/collectd/notify", 3 | "ElasticHostURL": "http://localhost:9200", 4 | "AlertManagerURL": "http://localhost:9093/api/v1/alerts", 5 | "ResetIndex": false, 6 | "Debug": true, 7 | "Prefetch": 0, 8 | "API": { 9 | "APIEndpointURL": "localhost:9999", 10 | "AMQP1PublishURL": "localhost:5672/collectd/alert2" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /configs/metrics/smartgateway_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "AMQP1MetricURL": "localhost/collectd/telemetry", 3 | "Exporterhost": "localhost", 4 | "Exporterport": 8081, 5 | "CPUStats": false, 6 | "DataCount": -1, 7 | "UseSample": false, 8 | "UseTimeStamp": true, 9 | "Debug": true, 10 | "Prefetch": 15000, 11 | "Sample": { 12 | "HostCount": 10, 13 | "PluginCount": 100, 14 | "DataCount": -1 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /configs/prometheus.rules.yml: -------------------------------------------------------------------------------- 1 | groups: 2 | - name: test-node 3 | interval: 30s 4 | rules: 5 | - record: job:svc_1_documents_loaded_count:avg_rate1s 6 | expr: avg(rate(svc_1_documents_loaded_count[1s])) by (job) 7 | - record: job:svc_2_documents_loaded_count:avg_rate1s 8 | expr: avg(rate(svc_2_documents_loaded_count[1s])) by (job) 9 | - record: job:svc_3_documents_loaded_count:avg_rate1s 10 | expr: avg(rate(svc_3_documents_loaded_count[1s])) by (job) 11 | - record: job:svc_4_documents_loaded_count:avg_rate1s 12 | expr: avg(rate(svc_4_documents_loaded_count[1s])) by (job) 13 | - record: job:svc_5_documents_loaded_count:avg_rate1s 14 | expr: avg(rate(svc_5_documents_loaded_count[1s])) by (job) 15 | - record: job:svc_6_documents_loaded_count:avg_rate1s 16 | expr: avg(rate(svc_6_documents_loaded_count[1s])) by (job) 17 | - record: job:svc_7_documents_loaded_count:avg_rate1s 18 | expr: avg(rate(svc_7_documents_loaded_count[1s])) by (job) 19 | - record: job:svc_8_documents_loaded_count:avg_rate1s 20 | expr: avg(rate(svc_8_documents_loaded_count[1s])) by (job) 21 | - record: job:svc_9_documents_loaded_count:avg_rate1s 22 | expr: avg(rate(svc_9_documents_loaded_count[1s])) by (job) 23 | - record: job:svc_10_documents_loaded_count:avg_rate1s 24 | expr: avg(rate(svc_10_documents_loaded_count[1s])) by (job) 25 | 26 | - name: openstack_exporter_status 27 | interval: 5s 28 | rules: 29 | - alert: openstack exporter down 30 | expr: absent(up{job="openstack"}) or sum(up{job="openstack"}) < 1 31 | for: 10s 32 | labels: 33 | severity: critical 34 | annotations: 35 | summary: Openstack monitoring service is down for 10s 36 | description: "{{ $labels.instance }} of job {{ $labels.job }} has been down for more than 10 secs." 37 | 38 | - name: COLLECTD_DOWN 39 | interval: 1s 40 | rules: 41 | # Alert for any instance that is unreachable for >5 minutes. 42 | - alert: Collectd_down 43 | #expr: absent(collectd_uptime) // this cant be predicted 44 | expr: collectd_last_metric_for_host_status==0 45 | for: 20s 46 | labels: 47 | severity: critical 48 | annotations: 49 | summary: "collectd in {{ $labels.source}} is down or no data" 50 | description: "collectd is not sending data from {{ $labels.source }} is has been down for more than 20s secs." 51 | 52 | - name: QPID_DOWN 53 | interval: 1s 54 | rules: 55 | # Alert for any instance that is unreachable for >5 minutes. 56 | - alert: qpid_router_down 57 | #expr: absent(collectd_uptime) 58 | expr: collectd_qpid_router_status==0 59 | for: 10s 60 | labels: 61 | severity: critical 62 | annotations: 63 | summary: "qpid dispatch router in {{ $labels.instance}} is down" 64 | description: "qpid dispatch router may have connection error is not sending data from {{ $labels.instance }} is has been down for more than 10s minutes." 65 | 66 | - name: ElasticSearch_Down 67 | interval: 1s 68 | rules: 69 | # Alert for any instance that is unreachable for >5 minutes. 70 | - alert: elasticsearch_down 71 | #expr: absent(collectd_uptime) 72 | expr: collectd_elasticsearch_status==0 73 | for: 10s 74 | labels: 75 | severity: critical 76 | annotations: 77 | summary: "ElasticSearch {{ $labels.instance}} is down" 78 | description: "ElasticSearch may have connection error in {{ $labels.instance }} is has been down for more than 10s" 79 | 80 | - name: SmartGateway_listener_high_latency 81 | interval: 1s 82 | rules: 83 | # Alert for any instance that is unreachable for >5 minutes. 84 | - alert: smartgateway_listerner_slow 85 | #expr: absent(collectd_uptime) 86 | expr: floor(collectd_last_pull_timestamp_seconds)-time()>6 87 | for: 10s 88 | labels: 89 | severity: critical 90 | annotations: 91 | summary: "{{$labels.source}} running in {{ $labels.instance}} is slow " 92 | description: "{{$labels.source}} running in {{ $labels.instance }} having high latency " 93 | 94 | - name: SmartGateway_listener_high_down 95 | interval: 1s 96 | rules: 97 | # Alert for any instance that is unreachable for >5 minutes. 98 | - alert: smartgateway_listerner_down 99 | #expr: absent(collectd_uptime) 100 | expr: absent(collectd_last_pull_timestamp_seconds) 101 | for: 20s 102 | labels: 103 | severity: critical 104 | annotations: 105 | summary: "Either metirc or event listener running in {{ $labels.instance}} is dead" 106 | description: "Either metirc or event listener running in {{ $labels.instance }} is dead" 107 | 108 | - name: InstanceDown 109 | interval: 5s 110 | rules: 111 | # Alert for any instance that is unreachable for >5 minutes. 112 | - alert: InstanceDown 113 | expr: up == 0 114 | for: 5m 115 | labels: 116 | severity: page 117 | annotations: 118 | summary: "Instance {{ $labels.instance }} down" 119 | description: "{{ $labels.instance }} of job {{ $labels.job }} has been down for more than 5 minutes." 120 | 121 | - name: API_status 122 | interval: 5s 123 | rules: 124 | - alert: Openstack Service is down 125 | expr: avg_over_time({__name__=~".*api" ,job="openstack"}[5s])==0 126 | for: 10s 127 | labels: 128 | severity: critical 129 | annotations: 130 | summary: openstack service {{$labels.service}} is down 131 | description: "openstack {{$labels.service}} api service (host :{{ $labels.instance }} of job {{ $labels.job }}) has been DOWN for more than 10 secs." 132 | 133 | - name: Node CPU Usage 134 | interval: 5s 135 | rules: 136 | - alert: High CPU usage 137 | expr: (avg by (exported_instance) (irate(collectd_cpu_total[5m]))) > 75 138 | for: 20s 139 | labels: 140 | severity: critical 141 | annotations: 142 | summary: "{{$labels.exported_instance}}: High CPU usage detected" 143 | description: "{{$labels.exported_instance}}: CPU usage is above 75% (current value is: {{ $value }})" 144 | 145 | #- alert: Openstack Service is up 146 | # expr: avg_over_time({__name__=~".*api" ,job="openstack"}[5s])==1 147 | # for: 10s 148 | # labels: 149 | # severity: info 150 | # annotations: 151 | # summary: openstack service {{$labels.service}} is up 152 | # description: "openstack {{$labels.service}} api service (host :{{ $labels.instance }} of job {{ $labels.job }}) is UP." 153 | -------------------------------------------------------------------------------- /docs/sa_smart_agent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrawatch/smart-gateway/ebc156206ddcb8b58cb2dd3b8896a2c774bf696f/docs/sa_smart_agent.png -------------------------------------------------------------------------------- /docs/scale_as_group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrawatch/smart-gateway/ebc156206ddcb8b58cb2dd3b8896a2c774bf696f/docs/scale_as_group.png -------------------------------------------------------------------------------- /docs/smartagentsetup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrawatch/smart-gateway/ebc156206ddcb8b58cb2dd3b8896a2c774bf696f/docs/smartagentsetup.png -------------------------------------------------------------------------------- /docs/smartgateway_setup.md: -------------------------------------------------------------------------------- 1 | # Smart Gateway Setup 2 | 3 | Notes about how to setup the Smart Gateway components from client to server. 4 | 5 | ## virthost 6 | 7 | ### Firewall 8 | 9 | firewall-cmd --add-port=20001/tcp 10 | firewall-cmd --add-port=5672/tcp 11 | 12 | ### QDR configuration 13 | 14 | cd ~ 15 | mkdir ~/qpid-dispatch 16 | cat > qpid-dispatch/qdrouterd.conf < distribution pattern 57 | # configurations: 58 | # 59 | address { 60 | prefix: closest 61 | distribution: closest 62 | } 63 | 64 | address { 65 | prefix: multicast 66 | distribution: multicast 67 | } 68 | 69 | address { 70 | prefix: unicast 71 | distribution: closest 72 | } 73 | 74 | address { 75 | prefix: exclusive 76 | distribution: closest 77 | } 78 | 79 | address { 80 | prefix: broadcast 81 | distribution: multicast 82 | } 83 | EOF 84 | 85 | 86 | ### Start QDR 87 | 88 | docker run -it --volume=`pwd`/qpid-dispatch/:/etc/qpid-dispatch.conf.d/ --net=host nfvpe/qpid-dispatch-router --config=/etc/qpid-dispatch.conf.d/qdrouterd.conf 89 | 90 | 91 | ## cloud-node-1 92 | 93 | ### QDR configuration 94 | 95 | cd ~ 96 | mkdir qpid-dispatch 97 | cat > qpid-dispatch/qdrouterd.conf < distribution pattern 130 | # configurations: 131 | # 132 | address { 133 | prefix: closest 134 | distribution: closest 135 | } 136 | 137 | address { 138 | prefix: multicast 139 | distribution: multicast 140 | } 141 | 142 | address { 143 | prefix: unicast 144 | distribution: closest 145 | } 146 | 147 | address { 148 | prefix: exclusive 149 | distribution: closest 150 | } 151 | 152 | address { 153 | prefix: broadcast 154 | distribution: multicast 155 | } 156 | 157 | ### Start QDR 158 | 159 | docker run -it --publish 172.17.0.1:5672:5672/tcp --volume `pwd`/qpid-dispatch/:/etc/qpid-dispatch.conf.d/:Z nfvpe/qpid-dispatch-router --config=/etc/qpid-dispatch.conf.d/qdrouterd.conf 160 | 161 | ### barometer-collectd configuration 162 | 163 | cd ~ 164 | mkdir collect_config 165 | cat > collect_config/barometer.conf < 168 | 169 | Host "172.17.0.1" 170 | Port "5672" 171 | # User "guest" 172 | # Password "guest" 173 | Address "collectd" 174 | # 175 | # Format JSON 176 | # PreSettle false 177 | # 178 | 179 | Format JSON 180 | PreSettle true 181 | Notify true 182 | 183 | 184 | Format JSON 185 | PreSettle false 186 | 187 | 188 | 189 | EOF 190 | 191 | ### Start barometer-collectd 192 | 193 | docker run -ti --net=host -v `pwd`/collect_config:/opt/collectd/etc/collectd.conf.d -v /var/run:/var/run -v /tmp:/tmp --privileged nfvpe/barometer-collectd 194 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/infrawatch/smart-gateway 2 | 3 | go 1.13 4 | 5 | require ( 6 | collectd.org v0.3.0 7 | github.com/MakeNowJust/heredoc v0.0.0-20171113091838-e9091a26100e 8 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 // indirect 9 | github.com/davecgh/go-spew v1.1.1 // indirect 10 | github.com/fortytw2/leaktest v1.3.0 // indirect 11 | github.com/gofrs/uuid v4.1.0+incompatible 12 | github.com/gogo/protobuf v1.3.1 // indirect 13 | github.com/golang/protobuf v1.1.0 // indirect 14 | github.com/json-iterator/go v0.0.0-20180701071628-ab8a2e0c74be 15 | github.com/mailru/easyjson v0.0.0-20180717111219-efc7eb8984d6 // indirect 16 | github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect 17 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 18 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 // indirect 19 | github.com/olivere/elastic v6.1.25+incompatible 20 | github.com/pkg/errors v0.8.0 // indirect 21 | github.com/prometheus/client_golang v0.9.0 22 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 23 | github.com/prometheus/common v0.0.0-20180518154759-7600349dcfe1 // indirect 24 | github.com/prometheus/procfs v0.0.0-20180705121852-ae68e2d4c00f // indirect 25 | github.com/stretchr/testify v1.3.0 26 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 // indirect 27 | qpid.apache.org v0.0.0-20190307183443-7bf92569070b 28 | ) 29 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | collectd.org v0.3.0 h1:iNBHGw1VvPJxH2B6RiFWFZ+vsjo1lCdRszBeOuwGi00= 2 | collectd.org v0.3.0/go.mod h1:A/8DzQBkF6abtvrT2j/AU/4tiBgJWYyh0y/oB/4MlWE= 3 | github.com/MakeNowJust/heredoc v0.0.0-20171113091838-e9091a26100e h1:eb0Pzkt15Bm7f2FFYv7sjY7NPFi3cPkS3tv1CcrFBWA= 4 | github.com/MakeNowJust/heredoc v0.0.0-20171113091838-e9091a26100e/go.mod h1:64YHyfSL2R96J44Nlwm39UHepQbyR5q10x7iYa1ks2E= 5 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0= 6 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 7 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= 11 | github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= 12 | github.com/gofrs/uuid v4.1.0+incompatible h1:sIa2eCvUTwgjbqXrPLfNwUf9S3i3mpH1O1atV+iL/Wk= 13 | github.com/gofrs/uuid v4.1.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 14 | github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= 15 | github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= 16 | github.com/golang/protobuf v1.1.0 h1:0iH4Ffd/meGoXqF2lSAhZHt8X+cPgkfn/cb6Cce5Vpc= 17 | github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 18 | github.com/json-iterator/go v0.0.0-20180701071628-ab8a2e0c74be h1:AHimNtVIpiBjPUhEF5KNCkrUyqTSA5zWUl8sQ2bfGBE= 19 | github.com/json-iterator/go v0.0.0-20180701071628-ab8a2e0c74be/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 20 | github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= 21 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 22 | github.com/mailru/easyjson v0.0.0-20180717111219-efc7eb8984d6 h1:8/+Y8SKf0xCZ8cCTfnrMdY7HNzlEjPAt3bPjalNb6CA= 23 | github.com/mailru/easyjson v0.0.0-20180717111219-efc7eb8984d6/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 24 | github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= 25 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 26 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 27 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 28 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= 29 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 30 | github.com/olivere/elastic v6.1.25+incompatible h1:Uo0bvFxzToTi0ukxheyzqRaAiz+Q+O/bZ/5/biX5Noc= 31 | github.com/olivere/elastic v6.1.25+incompatible/go.mod h1:J+q1zQJTgAz9woqsbVRqGeB5G1iqDKVBWLNSYW8yfJ8= 32 | github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= 33 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 34 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 35 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 36 | github.com/prometheus/client_golang v0.9.0 h1:tXuTFVHC03mW0D+Ua1Q2d1EAVqLTuggX50V0VLICCzY= 37 | github.com/prometheus/client_golang v0.9.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 38 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8= 39 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 40 | github.com/prometheus/common v0.0.0-20180518154759-7600349dcfe1 h1:osmNoEW2SCW3L7EX0km2LYM8HKpNWRiouxjE3XHkyGc= 41 | github.com/prometheus/common v0.0.0-20180518154759-7600349dcfe1/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 42 | github.com/prometheus/procfs v0.0.0-20180705121852-ae68e2d4c00f h1:c9M4CCa6g8WURSsbrl3lb/w/G1Z5xZpYvhhjdcVDOkE= 43 | github.com/prometheus/procfs v0.0.0-20180705121852-ae68e2d4c00f/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 44 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 45 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 46 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 47 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA= 48 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 49 | golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 50 | qpid.apache.org v0.0.0-20190307183443-7bf92569070b h1:ulU/uc022LxD2PTeDuEb285U7xaA6SQpnBcD9V0UnAk= 51 | qpid.apache.org v0.0.0-20190307183443-7bf92569070b/go.mod h1:LcmRBnAgu/8vrdGyrBmSmmLVRjUmahQY0AAvNkSmBYk= 52 | -------------------------------------------------------------------------------- /internal/pkg/amqp10/receiver.go: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | */ 19 | 20 | package amqp10 21 | 22 | import ( 23 | "fmt" 24 | "log" 25 | "os" 26 | "os/signal" 27 | "reflect" 28 | "strings" 29 | "sync" 30 | 31 | "github.com/infrawatch/smart-gateway/internal/pkg/cacheutil" 32 | "github.com/infrawatch/smart-gateway/internal/pkg/saconfig" 33 | "github.com/prometheus/client_golang/prometheus" 34 | "qpid.apache.org/amqp" 35 | "qpid.apache.org/electron" 36 | ) 37 | 38 | var debugr = func(format string, data ...interface{}) {} // Default no debugging output 39 | 40 | //AMQPServer msgcount -1 is infinite 41 | type AMQPServer struct { 42 | urlStr string 43 | debug bool 44 | msgcount int 45 | notifier chan string 46 | status chan int 47 | done chan bool 48 | connection electron.Connection 49 | method func(s *AMQPServer) (electron.Receiver, error) 50 | prefetch int 51 | amqpHandler *AMQPHandler 52 | uniqueName string 53 | reconnect bool 54 | collectinterval float64 55 | } 56 | 57 | //AMQPServerItem hold information about data source which is AMQPServer listening to. 58 | type AMQPServerItem struct { 59 | Server *AMQPServer 60 | DataSource saconfig.DataSource 61 | } 62 | 63 | //AMQPHandler ... 64 | type AMQPHandler struct { 65 | totalCount int 66 | totalProcessed int 67 | totalReconnectCount int 68 | totalCountDesc *prometheus.Desc 69 | totalProcessedDesc *prometheus.Desc 70 | totalReconnectCountDesc *prometheus.Desc 71 | } 72 | 73 | //NewAMQPServer ... 74 | func NewAMQPServer(urlStr string, debug bool, msgcount int, prefetch int, amqpHanlder *AMQPHandler, uniqueName string) *AMQPServer { 75 | if len(urlStr) == 0 { 76 | log.Println("No URL provided") 77 | //usage() 78 | os.Exit(1) 79 | } 80 | server := &AMQPServer{ 81 | urlStr: urlStr, 82 | debug: debug, 83 | notifier: make(chan string), 84 | status: make(chan int), 85 | done: make(chan bool), 86 | msgcount: msgcount, 87 | method: (*AMQPServer).connect, 88 | prefetch: prefetch, 89 | amqpHandler: amqpHanlder, 90 | uniqueName: uniqueName, 91 | reconnect: false, 92 | collectinterval: 30, 93 | } 94 | 95 | if debug { 96 | debugr = func(format string, data ...interface{}) { 97 | log.Printf(format, data...) 98 | } 99 | } 100 | // Spawn off the server's main loop immediately 101 | // not exported 102 | go server.start() 103 | 104 | return server 105 | } 106 | 107 | //GetHandler ... 108 | func (s *AMQPServer) GetHandler() *AMQPHandler { 109 | return s.amqpHandler 110 | } 111 | 112 | //NewAMQPHandler ... 113 | func NewAMQPHandler(source string) *AMQPHandler { 114 | plabels := prometheus.Labels{} 115 | plabels["source"] = source 116 | return &AMQPHandler{ 117 | totalCount: 0, 118 | totalProcessed: 0, 119 | totalReconnectCount: 0, 120 | totalCountDesc: prometheus.NewDesc("collectd_total_amqp_message_recv_count", 121 | "Total count of amqp message received.", 122 | nil, plabels, 123 | ), 124 | totalProcessedDesc: prometheus.NewDesc("collectd_total_amqp_processed_message_count", 125 | "Total count of amqp message processed.", 126 | nil, plabels, 127 | ), 128 | totalReconnectCountDesc: prometheus.NewDesc("collectd_total_amqp_reconnect_count", 129 | "Total count of amqp reconnection .", 130 | nil, plabels, 131 | ), 132 | } 133 | } 134 | 135 | //IncTotalMsgRcv ... 136 | func (a *AMQPHandler) IncTotalMsgRcv() { 137 | a.totalCount++ 138 | } 139 | 140 | //IncTotalMsgProcessed ... 141 | func (a *AMQPHandler) IncTotalMsgProcessed() { 142 | a.totalProcessed++ 143 | } 144 | 145 | //IncTotalReconnectCount ... 146 | func (a *AMQPHandler) IncTotalReconnectCount() { 147 | a.totalReconnectCount++ 148 | } 149 | 150 | //GetTotalMsgRcv ... 151 | func (a *AMQPHandler) GetTotalMsgRcv() int { 152 | return a.totalCount 153 | } 154 | 155 | //GetTotalMsgProcessed ... 156 | func (a *AMQPHandler) GetTotalMsgProcessed() int { 157 | return a.totalProcessed 158 | } 159 | 160 | //GetTotalReconnectCount ... 161 | func (a *AMQPHandler) GetTotalReconnectCount() int { 162 | return a.totalReconnectCount 163 | } 164 | 165 | //Describe ... 166 | func (a *AMQPHandler) Describe(ch chan<- *prometheus.Desc) { 167 | ch <- a.totalCountDesc 168 | ch <- a.totalProcessedDesc 169 | ch <- a.totalReconnectCountDesc 170 | } 171 | 172 | //Collect implements prometheus.Collector. 173 | func (a *AMQPHandler) Collect(ch chan<- prometheus.Metric) { 174 | ch <- prometheus.MustNewConstMetric(a.totalCountDesc, prometheus.CounterValue, float64(a.totalCount)) 175 | ch <- prometheus.MustNewConstMetric(a.totalProcessedDesc, prometheus.CounterValue, float64(a.totalProcessed)) 176 | ch <- prometheus.MustNewConstMetric(a.totalReconnectCountDesc, prometheus.CounterValue, float64(a.totalReconnectCount)) 177 | } 178 | 179 | //GetNotifier Get notifier 180 | func (s *AMQPServer) GetNotifier() chan string { 181 | return s.notifier 182 | } 183 | 184 | //GetStatus Get Status 185 | func (s *AMQPServer) GetStatus() chan int { 186 | return s.status 187 | } 188 | 189 | //GetDoneChan ... 190 | func (s *AMQPServer) GetDoneChan() chan bool { 191 | return s.done 192 | } 193 | 194 | //Close connections it is exported so users can force close 195 | func (s *AMQPServer) Close() { 196 | s.connection.Close(nil) 197 | debugr("Debug: close receiver connection %s", s.connection) 198 | } 199 | 200 | //UpdateMinCollectInterval ... 201 | func (s *AMQPServer) UpdateMinCollectInterval(interval float64) { 202 | if interval < s.collectinterval { 203 | s.collectinterval = interval 204 | } 205 | } 206 | 207 | //start starts amqp server 208 | func (s *AMQPServer) start() { 209 | msgBuffCount := 10 210 | if s.msgcount > 0 { 211 | msgBuffCount = s.msgcount 212 | } 213 | messages := make(chan amqp.Message, msgBuffCount) // Channel for messages from goroutines to main() 214 | connectionStatus := make(chan int) 215 | done := make(chan bool) 216 | 217 | defer close(done) 218 | defer close(messages) 219 | defer close(connectionStatus) 220 | 221 | go func() { 222 | r, err := s.method(s) 223 | if err != nil { 224 | log.Fatalf("Could not connect to Qpid-dispatch router. is it running? : %v", err) 225 | } 226 | connectionStatus <- 1 227 | untilCount := s.msgcount 228 | theloop: 229 | for { 230 | if rm, err := r.Receive(); err == nil { 231 | rm.Accept() 232 | debugr("Message ACKed: %v", rm.Message) 233 | messages <- rm.Message 234 | } else if err == electron.Closed { 235 | log.Printf("Channel closed...\n") 236 | return 237 | } else { 238 | log.Fatalf("Received error %v: %v", s.urlStr, err) 239 | } 240 | if untilCount > 0 { 241 | untilCount-- 242 | } 243 | if untilCount == 0 { 244 | break theloop 245 | } 246 | } 247 | done <- true 248 | s.done <- true 249 | s.Close() 250 | log.Println("Closed AMQP...letting loop know") 251 | }() 252 | 253 | msgloop: 254 | for { 255 | select { 256 | case <-done: 257 | debugr("Done received...\n") 258 | break msgloop 259 | case m := <-messages: 260 | debugr("Message received... %v\n", m.Body()) 261 | handler := s.GetHandler() 262 | if handler != nil { 263 | handler.IncTotalMsgRcv() 264 | } 265 | switch msg := m.Body().(type) { 266 | case amqp.Binary: 267 | s.notifier <- msg.String() 268 | case string: 269 | s.notifier <- msg 270 | default: 271 | // do nothing and report 272 | log.Printf("Invalid type of AMQP message received: %t", msg) 273 | } 274 | case status := <-connectionStatus: 275 | debugr("Status received...\n") 276 | s.status <- status 277 | } 278 | } 279 | } 280 | 281 | //connect Connect to an AMQP server returning an electron receiver 282 | func (s *AMQPServer) connect() (electron.Receiver, error) { 283 | // Wait for one goroutine per URL 284 | // Make name unique-ish 285 | container := electron.NewContainer(fmt.Sprintf("rcv[%v]", s.uniqueName)) 286 | url, err := amqp.ParseURL(s.urlStr) 287 | fatalIf(err) 288 | c, err := container.Dial("tcp", url.Host) // NOTE: Dial takes just the Host part of the URL 289 | if err != nil { 290 | log.Printf("AMQP Dial tcp %v\n", err) 291 | return nil, err 292 | } 293 | 294 | s.connection = c // Save connection so we can Close() when start() ends 295 | 296 | addr := strings.TrimPrefix(url.Path, "/") 297 | opts := []electron.LinkOption{electron.Source(addr)} 298 | if s.prefetch > 0 { 299 | debugr("Amqp Prefetch set to %d\n", s.prefetch) 300 | opts = append(opts, electron.Capacity(s.prefetch), electron.Prefetch(true)) 301 | } 302 | 303 | r, err := c.Receiver(opts...) 304 | return r, err 305 | } 306 | 307 | func fatalIf(err error) { 308 | if err != nil { 309 | log.Fatal(err) 310 | } 311 | } 312 | 313 | //SpawnSignalHandler spawns goroutine which will wait for interruption signal(s) 314 | // and end smart gateway in case any of the signal is received 315 | func SpawnSignalHandler(finish chan bool, watchedSignals ...os.Signal) { 316 | interruptChannel := make(chan os.Signal, 1) 317 | signal.Notify(interruptChannel, watchedSignals...) 318 | go func() { 319 | signalLoop: 320 | for sig := range interruptChannel { 321 | log.Printf("Stopping execution on caught signal: %+v\n", sig) 322 | close(finish) 323 | break signalLoop 324 | } 325 | }() 326 | } 327 | 328 | //SpawnQpidStatusReporter builds dynamic select for reporting status of AMQP connections 329 | func SpawnQpidStatusReporter(wg *sync.WaitGroup, applicationHealth *cacheutil.ApplicationHealthCache, qpidStatusCases []reflect.SelectCase) { 330 | wg.Add(1) 331 | go func() { 332 | defer wg.Done() 333 | finishCase := len(qpidStatusCases) - 1 334 | statusLoop: 335 | for { 336 | switch index, status, _ := reflect.Select(qpidStatusCases); index { 337 | case finishCase: 338 | break statusLoop 339 | default: 340 | // Note: status here is always very low integer, so we don't need to be afraid of int64>int conversion 341 | applicationHealth.QpidRouterState = int(status.Int()) 342 | } 343 | } 344 | log.Println("Closing QPID status reporter") 345 | }() 346 | } 347 | 348 | //CreateMessageLoopComponents creates signal select cases for configured AMQP1.0 connections and connects to all of thos 349 | func CreateMessageLoopComponents(config interface{}, finish chan bool, amqpHandler *AMQPHandler, uniqueName string) ([]reflect.SelectCase, []reflect.SelectCase, []AMQPServerItem) { 350 | var ( 351 | debug bool 352 | prefetch int 353 | connections []saconfig.AMQPConnection 354 | ) 355 | switch conf := config.(type) { 356 | case *saconfig.EventConfiguration: 357 | debug = conf.Debug 358 | prefetch = conf.Prefetch 359 | connections = conf.AMQP1Connections 360 | case *saconfig.MetricConfiguration: 361 | debug = conf.Debug 362 | prefetch = conf.Prefetch 363 | connections = conf.AMQP1Connections 364 | default: 365 | panic("Invalid type of configuration file struct.") 366 | } 367 | 368 | processingCases := make([]reflect.SelectCase, 0, len(connections)) 369 | qpidStatusCases := make([]reflect.SelectCase, 0, len(connections)) 370 | amqpServers := make([]AMQPServerItem, 0, len(connections)) 371 | for _, conn := range connections { 372 | amqpServer := NewAMQPServer(conn.URL, debug, -1, prefetch, amqpHandler, uniqueName) 373 | //create select case for this listener 374 | processingCases = append(processingCases, reflect.SelectCase{ 375 | Dir: reflect.SelectRecv, 376 | Chan: reflect.ValueOf(amqpServer.GetNotifier()), 377 | }) 378 | qpidStatusCases = append(qpidStatusCases, reflect.SelectCase{ 379 | Dir: reflect.SelectRecv, 380 | Chan: reflect.ValueOf(amqpServer.GetStatus()), 381 | }) 382 | amqpServers = append(amqpServers, AMQPServerItem{amqpServer, conn.DataSourceID}) 383 | } 384 | log.Println("Listening for AMQP1.0 messages") 385 | // include also case for finishing the loops 386 | processingCases = append(processingCases, reflect.SelectCase{ 387 | Dir: reflect.SelectRecv, 388 | Chan: reflect.ValueOf(finish), 389 | }) 390 | qpidStatusCases = append(qpidStatusCases, reflect.SelectCase{ 391 | Dir: reflect.SelectRecv, 392 | Chan: reflect.ValueOf(finish), 393 | }) 394 | return processingCases, qpidStatusCases, amqpServers 395 | } 396 | -------------------------------------------------------------------------------- /internal/pkg/amqp10/sender.go: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | */ 19 | 20 | package amqp10 21 | 22 | import ( 23 | "fmt" 24 | "log" 25 | "os" 26 | "strings" 27 | 28 | "qpid.apache.org/amqp" 29 | "qpid.apache.org/electron" 30 | ) 31 | 32 | var debugsf = func(format string, data ...interface{}) {} // Default no debugging output 33 | 34 | //AMQPSender msgcount -1 is infinite 35 | type AMQPSender struct { 36 | urlStr string 37 | debug bool 38 | connections chan electron.Connection 39 | acks chan electron.Outcome 40 | } 41 | 42 | //NewAMQPSender ... 43 | func NewAMQPSender(urlStr string, debug bool) *AMQPSender { 44 | if len(urlStr) == 0 { 45 | log.Println("No URL provided") 46 | //usage() 47 | os.Exit(1) 48 | } 49 | server := &AMQPSender{ 50 | urlStr: urlStr, 51 | debug: debug, 52 | connections: make(chan electron.Connection, 1), 53 | acks: make(chan electron.Outcome), 54 | } 55 | // Spawn off the server's main loop immediately 56 | // not exported 57 | if debug { 58 | debugsf = func(format string, data ...interface{}) { log.Printf(format, data...) } 59 | } 60 | 61 | return server 62 | } 63 | 64 | //Close connections it is exported so users can force close 65 | func (as *AMQPSender) Close() { 66 | c := <-as.connections 67 | c.Close(nil) 68 | debugsf("Debug: close sender connection %s", c) 69 | } 70 | 71 | // GetAckChannel returns electron.Outcome channel for receiving ACK when debug mode is turned on 72 | func (as *AMQPSender) GetAckChannel() chan electron.Outcome { 73 | return as.acks 74 | } 75 | 76 | //Send starts amqp server 77 | func (as *AMQPSender) Send(jsonmsg string) { 78 | debugsf("Debug: AMQP send is invoked") 79 | go func(body string) { 80 | container := electron.NewContainer(fmt.Sprintf("send[%v]", os.Getpid())) 81 | url, err := amqp.ParseURL(as.urlStr) 82 | fatalsIf(err) 83 | c, err := container.Dial("tcp", url.Host) // NOTE: Dial takes just the Host part of the URL 84 | fatalsIf(err) 85 | as.connections <- c // Save connection so we can Close() when start() ends 86 | addr := strings.TrimPrefix(url.Path, "/") 87 | s, err := c.Sender(electron.Target(addr)) 88 | fatalsIf(err) 89 | 90 | m := amqp.NewMessage() 91 | m.SetContentType("application/json") 92 | m.Marshal(body) 93 | 94 | debugsf("Debug:Sending alerts on a bus URL %s\n", body) 95 | 96 | if as.debug { 97 | s.SendAsync(m, as.acks, "smart-gateway-ack") 98 | } else { 99 | s.SendForget(m) 100 | } 101 | as.Close() 102 | }(jsonmsg) 103 | } 104 | 105 | func fatalsIf(err error) { 106 | if err != nil { 107 | log.Fatal(err) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /internal/pkg/api/handler.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "time" 9 | 10 | "github.com/infrawatch/smart-gateway/internal/pkg/amqp10" 11 | "github.com/infrawatch/smart-gateway/internal/pkg/cacheutil" 12 | "github.com/infrawatch/smart-gateway/internal/pkg/saconfig" 13 | "github.com/prometheus/client_golang/prometheus" 14 | ) 15 | 16 | var debugh = func(format string, data ...interface{}) {} // Default no debugging output 17 | 18 | type ( 19 | // Timestamp is a helper for (un)marhalling time 20 | Timestamp time.Time 21 | 22 | // HookMessage is the message we receive from Alertmanager 23 | HookMessage struct { 24 | Version string `json:"version"` 25 | GroupKey string `json:"groupKey"` 26 | Status string `json:"status"` 27 | Receiver string `json:"receiver"` 28 | GroupLabels map[string]string `json:"groupLabels"` 29 | CommonLabels map[string]string `json:"commonLabels"` 30 | CommonAnnotations map[string]string `json:"commonAnnotations"` 31 | ExternalURL string `json:"externalURL"` 32 | Alerts []Alert `json:"alerts"` 33 | } 34 | 35 | //Alert is a single alert. 36 | Alert struct { 37 | Labels map[string]string `json:"labels"` 38 | Annotations map[string]string `json:"annotations"` 39 | StartsAt string `json:"startsAt,omitempty"` 40 | EndsAt string `json:"EndsAt,omitempty"` 41 | } 42 | 43 | //Context ... 44 | Context struct { 45 | Config *saconfig.EventConfiguration 46 | AMQP1Sender *amqp10.AMQPSender 47 | } 48 | 49 | //Handler ... 50 | Handler struct { 51 | *Context 52 | H func(c *Context, w http.ResponseWriter, r *http.Request) (int, error) 53 | } 54 | ) 55 | 56 | //NewContext ... 57 | func NewContext(serverConfig saconfig.EventConfiguration) *Context { 58 | amqpPublishurl := fmt.Sprintf("amqp://%s", serverConfig.API.AMQP1PublishURL) 59 | amqpSender := amqp10.NewAMQPSender(amqpPublishurl, false) 60 | context := &Context{Config: &serverConfig, AMQP1Sender: amqpSender} 61 | if serverConfig.Debug { 62 | debugh = func(format string, data ...interface{}) { log.Printf(format, data...) } 63 | } 64 | return context 65 | } 66 | 67 | //ServeHTTP... 68 | func (ah Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 69 | // Updated to pass ah.appContext as a parameter to our handler type. 70 | status, err := ah.H(ah.Context, w, r) 71 | if err != nil { 72 | debugh("Debug:HTTP %d: %q", status, err) 73 | switch status { 74 | case http.StatusNotFound: 75 | http.NotFound(w, r) 76 | // And if we wanted a friendlier error page: 77 | // err := ah.renderTemplate(w, "http_404.tmpl", nil) 78 | case http.StatusInternalServerError: 79 | http.Error(w, http.StatusText(status), status) 80 | default: 81 | http.Error(w, http.StatusText(status), status) 82 | } 83 | } 84 | } 85 | 86 | //AlertHandler ... 87 | func AlertHandler(a *Context, w http.ResponseWriter, r *http.Request) (int, error) { 88 | var body HookMessage 89 | decoder := json.NewDecoder(r.Body) 90 | defer r.Body.Close() 91 | if err := decoder.Decode(&body); err != nil { 92 | http.Error(w, "invalid request body", 400) 93 | return http.StatusInternalServerError, err 94 | } 95 | 96 | debugh("API AlertHandler Body%#v\n", body) 97 | out, err := json.Marshal(body) 98 | if err != nil { 99 | panic(err) 100 | } 101 | debugh("Debug:Sending alerts to to AMQP") 102 | debugh("Debug:Alert on AMQP%#v\n", string(out)) 103 | a.AMQP1Sender.Send(string(out)) 104 | 105 | // We can shortcut this: since renderTemplate returns `error`, 106 | // our ServeHTTP method will return a HTTP 500 instead and won't 107 | // attempt to write a broken template out with a HTTP 200 status. 108 | // (see the postscript for how renderTemplate is implemented) 109 | // If it doesn't return an error, things will go as planned. 110 | return http.StatusOK, nil 111 | } 112 | 113 | //MetricHandler ...metric handlers 114 | type MetricHandler struct { 115 | applicationHealth *cacheutil.ApplicationHealthCache 116 | lastPull *prometheus.Desc 117 | qpidRouterState *prometheus.Desc 118 | } 119 | 120 | //EventMetricHandler .... 121 | type EventMetricHandler struct { 122 | applicationHealth *cacheutil.ApplicationHealthCache 123 | lastPull *prometheus.Desc 124 | qpidRouterState *prometheus.Desc 125 | elasticSearchState *prometheus.Desc 126 | } 127 | 128 | //NewAppStateMetricHandler ... 129 | func NewAppStateMetricHandler(applicationHealth *cacheutil.ApplicationHealthCache) *MetricHandler { 130 | plabels := prometheus.Labels{} 131 | plabels["source"] = "Metric Listener" 132 | return &MetricHandler{ 133 | applicationHealth: applicationHealth, 134 | lastPull: prometheus.NewDesc("collectd_last_pull_timestamp_seconds", 135 | "Unix timestamp of the last metrics pull in seconds.", 136 | nil, plabels, 137 | ), 138 | qpidRouterState: prometheus.NewDesc("collectd_qpid_router_status", 139 | "Metric listener router status ", 140 | nil, plabels, 141 | ), 142 | } 143 | } 144 | 145 | //NewAppStateEventMetricHandler ... 146 | func NewAppStateEventMetricHandler(applicationHealth *cacheutil.ApplicationHealthCache) *EventMetricHandler { 147 | plabels := prometheus.Labels{} 148 | plabels["source"] = "Event Listener" 149 | 150 | return &EventMetricHandler{ 151 | applicationHealth: applicationHealth, 152 | lastPull: prometheus.NewDesc("collectd_last_pull_timestamp_seconds", 153 | "Unix timestamp of the last event listener pull in seconds.", 154 | nil, plabels, 155 | ), 156 | qpidRouterState: prometheus.NewDesc("collectd_qpid_router_status", 157 | "Event listener router status ", 158 | nil, plabels, 159 | ), 160 | elasticSearchState: prometheus.NewDesc("collectd_elasticsearch_status", 161 | "Event listener ElasticSearch status ", 162 | nil, plabels, 163 | ), 164 | } 165 | } 166 | 167 | // Describe implements prometheus.Collector. 168 | func (metricHandler *MetricHandler) Describe(ch chan<- *prometheus.Desc) { 169 | ch <- metricHandler.lastPull 170 | ch <- metricHandler.qpidRouterState 171 | } 172 | 173 | // Describe implements prometheus.Collector. 174 | func (eventMetricHandler *EventMetricHandler) Describe(ch chan<- *prometheus.Desc) { 175 | ch <- eventMetricHandler.lastPull 176 | ch <- eventMetricHandler.qpidRouterState 177 | ch <- eventMetricHandler.elasticSearchState 178 | } 179 | 180 | // Collect implements prometheus.Collector. 181 | func (metricHandler *MetricHandler) Collect(ch chan<- prometheus.Metric) { 182 | ch <- prometheus.MustNewConstMetric(metricHandler.lastPull, prometheus.GaugeValue, float64(time.Now().Unix())) 183 | ch <- prometheus.MustNewConstMetric(metricHandler.qpidRouterState, prometheus.GaugeValue, float64(metricHandler.applicationHealth.QpidRouterState)) 184 | } 185 | 186 | // Collect implements prometheus.Collector. 187 | func (eventMetricHandler *EventMetricHandler) Collect(ch chan<- prometheus.Metric) { 188 | ch <- prometheus.MustNewConstMetric(eventMetricHandler.lastPull, prometheus.GaugeValue, float64(time.Now().Unix())) 189 | ch <- prometheus.MustNewConstMetric(eventMetricHandler.qpidRouterState, prometheus.GaugeValue, float64(eventMetricHandler.applicationHealth.QpidRouterState)) 190 | ch <- prometheus.MustNewConstMetric(eventMetricHandler.elasticSearchState, prometheus.GaugeValue, float64(eventMetricHandler.applicationHealth.ElasticSearchState)) 191 | } 192 | -------------------------------------------------------------------------------- /internal/pkg/cacheutil/cacheserver.go: -------------------------------------------------------------------------------- 1 | package cacheutil 2 | 3 | import ( 4 | "log" 5 | "sync" 6 | "time" 7 | 8 | "github.com/infrawatch/smart-gateway/internal/pkg/metrics/incoming" 9 | ) 10 | 11 | // MAXTTL to remove plugin is stale for 5 12 | var MAXTTL int64 = 300 13 | var freeList = make(chan *IncomingBuffer, 1000) 14 | var debugc = func(format string, data ...interface{}) {} // Default no debugging output 15 | 16 | //ApplicationHealthCache ... 17 | type ApplicationHealthCache struct { 18 | QpidRouterState int 19 | ElasticSearchState int 20 | LastAccess int64 //timestamp in seconds 21 | } 22 | 23 | //IncomingBuffer this is inut data send to cache server 24 | //IncomingBuffer ..its of type collectd or anything else 25 | type IncomingBuffer struct { 26 | data incoming.MetricDataFormat 27 | } 28 | 29 | //IncomingDataCache cache server converts it into this 30 | type IncomingDataCache struct { 31 | hosts map[string]*ShardedIncomingDataCache 32 | maxTTL int64 33 | lock *sync.RWMutex 34 | } 35 | 36 | //ShardedIncomingDataCache types of sharded cache collectd, influxdb etc 37 | //ShardedIncomingDataCache .. 38 | type ShardedIncomingDataCache struct { 39 | plugin map[string]incoming.MetricDataFormat 40 | lastAccess int64 41 | maxTTL int64 42 | lock *sync.RWMutex 43 | } 44 | 45 | //NewApplicationHealthCache .. 46 | func NewApplicationHealthCache() *ApplicationHealthCache { 47 | return &ApplicationHealthCache{ 48 | QpidRouterState: 0, 49 | LastAccess: 0, 50 | ElasticSearchState: 0, 51 | } 52 | } 53 | 54 | //NewCache .. . 55 | func NewCache(maxttl int64) IncomingDataCache { 56 | if maxttl == 0 { 57 | maxttl = MAXTTL 58 | } 59 | return IncomingDataCache{ 60 | hosts: make(map[string]*ShardedIncomingDataCache), 61 | maxTTL: maxttl, 62 | lock: new(sync.RWMutex), 63 | } 64 | } 65 | 66 | //NewShardedIncomingDataCache . 67 | func NewShardedIncomingDataCache(maxttl int64) *ShardedIncomingDataCache { 68 | return &ShardedIncomingDataCache{ 69 | plugin: make(map[string]incoming.MetricDataFormat), 70 | maxTTL: maxttl, 71 | lock: new(sync.RWMutex), 72 | } 73 | } 74 | 75 | //FlushAll Flush raw meterics data 76 | func (i *IncomingDataCache) FlushAll() { 77 | lock, allHosts := i.GetHosts() 78 | defer lock.Unlock() 79 | willDelete := []string{} 80 | for key, plugin := range allHosts { 81 | //fmt.Fprintln(w, hostname) 82 | plugin.FlushAllMetrics() 83 | //this will clean up all zero plugins 84 | if plugin.Size() == 0 { 85 | willDelete = append(willDelete, key) 86 | } 87 | } 88 | for _, key := range willDelete { 89 | delete(allHosts, key) 90 | log.Printf("Cleaned up host for %s", key) 91 | } 92 | } 93 | 94 | //Put .. 95 | func (i IncomingDataCache) Put(key string) { 96 | i.lock.Lock() 97 | defer i.lock.Unlock() 98 | i.hosts[key] = NewShardedIncomingDataCache(i.maxTTL) 99 | } 100 | 101 | // GetHosts locks the cache and returns the whole cache together with the lock. Caller needs 102 | // to explicitly unlocks after the operation is done. 103 | func (i IncomingDataCache) GetHosts() (*sync.RWMutex, map[string]*ShardedIncomingDataCache) { 104 | i.lock.Lock() 105 | return i.lock, i.hosts 106 | } 107 | 108 | //GetLastAccess ..Get last access time ... 109 | func (shard *ShardedIncomingDataCache) GetLastAccess() int64 { 110 | return shard.lastAccess 111 | } 112 | 113 | //Expired ... add expired test 114 | func (shard *ShardedIncomingDataCache) Expired() bool { 115 | //clean up if data is not access for max TTL specified 116 | return time.Now().Unix()-shard.GetLastAccess() > int64(shard.maxTTL) 117 | } 118 | 119 | //GetShard .. 120 | func (i IncomingDataCache) GetShard(key string) *ShardedIncomingDataCache { 121 | if i.hosts[key] == nil { 122 | i.Put(key) 123 | } 124 | return i.hosts[key] 125 | } 126 | 127 | //GetData ... 128 | func (shard *ShardedIncomingDataCache) GetData(itemKey string) incoming.MetricDataFormat { 129 | shard.lock.Lock() 130 | defer shard.lock.Unlock() 131 | return shard.plugin[itemKey] 132 | } 133 | 134 | //Size no of plugin per shard 135 | func (i IncomingDataCache) Size() int { 136 | i.lock.RLock() 137 | defer i.lock.RUnlock() 138 | return len(i.hosts) 139 | } 140 | 141 | //Size no of plugin per shard 142 | func (shard *ShardedIncomingDataCache) Size() int { 143 | shard.lock.RLock() 144 | defer shard.lock.RUnlock() 145 | return len(shard.plugin) 146 | } 147 | 148 | //SetData ... 149 | //value as is saved under in DataCache 150 | func (shard *ShardedIncomingDataCache) SetData(data incoming.MetricDataFormat) error { 151 | shard.lock.Lock() 152 | defer shard.lock.Unlock() 153 | if shard.plugin[data.GetItemKey()] == nil { 154 | shard.plugin[data.GetItemKey()] = incoming.NewFromDataSourceName(data.GetDataSourceName()) 155 | } 156 | shard.lastAccess = time.Now().Unix() 157 | metric := shard.plugin[data.GetItemKey()] 158 | metric.SetData(data) 159 | return nil 160 | } 161 | 162 | //CacheServer .. 163 | type CacheServer struct { 164 | cache IncomingDataCache 165 | ch chan *IncomingBuffer 166 | } 167 | 168 | //GetCache Get All hosts 169 | func (cs *CacheServer) GetCache() *IncomingDataCache { 170 | return &cs.cache 171 | } 172 | 173 | //NewCacheServer ... 174 | func NewCacheServer(maxTTL int64, debug bool) *CacheServer { 175 | server := &CacheServer{ 176 | cache: NewCache(maxTTL), 177 | ch: make(chan *IncomingBuffer, 1000), 178 | } 179 | if debug { 180 | debugc = func(format string, data ...interface{}) { log.Printf(format, data...) } 181 | } 182 | // Spawn off the server's main loop immediately 183 | go server.loop() 184 | return server 185 | } 186 | 187 | //Put .. 188 | func (cs *CacheServer) Put(incomingData incoming.MetricDataFormat) { 189 | var buffer *IncomingBuffer 190 | select { 191 | case buffer = <-freeList: 192 | //go one from buffer 193 | default: 194 | buffer = &IncomingBuffer{} 195 | } 196 | buffer.data = incomingData 197 | cs.ch <- buffer 198 | } 199 | 200 | func (cs CacheServer) loop() { 201 | debugc("Debug:CacheServer loop started") 202 | for { 203 | // Reuse buffer if there's room. 204 | buffer := <-cs.ch 205 | shard := cs.cache.GetShard(buffer.data.GetKey()) 206 | shard.SetData(buffer.data) 207 | select { 208 | case freeList <- buffer: 209 | // Buffer on free list; nothing more to do. 210 | default: 211 | // Free list full, just carry on. 212 | } 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /internal/pkg/cacheutil/processcache.go: -------------------------------------------------------------------------------- 1 | package cacheutil 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/infrawatch/smart-gateway/internal/pkg/tsdb" 7 | "github.com/prometheus/client_golang/prometheus" 8 | ) 9 | 10 | //AddHeartBeat ... 11 | func AddHeartBeat(instance string, value float64, ch chan<- prometheus.Metric) { 12 | m, err := tsdb.NewHeartBeatMetricByHost(instance, value) 13 | if err != nil { 14 | log.Printf("newHeartBeat: %v for %s", err, instance) 15 | } 16 | ch <- m 17 | } 18 | 19 | //AddMetricsByHostCount ... 20 | func AddMetricsByHostCount(instance string, value float64, ch chan<- prometheus.Metric) { 21 | m, err := tsdb.AddMetricsByHost(instance, value) 22 | if err != nil { 23 | log.Printf("AddMetricsByHost: %v for %s", err, instance) 24 | } 25 | ch <- m 26 | } 27 | 28 | //FlushPrometheusMetric generate Prometheus metrics 29 | func (shard *ShardedIncomingDataCache) FlushPrometheusMetric(usetimestamp bool, ch chan<- prometheus.Metric) int { 30 | shard.lock.Lock() 31 | defer shard.lock.Unlock() 32 | minMetricCreated := 0 //..minimum of one metrics created 33 | 34 | for _, dataInterface := range shard.plugin { 35 | if dataInterface.ISNew() { 36 | dataInterface.SetNew(false) 37 | for index := range dataInterface.GetValues() { 38 | m, err := tsdb.NewPrometheusMetric(usetimestamp, dataInterface.GetDataSourceName(), dataInterface, index) 39 | if err != nil { 40 | log.Printf("newMetric: %v", err) 41 | continue 42 | } 43 | ch <- m 44 | minMetricCreated++ 45 | } 46 | } else { 47 | //clean up if data is not access for max TTL specified 48 | if shard.Expired() { 49 | delete(shard.plugin, dataInterface.GetItemKey()) 50 | } 51 | } 52 | 53 | } 54 | return minMetricCreated 55 | } 56 | 57 | //FlushAllMetrics Generic Flushing metrics not used.. used only for testing 58 | func (shard *ShardedIncomingDataCache) FlushAllMetrics() { 59 | shard.lock.Lock() 60 | defer shard.lock.Unlock() 61 | for _, dataInterface := range shard.plugin { 62 | if dataInterface.ISNew() { 63 | dataInterface.SetNew(false) 64 | log.Printf("New Metrics %#v\n", dataInterface) 65 | } else { 66 | //clean up if data is not access for max TTL specified 67 | if shard.Expired() { 68 | delete(shard.plugin, dataInterface.GetItemKey()) 69 | log.Printf("Cleaned up plugin for %s", dataInterface.GetItemKey()) 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /internal/pkg/events/events.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "flag" 7 | "fmt" 8 | "io/ioutil" 9 | "log" 10 | "math/rand" 11 | "net/http" 12 | "os" 13 | "reflect" 14 | "strconv" 15 | "sync" 16 | 17 | "github.com/MakeNowJust/heredoc" 18 | "github.com/infrawatch/smart-gateway/internal/pkg/amqp10" 19 | "github.com/infrawatch/smart-gateway/internal/pkg/api" 20 | "github.com/infrawatch/smart-gateway/internal/pkg/cacheutil" 21 | "github.com/infrawatch/smart-gateway/internal/pkg/events/incoming" 22 | "github.com/infrawatch/smart-gateway/internal/pkg/saconfig" 23 | "github.com/infrawatch/smart-gateway/internal/pkg/saelastic" 24 | "github.com/prometheus/client_golang/prometheus" 25 | "github.com/prometheus/client_golang/prometheus/promhttp" 26 | ) 27 | 28 | const ( 29 | //EVENTSINDEXTYPE value is used for creating Elasticsearch indexes holding event data 30 | EVENTSINDEXTYPE = "event" 31 | //APIHOME value contains root API endpoint content 32 | APIHOME = ` 33 | 34 | 35 | Smart Gateway Event API 36 | 37 | 38 |

API

39 |
    40 |
  • /alerts POST alerts in JSON format on to AMQP message bus
  • 41 |
  • /metrics GET metric data
  • 42 |
43 | 44 | 45 | ` 46 | ) 47 | 48 | /*************** main routine ***********************/ 49 | // eventusage and command-line flags 50 | func eventusage() { 51 | doc := heredoc.Doc(` 52 | For running with config file use 53 | ********************* config ********************* 54 | $go run cmd/main.go -config smartgateway_config.json -servicetype events 55 | **************************************************`) 56 | 57 | fmt.Fprintln(os.Stderr, `Required command line argument missing`) 58 | fmt.Fprintln(os.Stdout, doc) 59 | flag.PrintDefaults() 60 | } 61 | 62 | var debuge = func(format string, data ...interface{}) {} // Default no debugging output 63 | 64 | //spawnAPIServer spawns goroutine which provides http API for alerts and metrics statistics for Prometheus 65 | func spawnAPIServer(wg *sync.WaitGroup, finish chan bool, serverConfig saconfig.EventConfiguration, metricHandler *api.EventMetricHandler, amqpHandler *amqp10.AMQPHandler) { 66 | prometheus.MustRegister(metricHandler, amqpHandler) 67 | // Including these stats kills performance when Prometheus polls with multiple targets 68 | prometheus.Unregister(prometheus.NewProcessCollector(prometheus.ProcessCollectorOpts{})) 69 | prometheus.Unregister(prometheus.NewGoCollector()) 70 | 71 | ctxt := api.NewContext(serverConfig) 72 | http.Handle("/alert", api.Handler{Context: ctxt, H: api.AlertHandler}) 73 | http.Handle("/metrics", promhttp.Handler()) 74 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 75 | w.Write([]byte(APIHOME)) 76 | }) 77 | srv := &http.Server{Addr: serverConfig.API.APIEndpointURL} 78 | // spawn shutdown signal handler 79 | go func() { 80 | //lint:ignore S1000 reason: we are waiting for channel close, value might not be ever received 81 | select { 82 | case <-finish: 83 | if err := srv.Shutdown(context.Background()); err != nil { 84 | log.Fatalf("Failed to stop API server: %s\n", err) 85 | // in case of error we need to allow wait group to end 86 | wg.Done() 87 | } 88 | } 89 | }() 90 | // spawn the API server 91 | wg.Add(1) 92 | go func() { 93 | defer wg.Done() 94 | log.Println("Started API server") 95 | if err := srv.ListenAndServe(); err != http.ErrServerClosed { 96 | log.Fatalf("Failed to start API server: %s\n", err.Error()) 97 | } else { 98 | log.Println("Closing API server") 99 | } 100 | }() 101 | } 102 | 103 | //notifyAlertManager generates alert from event for Prometheus Alert Manager 104 | func notifyAlertManager(wg *sync.WaitGroup, serverConfig saconfig.EventConfiguration, event *incoming.EventDataFormat, record string) { 105 | wg.Add(1) 106 | go func() { 107 | defer wg.Done() 108 | generatorURL := fmt.Sprintf("%s/%s/%s/%s", serverConfig.ElasticHostURL, (*event).GetIndexName(), EVENTSINDEXTYPE, record) 109 | alert, err := (*event).GeneratePrometheusAlertBody(generatorURL) 110 | if err != nil { 111 | log.Printf("Failed generate alert from event:\n- error: %s\n- event: %s\n", err, (*event).GetSanitized()) 112 | } 113 | debuge("Debug: Generated alert:\n%s\n", alert) 114 | var byteAlertBody = []byte(fmt.Sprintf("[%s]", alert)) 115 | req, _ := http.NewRequest("POST", serverConfig.AlertManagerURL, bytes.NewBuffer(byteAlertBody)) 116 | req.Header.Set("X-Custom-Header", "smartgateway") 117 | req.Header.Set("Content-Type", "application/json") 118 | 119 | client := &http.Client{} 120 | resp, err := client.Do(req) 121 | if err != nil { 122 | log.Printf("Failed to report alert to AlertManager:\n- error: %s\n- alert: %s\n", err, alert) 123 | body, _ := ioutil.ReadAll(resp.Body) 124 | defer resp.Body.Close() 125 | debuge("Debug:response Status:%s\n", resp.Status) 126 | debuge("Debug:response Headers:%s\n", resp.Header) 127 | debuge("Debug:response Body:%s\n", string(body)) 128 | } 129 | log.Println("Closing Alert Manager notifier") 130 | }() 131 | } 132 | 133 | //StartEvents is the entry point for running smart-gateway in events mode 134 | func StartEvents() { 135 | var wg sync.WaitGroup 136 | finish := make(chan bool) 137 | 138 | amqp10.SpawnSignalHandler(finish, os.Interrupt) 139 | log.SetFlags(log.LstdFlags | log.Lshortfile) 140 | 141 | // set flags for parsing options 142 | flag.Usage = eventusage 143 | fServiceType := flag.String("servicetype", "event", "Event type") 144 | fConfigLocation := flag.String("config", "", "Path to configuration file.") 145 | fUniqueName := flag.String("uname", "events-"+strconv.Itoa(rand.Intn(100)), "Unique name across application") 146 | flag.Parse() 147 | 148 | //load configuration from given config file or from cmdline parameters 149 | var serverConfig *saconfig.EventConfiguration 150 | if len(*fConfigLocation) > 0 { 151 | conf, err := saconfig.LoadConfiguration(*fConfigLocation, "event") 152 | if err != nil { 153 | log.Fatal("Config Parse Error: ", err) 154 | } 155 | serverConfig = conf.(*saconfig.EventConfiguration) 156 | serverConfig.ServiceType = *fServiceType 157 | } else { 158 | eventusage() 159 | os.Exit(1) 160 | } 161 | 162 | if serverConfig.Debug { 163 | debuge = func(format string, data ...interface{}) { log.Printf(format, data...) } 164 | } 165 | 166 | if len(serverConfig.AMQP1EventURL) == 0 && len(serverConfig.AMQP1Connections) == 0 { 167 | log.Println("Configuration option 'AMQP1EventURL' or 'AMQP1Connections' is required") 168 | eventusage() 169 | os.Exit(1) 170 | } 171 | 172 | if len(serverConfig.ElasticHostURL) == 0 { 173 | log.Println("Configuration option 'ElasticHostURL' is required") 174 | eventusage() 175 | os.Exit(1) 176 | } else { 177 | log.Printf("Elasticsearch configured at %s\n", serverConfig.ElasticHostURL) 178 | } 179 | 180 | if len(serverConfig.AlertManagerURL) > 0 { 181 | log.Printf("AlertManager configured at %s\n", serverConfig.AlertManagerURL) 182 | serverConfig.AlertManagerEnabled = true 183 | } else { 184 | log.Println("AlertManager disabled") 185 | } 186 | 187 | if len(serverConfig.API.APIEndpointURL) > 0 { 188 | debuge("API configured at %s\n", serverConfig.API.APIEndpointURL) 189 | serverConfig.APIEnabled = true 190 | } else { 191 | log.Println("API disabled") 192 | } 193 | 194 | if len(serverConfig.API.AMQP1PublishURL) > 0 { 195 | log.Printf("AMQP1.0 publish address configured at %s\n", serverConfig.API.AMQP1PublishURL) 196 | serverConfig.PublishEventEnabled = true 197 | } else { 198 | log.Println("AMQP1.0 publish address disabled") 199 | } 200 | 201 | if len(serverConfig.AMQP1EventURL) > 0 { 202 | //TO-DO(mmagr): Remove this in next major release 203 | serverConfig.AMQP1Connections = []saconfig.AMQPConnection{ 204 | saconfig.AMQPConnection{ 205 | URL: serverConfig.AMQP1EventURL, 206 | DataSourceID: saconfig.DataSourceCollectd, 207 | DataSource: "collectd", 208 | }, 209 | } 210 | } 211 | for _, conn := range serverConfig.AMQP1Connections { 212 | log.Printf("AMQP1.0 %s listen address configured at %s\n", conn.DataSource, conn.URL) 213 | } 214 | 215 | applicationHealth := cacheutil.NewApplicationHealthCache() 216 | metricHandler := api.NewAppStateEventMetricHandler(applicationHealth) 217 | amqpHandler := amqp10.NewAMQPHandler("Event Consumer") 218 | 219 | // Elastic connection 220 | elasticClient, err := saelastic.CreateClient(*serverConfig) 221 | 222 | if err != nil { 223 | log.Fatal(err.Error()) 224 | } 225 | log.Println("Connected to Elasticsearch") 226 | applicationHealth.ElasticSearchState = 1 227 | 228 | // API spawn 229 | if serverConfig.APIEnabled { 230 | spawnAPIServer(&wg, finish, *serverConfig, metricHandler, amqpHandler) 231 | } 232 | 233 | // AMQP connection(s) 234 | processingCases, qpidStatusCases, amqpServers := amqp10.CreateMessageLoopComponents(serverConfig, finish, amqpHandler, *fUniqueName) 235 | amqp10.SpawnQpidStatusReporter(&wg, applicationHealth, qpidStatusCases) 236 | 237 | // spawn handler manager 238 | handlerManager, err := NewEventHandlerManager(*serverConfig) 239 | if err != nil { 240 | log.Fatal(err.Error()) 241 | } 242 | 243 | // spawn event processor 244 | wg.Add(1) 245 | go func() { 246 | defer wg.Done() 247 | finishCase := len(processingCases) - 1 248 | processingLoop: 249 | for { 250 | switch index, msg, _ := reflect.Select(processingCases); index { 251 | case finishCase: 252 | break processingLoop 253 | default: 254 | // NOTE: below will panic for generic data source until the appropriate logic will be implemented 255 | event := incoming.NewFromDataSource(amqpServers[index].DataSource) 256 | amqpServers[index].Server.GetHandler().IncTotalMsgProcessed() 257 | err := event.ParseEvent(msg.String()) 258 | if err != nil { 259 | log.Printf("Failed to parse received event:\n- error: %s\n- event: %s\n", err, event) 260 | } 261 | 262 | process := true 263 | for _, handler := range handlerManager.Handlers[amqpServers[index].DataSource] { 264 | if handler.Relevant(event) { 265 | process, err = handler.Handle(event, elasticClient) 266 | if !process { 267 | if err != nil { 268 | log.Print(err.Error()) 269 | } 270 | break 271 | } 272 | } 273 | } 274 | if process { 275 | record, err := elasticClient.Create(event.GetIndexName(), EVENTSINDEXTYPE, event.GetRawData()) 276 | if err != nil { 277 | applicationHealth.ElasticSearchState = 0 278 | log.Printf("Failed to save event to Elasticsearch DB:\n- error: %s\n- event: %s\n", err, event) 279 | } else { 280 | applicationHealth.ElasticSearchState = 1 281 | } 282 | if serverConfig.AlertManagerEnabled { 283 | notifyAlertManager(&wg, *serverConfig, &event, record) 284 | } 285 | } 286 | } 287 | } 288 | log.Println("Closing event processor.") 289 | }() 290 | 291 | // do not end until all loop goroutines ends 292 | wg.Wait() 293 | log.Println("Exiting") 294 | } 295 | -------------------------------------------------------------------------------- /internal/pkg/events/handlers.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/infrawatch/smart-gateway/internal/pkg/events/incoming" 8 | "github.com/infrawatch/smart-gateway/internal/pkg/saconfig" 9 | "github.com/infrawatch/smart-gateway/internal/pkg/saelastic" 10 | ) 11 | 12 | // TODO: Implement this as pluggable system instead 13 | 14 | //EventHandler provides interface for all possible handler types 15 | type EventHandler interface { 16 | //Processes the event 17 | Handle(incoming.EventDataFormat, *saelastic.ElasticClient) (bool, error) 18 | //Relevant should return true if the handler is relevant for the givent event and so the handler should be used 19 | Relevant(incoming.EventDataFormat) bool 20 | } 21 | 22 | //EventHandlerManager holds all available handlers (and will be responsible 23 | //in future for loading all handler plugins). The plugins will be organize 24 | //per data source on which's events they could be applied 25 | type EventHandlerManager struct { 26 | Handlers map[saconfig.DataSource][]EventHandler 27 | } 28 | 29 | //NewEventHandlerManager loads all even handler plugins stated in events configuration 30 | func NewEventHandlerManager(config saconfig.EventConfiguration) (*EventHandlerManager, error) { 31 | manager := EventHandlerManager{} 32 | manager.Handlers = make(map[saconfig.DataSource][]EventHandler) 33 | for _, ds := range []saconfig.DataSource{saconfig.DataSourceCollectd, saconfig.DataSourceCeilometer, saconfig.DataSourceUniversal} { 34 | manager.Handlers[ds] = make([]EventHandler, 0) 35 | } 36 | 37 | for _, pluginPath := range config.HandlerPlugins { 38 | var ds saconfig.DataSource 39 | if ok := ds.SetFromString(pluginPath.DataSource); !ok { 40 | return &manager, fmt.Errorf("unknown datasource ''%s' for given event handler", pluginPath.DataSource) 41 | } 42 | manager.LoadHandlers(ds, pluginPath.Path) 43 | } 44 | 45 | //TODO: this just manually register the only handler we have now. Remove when the handler implementation will move out to plugin 46 | manager.Handlers[saconfig.DataSourceCollectd] = append(manager.Handlers[saconfig.DataSourceCollectd], ContainerHealthCheckHandler{"collectd_checks"}) 47 | return &manager, nil 48 | } 49 | 50 | //LoadHandlers will load handler plugins in future 51 | func (hand *EventHandlerManager) LoadHandlers(dataSource saconfig.DataSource, path string) error { 52 | 53 | return nil 54 | } 55 | 56 | //ContainerHealthCheckHandler serves as handler for events from collectd-sensubility's 57 | //results of check-container-health. 58 | type ContainerHealthCheckHandler struct { 59 | ElasticIndex string 60 | } 61 | 62 | type containerHealthCheckItem struct { 63 | Container string `json:"container"` 64 | Service string `json:"service"` 65 | Status string `json:"status"` 66 | Healthy int `json:"healthy"` 67 | } 68 | 69 | type list struct { 70 | Next *list 71 | Key string 72 | } 73 | 74 | //recursivle search for output field based on path 75 | func getOutputObject(path *list, data interface{}) (string, error) { 76 | var obj map[string]interface{} 77 | var ok bool 78 | 79 | if obj, ok = data.(map[string]interface{}); !ok { 80 | return "", fmt.Errorf("cannot search non-map objects") 81 | } 82 | if path.Next == nil { 83 | if output, ok := obj[path.Key].(string); ok { 84 | return output, nil 85 | } 86 | return "", fmt.Errorf("output should be of type 'string'") 87 | } 88 | if newData, ok := obj[path.Key]; ok { 89 | return getOutputObject(path.Next, newData) 90 | } 91 | return "", fmt.Errorf("input data does not contain path") 92 | } 93 | 94 | //Handle saves the event as separate document to ES in case the result output contains more than one item. 95 | //Returns true if event processing should continue (eg. event should be saved to ES) or false if otherwise. 96 | func (hand ContainerHealthCheckHandler) Handle(event incoming.EventDataFormat, elasticClient *saelastic.ElasticClient) (bool, error) { 97 | pathList := &list{ 98 | Key: "annotations", 99 | } 100 | pathList.Next = &list{ 101 | Key: "output", 102 | } 103 | 104 | if evt, ok := event.(*incoming.CollectdEvent); ok { 105 | rawData := evt.GetRawData() 106 | output, err := getOutputObject(pathList, rawData) 107 | if err != nil { 108 | return false, err 109 | } 110 | 111 | var outData []containerHealthCheckItem 112 | rawDataMap := rawData.(map[string]interface{}) 113 | if err := json.Unmarshal([]byte(output), &outData); err == nil { 114 | for _, item := range outData { 115 | rawDataMap["annotations"].(map[string]interface{})["output"] = item 116 | if _, err := elasticClient.Create(hand.ElasticIndex, EVENTSINDEXTYPE, rawDataMap); err != nil { 117 | // saving the splitted output failed for some reason, so we will play safe 118 | // and try to process event outside of handler 119 | return true, err 120 | } 121 | } 122 | } else { 123 | // We most probably received single item output, so we just proceed and save the event 124 | if _, err := elasticClient.Create(hand.ElasticIndex, EVENTSINDEXTYPE, rawData); err != nil { 125 | return false, err 126 | } 127 | } 128 | } 129 | 130 | //record, err := elasticClient.Create(event.GetIndexName(), EVENTSINDEXTYPE, event.GetRawData()) 131 | return false, nil 132 | } 133 | 134 | //Relevant returns true in case the event is suitable for processing with this handler, otherwise returns false. 135 | func (hand ContainerHealthCheckHandler) Relevant(event incoming.EventDataFormat) bool { 136 | if evt, ok := event.(*incoming.CollectdEvent); ok { 137 | rawData := evt.GetRawData() 138 | if data, ok := rawData.(map[string]interface{}); ok { 139 | if rawLabels, ok := data["labels"]; ok { 140 | if labels, ok := rawLabels.(map[string]interface{}); ok { 141 | if check, ok := labels["check"]; ok { 142 | if checkName, ok := check.(string); ok && checkName == "check-container-health" { 143 | return true 144 | } 145 | } 146 | } 147 | } 148 | } 149 | } 150 | return false 151 | } 152 | -------------------------------------------------------------------------------- /internal/pkg/events/incoming/ceilometer.go: -------------------------------------------------------------------------------- 1 | package incoming 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "regexp" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | //ceilomterGenericIndex value represents ElasticSearch index name for data from which it 13 | // is not possible to clearly construct indexs name 14 | const ceilometerGenericIndex = "ceilometer_generic" 15 | 16 | var ( 17 | rexForPayload = regexp.MustCompile(`\"payload\"\s*:\s*\[(.*)\]`) 18 | rexForOsloMessage = regexp.MustCompile(`"oslo.message"\s*:\s*"({.*})"`) 19 | ceilometerAlertSeverity = map[string]string{ 20 | "audit": "info", 21 | "info": "info", 22 | "sample": "info", 23 | "warn": "warning", 24 | "warning": "warning", 25 | "critical": "critical", 26 | "error": "critical", 27 | "AUDIT": "info", 28 | "INFO": "info", 29 | "SAMPLE": "info", 30 | "WARN": "warning", 31 | "WARNING": "warning", 32 | "CRITICAL": "critical", 33 | "ERROR": "critical", 34 | } 35 | ) 36 | 37 | //AlertKeySurrogate translates case for fields for AlertManager 38 | type AlertKeySurrogate struct { 39 | Parsed string 40 | Label string 41 | } 42 | 43 | //CeilometerEvent implements EventDataFormat interface and holds event message data from collectd. 44 | type CeilometerEvent struct { 45 | sanitized string 46 | parsed map[string]interface{} 47 | indexName string 48 | } 49 | 50 | //GetIndexName returns Elasticsearch index to which this event is or should be saved. 51 | func (evt *CeilometerEvent) GetIndexName() string { 52 | if evt.indexName == "" { 53 | result := ceilometerGenericIndex 54 | // use event_type from payload or fallback to message's event_type if N/A 55 | if payload, ok := evt.parsed["payload"]; ok { 56 | if typedPayload, ok := payload.(map[string]interface{}); ok { 57 | if val, ok := typedPayload["event_type"]; ok { 58 | if strVal, ok := val.(string); ok { 59 | result = strVal 60 | } 61 | } 62 | } 63 | } 64 | if result == ceilometerGenericIndex { 65 | if val, ok := evt.parsed["event_type"]; ok { 66 | if strVal, ok := val.(string); ok { 67 | result = strVal 68 | } 69 | } 70 | } 71 | // replace dotted notation and dashes with underscores 72 | parts := strings.Split(result, ".") 73 | if len(parts) > 1 { 74 | result = strings.Join(parts[:len(parts)-1], "_") 75 | } 76 | result = strings.ReplaceAll(result, "-", "_") 77 | if !strings.HasPrefix(result, "ceilometer_") { 78 | result = fmt.Sprintf("ceilometer_%s", result) 79 | } 80 | evt.indexName = result 81 | } 82 | return evt.indexName 83 | } 84 | 85 | //GetRawData returns sanitized and umarshalled event data. 86 | func (evt *CeilometerEvent) GetRawData() interface{} { 87 | return evt.parsed 88 | } 89 | 90 | //GetSanitized returns sanitized event data 91 | func (evt *CeilometerEvent) GetSanitized() string { 92 | return evt.sanitized 93 | } 94 | 95 | //sanitize search and removes all known issues in received data. 96 | func (evt *CeilometerEvent) sanitize(jsondata string) string { 97 | sanitized := jsondata 98 | // parse only relevant data 99 | sub := rexForOsloMessage.FindStringSubmatch(sanitized) 100 | if len(sub) == 2 { 101 | sanitized = rexForNestedQuote.ReplaceAllString(sub[1], `"`) 102 | } else { 103 | log.Printf("Failed to find oslo.message in Ceilometer event: %s\n", jsondata) 104 | } 105 | // avoid getting payload data wrapped in array 106 | item := rexForPayload.FindStringSubmatch(sanitized) 107 | if len(item) == 2 { 108 | sanitized = rexForPayload.ReplaceAllLiteralString(sanitized, fmt.Sprintf(`"payload":%s`, item[1])) 109 | } 110 | return sanitized 111 | } 112 | 113 | //ParseEvent sanitizes and unmarshals received event data. 114 | func (evt *CeilometerEvent) ParseEvent(data string) error { 115 | evt.sanitized = evt.sanitize(data) 116 | err := json.Unmarshal([]byte(evt.sanitized), &evt.parsed) 117 | if err != nil { 118 | log.Fatal(err) 119 | return err 120 | } 121 | // transforms traits key into map[string]interface{} 122 | if payload, ok := evt.parsed["payload"]; ok { 123 | newPayload := make(map[string]interface{}) 124 | if typedPayload, ok := payload.(map[string]interface{}); ok { 125 | if traitData, ok := typedPayload["traits"]; ok { 126 | if traits, ok := traitData.([]interface{}); ok { 127 | newTraits := make(map[string]interface{}) 128 | for _, value := range traits { 129 | if typedValue, ok := value.([]interface{}); ok { 130 | if len(typedValue) != 3 { 131 | return fmt.Errorf("parsed invalid trait (%v) in event: %s", value, data) 132 | } 133 | if traitType, ok := typedValue[1].(float64); ok { 134 | switch traitType { 135 | case 2: 136 | newTraits[typedValue[0].(string)] = typedValue[2].(float64) 137 | default: 138 | newTraits[typedValue[0].(string)] = typedValue[2].(string) 139 | } 140 | } else { 141 | return fmt.Errorf("parsed invalid trait (%v) in event: %s", value, data) 142 | } 143 | } else { 144 | return fmt.Errorf("parsed invalid trait (%v) in event: %s", value, data) 145 | } 146 | } 147 | newPayload["traits"] = newTraits 148 | } 149 | } 150 | for key, value := range typedPayload { 151 | if key != "traits" { 152 | newPayload[key] = value 153 | } 154 | } 155 | } 156 | (*evt).parsed["payload"] = newPayload 157 | } 158 | 159 | return nil 160 | } 161 | 162 | //GeneratePrometheusAlert generates PrometheusAlert from the event data 163 | func (evt *CeilometerEvent) GeneratePrometheusAlert(generatorURL string) PrometheusAlert { 164 | alert := PrometheusAlert{ 165 | Labels: make(map[string]string), 166 | Annotations: make(map[string]string), 167 | GeneratorURL: generatorURL, 168 | } 169 | // set labels 170 | alert.Labels["alertname"] = evt.GetIndexName() 171 | surrogates := []AlertKeySurrogate{ 172 | AlertKeySurrogate{"message_id", "messageId"}, 173 | AlertKeySurrogate{"publisher_id", "instance"}, 174 | AlertKeySurrogate{"event_type", "type"}, 175 | } 176 | for _, renameCase := range surrogates { 177 | if value, ok := evt.parsed[renameCase.Parsed]; ok { 178 | alert.Labels[renameCase.Label] = value.(string) 179 | } 180 | } 181 | if value, ok := evt.parsed["priority"]; ok { 182 | if severity, ok := ceilometerAlertSeverity[value.(string)]; ok { 183 | alert.Labels["severity"] = severity 184 | } else { 185 | alert.Labels["severity"] = unknownSeverity 186 | } 187 | } else { 188 | alert.Labels["severity"] = unknownSeverity 189 | } 190 | if value, ok := evt.parsed["publisher_id"].(string); ok { 191 | alert.Labels["sourceName"] = strings.Join([]string{"ceilometer", value}, "@") 192 | } 193 | assimilateMap(evt.parsed["payload"].(map[string]interface{}), &alert.Annotations) 194 | // set timestamp 195 | if value, ok := evt.parsed["timestamp"].(string); ok { 196 | // ensure timestamp is in RFC3339 197 | for _, layout := range []string{time.RFC3339, time.RFC3339Nano, time.ANSIC, isoTimeLayout} { 198 | stamp, err := time.Parse(layout, value) 199 | if err == nil { 200 | alert.StartsAt = stamp.Format(time.RFC3339) 201 | break 202 | } 203 | } 204 | } 205 | // generate SG-relevant data 206 | alert.SetName() 207 | alert.SetSummary() 208 | alert.Labels["alertsource"] = "SmartGateway" 209 | return alert 210 | } 211 | 212 | //GeneratePrometheusAlertBody generates alert body for Prometheus Alert manager API 213 | func (evt *CeilometerEvent) GeneratePrometheusAlertBody(generatorURL string) ([]byte, error) { 214 | return json.Marshal(evt.GeneratePrometheusAlert(generatorURL)) 215 | } 216 | -------------------------------------------------------------------------------- /internal/pkg/events/incoming/collectd.go: -------------------------------------------------------------------------------- 1 | package incoming 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "regexp" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | //collectdGenericIndex value represents ElasticSearch index name for data from which it 13 | // is not possible to clearly construct indexs name 14 | const collectdGenericIndex = "collectd_generic" 15 | 16 | var ( 17 | rexForInvalidVesStr = regexp.MustCompile(`":"[^",\\]+"[^",\\]+"`) 18 | rexForRemainedNested = regexp.MustCompile(`":"[^",]+\\\\\"[^",]+"`) 19 | rexForNestedQuote = regexp.MustCompile(`\\\"`) 20 | rexForVes = regexp.MustCompile(`"ves":"{(.*)}"`) 21 | collectdAlertSeverity = map[string]string{ 22 | "OKAY": "info", 23 | "WARNING": "warning", 24 | "FAILURE": "critical", 25 | } 26 | ) 27 | 28 | //CollectdEvent implements EventDataFormat interface and holds event message data from collectd. 29 | type CollectdEvent struct { 30 | sanitized string 31 | parsed map[string]interface{} 32 | indexName string 33 | } 34 | 35 | //GetIndexName returns Elasticsearch index to which this event is or should be saved. 36 | func (evt *CollectdEvent) GetIndexName() string { 37 | if evt.indexName == "" { 38 | result := collectdGenericIndex 39 | if val, ok := evt.parsed["labels"]; ok { 40 | switch rec := val.(type) { 41 | case map[string]interface{}: 42 | if value, ok := rec["alertname"].(string); ok { 43 | if index := strings.LastIndex(value, "_"); index > len("collectd_") { 44 | result = value[0:index] 45 | } else { 46 | result = value 47 | } 48 | } 49 | } 50 | } 51 | evt.indexName = result 52 | } 53 | return evt.indexName 54 | } 55 | 56 | //GetRawData returns sanitized and umarshalled event data. 57 | func (evt *CollectdEvent) GetRawData() interface{} { 58 | return evt.parsed 59 | } 60 | 61 | //GetSanitized returns sanitized event data 62 | func (evt *CollectdEvent) GetSanitized() string { 63 | return evt.sanitized 64 | } 65 | 66 | //sanitize search and removes all known issues in received data. 67 | func (evt *CollectdEvent) sanitize(jsondata string) string { 68 | // 1) if value for key "ves" is string, we convert it to json 69 | vesCleaned := jsondata 70 | sub := rexForVes.FindStringSubmatch(jsondata) 71 | if len(sub) == 2 { 72 | substr := sub[1] 73 | for { 74 | cleaned := rexForNestedQuote.ReplaceAllString(substr, `"`) 75 | if rexForInvalidVesStr.FindString(cleaned) == "" { 76 | substr = cleaned 77 | } 78 | if rexForRemainedNested.FindString(cleaned) == "" { 79 | break 80 | } 81 | } 82 | vesCleaned = rexForVes.ReplaceAllLiteralString(jsondata, fmt.Sprintf(`"ves":{%s}`, substr)) 83 | } 84 | // 2) event is received wrapped in array, so we remove it 85 | almostFinal := strings.TrimLeft(vesCleaned, "[") 86 | return strings.TrimRight(almostFinal, "]") 87 | } 88 | 89 | //ParseEvent sanitizes and unmarshals received event data. 90 | func (evt *CollectdEvent) ParseEvent(data string) error { 91 | evt.sanitized = evt.sanitize(data) 92 | err := json.Unmarshal([]byte(evt.sanitized), &evt.parsed) 93 | if err != nil { 94 | log.Fatal(err) 95 | return err 96 | } 97 | return nil 98 | } 99 | 100 | //assimilateMap recursively saves content of the given map to destination map of strings 101 | func assimilateMap(theMap map[string]interface{}, destination *map[string]string) { 102 | defer func() { //recover from any panic 103 | if r := recover(); r != nil { 104 | log.Printf("Panic:recovered in assimilateMap %v\n", r) 105 | } 106 | }() 107 | for key, val := range theMap { 108 | switch value := val.(type) { 109 | case map[string]interface{}: 110 | // go one level deeper in the map 111 | assimilateMap(value, destination) 112 | case []interface{}: 113 | // transform slice value to comma separated list and assimilate it 114 | aList := make([]string, 0, len(value)) 115 | for _, item := range value { 116 | if itm, ok := item.(string); ok { 117 | aList = append(aList, itm) 118 | } 119 | } 120 | (*destination)[key] = strings.Join(aList, ",") 121 | case float64, float32: 122 | (*destination)[key] = fmt.Sprintf("%f", value) 123 | case int: 124 | (*destination)[key] = fmt.Sprintf("%d", value) 125 | default: 126 | // assimilate KV pair 127 | (*destination)[key] = value.(string) 128 | } 129 | } 130 | } 131 | 132 | //GeneratePrometheusAlert generates PrometheusAlert from the event data 133 | func (evt *CollectdEvent) GeneratePrometheusAlert(generatorURL string) PrometheusAlert { 134 | alert := PrometheusAlert{ 135 | Labels: make(map[string]string), 136 | Annotations: make(map[string]string), 137 | GeneratorURL: generatorURL, 138 | } 139 | assimilateMap(evt.parsed["labels"].(map[string]interface{}), &alert.Labels) 140 | assimilateMap(evt.parsed["annotations"].(map[string]interface{}), &alert.Annotations) 141 | if value, ok := evt.parsed["startsAt"].(string); ok { 142 | // ensure timestamps is in RFC3339 143 | for _, layout := range []string{time.RFC3339, time.RFC3339Nano, time.ANSIC, isoTimeLayout} { 144 | stamp, err := time.Parse(layout, value) 145 | if err == nil { 146 | alert.StartsAt = stamp.Format(time.RFC3339) 147 | break 148 | } 149 | } 150 | } 151 | 152 | if value, ok := alert.Labels["severity"]; ok { 153 | if severity, ok := collectdAlertSeverity[value]; ok { 154 | alert.Labels["severity"] = severity 155 | } else { 156 | alert.Labels["severity"] = unknownSeverity 157 | } 158 | } else { 159 | alert.Labels["severity"] = unknownSeverity 160 | } 161 | 162 | alert.SetName() 163 | assimilateMap(evt.parsed["annotations"].(map[string]interface{}), &alert.Labels) 164 | alert.SetSummary() 165 | 166 | alert.Labels["alertsource"] = "SmartGateway" 167 | return alert 168 | } 169 | 170 | //GeneratePrometheusAlertBody generates alert body for Prometheus Alert manager API 171 | func (evt *CollectdEvent) GeneratePrometheusAlertBody(generatorURL string) ([]byte, error) { 172 | return json.Marshal(evt.GeneratePrometheusAlert(generatorURL)) 173 | } 174 | -------------------------------------------------------------------------------- /internal/pkg/events/incoming/formats.go: -------------------------------------------------------------------------------- 1 | package incoming 2 | 3 | import ( 4 | "sort" 5 | "strings" 6 | 7 | "github.com/infrawatch/smart-gateway/internal/pkg/saconfig" 8 | ) 9 | 10 | const ( 11 | isoTimeLayout = "2006-01-02 15:04:05.000000" 12 | unknownSeverity = "unknown" 13 | ) 14 | 15 | //EventDataFormat interface for storing event data from various sources 16 | type EventDataFormat interface { 17 | //GetIndexName returns Elasticsearch index to which this event is or should be saved. 18 | GetIndexName() string 19 | //GetRawData returns sanitized and umarshalled event data. 20 | GetRawData() interface{} 21 | //GetSanitized returns sanitized event data 22 | GetSanitized() string 23 | //ParseEvent sanitizes and unmarshals received event data. 24 | ParseEvent(string) error 25 | //GeneratePrometheusAlertBody generates alert body for Prometheus Alert manager API 26 | GeneratePrometheusAlertBody(string) ([]byte, error) 27 | //GeneratePrometheusAlertBody generates alert struct 28 | GeneratePrometheusAlert(string) PrometheusAlert 29 | } 30 | 31 | //PrometheusAlert represents data structure used for sending alerts to Prometheus Alert Manager 32 | type PrometheusAlert struct { 33 | Labels map[string]string `json:"labels"` 34 | Annotations map[string]string `json:"annotations"` 35 | StartsAt string `json:"startsAt,omitempty"` 36 | EndsAt string `json:"endsAt,omitempty"` 37 | GeneratorURL string `json:"generatorURL"` 38 | } 39 | 40 | //SetName generates unique name and description for the alert and creates new key/value pair for it in Labels 41 | func (alert *PrometheusAlert) SetName() { 42 | if _, ok := alert.Labels["name"]; !ok { 43 | keys := make([]string, 0, len(alert.Labels)) 44 | for k := range alert.Labels { 45 | keys = append(keys, k) 46 | } 47 | sort.Strings(keys) 48 | 49 | values := make([]string, 0, len(alert.Labels)-1) 50 | desc := make([]string, 0, len(alert.Labels)) 51 | for _, k := range keys { 52 | if k != "severity" { 53 | values = append(values, alert.Labels[k]) 54 | } 55 | desc = append(desc, alert.Labels[k]) 56 | } 57 | alert.Labels["name"] = strings.Join(values, "_") 58 | alert.Annotations["description"] = strings.Join(desc, " ") 59 | } 60 | } 61 | 62 | //SetSummary generates summary annotation in case it is empty 63 | func (alert *PrometheusAlert) SetSummary() { 64 | generate := false 65 | if _, ok := alert.Annotations["summary"]; ok { 66 | if alert.Annotations["summary"] == "" { 67 | generate = true 68 | } 69 | } else { 70 | generate = true 71 | } 72 | 73 | if generate { 74 | if val, ok := alert.Labels["summary"]; ok && alert.Labels["summary"] != "" { 75 | alert.Annotations["summary"] = val 76 | } else { 77 | values := make([]string, 0, 3) 78 | for _, key := range []string{"sourceName", "type", "eventName"} { 79 | if val, ok := alert.Labels[key]; ok { 80 | values = append(values, val) 81 | } 82 | } 83 | alert.Annotations["summary"] = strings.Join(values, " ") 84 | } 85 | } 86 | } 87 | 88 | //NewFromDataSource creates empty EventDataFormat according to given DataSource 89 | func NewFromDataSource(source saconfig.DataSource) EventDataFormat { 90 | switch source { 91 | case saconfig.DataSourceCollectd: 92 | return &CollectdEvent{} 93 | case saconfig.DataSourceCeilometer: 94 | return &CeilometerEvent{} 95 | } 96 | return nil 97 | } 98 | -------------------------------------------------------------------------------- /internal/pkg/metrics/incoming/ceilometer.go: -------------------------------------------------------------------------------- 1 | package incoming 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "regexp" 7 | "strings" 8 | 9 | jsoniter "github.com/json-iterator/go" 10 | ) 11 | 12 | var ( 13 | rexForPayload = regexp.MustCompile(`\"payload\"\s*:\s*\[(.*)\]`) 14 | rexForOsloMessage = regexp.MustCompile(`"oslo.message"\s*:\s*"({.*})"`) 15 | rexForNestedQuote = regexp.MustCompile(`\\\"`) 16 | ) 17 | 18 | const defaultCeilometerInterval = 5.0 19 | 20 | // CeilometerMetric struct represents a single instance of metric data formated and sent by Ceilometer 21 | type CeilometerMetric struct { 22 | WithDataSource 23 | Publisher string `json:"publisher_id"` 24 | Payload map[string]interface{} `json:"payload"` 25 | // analogy to collectd metric 26 | Plugin string 27 | PluginInstance string 28 | Type string 29 | TypeInstance string 30 | Values []float64 31 | new bool 32 | wholeID string 33 | } 34 | 35 | /*************************** MetricDataFormat interface ****************************/ 36 | 37 | func (c *CeilometerMetric) getwholeID() string { 38 | if c.wholeID == "" { 39 | if cnt, ok := c.Payload["counter_name"]; ok { 40 | c.wholeID = cnt.(string) 41 | } else { 42 | log.Printf("Did not find counter_name in metric payload: %v\n", c.Payload) 43 | c.wholeID = "unknown" 44 | } 45 | } 46 | return c.wholeID 47 | } 48 | 49 | //GetName returns name of Ceilometer "plugin" (analogically to CollectdMetric implementation) 50 | func (c *CeilometerMetric) GetName() string { 51 | return c.Plugin 52 | } 53 | 54 | //GetValues returns Values. The purpose of this method is to be able to get metric Values 55 | //from the interface object itself 56 | func (c *CeilometerMetric) GetValues() []float64 { 57 | return c.Values 58 | } 59 | 60 | //SetData generates naming and value data analogicaly to CollectdMetric from counter data and resource_id 61 | func (c *CeilometerMetric) SetData(data MetricDataFormat) { 62 | // example: counter_name=compute.instance.booting.time, resource_id=456 63 | // get Plugin -> compute 64 | if ceilo, ok := data.(*CeilometerMetric); ok { 65 | c.Payload = ceilo.Payload 66 | c.Publisher = ceilo.Publisher 67 | 68 | plugParts := strings.Split(ceilo.getwholeID(), ".") 69 | c.Plugin = plugParts[0] 70 | // get PluginInstance -> 456 71 | if resource, ok := ceilo.Payload["resource_id"]; ok { 72 | c.PluginInstance = resource.(string) 73 | } 74 | // get Type -> instance 75 | if len(plugParts) > 1 { 76 | c.Type = plugParts[1] 77 | } else { 78 | c.Type = plugParts[0] 79 | } 80 | // get TypeInstance -> booting 81 | if len(plugParts) > 2 { 82 | c.TypeInstance = plugParts[2] 83 | } 84 | 85 | values := make([]float64, 0, 1) 86 | if val, ok := ceilo.Payload["counter_volume"]; ok { 87 | values = append(values, val.(float64)) 88 | } else { 89 | log.Printf("Did not find counter_volume in metric payload: %v\n", ceilo.Payload) 90 | } 91 | c.Values = values 92 | c.SetNew(true) 93 | } 94 | } 95 | 96 | //sanitize search and removes all known issues in received data. 97 | //TODO: Move this function to apputils 98 | func (c *CeilometerMetric) sanitize(data string) string { 99 | sanitized := data 100 | // parse only relevant data 101 | sub := rexForOsloMessage.FindStringSubmatch(sanitized) 102 | if len(sub) == 2 { 103 | sanitized = rexForNestedQuote.ReplaceAllString(sub[1], `"`) 104 | } else { 105 | log.Printf("Failed to find oslo.message in given message: %s\n", data) 106 | } 107 | // avoid getting payload data wrapped in array 108 | item := rexForPayload.FindStringSubmatch(sanitized) 109 | if len(item) == 2 { 110 | sanitized = rexForPayload.ReplaceAllString(sanitized, fmt.Sprintf(`"payload": [%s]`, strings.Join(item[1:], ","))) 111 | } 112 | return sanitized 113 | } 114 | 115 | //ParseInputJSON ... make this function type agnostic 116 | func (c *CeilometerMetric) ParseInputJSON(data string) ([]MetricDataFormat, error) { 117 | dataPoints := make([]MetricDataFormat, 0) 118 | sanitized := c.sanitize(data) 119 | message := make(map[string]interface{}) 120 | 121 | var json = jsoniter.ConfigCompatibleWithStandardLibrary 122 | err := json.Unmarshal([]byte(sanitized), &message) 123 | if err != nil { 124 | return nil, fmt.Errorf("error parsing json: %s", err) 125 | } 126 | 127 | for _, pl := range message["payload"].([]interface{}) { 128 | dP := CeilometerMetric{} 129 | if _, ok := message["publisher_id"].(string); !ok { 130 | return nil, fmt.Errorf("\"publisher_id\" not of type string") 131 | } 132 | if _, ok := pl.(map[string]interface{}); !ok { 133 | return nil, fmt.Errorf("ceilometer metric payload not of type map[string]interface{}") 134 | } 135 | 136 | dP.Publisher = message["publisher_id"].(string) 137 | dP.Payload = pl.(map[string]interface{}) 138 | 139 | dP.DataSource.SetFromString("ceilometer") 140 | dP.SetNew(true) 141 | dP.SetData(&dP) 142 | dataPoints = append(dataPoints, &dP) 143 | } 144 | 145 | return dataPoints, nil 146 | } 147 | 148 | //GetKey ... 149 | func (c CeilometerMetric) GetKey() string { 150 | return c.Publisher 151 | } 152 | 153 | //GetItemKey returns name cache key analogically to CollectdMetric implementation 154 | func (c *CeilometerMetric) GetItemKey() string { 155 | parts := []string{c.getwholeID()} 156 | if c.PluginInstance != "" { 157 | parts = append(parts, c.PluginInstance) 158 | } 159 | return strings.Join(parts, "_") 160 | } 161 | 162 | //ParseInputByte is not really used. It is here just to implement MetricDataFormat 163 | //TODO: Remove this method from here and also CollectdMetric 164 | func (c *CeilometerMetric) ParseInputByte(data []byte) error { 165 | _, err := c.ParseInputJSON(string(data)) 166 | return err 167 | } 168 | 169 | //GetInterval returns hardcoded defaultCeilometerInterval, because Ceilometer metricDesc 170 | //does not contain interval information (are not periodically sent at all) and any reasonable 171 | //interval might be needed for expiry setting for Prometheus 172 | //TODO: Make this configurable 173 | func (c *CeilometerMetric) GetInterval() float64 { 174 | return defaultCeilometerInterval 175 | } 176 | 177 | //SetNew ... 178 | func (c *CeilometerMetric) SetNew(new bool) { 179 | c.new = new 180 | } 181 | 182 | //ISNew ... 183 | func (c *CeilometerMetric) ISNew() bool { 184 | return c.new 185 | } 186 | 187 | /*************************** tsdb.TSDB interface *****************************/ 188 | 189 | //GetLabels ... 190 | func (c *CeilometerMetric) GetLabels() map[string]string { 191 | labels := make(map[string]string) 192 | if c.TypeInstance != "" { 193 | labels[c.Plugin] = c.TypeInstance 194 | } else { 195 | labels[c.Plugin] = c.PluginInstance 196 | } 197 | labels["publisher"] = c.Publisher 198 | if ctype, ok := c.Payload["counter_type"].(string); ok { 199 | labels["type"] = ctype 200 | } else { 201 | labels["type"] = "base" 202 | } 203 | if cproj, ok := c.Payload["project_id"].(string); ok { 204 | labels["project"] = cproj 205 | } 206 | if cres, ok := c.Payload["resource_id"].(string); ok { 207 | labels["resource"] = cres 208 | } 209 | if cunit, ok := c.Payload["counter_unit"].(string); ok { 210 | labels["unit"] = cunit 211 | } 212 | if cname, ok := c.Payload["counter_name"].(string); ok { 213 | labels["counter"] = cname 214 | } 215 | return labels 216 | } 217 | 218 | //GetMetricName ... 219 | func (c *CeilometerMetric) GetMetricName(index int) string { 220 | nameParts := []string{"ceilometer"} 221 | cNameShards := strings.Split(c.getwholeID(), ".") 222 | nameParts = append(nameParts, cNameShards...) 223 | return strings.Join(nameParts, "_") 224 | } 225 | 226 | //GetMetricDesc ... 227 | func (c *CeilometerMetric) GetMetricDesc(index int) string { 228 | dstype := "counter" 229 | if ctype, ok := c.Payload["counter_type"]; ok { 230 | dstype = ctype.(string) 231 | } 232 | return fmt.Sprintf("Service Telemetry exporter: '%s' Type: '%s' Dstype: '%s' Dsname: '%s'", 233 | c.Plugin, c.Type, dstype, c.getwholeID()) 234 | } 235 | -------------------------------------------------------------------------------- /internal/pkg/metrics/incoming/collectd.go: -------------------------------------------------------------------------------- 1 | package incoming 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "strconv" 7 | 8 | "collectd.org/cdtime" 9 | "github.com/json-iterator/go" 10 | ) 11 | 12 | // CollectdMetric struct represents metric data formated and sent by collectd 13 | type CollectdMetric struct { 14 | WithDataSource 15 | Values []float64 `json:"values"` 16 | Dstypes []string `json:"dstypes"` 17 | Dsnames []string `json:"dsnames"` 18 | Time cdtime.Time `json:"time"` 19 | Interval float64 `json:"interval"` 20 | Host string `json:"host"` 21 | Plugin string `json:"plugin"` 22 | PluginInstance string `json:"plugin_instance"` 23 | Type string `json:"type"` 24 | TypeInstance string `json:"type_instance"` 25 | new bool 26 | } 27 | 28 | /*************************** MetricDataFormat interface ****************************/ 29 | 30 | //GetValues returns Values. The purpose of this method is to be able to get metric Values 31 | //from the interface object itself 32 | func (c CollectdMetric) GetValues() []float64 { 33 | return c.Values 34 | } 35 | 36 | // GetName implement interface 37 | func (c CollectdMetric) GetName() string { 38 | return c.Plugin 39 | } 40 | 41 | // GetKey ... 42 | func (c CollectdMetric) GetKey() string { 43 | return c.Host 44 | } 45 | 46 | //ParseInputByte ... 47 | //TODO(mmagr): probably unify interface with ParseInputJSON 48 | func (c *CollectdMetric) ParseInputByte(data []byte) error { 49 | cparse := make([]CollectdMetric, 1) 50 | var json = jsoniter.ConfigCompatibleWithStandardLibrary 51 | err := json.Unmarshal(data, &cparse) 52 | if err != nil { 53 | log.Printf("Error parsing InputByte: %s", err) 54 | return err 55 | } 56 | c1 := cparse[0] 57 | c1.SetNew(true) 58 | c.SetData(&c1) 59 | return nil 60 | } 61 | 62 | //SetNew ... 63 | func (c *CollectdMetric) SetNew(new bool) { 64 | c.new = new 65 | } 66 | 67 | //GetInterval ... 68 | func (c *CollectdMetric) GetInterval() float64 { 69 | return c.Interval 70 | } 71 | 72 | // ISNew .. 73 | func (c *CollectdMetric) ISNew() bool { 74 | return c.new 75 | } 76 | 77 | //DSName newName converts one data source of a value list to a string 78 | //representation. 79 | func (c *CollectdMetric) DSName(index int) string { 80 | if c.Dsnames != nil { 81 | return c.Dsnames[index] 82 | } else if len(c.Values) != 1 { 83 | //TODO(mmagr): verify validity of above conditional later 84 | return strconv.FormatInt(int64(index), 10) 85 | } 86 | return "value" 87 | } 88 | 89 | //SetData ... 90 | func (c *CollectdMetric) SetData(data MetricDataFormat) { 91 | if collectd, ok := data.(*CollectdMetric); ok { // type assert on it 92 | if c.Host != collectd.Host { 93 | c.Host = collectd.Host 94 | } 95 | if c.Plugin != collectd.Plugin { 96 | c.Plugin = collectd.Plugin 97 | } 98 | c.Interval = collectd.Interval 99 | c.Values = collectd.Values 100 | c.Dsnames = collectd.Dsnames 101 | c.Dstypes = collectd.Dstypes 102 | c.Time = collectd.Time 103 | if c.PluginInstance != collectd.PluginInstance { 104 | c.PluginInstance = collectd.PluginInstance 105 | } 106 | if c.Type != collectd.Type { 107 | c.Type = collectd.Type 108 | } 109 | if c.TypeInstance != collectd.TypeInstance { 110 | c.TypeInstance = collectd.TypeInstance 111 | } 112 | c.SetNew(true) 113 | } 114 | } 115 | 116 | //ParseInputJSON ... 117 | func (c *CollectdMetric) ParseInputJSON(jsonString string) ([]MetricDataFormat, error) { 118 | collect := []CollectdMetric{} 119 | jsonBlob := []byte(jsonString) 120 | var json = jsoniter.ConfigCompatibleWithStandardLibrary 121 | err := json.Unmarshal(jsonBlob, &collect) 122 | if err != nil { 123 | log.Println("Error parsing json:", err) 124 | return nil, err 125 | } 126 | retDtype := make([]MetricDataFormat, len(collect)) 127 | for index, rt := range collect { 128 | rt.DataSource.SetFromString("collectd") 129 | retDtype[index] = &rt 130 | } 131 | return retDtype, nil 132 | } 133 | 134 | /*************************** tsdb.TSDB interface *****************************/ 135 | 136 | //GetLabels ... 137 | func (c CollectdMetric) GetLabels() map[string]string { 138 | labels := map[string]string{} 139 | if c.PluginInstance != "" { 140 | labels[c.Plugin] = c.PluginInstance 141 | } 142 | if c.TypeInstance != "" { 143 | if c.PluginInstance == "" { 144 | labels[c.Plugin] = c.TypeInstance 145 | } else { 146 | labels["type"] = c.TypeInstance 147 | } 148 | } 149 | // Make sure that "type" and c.Plugin labels always 150 | // exists. Otherwise, Prometheus checks fail 151 | // 152 | if _, typeexist := labels["type"]; !typeexist { 153 | labels["type"] = "base" 154 | } 155 | if _, typeexist := labels[c.Plugin]; !typeexist { 156 | labels[c.Plugin] = "base" 157 | } 158 | 159 | labels["instance"] = c.Host 160 | 161 | return labels 162 | } 163 | 164 | //GetMetricDesc newDesc converts one data source of a value list to a Prometheus description. 165 | func (c CollectdMetric) GetMetricDesc(index int) string { 166 | help := fmt.Sprintf("Service Telemetry exporter: '%s' Type: '%s' Dstype: '%s' Dsname: '%s'", 167 | c.Plugin, c.Type, c.Dstypes[index], c.DSName(index)) 168 | return help 169 | } 170 | 171 | //GetMetricName ... 172 | func (c CollectdMetric) GetMetricName(index int) string { 173 | name := "collectd_" + c.Plugin + "_" + c.Type 174 | if c.Plugin == c.Type { 175 | name = "collectd_" + c.Type 176 | } 177 | 178 | if dsname := c.DSName(index); dsname != "value" { 179 | name += "_" + dsname 180 | } 181 | 182 | switch c.Dstypes[index] { 183 | case "counter", "derive": 184 | name += "_total" 185 | } 186 | return name 187 | } 188 | 189 | //GetItemKey ... 190 | func (c CollectdMetric) GetItemKey() string { 191 | name := c.Plugin + "_" + c.Type 192 | if c.Plugin == c.Type { 193 | name = c.Type 194 | } 195 | if c.PluginInstance != "" { 196 | name += "_" + c.PluginInstance 197 | } 198 | if c.TypeInstance != "" { 199 | name += "_" + c.TypeInstance 200 | } 201 | return name 202 | } 203 | -------------------------------------------------------------------------------- /internal/pkg/metrics/incoming/formats.go: -------------------------------------------------------------------------------- 1 | package incoming 2 | 3 | import ( 4 | "github.com/infrawatch/smart-gateway/internal/pkg/saconfig" 5 | ) 6 | 7 | //MetricDataFormat ... 8 | type MetricDataFormat interface { 9 | GetName() string 10 | SetData(data MetricDataFormat) 11 | ParseInputJSON(json string) ([]MetricDataFormat, error) 12 | GetKey() string 13 | GetItemKey() string 14 | ParseInputByte(data []byte) error 15 | GetInterval() float64 16 | SetNew(new bool) 17 | ISNew() bool 18 | GetValues() []float64 19 | GetDataSourceName() string 20 | } 21 | 22 | //WithDataSource is composition struct for adding DataSource parameter 23 | type WithDataSource struct { 24 | DataSource saconfig.DataSource 25 | } 26 | 27 | //GetDataSourceName returns string representation of DataSource 28 | func (ds WithDataSource) GetDataSourceName() string { 29 | return ds.DataSource.String() 30 | } 31 | 32 | //NewFromDataSource creates empty DataType according to given DataSource 33 | func NewFromDataSource(source saconfig.DataSource) MetricDataFormat { 34 | switch source { 35 | case saconfig.DataSourceCollectd: 36 | return newCollectdMetric( /*...*/ ) 37 | case saconfig.DataSourceCeilometer: 38 | return newCeilometerMetric() 39 | } 40 | return nil 41 | } 42 | 43 | //NewFromDataSourceName creates empty DataType according to given name of DataSource 44 | func NewFromDataSourceName(source string) MetricDataFormat { 45 | switch source { 46 | case saconfig.DataSourceCollectd.String(): 47 | return newCollectdMetric( /*...*/ ) 48 | case saconfig.DataSourceCeilometer.String(): 49 | return newCeilometerMetric() 50 | } 51 | return nil 52 | } 53 | 54 | //newCollectd -- avoid calling this . Use factory method in incoming package 55 | func newCollectdMetric() *CollectdMetric { 56 | metric := new(CollectdMetric) 57 | metric.DataSource = saconfig.DataSourceCollectd 58 | return metric 59 | } 60 | 61 | func newCeilometerMetric() *CeilometerMetric { 62 | metric := new(CeilometerMetric) 63 | metric.DataSource = saconfig.DataSourceCeilometer 64 | return metric 65 | } 66 | 67 | //ParseByte parse incoming data 68 | func ParseByte(dataItem MetricDataFormat, data []byte) error { 69 | return dataItem.ParseInputByte(data) 70 | } 71 | -------------------------------------------------------------------------------- /internal/pkg/metrics/metrics.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "math/rand" 8 | "net/http" 9 | "net/http/pprof" 10 | "os" 11 | "reflect" 12 | "strconv" 13 | "sync" 14 | "time" 15 | 16 | "github.com/MakeNowJust/heredoc" 17 | "github.com/infrawatch/smart-gateway/internal/pkg/amqp10" 18 | "github.com/infrawatch/smart-gateway/internal/pkg/api" 19 | "github.com/infrawatch/smart-gateway/internal/pkg/cacheutil" 20 | "github.com/infrawatch/smart-gateway/internal/pkg/metrics/incoming" 21 | "github.com/infrawatch/smart-gateway/internal/pkg/saconfig" 22 | "github.com/prometheus/client_golang/prometheus" 23 | "github.com/prometheus/client_golang/prometheus/promhttp" 24 | ) 25 | 26 | //MetricHandlerHTML contains HTML for default endpoint 27 | const MetricHandlerHTML = ` 28 | 29 | Collectd Exporter 30 | 31 |

Collectd Exporter

32 |

Metrics

33 | 34 | 35 | ` 36 | 37 | var ( 38 | debugm = func(format string, data ...interface{}) {} // Default no debugging output 39 | debugs = func(count int) {} // Default no debugging output 40 | ) 41 | 42 | /*************** HTTP HANDLER***********************/ 43 | type cacheHandler struct { 44 | useTimestamp bool 45 | cache *cacheutil.IncomingDataCache 46 | appstate *api.MetricHandler 47 | } 48 | 49 | // Describe implements prometheus.Collector. 50 | func (c *cacheHandler) Describe(ch chan<- *prometheus.Desc) { 51 | c.appstate.Describe(ch) 52 | } 53 | 54 | //Collect implements prometheus.Collector. 55 | //need improvement add lock etc etc 56 | func (c *cacheHandler) Collect(ch chan<- prometheus.Metric) { 57 | //lastPull.Set(float64(time.Now().UnixNano()) / 1e9) 58 | c.appstate.Collect(ch) 59 | var metricCount int 60 | //ch <- lastPull 61 | lock, allHosts := c.cache.GetHosts() 62 | defer lock.Unlock() 63 | debugm("Debug:Prometheus is requesting to scrape metrics...") 64 | for key, plugin := range allHosts { 65 | debugm("Debug:Getting metrics for host %s with total plugin size %d\n", key, plugin.Size()) 66 | metricCount = plugin.FlushPrometheusMetric(c.useTimestamp, ch) 67 | if metricCount > 0 { 68 | // add heart if there is atleast one new metrics for the host 69 | debugm("Debug:Adding heartbeat for host %s.", key) 70 | cacheutil.AddHeartBeat(key, 1.0, ch) 71 | } else { 72 | cacheutil.AddHeartBeat(key, 0.0, ch) 73 | } 74 | //add count of metrics 75 | cacheutil.AddMetricsByHostCount(key, float64(metricCount), ch) 76 | //this will clean up all zero plugins 77 | if plugin.Size() == 0 { 78 | debugm("Debug:Cleaning all empty plugins.") 79 | debugm("Debug:Deleting host key %s\n", key) 80 | delete(allHosts, key) 81 | debugm("Debug:Cleaned up cache for host %s", key) 82 | } 83 | } 84 | } 85 | 86 | /*************** main routine ***********************/ 87 | // metricusage and command-line flags 88 | func metricusage() { 89 | doc := heredoc.Doc(` 90 | ********************* config ********************* 91 | $go run cmd/main.go -config smartgateway_config.json -servicetype metrics 92 | **************************************************`) 93 | 94 | fmt.Fprintln(os.Stderr, `Required commandline argument missing`) 95 | fmt.Fprintln(os.Stdout, doc) 96 | flag.PrintDefaults() 97 | } 98 | 99 | //StartMetrics ... entry point to metrics 100 | func StartMetrics() { 101 | var wg sync.WaitGroup 102 | finish := make(chan bool) 103 | 104 | amqp10.SpawnSignalHandler(finish, os.Interrupt) 105 | 106 | // set flags for parsing options 107 | flag.Usage = metricusage 108 | fServiceType := flag.String("servicetype", "metrics", "Metric type") 109 | fConfigLocation := flag.String("config", "", "Path to configuration file.") 110 | fUniqueName := flag.String("uname", "metrics-"+strconv.Itoa(rand.Intn(100)), "Unique name across application") 111 | flag.Parse() 112 | 113 | var serverConfig *saconfig.MetricConfiguration 114 | if len(*fConfigLocation) > 0 { //load configuration 115 | conf, err := saconfig.LoadConfiguration(*fConfigLocation, "metric") 116 | if err != nil { 117 | log.Fatal("Config Parse Error: ", err) 118 | } 119 | serverConfig = conf.(*saconfig.MetricConfiguration) 120 | serverConfig.ServiceType = *fServiceType 121 | } else { 122 | metricusage() 123 | os.Exit(1) 124 | } 125 | if serverConfig.Debug { 126 | debugm = func(format string, data ...interface{}) { log.Printf(format, data...) } 127 | } 128 | 129 | if len(serverConfig.AMQP1MetricURL) == 0 && len(serverConfig.AMQP1Connections) == 0 { 130 | log.Println("Configuration option 'AMQP1MetricURL' or 'AMQP1Connections' is required") 131 | metricusage() 132 | os.Exit(1) 133 | } 134 | 135 | if len(serverConfig.AMQP1MetricURL) > 0 { 136 | serverConfig.AMQP1Connections = []saconfig.AMQPConnection{ 137 | saconfig.AMQPConnection{ 138 | URL: serverConfig.AMQP1MetricURL, 139 | DataSourceID: saconfig.DataSourceCollectd, 140 | DataSource: "collectd", 141 | }, 142 | } 143 | } 144 | 145 | for _, conn := range serverConfig.AMQP1Connections { 146 | log.Printf("AMQP1.0 %s listen address configured at %s\n", conn.DataSource, conn.URL) 147 | } 148 | 149 | applicationHealth := cacheutil.NewApplicationHealthCache() 150 | metricHandler := api.NewAppStateMetricHandler(applicationHealth) 151 | amqpHandler := amqp10.NewAMQPHandler("Metric Consumer") 152 | //Cache sever to process and serve the exporter 153 | cacheServer := cacheutil.NewCacheServer(cacheutil.MAXTTL, serverConfig.Debug) 154 | cacheHandler := &cacheHandler{useTimestamp: serverConfig.UseTimeStamp, cache: cacheServer.GetCache(), appstate: metricHandler} 155 | prometheus.MustRegister(cacheHandler, amqpHandler) 156 | 157 | if !serverConfig.CPUStats { 158 | // Including these stats kills performance when Prometheus polls with multiple targets 159 | prometheus.Unregister(prometheus.NewProcessCollector(prometheus.ProcessCollectorOpts{})) 160 | prometheus.Unregister(prometheus.NewGoCollector()) 161 | } 162 | //Set up Metric Exporter 163 | handler := http.NewServeMux() 164 | handler.Handle("/metrics", promhttp.Handler()) 165 | handler.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 166 | w.Write([]byte(MetricHandlerHTML)) 167 | }) 168 | // Register pprof handlers 169 | handler.HandleFunc("/debug/pprof/", pprof.Index) 170 | handler.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) 171 | handler.HandleFunc("/debug/pprof/profile", pprof.Profile) 172 | handler.HandleFunc("/debug/pprof/symbol", pprof.Symbol) 173 | handler.HandleFunc("/debug/pprof/trace", pprof.Trace) 174 | 175 | debugm("Debug: Config %#v\n", serverConfig) 176 | //run exporter for prometheus to scrape 177 | go func() { 178 | metricsURL := fmt.Sprintf("%s:%d", serverConfig.Exporterhost, serverConfig.Exporterport) 179 | log.Printf("Metric server at : %s\n", metricsURL) 180 | log.Fatal(http.ListenAndServe(metricsURL, handler)) 181 | }() 182 | time.Sleep(2 * time.Second) 183 | log.Println("HTTP server is ready....") 184 | 185 | // AMQP connection(s) 186 | processingCases, qpidStatusCases, amqpServers := amqp10.CreateMessageLoopComponents(serverConfig, finish, amqpHandler, *fUniqueName) 187 | amqp10.SpawnQpidStatusReporter(&wg, applicationHealth, qpidStatusCases) 188 | 189 | // spawn metric processor 190 | wg.Add(1) 191 | go func() { 192 | defer wg.Done() 193 | finishCase := len(processingCases) - 1 194 | processingLoop: 195 | for { 196 | switch index, msg, _ := reflect.Select(processingCases); index { 197 | case finishCase: 198 | break processingLoop 199 | default: 200 | debugm("Debug: Getting incoming data from notifier channel : %#v\n", msg) 201 | metric := incoming.NewFromDataSource(amqpServers[index].DataSource) 202 | amqpServers[index].Server.GetHandler().IncTotalMsgProcessed() 203 | metrics, _ := metric.ParseInputJSON(msg.String()) 204 | for _, m := range metrics { 205 | amqpServers[index].Server.UpdateMinCollectInterval(m.GetInterval()) 206 | cacheServer.Put(m) 207 | } 208 | debugs(len(metrics)) 209 | } 210 | } 211 | log.Println("Closing event processor.") 212 | }() 213 | 214 | // do not end until all loop goroutines ends 215 | wg.Wait() 216 | log.Println("Exiting") 217 | } 218 | -------------------------------------------------------------------------------- /internal/pkg/saconfig/config.go: -------------------------------------------------------------------------------- 1 | package saconfig 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | ) 9 | 10 | /************************** DataSource implementation **************************/ 11 | 12 | //DataSource indentifies a format of incoming data in the message bus channel. 13 | type DataSource int 14 | 15 | const ( 16 | //DataSourceUniversal marks all types of data sorces which send data in smart-gateway universal format 17 | DataSourceUniversal DataSource = iota 18 | //DataSourceCollectd marks collectd as data source for metrics and(or) events 19 | DataSourceCollectd 20 | //DataSourceCeilometer marks Ceilometer as data source for metrics and(or) events 21 | DataSourceCeilometer 22 | ) 23 | 24 | //String returns human readable data type identification. 25 | func (src DataSource) String() string { 26 | return []string{"universal", "collectd", "ceilometer"}[src] 27 | } 28 | 29 | //SetFromString resets value according to given human readable identification. Returns false if invalid identification was given. 30 | func (src *DataSource) SetFromString(name string) bool { 31 | for index, value := range []string{"universal", "collectd", "ceilometer"} { 32 | if name == value { 33 | *src = DataSource(index) 34 | return true 35 | } 36 | } 37 | return false 38 | } 39 | 40 | /*********************** AMQPConnection implementation ***********************/ 41 | 42 | //AMQPConnection identifies single messagebus connection and expected format of incoming data. 43 | type AMQPConnection struct { 44 | URL string `json:"URL"` 45 | DataSource string `json:"DataSource"` 46 | DataSourceID DataSource 47 | } 48 | 49 | /********************* EventConfiguration implementation *********************/ 50 | 51 | //EventAPIConfig ... 52 | type EventAPIConfig struct { 53 | APIEndpointURL string `json:"APIEndpointURL"` //API endpoint 54 | AMQP1PublishURL string `json:"AMQP1PublishURL"` // new amqp address to send notifications 55 | } 56 | 57 | //HandlerPath holds information about location of handler plugin and a data source type stream it should be applied on 58 | type HandlerPath struct { 59 | Path string `json:"Path"` 60 | DataSource string `json:"DataSource"` 61 | } 62 | 63 | //EventConfiguration ... 64 | type EventConfiguration struct { 65 | Debug bool `json:"Debug"` 66 | AMQP1EventURL string `json:"AMQP1EventURL"` 67 | AMQP1Connections []AMQPConnection `json:"AMQP1Connections"` 68 | ElasticHostURL string `json:"ElasticHostURL"` 69 | UseBasicAuth bool `json:"UseBasicAuth"` 70 | ElasticUser string `json:"ElasticUser"` 71 | ElasticPass string `json:"ElasticPass"` 72 | API EventAPIConfig `json:"API"` 73 | AlertManagerURL string `json:"AlertManagerURL"` 74 | AlertManagerEnabled bool `json:"AlertManagerEnabled"` 75 | APIEnabled bool `json:"APIEnabled"` 76 | PublishEventEnabled bool `json:"PublishEventEnabled"` 77 | ResetIndex bool `json:"ResetIndex"` 78 | Prefetch int `json:"Prefetch"` 79 | UniqueName string `json:"UniqueName"` 80 | ServiceType string `json:"ServiceType"` 81 | IgnoreString string `json:"-"` //TODO(mmagr): ? 82 | UseTLS bool `json:"UseTls"` 83 | TLSServerName string `json:"TlsServerName"` 84 | TLSClientCert string `json:"TlsClientCert"` 85 | TLSClientKey string `json:"TlsClientKey"` 86 | TLSCaCert string `json:"TlsCaCert"` 87 | HandlerPlugins []HandlerPath `json:"HandlerPlugin"` 88 | } 89 | 90 | /******************** MetricConfiguration implementation *********************/ 91 | 92 | //MetricConfiguration ... 93 | type MetricConfiguration struct { 94 | Debug bool `json:"Debug"` 95 | AMQP1MetricURL string `json:"AMQP1MetricURL"` 96 | AMQP1Connections []AMQPConnection `json:"AMQP1Connections"` 97 | CPUStats bool `json:"CPUStats"` 98 | Exporterhost string `json:"Exporterhost"` 99 | Exporterport int `json:"Exporterport"` 100 | Prefetch int `json:"Prefetch"` 101 | DataCount int `json:"DataCount"` //-1 for ever which is default //TODO(mmagr): config implementation does not have a way to for default value, implement one? 102 | UseTimeStamp bool `json:"UseTimeStamp"` 103 | UniqueName string `json:"UniqueName"` 104 | ServiceType string `json:"ServiceType"` 105 | IgnoreString string `json:"-"` //TODO(mmagr): ? 106 | } 107 | 108 | /*****************************************************************************/ 109 | 110 | //LoadConfiguration loads and unmarshals configuration file by given path and type 111 | func LoadConfiguration(path string, confType string) (interface{}, error) { 112 | file, err := ioutil.ReadFile(path) 113 | if err != nil { 114 | log.Fatal("Config File Missing.", err) 115 | } 116 | 117 | var config interface{} 118 | switch confType { 119 | case "metric": 120 | config = new(MetricConfiguration) 121 | case "event": 122 | config = new(EventConfiguration) 123 | } 124 | err = json.Unmarshal(file, &config) 125 | 126 | var connections []AMQPConnection 127 | switch confType { 128 | case "metric": 129 | connections = config.(*MetricConfiguration).AMQP1Connections 130 | case "event": 131 | connections = config.(*EventConfiguration).AMQP1Connections 132 | } 133 | for index, conn := range connections { 134 | var dts *DataSource 135 | switch confType { 136 | case "metric": 137 | dts = &config.(*MetricConfiguration).AMQP1Connections[index].DataSourceID 138 | case "event": 139 | dts = &config.(*EventConfiguration).AMQP1Connections[index].DataSourceID 140 | } 141 | if ok := dts.SetFromString(conn.DataSource); !ok { 142 | err = fmt.Errorf("invalid AMQP connection data source '%s'", conn.DataSource) 143 | } 144 | } 145 | return config, err 146 | } 147 | -------------------------------------------------------------------------------- /internal/pkg/saelastic/client.go: -------------------------------------------------------------------------------- 1 | package saelastic 2 | 3 | import ( 4 | "context" 5 | "crypto/sha1" 6 | "crypto/tls" 7 | "crypto/x509" 8 | "encoding/hex" 9 | "encoding/json" 10 | "io/ioutil" 11 | "log" 12 | "net/http" 13 | "time" 14 | 15 | "github.com/gofrs/uuid" 16 | "github.com/infrawatch/smart-gateway/internal/pkg/saconfig" 17 | "github.com/olivere/elastic" 18 | ) 19 | 20 | var debuges = func(format string, data ...interface{}) {} // Default no debugging output 21 | 22 | //ElasticClient .... 23 | type ElasticClient struct { 24 | client *elastic.Client 25 | ctx context.Context 26 | } 27 | 28 | //InitAllMappings removes all indices with prefixes used by smart-gateway 29 | func (ec *ElasticClient) InitAllMappings() { 30 | ec.DeleteIndex(string("collectd_*")) 31 | ec.DeleteIndex(string("ceilometer_*")) 32 | ec.DeleteIndex(string("generic_*")) 33 | } 34 | 35 | //createTLSClient creates http.Client for elastic.Client with enabled 36 | //cert-based authentication 37 | func createTLSClient(serverName string, certFile string, keyFile string, caFile string) (*http.Client, error) { 38 | cert, err := tls.LoadX509KeyPair(certFile, keyFile) 39 | if err != nil { 40 | log.Fatal(err) 41 | return &http.Client{}, err 42 | } 43 | 44 | ca, err := ioutil.ReadFile(caFile) 45 | if err != nil { 46 | log.Fatal(err) 47 | return &http.Client{}, err 48 | } 49 | certPool := x509.NewCertPool() 50 | certPool.AppendCertsFromPEM(ca) 51 | 52 | tlsConfig := &tls.Config{ 53 | Certificates: []tls.Certificate{cert}, 54 | RootCAs: certPool, 55 | } 56 | if len(serverName) == 0 { 57 | tlsConfig.InsecureSkipVerify = true 58 | } else { 59 | tlsConfig.ServerName = serverName 60 | } 61 | debuges("InsecureSkipVerify is set to %t", tlsConfig.InsecureSkipVerify) 62 | 63 | return &http.Client{ 64 | Transport: &http.Transport{TLSClientConfig: tlsConfig}, 65 | }, nil 66 | } 67 | 68 | //CreateClient .... 69 | func CreateClient(config saconfig.EventConfiguration) (*ElasticClient, error) { 70 | if config.Debug { 71 | debuges = func(format string, data ...interface{}) { log.Printf(format, data...) } 72 | } 73 | 74 | var elasticClient *ElasticClient 75 | elasticOpts := []elastic.ClientOptionFunc{elastic.SetHealthcheckInterval(5 * time.Second), elastic.SetURL(config.ElasticHostURL)} 76 | // add transport with TLS enabled in case it is required 77 | if config.UseTLS { 78 | tlsClient, err := createTLSClient(config.TLSServerName, config.TLSClientCert, config.TLSClientKey, config.TLSCaCert) 79 | if err != nil { 80 | return elasticClient, nil 81 | } 82 | elasticOpts = append(elasticOpts, elastic.SetHttpClient(tlsClient), elastic.SetScheme("https")) 83 | } 84 | 85 | if config.UseBasicAuth { 86 | elasticOpts = append(elasticOpts, elastic.SetBasicAuth(config.ElasticUser, config.ElasticPass)) 87 | } 88 | 89 | eclient, err := elastic.NewClient(elasticOpts...) 90 | if err != nil { 91 | return elasticClient, err 92 | } 93 | elasticClient = &ElasticClient{client: eclient, ctx: context.Background()} 94 | if config.ResetIndex { 95 | elasticClient.InitAllMappings() 96 | } 97 | debuges("Debug:ElasticSearch client created.") 98 | return elasticClient, nil 99 | } 100 | 101 | //IndexExists ... 102 | func (ec *ElasticClient) IndexExists(index string) *elastic.IndicesExistsService { 103 | return ec.client.IndexExists(index) 104 | } 105 | 106 | //GetContext ... 107 | func (ec *ElasticClient) GetContext() context.Context { 108 | return ec.ctx 109 | } 110 | 111 | //CreateIndex ... 112 | func (ec *ElasticClient) CreateIndex(index string, mapping string) { 113 | 114 | exists, err := ec.client.IndexExists(string(index)).Do(ec.ctx) 115 | if err != nil { 116 | // Handle error nothing to do index exists 117 | debuges("Debug:ElasticSearch indexExists returned an error: %s", err) 118 | } 119 | if !exists { 120 | // Index does not exist yet. 121 | // Create a new index. 122 | createIndex, err := ec.client.CreateIndex(string(index)).BodyString(mapping).Do(ec.ctx) 123 | if err != nil { 124 | // Handle error 125 | panic(err) 126 | } 127 | if !createIndex.Acknowledged { 128 | // Not acknowledged 129 | log.Println("Index Not acknowledged") 130 | } 131 | } 132 | 133 | } 134 | 135 | //genUUIDv4 ... 136 | func genUUIDv4() string { 137 | id, _ := uuid.NewV4() 138 | debuges("Debug:github.com/satori/go.uuid: %s\n", id) 139 | return id.String() 140 | } 141 | 142 | // Generate an id based on the data itself to prevent duplicate events from multiple (HA) instances of the SG 143 | func genHashedID(jsondata interface{}) string { 144 | dataBytes, err := json.Marshal(jsondata) 145 | if err != nil { 146 | log.Printf("Error during event encoding: %v\n", err) 147 | uuid := genUUIDv4() 148 | log.Printf("Will return a UUID string instead (duplicate events may occur): %v\n", uuid) 149 | return uuid 150 | } 151 | eventHashBytes := sha1.Sum(dataBytes) 152 | eventHashHex := hex.EncodeToString(eventHashBytes[:]) 153 | return eventHashHex 154 | } 155 | 156 | //Create ... it can be BodyJson or BodyString.. BodyJson needs struct defined 157 | func (ec *ElasticClient) Create(indexname string, indextype string, jsondata interface{}) (string, error) { 158 | ctx := ec.ctx 159 | id := genHashedID(jsondata) 160 | 161 | debuges("Debug:Printing body %s\n", jsondata) 162 | result, err := ec.client.Index(). 163 | Index(string(indexname)). 164 | Type(string(indextype)). 165 | Id(id). 166 | BodyJson(jsondata). 167 | Do(ctx) 168 | if err != nil { 169 | // Handle error 170 | debuges("Create document Error %#v", err) 171 | return id, err 172 | } 173 | debuges("Debug:Indexed %s to index %s, type %s\n", result.Id, result.Index, result.Type) 174 | // Flush to make sure the documents got written. 175 | // Flush asks Elasticsearch to free memory from the index and 176 | // flush data to disk. 177 | _, err = ec.client.Flush().Index(string(indexname)).Do(ctx) 178 | return id, err 179 | 180 | } 181 | 182 | //DeleteIndex ... 183 | func (ec *ElasticClient) DeleteIndex(index string) error { 184 | // Delete an index. 185 | deleteIndex, err := ec.client.DeleteIndex(string(index)).Do(ec.ctx) 186 | if err != nil { 187 | // Handle error 188 | //panic(err) 189 | return err 190 | } 191 | if !deleteIndex.Acknowledged { 192 | debuges("Debug:ElasticSearch DeleteIndex not acknowledged") 193 | } 194 | return nil 195 | } 196 | 197 | //Delete .... 198 | func (ec *ElasticClient) Delete(indexname string, indextype string, id string) error { 199 | _, err := ec.client.Delete(). 200 | Index(string(indexname)). 201 | Type(string(indextype)). 202 | Id(id). 203 | Do(ec.ctx) 204 | return err 205 | } 206 | 207 | //Get .... 208 | func (ec *ElasticClient) Get(indexname string, indextype string, id string) (*elastic.GetResult, error) { 209 | result, err := ec.client.Get(). 210 | Index(string(indexname)). 211 | Type(string(indextype)). 212 | Id(id). 213 | Do(ec.ctx) 214 | if err != nil { 215 | // Handle error 216 | return nil, err 217 | } 218 | if result.Found { 219 | debuges("Debug:Got document %s in version %d from index %s, type %s\n", result.Id, result.Version, result.Index, result.Type) 220 | } 221 | return result, nil 222 | } 223 | 224 | //Search .. 225 | func (ec *ElasticClient) Search(indexname string) *elastic.SearchResult { 226 | termQuery := elastic.NewTermQuery("user", "olivere") 227 | searchResult, err := ec.client.Search(). 228 | Index(indexname). // search in index "twitter" 229 | Query(termQuery). // specify the query 230 | Sort("user", true). // sort by "user" field, ascending 231 | From(0).Size(10). // take documents 0-9 232 | Pretty(true). // pretty print request and response JSON 233 | Do(ec.ctx) // execute 234 | if err != nil { 235 | // Handle error 236 | panic(err) 237 | } 238 | debuges("Debug:Query took %d milliseconds\n", searchResult.TookInMillis) 239 | return searchResult 240 | } 241 | -------------------------------------------------------------------------------- /internal/pkg/tsdb/prometheus.go: -------------------------------------------------------------------------------- 1 | package tsdb 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "time" 7 | 8 | "github.com/infrawatch/smart-gateway/internal/pkg/metrics/incoming" 9 | "github.com/infrawatch/smart-gateway/internal/pkg/saconfig" 10 | "github.com/prometheus/client_golang/prometheus" 11 | ) 12 | 13 | // Additional timestamp formats possibly used by sources of metric data 14 | const ( 15 | isoTimeLayout = "2006-01-02 15:04:05.000000" 16 | RFC3339Python = "2006-01-02T15:04:05.000000" 17 | ) 18 | 19 | //TSDB interface 20 | type TSDB interface { 21 | //prometheus specific reflect 22 | GetLabels() map[string]string 23 | GetMetricName(index int) string 24 | GetMetricDesc(index int) string 25 | } 26 | 27 | var ( 28 | metricNameRe = regexp.MustCompile("[^a-zA-Z0-9_:]") 29 | ) 30 | 31 | //NewHeartBeatMetricByHost ... 32 | func NewHeartBeatMetricByHost(instance string, value float64) (prometheus.Metric, error) { 33 | valueType := prometheus.GaugeValue 34 | metricName := "collectd_last_metric_for_host_status" 35 | help := "Status of metrics for host currently active." 36 | 37 | plabels := prometheus.Labels{} 38 | plabels["instance"] = instance 39 | desc := prometheus.NewDesc(metricName, help, []string{}, plabels) 40 | return prometheus.NewConstMetric(desc, valueType, value) 41 | } 42 | 43 | //AddMetricsByHost ... 44 | func AddMetricsByHost(instance string, value float64) (prometheus.Metric, error) { 45 | valueType := prometheus.GaugeValue 46 | metricName := "collectd_metric_per_host" 47 | help := "No of metrics for host currently read." 48 | 49 | plabels := prometheus.Labels{} 50 | plabels["instance"] = instance 51 | desc := prometheus.NewDesc(metricName, help, []string{}, plabels) 52 | return prometheus.NewConstMetric(desc, valueType, value) 53 | } 54 | 55 | //NewPrometheusMetric converts one data source of a value list to a Prometheus metric. 56 | func NewPrometheusMetric(usetimestamp bool, format string, metric incoming.MetricDataFormat, index int) (prometheus.Metric, error) { 57 | var ( 58 | timestamp time.Time 59 | valueType prometheus.ValueType 60 | metricName, help string 61 | labels map[string]string 62 | value float64 63 | ) 64 | 65 | if format == saconfig.DataSourceCollectd.String() { 66 | collectd := metric.(*incoming.CollectdMetric) 67 | switch collectd.Dstypes[index] { 68 | case "gauge": 69 | valueType = prometheus.GaugeValue 70 | case "derive", "counter": 71 | valueType = prometheus.CounterValue 72 | default: 73 | return nil, fmt.Errorf("unknown name of value type: %s", collectd.Dstypes[index]) 74 | } 75 | timestamp = collectd.Time.Time() 76 | help = collectd.GetMetricDesc(index) 77 | metricName = metricNameRe.ReplaceAllString(collectd.GetMetricName(index), "_") 78 | labels = collectd.GetLabels() 79 | value = collectd.Values[index] 80 | } else if format == saconfig.DataSourceCeilometer.String() { 81 | ceilometer := metric.(*incoming.CeilometerMetric) 82 | if ctype, ok := ceilometer.Payload["counter_type"]; ok { 83 | if counterType, ok := ctype.(string); ok { 84 | switch counterType { 85 | case "gauge": 86 | valueType = prometheus.GaugeValue 87 | default: 88 | valueType = prometheus.CounterValue 89 | } 90 | } else { 91 | return nil, fmt.Errorf("invalid counter_type in metric payload: %s", ceilometer.Payload) 92 | } 93 | } else { 94 | return nil, fmt.Errorf("did not find counter_type in metric payload: %s", ceilometer.Payload) 95 | } 96 | if ts, ok := ceilometer.Payload["timestamp"]; ok { 97 | for _, layout := range []string{time.RFC3339, time.RFC3339Nano, time.ANSIC, RFC3339Python, isoTimeLayout} { 98 | if stamp, err := time.Parse(layout, ts.(string)); err == nil { 99 | timestamp = stamp 100 | break 101 | } 102 | } 103 | if timestamp.IsZero() { 104 | return nil, fmt.Errorf("invalid timestamp in metric payload: %s", ceilometer.Payload) 105 | } 106 | } else { 107 | return nil, fmt.Errorf("did not find timestamp in metric payload: %s", ceilometer.Payload) 108 | } 109 | //help = ceilometer.GetMetricDesc(index) 110 | help = "" 111 | metricName = metricNameRe.ReplaceAllString(ceilometer.GetMetricName(index), "_") 112 | labels = ceilometer.GetLabels() 113 | value = ceilometer.Values[index] 114 | } 115 | 116 | plabels := prometheus.Labels{} 117 | for key, value := range labels { 118 | plabels[key] = value 119 | } 120 | desc := prometheus.NewDesc(metricName, help, []string{}, plabels) 121 | if usetimestamp { 122 | return prometheus.NewMetricWithTimestamp( 123 | timestamp, 124 | prometheus.MustNewConstMetric(desc, valueType, value), 125 | ), nil 126 | } 127 | return prometheus.NewConstMetric(desc, valueType, value) 128 | } 129 | -------------------------------------------------------------------------------- /tests/internal_pkg/amqp10_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/infrawatch/smart-gateway/internal/pkg/amqp10" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | const QDRURL = "amqp://127.0.0.1:5672/collectd/telemetry" 12 | const QDRMsg = "{\"message\": \"smart gateway test\"}" 13 | 14 | func TestSendAndReceiveMessage(t *testing.T) { 15 | sender := amqp10.NewAMQPSender(QDRURL, true) 16 | receiver := amqp10.NewAMQPServer(QDRURL, true, 1, 0, nil, "metrics-test") 17 | ackChan := sender.GetAckChannel() 18 | t.Run("Test receive", func(t *testing.T) { 19 | t.Parallel() 20 | data := <-receiver.GetNotifier() 21 | assert.Equal(t, QDRMsg, data) 22 | fmt.Printf("Finished send") 23 | }) 24 | t.Run("Test send and ACK", func(t *testing.T) { 25 | t.Parallel() 26 | sender.Send(QDRMsg) 27 | // otherwise receiver blocks 28 | assert.Equal(t, 1, <-receiver.GetStatus()) 29 | assert.Equal(t, true, <-receiver.GetDoneChan()) 30 | outcome := <-ackChan 31 | assert.Equal(t, "smart-gateway-ack", outcome.Value.(string)) 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /tests/internal_pkg/cacheutil_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | "github.com/infrawatch/smart-gateway/internal/pkg/cacheutil" 9 | "github.com/infrawatch/smart-gateway/internal/pkg/metrics/incoming" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | /*----------------------------- helper functions -----------------------------*/ 14 | 15 | //GenerateSampleCacheData .... 16 | func GenerateSampleCacheData(cs *cacheutil.CacheServer, key string, itemCount int) { 17 | for j := 0; j < itemCount; j++ { 18 | pluginname := fmt.Sprintf("plugin_name_%d", j) 19 | // see incoming_collectd_test.go 20 | newSample := GenerateSampleCollectdData(key, pluginname) 21 | cs.Put(newSample) 22 | } 23 | } 24 | 25 | /*----------------------------------------------------------------------------*/ 26 | 27 | func TestCacheServer(t *testing.T) { 28 | pluginCount := 10 29 | hostname := "hostname" 30 | server := cacheutil.NewCacheServer(0, true) 31 | dataCache := server.GetCache() 32 | t.Run("Test IncommingDataCache", func(t *testing.T) { 33 | // test cache Size (1 hor each host) 34 | GenerateSampleCacheData(server, hostname, pluginCount) 35 | time.Sleep(time.Millisecond) 36 | assert.Equal(t, 1, dataCache.Size()) 37 | GenerateSampleCacheData(server, "hostname2", 1) 38 | time.Sleep(time.Millisecond) 39 | assert.Equal(t, 2, dataCache.Size()) 40 | // test shard Size (1 for each record for each host) 41 | assert.Equal(t, pluginCount, dataCache.GetShard(hostname).Size()) 42 | assert.Equal(t, 1, dataCache.GetShard("hostname2").Size()) 43 | // test SetData 44 | sample := GenerateSampleCollectdData("shardtest", "foo") 45 | key := sample.GetItemKey() 46 | shard := dataCache.GetShard("hostname2") 47 | assert.Equal(t, 1, shard.Size()) 48 | shard.SetData(sample) 49 | assert.Equal(t, 2, shard.Size()) 50 | //TODO(mmagr): below is not right (expected should be "hostname2" or sample 51 | // should be saved to appropriate shard), but will fix that in later release 52 | data := shard.GetData(key) 53 | assert.NotNil(t, data) 54 | assert.Equal(t, "shardtest", data.(*incoming.CollectdMetric).Host) 55 | }) 56 | } 57 | 58 | func TestCacheServerCleanUp(t *testing.T) { 59 | pluginCount := 10 60 | hostname := "hostname" 61 | server := cacheutil.NewCacheServer(1, true) 62 | dataCache := server.GetCache() 63 | 64 | t.Run("Test FlushAll", func(t *testing.T) { 65 | GenerateSampleCacheData(server, hostname, pluginCount) 66 | time.Sleep(time.Millisecond) 67 | assert.Equal(t, 1, dataCache.Size()) 68 | assert.Equal(t, pluginCount, dataCache.GetShard(hostname).Size()) 69 | shard := dataCache.GetShard("hostname") 70 | assert.Equal(t, false, shard.Expired()) 71 | time.Sleep(time.Second * 2) 72 | assert.Equal(t, true, shard.Expired()) 73 | dataCache.FlushAll() 74 | //TODO(mmagr): does it make sense to need to call FlushAll twice to first mark 75 | // records as not new and delete it only after second call? 76 | assert.Equal(t, 1, dataCache.Size()) 77 | dataCache.FlushAll() 78 | assert.Equal(t, 0, dataCache.Size()) 79 | }) 80 | } 81 | -------------------------------------------------------------------------------- /tests/internal_pkg/elasticsearch_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path" 7 | "testing" 8 | 9 | "github.com/infrawatch/smart-gateway/internal/pkg/events/incoming" 10 | "github.com/infrawatch/smart-gateway/internal/pkg/saconfig" 11 | "github.com/infrawatch/smart-gateway/internal/pkg/saelastic" 12 | ) 13 | 14 | //COLLECTD 15 | const ( 16 | elastichost = "http://127.0.0.1:9200" 17 | testCACert = `-----BEGIN CERTIFICATE----- 18 | MIIDSTCCAjGgAwIBAgIUVLbF9klC/t0fQoG35GAVTjU6tYEwDQYJKoZIhvcNAQEL 19 | BQAwNDEyMDAGA1UEAxMpRWxhc3RpYyBDZXJ0aWZpY2F0ZSBUb29sIEF1dG9nZW5l 20 | cmF0ZWQgQ0EwHhcNMTkwOTAzMDkwNTUxWhcNMjIwOTAyMDkwNTUxWjA0MTIwMAYD 21 | VQQDEylFbGFzdGljIENlcnRpZmljYXRlIFRvb2wgQXV0b2dlbmVyYXRlZCBDQTCC 22 | ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKKOQqQFVvlBqFc9K9ESM49+ 23 | RFqNXdeStK+sVkZ9WkvmfSfj5h91O9BXev88n9dqcifmbS99KiT6ExzX3RO1NDxq 24 | mIHGiscaalYA7gJlbF90cqvuy4ejNs50DDgSAeDLTHEn+q5PJeY7uQweQQ1usnFR 25 | DbevOH/ubjdNRlTlockl1iYd8voQoRNxCgeN8JKd1XDyXXQm+sdZP87hnMgfDj4A 26 | r88TkhbXTFhtWcU7aLi/uNq0u/3CfJwkwvH7SFuqv/qnqXXu+7vaA+zifGSHmIMS 27 | GX47Ki4ordGv75hFs70gI3qtgq5Ce1+4sGl05Ime/4+iRoj2S/EKrbSejnOklgMC 28 | AwEAAaNTMFEwHQYDVR0OBBYEFCvqtlWPfEyQCOus3n+NjVJrmsYdMB8GA1UdIwQY 29 | MBaAFCvqtlWPfEyQCOus3n+NjVJrmsYdMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI 30 | hvcNAQELBQADggEBABPy/tMJypO4TIEakRfUAjo23za3DSH4aN9FjuF5dOnBAKU3 31 | 6Wxf2abDwaTUTh/wnuBrh8ubuFWQyqCEL8+ncxjgeEpOHpvbxrnVfFQxDt7rdAqK 32 | VRGddwUCaHgJ1ZBdhrLuSmWwaXsQL4q2F4dLifq/BIdOPvT3lHzPh/D5sdCcPVrX 33 | V2j6pIReP/TfM+7NIlLSL+xPTjMV1lTFMupYrZDUouB5lkqyNgO0/eXcBPjFjdVz 34 | 5Kx1xUfPcx8oSotFlrqA4eXfeQBFr9dJDsTeEZNSUM41TQKRoPn4qdPNQ/QPoJgR 35 | Mig5sWoQl+8PDYeSCcgmmWF/uPpAt9bORvtmj8U= 36 | -----END CERTIFICATE-----` 37 | testClientCert = `-----BEGIN CERTIFICATE----- 38 | MIIDTjCCAjagAwIBAgIUC1CKg5RQAEHSl672tLWVHwQ6UCswDQYJKoZIhvcNAQEL 39 | BQAwNDEyMDAGA1UEAxMpRWxhc3RpYyBDZXJ0aWZpY2F0ZSBUb29sIEF1dG9nZW5l 40 | cmF0ZWQgQ0EwHhcNMTkwOTAzMDkwNTUyWhcNMjIwOTAyMDkwNTUyWjARMQ8wDQYD 41 | VQQDEwZub2RlLTEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCMBgck 42 | nkC4ZcRSsrvw/TSLUJ+Fnzox9mHmvawItfhIjrhPpcz8kgEQ2NvTSFQ5i6mt3wca 43 | bUqCRrqJ7HZ4Lk4epPbL50GYFn/I98oBqI6SH3I/At6ZnTUcUgwZCaWZ8iHrQ3Bd 44 | EP+LWuAIRs1IH9Kg/+uP7q1zhb3yEUx84PBNNVNC10i5w1Gtd2LsgQis8mA2zLG5 45 | IjVeAyLe1zyc4oM74TxULr+vRv5gZFGJMbO9FXq/ztNOwv1YQ2RatNY3aEk/NMBj 46 | pUuGuCxMdTcU5/sOtaLIroaCR6BNNe1B3RxnBuqyxvmwwk+RlXchqPtMWEW7XDBI 47 | tO/jLSC/zkbD4yEnAgMBAAGjezB5MB0GA1UdDgQWBBS1aF7Zhl3xRhkkWsimErYf 48 | 9gaH+DAfBgNVHSMEGDAWgBQr6rZVj3xMkAjrrN5/jY1Sa5rGHTAsBgNVHREEJTAj 49 | gglsb2NhbGhvc3SHBH8AAAGHEAAAAAAAAAAAAAAAAAAAAAEwCQYDVR0TBAIwADAN 50 | BgkqhkiG9w0BAQsFAAOCAQEAEqprw19/7A22xSxlwxDgpB7aE7Cxyn3GfMxsb3vE 51 | 6h+oIoCwEHTKPqeJLMF/SnLLjdqRTQ43nU2bKpjfTJ8lSzX6ccNWVoMKMUNkSBkU 52 | FMmR8e/gaqWTPiRqcSJfuVwG4L06F7wcyHSqBgkJBErdttWHbFXmYdhleui7xDg5 53 | whi8l6c7TS2qMuLo1JnvvyfoEvxuo8RKvji11t+ZuSrXp0fq9dFQEgnzAoekLutO 54 | ygoZsqvrMRK2F0U4XS9e2JGyLMOz0oxvUtZMRVFVtR5AUpmzdz42LGWnT4xxL6jO 55 | vB6iVwxM7ZjAGrAJg8hOTvSTn/0X5HqCCNwrQ2tQyfuZ7g== 56 | -----END CERTIFICATE-----` 57 | testClientKey = `-----BEGIN RSA PRIVATE KEY----- 58 | MIIEpAIBAAKCAQEAjAYHJJ5AuGXEUrK78P00i1CfhZ86MfZh5r2sCLX4SI64T6XM 59 | /JIBENjb00hUOYuprd8HGm1Kgka6iex2eC5OHqT2y+dBmBZ/yPfKAaiOkh9yPwLe 60 | mZ01HFIMGQmlmfIh60NwXRD/i1rgCEbNSB/SoP/rj+6tc4W98hFMfODwTTVTQtdI 61 | ucNRrXdi7IEIrPJgNsyxuSI1XgMi3tc8nOKDO+E8VC6/r0b+YGRRiTGzvRV6v87T 62 | TsL9WENkWrTWN2hJPzTAY6VLhrgsTHU3FOf7DrWiyK6GgkegTTXtQd0cZwbqssb5 63 | sMJPkZV3Iaj7TFhFu1wwSLTv4y0gv85Gw+MhJwIDAQABAoIBABGWBDmmIozGQ0T7 64 | q70VoA7LPm3C1MVHo34eXkftytQaEK34Leme0MFz6w/7KpDbqKDsvPClv1DjXzRJ 65 | XYu0jR0uLMzpK4TVdpEgBd/1coqJpoihbKGwa+Y1q81NN95A2d+5ZZhatS2kaTTA 66 | 57FiRcrwuX4nROOYbYXEhG2+to+LrkpEGqG+wgroWMuClfFcPxfnp5thiX1lcP5X 67 | t1L9IsbHrpxGA5HO+4gtLAmgtA2OVXZ6R2eJVlrFbhrfqoN1JiixrWLoCrbS1vpn 68 | dPLG0bOpm6RqiH627+hUVLSYtKNtj9T8FbWlHXCMH19MXG1DwTPaI+RJCb3AasFl 69 | saIKyzkCgYEAzEwy/odj5mrWIWCBnUSkwBUHCxSFtvrGdzwcVJA/2CGyaF49Hkkc 70 | HM2c4v1QjVlSn5niqr8EyKcU/pbq83fUiO7AT8qH9JDlyWCkuslJ/C22/KhGnzFl 71 | g5f4UXr8/DoLP05a+cP5AorhhuxRTjxTUD5lMk81ipXgnGp4jytHOSsCgYEAr3W2 72 | RRnKQr6u8zyVKUlR31OLAgT16nAVkC3rl2FPyf02d4bz5+Gv7LPxYbUg8zAWvMR1 73 | ArdYAh3zjnfAYzoBPfCakXxc9Hbwl7GMBOs9UOEz6sSOcQ8G6t/Xx5oLeXcXvxgJ 74 | whpLPfu8zgucqy1PzoeXTKY1dzEthgy6nGPgwfUCgYEAmS75fYgfDAJHlLc7+KQj 75 | tDMQGOrGaDEY5waXZ4DRnkmF8GPZCABhp+c0H6842wOCxFEqeETKXXmKcGrQuMW9 76 | Av+iCzIdRu/unFRur++GHiRY9JFogq0TJNyqQM4rKySKkmk6JdUfvRxNhlFjlXn+ 77 | LkjasCJcTxGaXS4oP5F/0gkCgYAnkpfqW9e3WARjTa2ioyu4/8GhUfcYyfDDFOhG 78 | uybguqBXMvO9v7QK4ca2L8DfuF/YcUKmuy05RQIShsW4W3O+QY7K806PwGeg/uVC 79 | kr/AhxpLf8tUinwX6yZimUavPYH4knZY9c80iptZqVrLbKvMO96O5gm2+Tt4OVS5 80 | QvmFJQKBgQCKmrVQ9at1oNwzPiEIlQszyZ9n5vrxi1EbpnRZjAv9KBErjLVJOEA7 81 | u+Jpmr1o0z9CPvXFmdWGdF2dJrgBImgQnlsNVctK8x1m0azfduPcgPgKSlnMdnRS 82 | wg4Luw64Vn3osASCHv5gwoIgBepLpOby7KrCEvOwuFyB9QGZXXIxBQ== 83 | -----END RSA PRIVATE KEY-----` 84 | ) 85 | 86 | /* 87 | func TestMain(t *testing.T) { 88 | config := saconfig.EventConfiguration{ 89 | Debug: false, 90 | ElasticHostURL: elastichost, 91 | UseTLS: false, 92 | TLSClientCert: "", 93 | TLSClientKey: "", 94 | TLSCaCert: "", 95 | } 96 | 97 | client, err := saelastic.CreateClient(config) 98 | if err != nil { 99 | t.Fatalf("Failed to connect to elastic search: %s", err) 100 | } else { 101 | defer func() { 102 | client.DeleteIndex(string(CONNECTIVITYINDEXTEST)) 103 | client.DeleteIndex(string(PROCEVENTINDEXTEST)) 104 | client.DeleteIndex(string(SYSEVENTINDEXTEST)) 105 | client.DeleteIndex(string(GENERICINDEXTEST)) 106 | }() 107 | } 108 | 109 | t.Run("Test create and delete", func(t *testing.T) { 110 | indexName, _, err := saelastic.GetIndexNameType(connectivitydata) 111 | if err != nil { 112 | t.Errorf("Failed to get indexname and type%s", err) 113 | return 114 | } 115 | 116 | testIndexname := fmt.Sprintf("%s_%s", indexName, "test") 117 | client.DeleteIndex(testIndexname) 118 | client.CreateIndex(testIndexname, saemapping.ConnectivityMapping) 119 | exists, err := client.IndexExists(string(testIndexname)).Do(client.GetContext()) 120 | if exists == false || err != nil { 121 | t.Errorf("Failed to create index %s", err) 122 | } 123 | err = client.DeleteIndex(testIndexname) 124 | if err != nil { 125 | t.Errorf("Failed to Delete index %s", err) 126 | } 127 | }) 128 | 129 | t.Run("Test connectivity data create", func(t *testing.T) { 130 | indexName, IndexType, err := saelastic.GetIndexNameType(connectivitydata) 131 | if err != nil { 132 | t.Errorf("Failed to get indexname and type%s", err) 133 | return 134 | } 135 | testIndexname := fmt.Sprintf("%s_%s", indexName, "test") 136 | err = client.DeleteIndex(testIndexname) 137 | 138 | client.CreateIndex(testIndexname, saemapping.ConnectivityMapping) 139 | exists, err := client.IndexExists(string(testIndexname)).Do(client.GetContext()) 140 | if exists == false || err != nil { 141 | t.Errorf("Failed to create index %s", err) 142 | } 143 | 144 | id, err := client.Create(testIndexname, IndexType, connectivitydata) 145 | if err != nil { 146 | t.Errorf("Failed to create data %s\n", err.Error()) 147 | } else { 148 | log.Printf("document id %#v\n", id) 149 | } 150 | result, err := client.Get(testIndexname, IndexType, id) 151 | if err != nil { 152 | t.Errorf("Failed to get data %s", err) 153 | } else { 154 | log.Printf("Data %#v", result) 155 | } 156 | deleteErr := client.Delete(testIndexname, IndexType, id) 157 | if deleteErr != nil { 158 | t.Errorf("Failed to delete data %s", deleteErr) 159 | } 160 | 161 | err = client.DeleteIndex(testIndexname) 162 | if err != nil { 163 | t.Errorf("Failed to Delete index %s", err) 164 | } 165 | }) 166 | } 167 | */ 168 | 169 | func TestTls(t *testing.T) { 170 | dir, err := ioutil.TempDir("", "sg-test-tls") 171 | if err != nil { 172 | t.Fatalf("Failed to create temporary directory: %s", err) 173 | } 174 | defer os.RemoveAll(dir) 175 | 176 | verifyConnection := true 177 | clientCert := os.Getenv("SA_TESTS_ES_CLIENT_CERT") 178 | if len(clientCert) == 0 { 179 | verifyConnection = false 180 | clientCert = path.Join(dir, "client.cert") 181 | err = ioutil.WriteFile(clientCert, []byte(testClientCert), 0644) 182 | if err != nil { 183 | t.Fatalf("Failed to create temporary client cert: %s", err) 184 | } 185 | } 186 | clientKey := os.Getenv("SA_TESTS_ES_CLIENT_KEY") 187 | if len(clientKey) == 0 { 188 | verifyConnection = false 189 | clientKey = path.Join(dir, "client.key") 190 | err = ioutil.WriteFile(clientKey, []byte(testClientKey), 0644) 191 | if err != nil { 192 | t.Fatalf("Failed to create temporary client key: %s", err) 193 | } 194 | } 195 | caCert := os.Getenv("SA_TESTS_ES_CA_CERT") 196 | if len(caCert) == 0 { 197 | verifyConnection = false 198 | caCert = path.Join(dir, "ca.cert") 199 | err = ioutil.WriteFile(caCert, []byte(testCACert), 0644) 200 | if err != nil { 201 | t.Fatalf("Failed to create temporary ca cert: %s", err) 202 | } 203 | } 204 | 205 | t.Run("Test insecure connection", func(t *testing.T) { 206 | config := saconfig.EventConfiguration{ 207 | Debug: false, 208 | ElasticHostURL: elastichost, 209 | UseTLS: true, 210 | TLSClientCert: clientCert, 211 | TLSClientKey: clientKey, 212 | TLSCaCert: caCert, 213 | TLSServerName: "", 214 | } 215 | 216 | _, err = saelastic.CreateClient(config) 217 | if err != nil && verifyConnection { 218 | t.Fatalf("Failed to connect to elastic search using TLS: %s", err) 219 | } 220 | }) 221 | 222 | t.Run("Test unset ServerName", func(t *testing.T) { 223 | config := saconfig.EventConfiguration{ 224 | Debug: false, 225 | ElasticHostURL: elastichost, 226 | UseTLS: true, 227 | TLSClientCert: clientCert, 228 | TLSClientKey: clientKey, 229 | TLSCaCert: caCert, 230 | } 231 | 232 | _, err = saelastic.CreateClient(config) 233 | if err != nil && verifyConnection { 234 | t.Fatalf("Failed to connect to elastic search using TLS: %s", err) 235 | } 236 | }) 237 | 238 | } 239 | 240 | func TestStoreCeilometerEvent(t *testing.T) { 241 | t.Run("Store event with differing trait types", func(t *testing.T) { 242 | config := saconfig.EventConfiguration{ 243 | Debug: false, 244 | ElasticHostURL: elastichost, 245 | UseTLS: false, 246 | } 247 | client, err := saelastic.CreateClient(config) 248 | if err != nil { 249 | t.Fatalf("Failed to connect to elastic: %s", err) 250 | } 251 | event := incoming.NewFromDataSource(saconfig.DataSourceCeilometer) 252 | event.ParseEvent(ceiloEventDataWithTraits) 253 | resp, err := client.Create(event.GetIndexName(), "event", event.GetRawData()) 254 | if err != nil { 255 | t.Fatalf("Failed to store index to elastic search: %ss", err) 256 | } 257 | result, err := client.Get(event.GetIndexName(), "event", resp) 258 | if err != nil { 259 | t.Fatalf("Error querying Elastic Search: %s", err) 260 | } 261 | if !result.Found { 262 | t.Fatal("Stored index not found") 263 | } 264 | }) 265 | } 266 | 267 | /*func TestIndexCheckConnectivity(t *testing.T) { 268 | indexName, indexType, err := saelastic.GetIndexNameType(connectivitydata) 269 | if err != nil { 270 | t.Errorf("Failed to get indexname and type%s", err) 271 | } 272 | if indexType != saelastic.CONNECTIVITYINDEXTYPE { 273 | t.Errorf("Excepected Index Type %s Got %s", saelastic.CONNECTIVITYINDEXTYPE, indexType) 274 | } 275 | if string(saelastic.CONNECTIVITYINDEX) != indexName { 276 | t.Errorf("Excepected Index %s Got %s", saelastic.CONNECTIVITYINDEX, indexName) 277 | } 278 | } 279 | }*/ 280 | -------------------------------------------------------------------------------- /tests/internal_pkg/events_incoming_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/infrawatch/smart-gateway/internal/pkg/events/incoming" 7 | "github.com/infrawatch/smart-gateway/internal/pkg/saconfig" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | const ( 12 | // alert testing event 13 | eventForAlert = `{"labels":{"alertname":"collectd_connectivity_gauge","instance":"nfvha-comp-03","connectivity":"eno2","type":"interface_status","severity":"OKAY","service":"collectd"},"annotations":{"summary":"","ves":{"domain":"stateChange","eventId":"39996","eventName":"interface eno2 up","lastEpochMicrosec":"1523292316174821","priority":"high","reportingEntityName":"collectd connectivity plugin","sourceName":"eno2","version":"1","slicetest":["item1","item2","item3"],"stateChangeFields":{"newState":"inService","oldState":"outOfService","stateChangeFieldsVersion":"1","stateInterface":"eno2"}}},"startsAt":"2018-04-09T16:45:16.174815108Z"}` 14 | // collectd messages 15 | connectivityEventData = "[{\"labels\":{\"alertname\":\"collectd_connectivity_gauge\",\"instance\":\"d60b3c68f23e\",\"connectivity\":\"eno2\",\"type\":\"interface_status\",\"severity\":\"FAILURE\",\"service\":\"collectd\"},\"annotations\":{\"summary\":\"\",\"ves\":\"{\\\"domain\\\":\\\"stateChange\\\",\\\"eventId\\\":2,\\\"eventName\\\":\\\"interface eno2 up\\\",\\\"lastEpochMicrosec\\\":1518790014024924,\\\"priority\\\":\\\"high\\\",\\\"reportingEntityName\\\":\\\"collectd connectivity plugin\\\",\\\"sequence\\\":0,\\\"sourceName\\\":\\\"eno2\\\",\\\"startEpochMicrosec\\\":1518790009881440,\\\"version\\\":1.0,\\\"stateChangeFields\\\":{\\\"newState\\\":\\\"outOfService\\\",\\\"oldState\\\":\\\"inService\\\",\\\"stateChangeFieldsVersion\\\":1.0,\\\"stateInterface\\\":\\\"eno2\\\"}}\"},\"startsAt\":\"2018-02-16T14:06:54.024856417Z\"}]" 16 | procEventData1 = "[{\"labels\":{\"alertname\":\"collectd_procevent_gauge\",\"instance\":\"d60b3c68f23e\",\"procevent\":\"bla.py\",\"type\":\"process_status\",\"severity\":\"FAILURE\",\"service\":\"collectd\"},\"annotations\":{\"summary\":\"\",\"ves\":\"{\\\"domain\\\":\\\"fault\\\",\\\"eventId\\\":3,\\\"eventName\\\":\\\"process bla.py (8537) down\\\",\\\"lastEpochMicrosec\\\":1518791119579620,\\\"priority\\\":\\\"high\\\",\\\"reportingEntityName\\\":\\\"collectd procevent plugin\\\",\\\"sequence\\\":0,\\\"sourceName\\\":\\\"bla.py\\\",\\\"startEpochMicrosec\\\":1518791111336973,\\\"version\\\":1.0,\\\"faultFields\\\":{\\\"alarmCondition\\\":\\\"process bla.py (8537) state change\\\",\\\"alarmInterfaceA\\\":\\\"bla.py\\\",\\\"eventSeverity\\\":\\\"CRITICAL\\\",\\\"eventSourceType\\\":\\\"process\\\",\\\"faultFieldsVersion\\\":1.0,\\\"specificProblem\\\":\\\"process bla.py (8537) down\\\",\\\"vfStatus\\\":\\\"Ready to terminate\\\"}}\"},\"startsAt\":\"2018-02-16T14:25:19.579573212Z\"}]" 17 | procEventData2 = `[{"labels":{"alertname":"collectd_interface_if_octets","instance":"localhost.localdomain","interface":"lo","severity":"FAILURE","service":"collectd"},"annotations":{"summary":"Host localhost.localdomain, plugin interface (instance lo) type if_octets: Data source \"rx\" is currently 43596.224329. That is above the failure threshold of 0.000000.","DataSource":"rx","CurrentValue":"43596.2243286703","WarningMin":"nan","WarningMax":"nan","FailureMin":"nan","FailureMax":"0"},"startsAt":"2019-09-18T21:11:19.281603240Z"}]` 18 | ovsEventData = `[{"labels":{"alertname":"collectd_ovs_events_gauge","instance":"nfvha-comp-03","ovs_events":"br0","type":"link_status","severity":"OKAY","service":"collectd"},"annotations":{"summary":"link state of \"br0\" interface has been changed to \"UP\"","uuid":"c52f2aca-3cb1-48e3-bba3-100b54303d84"},"startsAt":"2018-02-22T20:12:19.547955618Z"}]` 19 | sensubilityEventData = `{"labels":{"check":"elastic-check","client":"wubba.lubba.dub.dub.redhat.com","severity":"FAILURE"},"annotations":{"command":"podman ps | grep elastic || exit 2","duration":0.043278607,"executed":1601900769,"issued":1601900769,"output":"time=\"2020-10-05T14:26:09+02:00\" level=error msg=\"cannot mkdir /run/user/0/libpod: mkdir /run/user/0/libpod: permission denied\"\n","status":2,"ves":"{\"commonEventHeader\":{\"domain\":\"heartbeat\",\"eventType\":\"checkResult\",\"eventId\":\"wubba.lubba.dub.dub.redhat.com-elastic-check\",\"priority\":\"High\",\"reportingEntityId\":\"918e8d04-c5ae-4e20-a763-8eb4f1af7c80\",\"reportingEntityName\":\"wubba.lubba.dub.dub.redhat.com\",\"sourceId\":\"918e8d04-c5ae-4e20-a763-8eb4f1af7c80\",\"sourceName\":\"wubba.lubba.dub.dub.redhat.com-collectd-sensubility\",\"startingEpochMicrosec\":1601900769,\"lastEpochMicrosec\":1601900769},\"heartbeatFields\":{\"additionalFields\":{\"check\":\"elastic-check\",\"command\":\"podman ps | grep elastic || exit 2 || $0\",\"duration\":\"0.043279\",\"executed\":\"1601900769\",\"issued\":\"1601900769\",\"output\":\"time=\\\"2020-10-05T14:26:09+02:00\\\" level=error msg=\\\"cannot mkdir /run/user/0/libpod: mkdir /run/user/0/libpod: permission denied\\\"\\n\",\"status\":\"2\"}}}"},"startsAt":"2020-10-05T14:26:09+02:00"}` 20 | // ceilometer messages 21 | ceiloEventData = `{"request":{"oslo.version":"2.0","oslo.message":"{\"message_id\":\"7936fc72-21ac-4536-b7a4-02ef4729f37e\",\"publisher_id\":\"compute.host1\",\"timestamp\":\"2020-01-06 20:22:42.094902\",\"priority\":\"warn\",\"event_type\":\"compute.create_instance.start\",\"payload\":[{\"instance_id\":\"foobar\"}]}"}],"context":{}}}` 22 | ceiloEventDataWithTraits = `{"request": {"oslo.version": "2.0", "oslo.message": "{\"message_id\": \"4c9fbb58-c82d-4ca5-9f4c-2c61d0693214\", \"publisher_id\": \"telemetry.publisher.controller-0.redhat.local\", \"event_type\": \"event\", \"priority\": \"SAMPLE\", \"payload\": [{\"message_id\": \"084c0bca-0d19-40c0-a724-9916e4815845\", \"event_type\": \"image.delete\", \"generated\": \"2020-03-06T14:13:29.497096\", \"traits\": [[\"service\", 1, \"image.localhost\"], [\"project_id\", 1, \"0f500647077b47f08a8ca9181e9b7aef\"], [\"user_id\", 1, \"0f500647077b47f08a8ca9181e9b7aef\"], [\"resource_id\", 1, \"c4f7e00b-df85-4b77-9e1a-26a1de4d5735\"], [\"name\", 1, \"cirros\"], [\"status\", 1, \"deleted\"], [\"created_at\", 4, \"2020-03-06T14:01:07\"], [\"deleted_at\", 4, \"2020-03-06T14:13:29\"], [\"size\", 2, 13287936]], \"raw\": {}, \"message_signature\": \"77e798b842991f9c0c35bda265fdf86075b4a1e58309db1d2adbf89386a3859e\"}], \"timestamp\": \"2020-03-06 14:13:30.057411\"}"}, "context": {}}}` 23 | // generic messages 24 | ) 25 | 26 | var ( 27 | // collectd events 28 | connectivityEvent = map[string]interface{}{ 29 | "labels": map[string]interface{}{ 30 | "alertname": "collectd_connectivity_gauge", 31 | "instance": "d60b3c68f23e", 32 | "connectivity": "eno2", 33 | "type": "interface_status", 34 | "severity": "FAILURE", 35 | "service": "collectd", 36 | }, 37 | "annotations": map[string]interface{}{ 38 | "summary": "", 39 | "ves": map[string]interface{}{ 40 | "domain": "stateChange", 41 | "eventId": 2.0, 42 | "eventName": "interface eno2 up", 43 | "lastEpochMicrosec": float64(1518790014024924), 44 | "priority": "high", 45 | "reportingEntityName": "collectd connectivity plugin", 46 | "sequence": 0.0, 47 | "sourceName": "eno2", 48 | "startEpochMicrosec": float64(1518790009881440), 49 | "version": 1.0, 50 | "stateChangeFields": map[string]interface{}{ 51 | "newState": "outOfService", 52 | "oldState": "inService", 53 | "stateChangeFieldsVersion": 1.0, 54 | "stateInterface": "eno2", 55 | }, 56 | }, 57 | }, 58 | "startsAt": "2018-02-16T14:06:54.024856417Z", 59 | } 60 | procEvent1 = map[string]interface{}{ 61 | "labels": map[string]interface{}{ 62 | "alertname": "collectd_procevent_gauge", 63 | "instance": "d60b3c68f23e", 64 | "procevent": "bla.py", 65 | "type": "process_status", 66 | "severity": "FAILURE", 67 | "service": "collectd", 68 | }, 69 | "annotations": map[string]interface{}{ 70 | "summary": "", 71 | "ves": map[string]interface{}{ 72 | "domain": "fault", 73 | "eventId": 3.0, 74 | "eventName": "process bla.py (8537) down", 75 | "lastEpochMicrosec": float64(1518791119579620), 76 | "priority": "high", 77 | "reportingEntityName": "collectd procevent plugin", 78 | "sequence": 0.0, 79 | "sourceName": "bla.py", 80 | "startEpochMicrosec": float64(1518791111336973), 81 | "version": 1.0, 82 | "faultFields": map[string]interface{}{ 83 | "alarmCondition": "process bla.py (8537) state change", 84 | "alarmInterfaceA": "bla.py", 85 | "eventSeverity": "CRITICAL", 86 | "eventSourceType": "process", 87 | "faultFieldsVersion": 1.0, 88 | "specificProblem": "process bla.py (8537) down", 89 | "vfStatus": "Ready to terminate", 90 | }, 91 | }, 92 | }, 93 | "startsAt": "2018-02-16T14:25:19.579573212Z", 94 | } 95 | procEvent2 = map[string]interface{}{ 96 | "labels": map[string]interface{}{ 97 | "alertname": "collectd_interface_if_octets", 98 | "instance": "localhost.localdomain", 99 | "interface": "lo", 100 | "severity": "FAILURE", 101 | "service": "collectd", 102 | }, 103 | "annotations": map[string]interface{}{ 104 | "summary": "Host localhost.localdomain, plugin interface (instance lo) type if_octets: Data source \"rx\" is currently 43596.224329. That is above the failure threshold of 0.000000.", 105 | "DataSource": "rx", 106 | "CurrentValue": "43596.2243286703", 107 | "WarningMin": "nan", 108 | "WarningMax": "nan", 109 | "FailureMin": "nan", 110 | "FailureMax": "0", 111 | }, 112 | "startsAt": "2019-09-18T21:11:19.281603240Z", 113 | } 114 | ovsEvent = map[string]interface{}{ 115 | "labels": map[string]interface{}{ 116 | "alertname": "collectd_ovs_events_gauge", 117 | "instance": "nfvha-comp-03", 118 | "ovs_events": "br0", 119 | "type": "link_status", 120 | "severity": "OKAY", 121 | "service": "collectd", 122 | }, 123 | "annotations": map[string]interface{}{ 124 | "summary": "link state of \"br0\" interface has been changed to \"UP\"", 125 | "uuid": "c52f2aca-3cb1-48e3-bba3-100b54303d84", 126 | }, 127 | "startsAt": "2018-02-22T20:12:19.547955618Z", 128 | } 129 | sensubilityEvent = map[string]interface{}{ 130 | "labels": map[string]interface{}{ 131 | "check": "elastic-check", 132 | "client": "wubba.lubba.dub.dub.redhat.com", 133 | "severity": "FAILURE", 134 | }, 135 | "annotations": map[string]interface{}{ 136 | "command": "podman ps | grep elastic || exit 2", 137 | "duration": 0.043278607, 138 | "executed": float64(1601900769), 139 | "issued": float64(1601900769), 140 | "output": "time=\"2020-10-05T14:26:09+02:00\" level=error msg=\"cannot mkdir /run/user/0/libpod: mkdir /run/user/0/libpod: permission denied\"\n", 141 | "status": float64(2), 142 | "ves": map[string]interface{}{ 143 | "commonEventHeader": map[string]interface{}{ 144 | "domain": "heartbeat", 145 | "eventType": "checkResult", 146 | "eventId": "wubba.lubba.dub.dub.redhat.com-elastic-check", 147 | "priority": "High", 148 | "reportingEntityId": "918e8d04-c5ae-4e20-a763-8eb4f1af7c80", 149 | "reportingEntityName": "wubba.lubba.dub.dub.redhat.com", 150 | "sourceId": "918e8d04-c5ae-4e20-a763-8eb4f1af7c80", 151 | "sourceName": "wubba.lubba.dub.dub.redhat.com-collectd-sensubility", 152 | "startingEpochMicrosec": float64(1601900769), 153 | "lastEpochMicrosec": float64(1601900769), 154 | }, 155 | "heartbeatFields": map[string]interface{}{ 156 | "additionalFields": map[string]interface{}{ 157 | "check": "elastic-check", 158 | "command": "podman ps | grep elastic || exit 2 || $0", 159 | "duration": "0.043279", 160 | "executed": "1601900769", 161 | "issued": "1601900769", 162 | "output": "time=\"2020-10-05T14:26:09+02:00\" level=error msg=\"cannot mkdir /run/user/0/libpod: mkdir /run/user/0/libpod: permission denied\"\\n", 163 | "status": "2", 164 | }, 165 | }, 166 | }, 167 | }, 168 | "startsAt": "2020-10-05T14:26:09+02:00", 169 | } 170 | // ceilometer events 171 | ceiloEventWithTraits = map[string]interface{}{ 172 | "message_id": "4c9fbb58-c82d-4ca5-9f4c-2c61d0693214", 173 | "publisher_id": "telemetry.publisher.controller-0.redhat.local", 174 | "timestamp": "2020-03-06 14:13:30.057411", 175 | "priority": "SAMPLE", 176 | "event_type": "event", 177 | "payload": map[string]interface{}{ 178 | "message_id": "084c0bca-0d19-40c0-a724-9916e4815845", 179 | "event_type": "image.delete", 180 | "generated": "2020-03-06T14:13:29.497096", 181 | "traits": map[string]interface{}{ 182 | "service": "image.localhost", 183 | "project_id": "0f500647077b47f08a8ca9181e9b7aef", 184 | "user_id": "0f500647077b47f08a8ca9181e9b7aef", 185 | "resource_id": "c4f7e00b-df85-4b77-9e1a-26a1de4d5735", 186 | "name": "cirros", 187 | "status": "deleted", 188 | "created_at": "2020-03-06T14:01:07", 189 | "deleted_at": "2020-03-06T14:13:29", 190 | "size": float64(13287936), 191 | }, 192 | "raw": map[string]interface{}{}, 193 | "message_signature": "77e798b842991f9c0c35bda265fdf86075b4a1e58309db1d2adbf89386a3859e", 194 | }, 195 | } 196 | // generic events 197 | ) 198 | 199 | type EventDataParsingTestMatrix struct { 200 | Dirty string 201 | Parsed map[string]interface{} 202 | IndexName string 203 | } 204 | 205 | type EventDataParsingTestRun struct { 206 | Source saconfig.DataSource 207 | Matrix []EventDataParsingTestMatrix 208 | } 209 | 210 | func TestEventDataParsing(t *testing.T) { 211 | testRuns := []EventDataParsingTestRun{ 212 | EventDataParsingTestRun{ 213 | saconfig.DataSourceCollectd, 214 | []EventDataParsingTestMatrix{ 215 | EventDataParsingTestMatrix{procEventData1, procEvent1, "collectd_procevent"}, 216 | EventDataParsingTestMatrix{procEventData2, procEvent2, "collectd_interface_if"}, 217 | EventDataParsingTestMatrix{ovsEventData, ovsEvent, "collectd_ovs_events"}, 218 | EventDataParsingTestMatrix{connectivityEventData, connectivityEvent, "collectd_connectivity"}, 219 | EventDataParsingTestMatrix{sensubilityEventData, sensubilityEvent, "collectd_generic"}, 220 | }, 221 | }, 222 | EventDataParsingTestRun{ 223 | saconfig.DataSourceCeilometer, 224 | []EventDataParsingTestMatrix{ 225 | EventDataParsingTestMatrix{ceiloEventDataWithTraits, ceiloEventWithTraits, "ceilometer_image"}, 226 | }, 227 | }, 228 | } 229 | for _, run := range testRuns { 230 | for _, testCase := range run.Matrix { 231 | evt := incoming.NewFromDataSource(run.Source) 232 | err := evt.ParseEvent(testCase.Dirty) 233 | if err != nil { 234 | t.Errorf("Failed event parsing case: %s", err.Error()) 235 | } 236 | assert.Equal(t, testCase.Parsed, evt.GetRawData()) 237 | assert.Equal(t, testCase.IndexName, evt.GetIndexName()) 238 | } 239 | } 240 | } 241 | 242 | type EventAlertDataMatrix struct { 243 | Label string 244 | Expected string 245 | } 246 | 247 | type EventAlertTestRun struct { 248 | Source saconfig.DataSource 249 | Event string 250 | LabelsMatrix []EventAlertDataMatrix 251 | AnnotationsMatrix []EventAlertDataMatrix 252 | Timestamp string 253 | } 254 | 255 | func TestGenerateAlert(t *testing.T) { 256 | testRuns := []EventAlertTestRun{ 257 | EventAlertTestRun{ 258 | saconfig.DataSourceCollectd, 259 | eventForAlert, 260 | []EventAlertDataMatrix{ 261 | {"alertname", "collectd_connectivity_gauge"}, 262 | {"instance", "nfvha-comp-03"}, 263 | {"connectivity", "eno2"}, 264 | {"type", "interface_status"}, 265 | {"severity", "info"}, 266 | //AlertDataMatrix{"service", "collectd"}, 267 | {"domain", "stateChange"}, 268 | {"eventId", "39996"}, 269 | {"eventName", "interface eno2 up"}, 270 | {"lastEpochMicrosec", "1523292316174821"}, 271 | {"priority", "high"}, 272 | {"reportingEntityName", "collectd connectivity plugin"}, 273 | {"sourceName", "eno2"}, 274 | {"version", "1"}, 275 | {"newState", "inService"}, 276 | {"oldState", "outOfService"}, 277 | {"stateChangeFieldsVersion", "1"}, 278 | {"stateInterface", "eno2"}, 279 | {"summary", ""}, 280 | {"name", "collectd_connectivity_gauge_eno2_nfvha-comp-03_collectd_interface_status"}, 281 | {"slicetest", "item1,item2,item3"}, 282 | }, 283 | []EventAlertDataMatrix{ 284 | {"summary", "eno2 interface_status interface eno2 up"}, 285 | {"description", "collectd_connectivity_gauge eno2 nfvha-comp-03 collectd info interface_status"}, 286 | }, 287 | "2018-04-09T16:45:16Z", 288 | }, 289 | EventAlertTestRun{ 290 | saconfig.DataSourceCeilometer, 291 | ceiloEventData, 292 | []EventAlertDataMatrix{ 293 | {"alertname", "ceilometer_compute_create_instance"}, 294 | {"instance", "compute.host1"}, 295 | {"type", "compute.create_instance.start"}, 296 | {"severity", "warning"}, 297 | }, 298 | []EventAlertDataMatrix{ 299 | {"instance_id", "foobar"}, 300 | }, 301 | "2020-01-06T20:22:42Z", 302 | }, 303 | } 304 | for _, run := range testRuns { 305 | // mock event 306 | evt := incoming.NewFromDataSource(run.Source) 307 | evt.ParseEvent(run.Event) 308 | // mock alert 309 | eventAlert := evt.GeneratePrometheusAlert("https://this/is/test") 310 | t.Run("Verify proper parsing of event data to Labels", func(t *testing.T) { 311 | for _, testCase := range run.LabelsMatrix { 312 | assert.Equalf(t, testCase.Expected, eventAlert.Labels[testCase.Label], "Unexpected label for %s", testCase.Label) 313 | } 314 | }) 315 | t.Run("Verify proper parsing of event data to Annotations", func(t *testing.T) { 316 | for _, testCase := range run.AnnotationsMatrix { 317 | assert.Equalf(t, testCase.Expected, eventAlert.Annotations[testCase.Label], "Unexpected annotation for %s", testCase.Label) 318 | } 319 | }) 320 | t.Run("Verify proper parsing of rest of data", func(t *testing.T) { 321 | assert.Equal(t, run.Timestamp, eventAlert.StartsAt) 322 | }) 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /tests/internal_pkg/messages/formatCeilometerMessage.py: -------------------------------------------------------------------------------- 1 | import json 2 | from jsonschema import validate, ValidationError 3 | import string 4 | import sys, getopt 5 | 6 | testSchema = { 7 | 'type':'object', 8 | 'properties': { 9 | 'testInput': 10 | { 11 | 'type':'object', 12 | 'properties': { 13 | 'request': { 14 | 'type':'object', 15 | 'properties': { 16 | 'oslo.message': {'type':'string'}, 17 | }, 18 | 'required': ['oslo.message'] 19 | }, 20 | }, 21 | 'required':['request'] 22 | }, 23 | }, 24 | 'required': ['testInput'] 25 | } 26 | 27 | osloSchema = { 28 | 'type':'object', 29 | 'properties': { 30 | 'publisher_id': {'type':'string'}, 31 | 'payload': {'type':'array'}, 32 | }, 33 | 'required': ['publisher_id','payload'] 34 | } 35 | 36 | payloadSchema = { 37 | 'type':'object', 38 | 'properties': { 39 | 'counter_name': {'type': 'string'}, 40 | 'resource_id': {'type': 'string'}, 41 | 'counter_volume': {'type': 'number'}, 42 | }, 43 | 'required': ['counter_name','resource_id','counter_volume'] 44 | } 45 | 46 | 47 | def generateResultsFromJSON(data): 48 | try: 49 | validate(instance=data, schema=testSchema) 50 | osloMessageJSON = data['testInput']['request']['oslo.message'] 51 | 52 | message = json.loads(osloMessageJSON) 53 | validate(instance=message,schema=osloSchema) 54 | 55 | results = [] 56 | for pl in message['payload']: 57 | validate(instance=pl,schema=payloadSchema) 58 | 59 | pluginAttr = pl["counter_name"].split('.') 60 | pt = '' 61 | typeInstance = '' 62 | if len(pluginAttr) > 1: 63 | pt = pluginAttr[1] 64 | if len(pluginAttr) > 2: 65 | typeInstance = pluginAttr[2] 66 | else: 67 | pt = pluginAttr[0] 68 | 69 | pluginInstance = pl['resource_id'] 70 | plugin = pluginAttr[0] 71 | publisher = message['publisher_id'] 72 | 73 | metricGenerated = { 74 | 'publisher': publisher, 75 | 'plugin': plugin, 76 | 'plugin_instance': pl['resource_id'], 77 | 'type': pt, 78 | 'values': [pl['counter_volume']], 79 | 'name': plugin, 80 | 'key': publisher, 81 | 'item_key': genItemKey(pl, pluginInstance), 82 | 'type_instance': typeInstance, 83 | 'labels': genLabels(pl, plugin, typeInstance, pluginInstance,publisher), 84 | 'description': genDescription(pl, plugin, pt), 85 | 'metric_name': genMetricName(pluginAttr), 86 | } 87 | 88 | results.append(metricGenerated) 89 | return results 90 | 91 | except ValidationError as e: 92 | print(e) 93 | print("---------") 94 | print(e.absolute_path) 95 | 96 | print("---------") 97 | print(e.absolute_schema_path) 98 | 99 | def genMetricName(cNameParts): 100 | name = ["ceilometer"] 101 | name = name + cNameParts 102 | return '_'.join(name) 103 | 104 | 105 | def genDescription(payload, plugin, typ): 106 | id = payload["counter_name"] 107 | dstype = 'counter' 108 | if 'counter_type' in payload: 109 | dstype = payload['counter_type'] 110 | return "Service Telemetry exporter: '{plugin}' Type: '{Type}' Dstype: '{Dstype}' Dsname: '{ID}'".format(plugin=plugin, Type=typ, Dstype=dstype, ID=id) 111 | 112 | 113 | def genItemKey(payload, pluginInstance): 114 | key = [payload["counter_name"]] 115 | if pluginInstance: 116 | key.append(pluginInstance) 117 | return "_".join(key) 118 | 119 | def genLabels(payload, plugin, typeInstance, pluginInstance, publisher): 120 | labels = {"publisher": publisher} 121 | if typeInstance: 122 | labels[plugin] = typeInstance 123 | else: 124 | labels[plugin] = pluginInstance 125 | 126 | if "counter_type" in payload: 127 | labels["type"] = payload["counter_type"] 128 | else: 129 | labels["type"] = "base" 130 | if "project_id" in payload: 131 | labels["project"] = payload["project_id"] 132 | if "resource_id" in payload: 133 | labels["resource"] = payload["resource_id"] 134 | if "counter_unit" in payload: 135 | labels["unit"] = payload["counter_unit"] 136 | if "counter_name" in payload: 137 | labels["counter"] = payload["counter_name"] 138 | return labels 139 | 140 | def main(cmd, argv): 141 | inputFile = '' 142 | testName = '' 143 | 144 | try: 145 | opts, args = getopt.getopt(argv,"f:t:",["file=","test="]) 146 | 147 | for opt, arg in opts: 148 | if opt in ('-f','--file'): 149 | inputFile = arg 150 | elif opt in ('-t','--test'): 151 | testName = arg 152 | 153 | with open(inputFile,'r') as jsonFile: 154 | data = json.load(jsonFile) 155 | if testName in data: 156 | data[testName]["validatedResults"] = generateResultsFromJSON(data[testName]) 157 | print(json.dumps(data, indent=2)) 158 | else: 159 | print("Could not find test '%s' in %s" % (testName,inputFile)) 160 | 161 | except getopt.GetoptError: 162 | print("%s -f -t " % cmd) 163 | 164 | 165 | if __name__ == "__main__": 166 | main(sys.argv[0],sys.argv[1:]) -------------------------------------------------------------------------------- /tests/internal_pkg/messages/metric-tests.json: -------------------------------------------------------------------------------- 1 | { 2 | "CeilometerMetrics": { 3 | "description": "test ceilometer metric parsing", 4 | "testInput": { 5 | "request": { 6 | "oslo.version": "2.0", 7 | "oslo.message": "{\"message_id\": \"37f64423-db31-4cfb-8c9d-06f9c0fad04a\", \"publisher_id\": \"telemetry.publisher.controller-0.redhat.local\", \"event_type\": \"metering\", \"priority\": \"SAMPLE\", \"payload\": [{\"source\": \"openstack\", \"counter_name\": \"disk.ephemeral.size\", \"counter_type\": \"gauge\", \"counter_unit\": \"GB\", \"counter_volume\": 0, \"user_id\": \"3ee72fcd74aa4439bb07fa69f1bc7169\", \"project_id\": \"e56191ef77744c599dbcecae6af176bb\", \"resource_id\": \"d8bd99b6-6fd8-4c02-a2e3-efbf596df636\", \"timestamp\": \"2020-09-14T16:12:49.939250+00:00\", \"resource_metadata\": {\"host\": \"compute-0.redhat.local\", \"flavor_id\": \"71cd0af1-afd3-4ee4-b918-cec05bf89578\", \"flavor_name\": \"m1.tiny\", \"display_name\": \"new-instance\", \"image_ref\": \"45333e02-643d-4f4f-a817-065060753983\", \"launched_at\": \"2020-09-14T16:12:49.839122\", \"created_at\": \"2020-09-14 16:12:39+00:00\"}, \"message_id\": \"22a54880-f6a5-11ea-b0f2-525400971e97\", \"monotonic_time\": null, \"message_signature\": \"be55d63bd5d876a62ab52824104128eedfa0619386e8569e326ccef4dcf0d9db\"}, {\"source\": \"openstack\", \"counter_name\": \"disk.root.size\", \"counter_type\": \"gauge\", \"counter_unit\": \"GB\", \"counter_volume\": 1, \"user_id\": \"3ee72fcd74aa4439bb07fa69f1bc7169\", \"project_id\": \"e56191ef77744c599dbcecae6af176bb\", \"resource_id\": \"d8bd99b6-6fd8-4c02-a2e3-efbf596df636\", \"timestamp\": \"2020-09-14T16:12:49.939250+00:00\", \"resource_metadata\": {\"host\": \"compute-0.redhat.local\", \"flavor_id\": \"71cd0af1-afd3-4ee4-b918-cec05bf89578\", \"flavor_name\": \"m1.tiny\", \"display_name\": \"new-instance\", \"image_ref\": \"45333e02-643d-4f4f-a817-065060753983\", \"launched_at\": \"2020-09-14T16:12:49.839122\", \"created_at\": \"2020-09-14 16:12:39+00:00\"}, \"message_id\": \"22a55c80-f6a5-11ea-b0f2-525400971e97\", \"monotonic_time\": null, \"message_signature\": \"bc0b987d71fe9f0d5d902347f22a0a20b2d975344b4c948572cae4dae553e960\"}, {\"source\": \"openstack\", \"counter_name\": \"compute.instance.booting.time\", \"counter_type\": \"gauge\", \"counter_unit\": \"sec\", \"counter_volume\": 10.839122, \"user_id\": null, \"project_id\": \"e56191ef77744c599dbcecae6af176bb\", \"resource_id\": \"d8bd99b6-6fd8-4c02-a2e3-efbf596df636\", \"timestamp\": \"2020-09-14T16:12:49.939250+00:00\", \"resource_metadata\": {\"host\": \"compute-0.redhat.local\", \"flavor_id\": \"71cd0af1-afd3-4ee4-b918-cec05bf89578\", \"flavor_name\": \"m1.tiny\", \"display_name\": \"new-instance\", \"image_ref\": \"45333e02-643d-4f4f-a817-065060753983\", \"launched_at\": \"2020-09-14T16:12:49.839122\", \"created_at\": \"2020-09-14 16:12:39+00:00\"}, \"message_id\": \"22a574d6-f6a5-11ea-b0f2-525400971e97\", \"monotonic_time\": null, \"message_signature\": \"fd7f1e2fdb34b7beb836d0ead178289f7c36f39bcd68acfd0719848667c58a13\"}, {\"source\": \"openstack\", \"counter_name\": \"vcpus\", \"counter_type\": \"gauge\", \"counter_unit\": \"vcpu\", \"counter_volume\": 2, \"user_id\": \"3ee72fcd74aa4439bb07fa69f1bc7169\", \"project_id\": \"e56191ef77744c599dbcecae6af176bb\", \"resource_id\": \"d8bd99b6-6fd8-4c02-a2e3-efbf596df636\", \"timestamp\": \"2020-09-14T16:12:49.939250+00:00\", \"resource_metadata\": {\"host\": \"compute-0.redhat.local\", \"flavor_id\": \"71cd0af1-afd3-4ee4-b918-cec05bf89578\", \"flavor_name\": \"m1.tiny\", \"display_name\": \"new-instance\", \"image_ref\": \"45333e02-643d-4f4f-a817-065060753983\", \"launched_at\": \"2020-09-14T16:12:49.839122\", \"created_at\": \"2020-09-14 16:12:39+00:00\"}, \"message_id\": \"22a5821e-f6a5-11ea-b0f2-525400971e97\", \"monotonic_time\": null, \"message_signature\": \"d3f107c2ef6bb1b06e1a9975d1f1ff0bdc51432adff39403db2a1f6a9773b99d\"}, {\"source\": \"openstack\", \"counter_name\": \"memory\", \"counter_type\": \"gauge\", \"counter_unit\": \"MB\", \"counter_volume\": 512, \"user_id\": \"3ee72fcd74aa4439bb07fa69f1bc7169\", \"project_id\": \"e56191ef77744c599dbcecae6af176bb\", \"resource_id\": \"d8bd99b6-6fd8-4c02-a2e3-efbf596df636\", \"timestamp\": \"2020-09-14T16:12:49.939250+00:00\", \"resource_metadata\": {\"host\": \"compute-0.redhat.local\", \"flavor_id\": \"71cd0af1-afd3-4ee4-b918-cec05bf89578\", \"flavor_name\": \"m1.tiny\", \"display_name\": \"new-instance\", \"image_ref\": \"45333e02-643d-4f4f-a817-065060753983\", \"launched_at\": \"2020-09-14T16:12:49.839122\", \"created_at\": \"2020-09-14 16:12:39+00:00\"}, \"message_id\": \"22a591dc-f6a5-11ea-b0f2-525400971e97\", \"monotonic_time\": null, \"message_signature\": \"9dcf78e3cd43e7fcd66cda5cf33511150e79a086a634bd9d087bb567e4985980\"}], \"timestamp\": \"2020-09-14 16:12:49.954128\"}" 8 | }, 9 | "context": {} 10 | }, 11 | "validatedResults": [ 12 | { 13 | "publisher": "telemetry.publisher.controller-0.redhat.local", 14 | "plugin": "disk", 15 | "plugin_instance": "d8bd99b6-6fd8-4c02-a2e3-efbf596df636", 16 | "type": "ephemeral", 17 | "values": [ 18 | 0 19 | ], 20 | "name": "disk", 21 | "key": "telemetry.publisher.controller-0.redhat.local", 22 | "item_key": "disk.ephemeral.size_d8bd99b6-6fd8-4c02-a2e3-efbf596df636", 23 | "type_instance": "size", 24 | "labels": { 25 | "publisher": "telemetry.publisher.controller-0.redhat.local", 26 | "disk": "size", 27 | "type": "gauge", 28 | "project": "e56191ef77744c599dbcecae6af176bb", 29 | "resource": "d8bd99b6-6fd8-4c02-a2e3-efbf596df636", 30 | "unit": "GB", 31 | "counter": "disk.ephemeral.size" 32 | }, 33 | "description": "Service Telemetry exporter: 'disk' Type: 'ephemeral' Dstype: 'gauge' Dsname: 'disk.ephemeral.size'", 34 | "metric_name": "ceilometer_disk_ephemeral_size" 35 | }, 36 | { 37 | "publisher": "telemetry.publisher.controller-0.redhat.local", 38 | "plugin": "disk", 39 | "plugin_instance": "d8bd99b6-6fd8-4c02-a2e3-efbf596df636", 40 | "type": "root", 41 | "values": [ 42 | 1 43 | ], 44 | "name": "disk", 45 | "key": "telemetry.publisher.controller-0.redhat.local", 46 | "item_key": "disk.root.size_d8bd99b6-6fd8-4c02-a2e3-efbf596df636", 47 | "type_instance": "size", 48 | "labels": { 49 | "publisher": "telemetry.publisher.controller-0.redhat.local", 50 | "disk": "size", 51 | "type": "gauge", 52 | "project": "e56191ef77744c599dbcecae6af176bb", 53 | "resource": "d8bd99b6-6fd8-4c02-a2e3-efbf596df636", 54 | "unit": "GB", 55 | "counter": "disk.root.size" 56 | }, 57 | "description": "Service Telemetry exporter: 'disk' Type: 'root' Dstype: 'gauge' Dsname: 'disk.root.size'", 58 | "metric_name": "ceilometer_disk_root_size" 59 | }, 60 | { 61 | "publisher": "telemetry.publisher.controller-0.redhat.local", 62 | "plugin": "compute", 63 | "plugin_instance": "d8bd99b6-6fd8-4c02-a2e3-efbf596df636", 64 | "type": "instance", 65 | "values": [ 66 | 10.839122 67 | ], 68 | "name": "compute", 69 | "key": "telemetry.publisher.controller-0.redhat.local", 70 | "item_key": "compute.instance.booting.time_d8bd99b6-6fd8-4c02-a2e3-efbf596df636", 71 | "type_instance": "booting", 72 | "labels": { 73 | "publisher": "telemetry.publisher.controller-0.redhat.local", 74 | "compute": "booting", 75 | "type": "gauge", 76 | "project": "e56191ef77744c599dbcecae6af176bb", 77 | "resource": "d8bd99b6-6fd8-4c02-a2e3-efbf596df636", 78 | "unit": "sec", 79 | "counter": "compute.instance.booting.time" 80 | }, 81 | "description": "Service Telemetry exporter: 'compute' Type: 'instance' Dstype: 'gauge' Dsname: 'compute.instance.booting.time'", 82 | "metric_name": "ceilometer_compute_instance_booting_time" 83 | }, 84 | { 85 | "publisher": "telemetry.publisher.controller-0.redhat.local", 86 | "plugin": "vcpus", 87 | "plugin_instance": "d8bd99b6-6fd8-4c02-a2e3-efbf596df636", 88 | "type": "vcpus", 89 | "values": [ 90 | 2 91 | ], 92 | "name": "vcpus", 93 | "key": "telemetry.publisher.controller-0.redhat.local", 94 | "item_key": "vcpus_d8bd99b6-6fd8-4c02-a2e3-efbf596df636", 95 | "type_instance": "", 96 | "labels": { 97 | "publisher": "telemetry.publisher.controller-0.redhat.local", 98 | "vcpus": "d8bd99b6-6fd8-4c02-a2e3-efbf596df636", 99 | "type": "gauge", 100 | "project": "e56191ef77744c599dbcecae6af176bb", 101 | "resource": "d8bd99b6-6fd8-4c02-a2e3-efbf596df636", 102 | "unit": "vcpu", 103 | "counter": "vcpus" 104 | }, 105 | "description": "Service Telemetry exporter: 'vcpus' Type: 'vcpus' Dstype: 'gauge' Dsname: 'vcpus'", 106 | "metric_name": "ceilometer_vcpus" 107 | }, 108 | { 109 | "publisher": "telemetry.publisher.controller-0.redhat.local", 110 | "plugin": "memory", 111 | "plugin_instance": "d8bd99b6-6fd8-4c02-a2e3-efbf596df636", 112 | "type": "memory", 113 | "values": [ 114 | 512 115 | ], 116 | "name": "memory", 117 | "key": "telemetry.publisher.controller-0.redhat.local", 118 | "item_key": "memory_d8bd99b6-6fd8-4c02-a2e3-efbf596df636", 119 | "type_instance": "", 120 | "labels": { 121 | "publisher": "telemetry.publisher.controller-0.redhat.local", 122 | "memory": "d8bd99b6-6fd8-4c02-a2e3-efbf596df636", 123 | "type": "gauge", 124 | "project": "e56191ef77744c599dbcecae6af176bb", 125 | "resource": "d8bd99b6-6fd8-4c02-a2e3-efbf596df636", 126 | "unit": "MB", 127 | "counter": "memory" 128 | }, 129 | "description": "Service Telemetry exporter: 'memory' Type: 'memory' Dstype: 'gauge' Dsname: 'memory'", 130 | "metric_name": "ceilometer_memory" 131 | } 132 | ] 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /tests/internal_pkg/metrics_incoming_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "math/rand" 7 | "reflect" 8 | "testing" 9 | "time" 10 | 11 | "collectd.org/cdtime" 12 | "github.com/infrawatch/smart-gateway/internal/pkg/metrics/incoming" 13 | "github.com/infrawatch/smart-gateway/internal/pkg/saconfig" 14 | jsoniter "github.com/json-iterator/go" 15 | "github.com/stretchr/testify/assert" 16 | ) 17 | 18 | type IncommingCollecdDataMatrix struct { 19 | Field string 20 | Expected string 21 | } 22 | 23 | var ( 24 | json = jsoniter.ConfigCompatibleWithStandardLibrary 25 | ) 26 | 27 | //CeilometerMetricTemplate holds correct parsings for comparing against parsed results 28 | type CeilometerMetricTestTemplate struct { 29 | TestInput jsoniter.RawMessage `json:"testInput"` 30 | ValidatedResults []*struct { 31 | Publisher string `json:"publisher"` 32 | Plugin string `json:"plugin"` 33 | PluginInstance string `json:"plugin_instance"` 34 | Type string `json:"type"` 35 | TypeInstance string `json:"type_instance"` 36 | Name string `json:"name"` 37 | Key string `json:"key"` 38 | ItemKey string `json:"item_Key"` 39 | Description string `json:"description"` 40 | MetricName string `json:"metric_name"` 41 | Labels map[string]string `json:"labels"` 42 | Values []float64 `json:"values"` 43 | ISNew bool 44 | Interval float64 45 | DataSource saconfig.DataSource 46 | } `json:"validatedResults"` 47 | } 48 | 49 | func CeilometerMetricTestTemplateFromJSON(jsonData string) (*CeilometerMetricTestTemplate, error) { 50 | var testData CeilometerMetricTestTemplate 51 | err := json.Unmarshal([]byte(jsonData), &testData) 52 | if err != nil { 53 | return nil, fmt.Errorf("error parsing json: %s", err) 54 | } 55 | 56 | for _, r := range testData.ValidatedResults { 57 | r.Interval = 5.0 58 | r.DataSource = saconfig.DataSourceCeilometer 59 | r.ISNew = true 60 | } 61 | return &testData, nil 62 | } 63 | 64 | /*----------------------------- helper functions -----------------------------*/ 65 | //GenerateSampleCollectdData ... 66 | func GenerateSampleCollectdData(hostname string, pluginname string) *incoming.CollectdMetric { 67 | citfc := incoming.NewFromDataSource(saconfig.DataSourceCollectd) 68 | collectd := citfc.(*incoming.CollectdMetric) 69 | collectd.Host = hostname 70 | collectd.Plugin = pluginname 71 | collectd.Type = "collectd" 72 | collectd.PluginInstance = "pluginnameinstance" 73 | collectd.Dstypes = []string{"gauge", "derive"} 74 | collectd.Dsnames = []string{"value1", "value2"} 75 | collectd.TypeInstance = "idle" 76 | collectd.Values = []float64{rand.Float64(), rand.Float64()} 77 | collectd.Time = cdtime.New(time.Now()) 78 | return collectd 79 | } 80 | 81 | //GetFieldStr ... 82 | func GetFieldStr(dataItem incoming.MetricDataFormat, field string) string { 83 | r := reflect.ValueOf(dataItem) 84 | f := reflect.Indirect(r).FieldByName(field) 85 | return string(f.String()) 86 | } 87 | 88 | /*----------------------------------------------------------------------------*/ 89 | 90 | func TestCollectdIncoming(t *testing.T) { 91 | emptySample := incoming.NewFromDataSource(saconfig.DataSourceCollectd) 92 | sample := GenerateSampleCollectdData("hostname", "pluginname") 93 | jsonBytes, err := json.Marshal([]*incoming.CollectdMetric{sample}) 94 | if err != nil { 95 | t.Error("Failed to marshal incoming.Collectd to JSON") 96 | } 97 | jsonString := string(jsonBytes) 98 | 99 | t.Run("Test initialization of empty incoming.Collectd sample", func(t *testing.T) { 100 | assert.Emptyf(t, GetFieldStr(emptySample, "Plugin"), "Collectd data is not empty.") 101 | // test DSName behaviour 102 | if emptyCollectd, ok := emptySample.(*incoming.CollectdMetric); ok { 103 | assert.Equal(t, "666", emptyCollectd.DSName(666)) 104 | emptyCollectd.Values = []float64{1} 105 | assert.Equal(t, "value", emptyCollectd.DSName(666)) 106 | } else { 107 | t.Errorf("Failed to convert empty incoming.MetricDataFormat to empty incoming.CollectdMetric") 108 | } 109 | // test loading values from []byte and string 110 | _, errr := emptySample.ParseInputJSON("Error Json") 111 | assert.Error(t, errr, "Expected error got nil") 112 | data := []IncommingCollecdDataMatrix{ 113 | {"Host", GetFieldStr(sample, "Host")}, 114 | {"Plugin", GetFieldStr(sample, "Plugin")}, 115 | {"Type", GetFieldStr(sample, "Type")}, 116 | {"PluginInstance", GetFieldStr(sample, "PluginInstance")}, 117 | {"Dstypes", GetFieldStr(sample, "Dstypes")}, 118 | {"Dsnames", GetFieldStr(sample, "Dsnames")}, 119 | {"TypeInstance", GetFieldStr(sample, "TypeInstance")}, 120 | {"Values", GetFieldStr(sample, "Values")}, 121 | {"Time", GetFieldStr(sample, "Time")}, 122 | } 123 | sample2, errr := emptySample.ParseInputJSON(jsonString) 124 | if errr == nil { 125 | for _, testCase := range data { 126 | assert.Equal(t, testCase.Expected, GetFieldStr(sample2[0], testCase.Field)) 127 | } 128 | } else { 129 | t.Errorf("Failed to initialize using ParseInputJSON: %s", err) 130 | } 131 | errr = emptySample.ParseInputByte([]byte("error string")) 132 | assert.Error(t, errr, "Expected error got nil") 133 | esample := incoming.NewFromDataSource(saconfig.DataSourceCollectd) 134 | errs := esample.ParseInputByte(jsonBytes) 135 | if errs == nil { 136 | sample3 := esample.(*incoming.CollectdMetric) 137 | for _, testCase := range data { 138 | assert.Equal(t, testCase.Expected, GetFieldStr(sample3, testCase.Field)) 139 | } 140 | } else { 141 | t.Errorf("Failed to initialize using ParseInputByte: %s", err) 142 | } 143 | }) 144 | 145 | t.Run("Test incoming.Collectd sample", func(t *testing.T) { 146 | assert.NotEmptyf(t, jsonBytes, "Empty sample string generated") 147 | // test DSName behaviour 148 | for index := range sample.Values { 149 | assert.Equal(t, fmt.Sprintf("value%d", index+1), sample.DSName(index)) 150 | } 151 | assert.Equal(t, "pluginname", sample.GetName()) 152 | // test GetItemKey behaviour 153 | assert.Equal(t, "pluginname_collectd_pluginnameinstance_idle", sample.GetItemKey()) 154 | hold := sample.Type 155 | sample.Type = sample.Plugin 156 | assert.Equal(t, "pluginname_pluginnameinstance_idle", sample.GetItemKey()) 157 | sample.Type = hold 158 | // test GetLabels behaviour 159 | labels := sample.GetLabels() 160 | assert.Contains(t, labels, "type") 161 | assert.Contains(t, labels, sample.Plugin) 162 | assert.Contains(t, labels, "instance") 163 | // test GetMetricDesc behaviour 164 | metricDesc := "Service Telemetry exporter: 'pluginname' Type: 'collectd' Dstype: 'gauge' Dsname: 'value1'" 165 | assert.Equal(t, metricDesc, sample.GetMetricDesc(0)) 166 | // test GetMetricName behaviour 167 | metricName := "collectd_pluginname_collectd_value1" 168 | assert.Equal(t, metricName, sample.GetMetricName(0)) 169 | sample.Type = sample.Plugin 170 | metricName = "collectd_pluginname_value1" 171 | assert.Equal(t, metricName, sample.GetMetricName(0)) 172 | sample.Dstypes = []string{"counter", "derive"} 173 | metricName1 := "collectd_pluginname_value1_total" 174 | metricName2 := "collectd_pluginname_value2_total" 175 | assert.Equal(t, metricName1, sample.GetMetricName(0)) 176 | assert.Equal(t, metricName2, sample.GetMetricName(1)) 177 | }) 178 | } 179 | 180 | func TestCeilometerIncoming(t *testing.T) { 181 | cm := incoming.NewFromDataSource(saconfig.DataSourceCeilometer) 182 | metric := cm.(*incoming.CeilometerMetric) 183 | 184 | var tests = make(map[string]jsoniter.RawMessage) 185 | 186 | testDataJSON, err := ioutil.ReadFile("messages/metric-tests.json") 187 | if err != nil { 188 | t.Errorf("Failed loading test data: %s\n", err) 189 | } 190 | 191 | err = json.Unmarshal([]byte(testDataJSON), &tests) 192 | if err != nil { 193 | t.Errorf("error parsing json: %s", err) 194 | } 195 | 196 | t.Run("Test parsing of Ceilometer message", func(t *testing.T) { 197 | testData, err := CeilometerMetricTestTemplateFromJSON(string(tests["CeilometerMetrics"])) 198 | if err != nil { 199 | t.Errorf("Failed loading ceilometer metric test data: %s", err) 200 | } 201 | metrics, err := metric.ParseInputJSON(string(testData.TestInput)) 202 | if err != nil { 203 | t.Errorf("Ceilometer message parsing failed: %s\n", err) 204 | } 205 | 206 | for index, standard := range testData.ValidatedResults { 207 | if m, ok := metrics[index].(*incoming.CeilometerMetric); ok { 208 | assert.Equal(t, standard.Publisher, m.Publisher) 209 | assert.Equal(t, standard.Plugin, m.Plugin) 210 | assert.Equal(t, standard.PluginInstance, m.PluginInstance) 211 | assert.Equal(t, standard.Type, m.Type) 212 | assert.Equal(t, standard.TypeInstance, m.TypeInstance) 213 | assert.Equal(t, standard.Values, m.GetValues()) 214 | assert.Equal(t, standard.DataSource, m.DataSource) 215 | assert.Equal(t, standard.Interval, m.GetInterval()) 216 | assert.Equal(t, standard.ItemKey, m.GetItemKey()) 217 | assert.Equal(t, standard.Key, m.GetKey()) 218 | assert.Equal(t, standard.Labels, m.GetLabels()) 219 | assert.Equal(t, standard.Name, m.GetName()) 220 | assert.Equal(t, standard.ISNew, m.ISNew()) 221 | assert.Equal(t, standard.Description, m.GetMetricDesc(0)) 222 | assert.Equal(t, standard.MetricName, m.GetMetricName(0)) 223 | } 224 | } 225 | }) 226 | } 227 | 228 | func TestCeilometerGetLabels(t *testing.T) { 229 | // test data 230 | cm := incoming.CeilometerMetric{ 231 | Payload: map[string]interface{}{}, 232 | } 233 | 234 | t.Run("Missing fields", func(t *testing.T) { 235 | cm.GetLabels() 236 | }) 237 | 238 | t.Run("Wrong field type", func(t *testing.T) { 239 | cm.Payload["project_id"] = nil 240 | cm.Payload["resource_id"] = nil 241 | cm.Payload["counter_unit"] = nil 242 | cm.Payload["counter_name"] = nil 243 | cm.GetLabels() 244 | }) 245 | } 246 | -------------------------------------------------------------------------------- /tests/internal_pkg/saconfig_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "reflect" 7 | "testing" 8 | 9 | "github.com/infrawatch/smart-gateway/internal/pkg/saconfig" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | const ( 14 | EventsConfig = ` 15 | { 16 | "AMQP1Connections": [ 17 | {"Url": "127.0.0.1:5672/collectd/notify", "DataSource": "collectd"}, 18 | {"Url": "127.0.0.1:5672/ceilometer/event.sample", "DataSource": "ceilometer"}, 19 | {"Url": "127.0.0.1:5672/universal/events", "DataSource": "universal"} 20 | ], 21 | "AMQP1EventURL": "127.0.0.1:5672/collectd/notify", 22 | "ElasticHostURL": "http://127.0.0.1:9200", 23 | "AlertManagerURL": "http://127.0.0.1:9093/api/v1/alerts", 24 | "ResetIndex": false, 25 | "Debug": true, 26 | "Prefetch": 101, 27 | "API": { 28 | "APIEndpointURL": "http://127.0.0.1:8082", 29 | "AMQP1PublishURL": "127.0.0.1:5672/collectd/alert" 30 | } 31 | 32 | } 33 | ` 34 | MetricsConfig = ` 35 | { 36 | "AMQP1Connections": [ 37 | {"Url": "127.0.0.1:5672/collectd/telemetry", "DataSource": "collectd"}, 38 | {"Url": "127.0.0.1:5672/ceilometer/metering.sample", "DataSource": "ceilometer"}, 39 | {"Url": "127.0.0.1:5672/universal/telemetry", "DataSource": "universal"} 40 | ], 41 | "AMQP1MetricURL": "127.0.0.1:5672/collectd/telemetry", 42 | "Exporterhost": "localhost", 43 | "Exporterport": 8081, 44 | "CPUStats": false, 45 | "DataCount": -1, 46 | "UseSample": false, 47 | "UseTimeStamp": true, 48 | "Debug": false, 49 | "Prefetch": 102, 50 | "Sample": { 51 | "HostCount": 10, 52 | "PluginCount": 100, 53 | "DataCount": -1 54 | } 55 | } 56 | ` 57 | ) 58 | 59 | /*----------------------------- helper functions -----------------------------*/ 60 | 61 | //GenerateSampleCacheData .... 62 | func GenerateTestConfig(content string) (string, error) { 63 | file, err := ioutil.TempFile(".", "smart_gateway_config_test") 64 | if err != nil { 65 | return "", err 66 | } 67 | defer file.Close() 68 | file.WriteString(content) 69 | if err != nil { 70 | return "", err 71 | } 72 | return file.Name(), nil 73 | } 74 | 75 | /*----------------------------------------------------------------------------*/ 76 | 77 | type ConfigDataMatrix struct { 78 | Field string 79 | Value interface{} 80 | } 81 | 82 | type ConfigDataTestRun struct { 83 | Name string 84 | Content string 85 | Loader string 86 | Matrix []ConfigDataMatrix 87 | } 88 | 89 | func TestUnstructuredData(t *testing.T) { 90 | testRuns := []ConfigDataTestRun{ 91 | { 92 | Name: "Test events config", 93 | Content: EventsConfig, 94 | Loader: "event", 95 | Matrix: []ConfigDataMatrix{ 96 | {"AMQP1EventURL", "127.0.0.1:5672/collectd/notify"}, 97 | {"ElasticHostURL", "http://127.0.0.1:9200"}, 98 | {"AlertManagerURL", "http://127.0.0.1:9093/api/v1/alerts"}, 99 | {"ResetIndex", false}, 100 | {"Debug", true}, 101 | {"Prefetch", 101}, 102 | }, 103 | }, 104 | { 105 | Name: "Test metrics config", 106 | Content: MetricsConfig, 107 | Loader: "metric", 108 | Matrix: []ConfigDataMatrix{ 109 | {"AMQP1MetricURL", "127.0.0.1:5672/collectd/telemetry"}, 110 | {"Exporterhost", "localhost"}, 111 | {"Exporterport", 8081}, 112 | {"CPUStats", false}, 113 | {"DataCount", -1}, 114 | {"UseTimeStamp", true}, 115 | {"Debug", false}, 116 | {"Prefetch", 102}, 117 | }, 118 | }, 119 | } 120 | 121 | for _, run := range testRuns { 122 | confPath, err := GenerateTestConfig(run.Content) 123 | if err != nil { 124 | t.Fatal(err) 125 | } 126 | defer os.Remove(confPath) 127 | cfg, err := saconfig.LoadConfiguration(confPath, run.Loader) 128 | if err != nil { 129 | t.Fatal(err) 130 | } 131 | if run.Loader == "metric" { 132 | cfg = cfg.(*saconfig.MetricConfiguration) 133 | } else { 134 | cfg = cfg.(*saconfig.EventConfiguration) 135 | } 136 | t.Run(run.Name, func(t *testing.T) { 137 | reflectedVal := reflect.ValueOf(cfg) 138 | for _, testCase := range run.Matrix { 139 | field := reflectedVal.Elem().FieldByName(testCase.Field) 140 | if !field.IsValid() { 141 | t.Fail() 142 | } 143 | value := reflect.ValueOf(testCase.Value) 144 | if !field.IsValid() { 145 | t.Errorf("Failed to parse field %s.", testCase.Field) 146 | continue 147 | } 148 | switch value.Type().String() { 149 | case "bool": 150 | assert.Equal(t, value.Bool(), field.Bool()) 151 | case "int": 152 | assert.Equal(t, value.Int(), field.Int()) 153 | default: 154 | assert.Equal(t, value.String(), field.String()) 155 | } 156 | } 157 | }) 158 | } 159 | } 160 | 161 | func TestEventStructuredData(t *testing.T) { 162 | confPath, err := GenerateTestConfig(EventsConfig) 163 | if err != nil { 164 | t.Fatal(err) 165 | } 166 | defer os.Remove(confPath) 167 | cfg, err := saconfig.LoadConfiguration(confPath, "event") 168 | if err != nil { 169 | t.Fatal(err) 170 | } 171 | 172 | t.Run("Test structured data of event config", func(t *testing.T) { 173 | apiStruct := saconfig.EventAPIConfig{APIEndpointURL: "http://127.0.0.1:8082", AMQP1PublishURL: "127.0.0.1:5672/collectd/alert"} 174 | assert.Equal(t, apiStruct, cfg.(*saconfig.EventConfiguration).API) 175 | }) 176 | 177 | t.Run("Test structured AMQP connections", func(t *testing.T) { 178 | connStruct := []saconfig.AMQPConnection{ 179 | saconfig.AMQPConnection{URL: "127.0.0.1:5672/collectd/notify", DataSource: "collectd", DataSourceID: 1}, 180 | saconfig.AMQPConnection{URL: "127.0.0.1:5672/ceilometer/event.sample", DataSource: "ceilometer", DataSourceID: 2}, 181 | saconfig.AMQPConnection{URL: "127.0.0.1:5672/universal/events", DataSource: "universal", DataSourceID: 0}, 182 | } 183 | assert.Equal(t, connStruct, cfg.(*saconfig.EventConfiguration).AMQP1Connections) 184 | }) 185 | } 186 | 187 | /* enable this after implementing []AMQP1Connections in metrics.go 188 | func TestMetricStructuredData(t *testing.T) { 189 | confPath, err := GenerateTestConfig(MetricsConfig) 190 | if err != nil { 191 | t.Fatal(err) 192 | } 193 | defer os.Remove(confPath) 194 | cfg, err := saconfig.LoadConfiguration(confPath, "metric") 195 | if err != nil { 196 | t.Fatal(err) 197 | } 198 | 199 | t.Run("Test structured AMQP connections", func(t *testing.T) { 200 | connStruct := []saconfig.AMQPConnection{ 201 | saconfig.AMQPConnection{URL: "127.0.0.1:5672/collectd/telemetry", DataSource: "collectd", DataSourceID: 1}, 202 | saconfig.AMQPConnection{URL: "127.0.0.1:5672/ceilometer/metering.sample", DataSource: "ceilometer", DataSourceID: 2}, 203 | saconfig.AMQPConnection{URL: "127.0.0.1:5672/universal/metrics", DataSource: "universal", DataSourceID: 0}, 204 | } 205 | assert.Equal(t, connStruct, cfg.(*saconfig.MetricConfiguration).AMQP1Connections) 206 | }) 207 | } 208 | */ 209 | -------------------------------------------------------------------------------- /tests/internal_pkg/tsdb_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/infrawatch/smart-gateway/internal/pkg/metrics/incoming" 8 | "github.com/infrawatch/smart-gateway/internal/pkg/tsdb" 9 | "github.com/prometheus/client_golang/prometheus" 10 | dto "github.com/prometheus/client_model/go" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | /*----------------------------- helper functions -----------------------------*/ 15 | 16 | //GenerateSampleCacheData .... 17 | func GenerateCollectdMetric(hostname string, pluginname string, useTimestamp bool, index int) (*incoming.CollectdMetric, prometheus.Metric, dto.Metric) { 18 | sample := GenerateSampleCollectdData(hostname, pluginname) 19 | collectdMetric, _ := tsdb.NewPrometheusMetric(useTimestamp, "collectd", sample, index) 20 | metric := dto.Metric{} 21 | collectdMetric.Write(&metric) 22 | return sample, collectdMetric, metric 23 | } 24 | 25 | /*----------------------------------------------------------------------------*/ 26 | 27 | func TestTimestamp(t *testing.T) { 28 | sample, _, metric := GenerateCollectdMetric("hostname", "pluginname", true, 0) 29 | assert.Equal(t, sample.Time.Time().UnixNano()/1000000, metric.GetTimestampMs()) 30 | 31 | sample, _, metric = GenerateCollectdMetric("hostname", "pluginname", false, 0) 32 | assert.NotEqual(t, sample.Time.Time().UnixNano()/1000000, metric.GetTimestampMs()) 33 | } 34 | 35 | func TestCollectdMetric(t *testing.T) { 36 | t.Run("Test prometeus metric values", func(t *testing.T) { 37 | sample, collectdMetric, metric := GenerateCollectdMetric("hostname", "pluginname", true, 0) 38 | assert.True(t, strings.HasPrefix(collectdMetric.Desc().String(), "Desc{fqName: \"collectd_pluginname_collectd_value1\"")) 39 | assert.Equal(t, sample.Values[0], metric.GetGauge().GetValue()) 40 | assert.Equal(t, 0.0, metric.GetCounter().GetValue()) 41 | 42 | sample, collectdMetric, metric = GenerateCollectdMetric("hostname", "pluginname", true, 1) 43 | assert.True(t, strings.HasPrefix(collectdMetric.Desc().String(), "Desc{fqName: \"collectd_pluginname_collectd_value2_total\"")) 44 | assert.Equal(t, sample.Values[1], metric.GetCounter().GetValue()) 45 | assert.Equal(t, 0.0, metric.GetGauge().GetValue()) 46 | }) 47 | 48 | t.Run("Test heart beat metric", func(t *testing.T) { 49 | collectdMetric, _ := tsdb.NewHeartBeatMetricByHost("test_heartbeat", 66.6) 50 | assert.True(t, strings.HasPrefix(collectdMetric.Desc().String(), "Desc{fqName: \"collectd_last_metric_for_host_status\"")) 51 | metric := dto.Metric{} 52 | collectdMetric.Write(&metric) 53 | assert.Equal(t, 66.6, metric.GetGauge().GetValue()) 54 | assert.Equal(t, "test_heartbeat", metric.GetLabel()[0].GetValue()) 55 | }) 56 | 57 | t.Run("Test metric by host", func(t *testing.T) { 58 | collectdMetric, _ := tsdb.AddMetricsByHost("test_host", 666.0) 59 | assert.True(t, strings.HasPrefix(collectdMetric.Desc().String(), "Desc{fqName: \"collectd_metric_per_host\"")) 60 | metric := dto.Metric{} 61 | collectdMetric.Write(&metric) 62 | assert.Equal(t, 666.0, metric.GetGauge().GetValue()) 63 | assert.Equal(t, "test_host", metric.GetLabel()[0].GetValue()) 64 | }) 65 | } 66 | --------------------------------------------------------------------------------