├── .dockerignore ├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── .promu.yml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── VERSION ├── go.mod ├── go.sum ├── script-exporter.yml ├── script_exporter.go └── script_exporter_test.go /.dockerignore: -------------------------------------------------------------------------------- 1 | .build/ 2 | .tarballs/ 3 | 4 | !.build/linux-amd64 5 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | on: [push] 2 | name: CI 3 | jobs: 4 | test: 5 | strategy: 6 | matrix: 7 | go-version: [1.19] 8 | os: [ubuntu-latest, macos-latest] 9 | runs-on: ${{ matrix.os }} 10 | steps: 11 | - uses: actions/setup-go@v3 12 | with: 13 | go-version: ${{ matrix.go-version }} 14 | - uses: actions/checkout@v3 15 | - run: make test vet 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .build 2 | script_exporter 3 | *.tar.gz 4 | -------------------------------------------------------------------------------- /.promu.yml: -------------------------------------------------------------------------------- 1 | go: 2 | version: 1.14.1 3 | cgo: true 4 | repository: 5 | path: github.com/adhocteam/script_exporter 6 | build: 7 | flags: -a -tags 'netgo static_build' 8 | ldflags: | 9 | -X github.com/prometheus/common/version.Version={{.Version}} 10 | -X github.com/prometheus/common/version.Revision={{.Revision}} 11 | -X github.com/prometheus/common/version.Branch={{.Branch}} 12 | -X github.com/prometheus/common/version.BuildUser={{user}}@{{host}} 13 | -X github.com/prometheus/common/version.BuildDate={{date "20060102-15:04:05"}} 14 | tarball: 15 | files: 16 | - LICENSE 17 | crossbuild: 18 | platforms: 19 | - linux/amd64 20 | # - linux/386 21 | # - darwin/amd64 22 | # - darwin/386 23 | # - windows/amd64 24 | # - windows/386 25 | # - netbsd/amd64 26 | # - netbsd/386 27 | # - linux/arm 28 | # - linux/arm64 29 | # - netbsd/arm 30 | # - linux/ppc64 31 | # - linux/ppc64le 32 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.14.1-alpine AS build-env 2 | 3 | RUN apk add --update git gcc libc-dev 4 | RUN go get -u github.com/prometheus/promu 5 | 6 | RUN mkdir script_exporter 7 | COPY .promu.yml script_exporter.go go.mod go.sum /go/script_exporter/ 8 | 9 | WORKDIR /go/script_exporter 10 | RUN promu build 11 | 12 | FROM alpine:3.11 13 | LABEL upstream="https://github.com/adhocteam/script_exporter" 14 | LABEL maintainer="james.kassemi@adhocteam.us" 15 | RUN apk add --no-cache bash 16 | COPY --from=build-env /go/script_exporter/script_exporter /bin/script-exporter 17 | COPY script-exporter.yml /etc/script-exporter/config.yml 18 | 19 | EXPOSE 9172 20 | ENTRYPOINT [ "/bin/script-exporter" ] 21 | CMD ["-config.file=/etc/script-exporter/config.yml"] 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2016 Ad Hoc, LLC 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2015 The Prometheus Authors 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | 14 | GO := GO111MODULE=on go 15 | GOPATH := $(shell go env GOPATH) 16 | PROMU := $(GOPATH)/bin/promu 17 | 18 | PREFIX ?= $(shell pwd) 19 | BIN_DIR ?= $(shell pwd) 20 | DOCKER_IMAGE_NAME ?= adhocteam/script-exporter 21 | DOCKER_IMAGE_TAG ?= $(subst /,-,$(shell git rev-parse --abbrev-ref HEAD)) 22 | 23 | 24 | all: format build test 25 | 26 | style: 27 | @echo ">> checking code style" 28 | @! gofmt -d $(shell find . -path ./vendor -prune -o -name '*.go' -print) | grep '^' 29 | 30 | test: 31 | @echo ">> running tests" 32 | @$(GO) test ./... 33 | 34 | format: 35 | @echo ">> formatting code" 36 | @$(GO) fmt ./... 37 | 38 | vet: 39 | @echo ">> vetting code" 40 | @$(GO) vet ./... 41 | 42 | build: promu 43 | @echo ">> building binaries" 44 | @$(PROMU) build --prefix $(PREFIX) 45 | 46 | tarball: promu 47 | @echo ">> building release tarball" 48 | @$(PROMU) tarball --prefix $(PREFIX) $(BIN_DIR) 49 | 50 | crossbuild: promu 51 | @echo ">> building" 52 | @$(PROMU) crossbuild 53 | 54 | docker: 55 | @echo ">> building docker image" 56 | @docker build -t "$(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG)" . 57 | 58 | release: 59 | @$(PROMU) crossbuild tarballs 60 | @$(PROMU) release .tarballs 61 | 62 | promu: 63 | @GOOS=$(shell uname -s | tr A-Z a-z) \ 64 | GOARCH=$(subst x86_64,amd64,$(patsubst i%86,386,$(shell uname -m))) \ 65 | $(GO) get -u github.com/prometheus/promu 66 | 67 | 68 | .PHONY: all style format build crossbuild test vet tarball docker promu 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Script Exporter 2 | 3 | GitHub: https://github.com/adhocteam/script_exporter 4 | 5 | Prometheus exporter written to execute and collect metrics on script exit status 6 | and duration. Designed to allow the execution of probes where support for the 7 | probe type wasn't easily configured with the Prometheus blackbox exporter. 8 | 9 | Minimum supported Go Version: 1.13.1 10 | 11 | ## Sample Configuration 12 | 13 | ```yaml 14 | scripts: 15 | - name: success 16 | script: sleep 5 17 | 18 | - name: failure 19 | script: sleep 2 && exit 1 20 | 21 | - name: timeout 22 | script: sleep 5 23 | timeout: 1 24 | ``` 25 | 26 | ## Running 27 | 28 | You can run via docker with: 29 | 30 | ``` 31 | docker run -d -p 9172:9172 --name script-exporter \ 32 | -v `pwd`/script-exporter.yml:/etc/script-exporter/config.yml:ro \ 33 | adhocteam/script-exporter:master \ 34 | -config.file=/etc/script-exporter/config.yml \ 35 | -web.listen-address=":9172" \ 36 | -web.telemetry-path="/metrics" \ 37 | -config.shell="/bin/sh" 38 | ``` 39 | 40 | You'll need to customize the docker image or use the binary on the host system 41 | to install tools such as curl for certain scenarios. 42 | 43 | ## Probing 44 | 45 | To return the script exporter internal metrics exposed by the default Prometheus 46 | handler: 47 | 48 | `$ curl http://localhost:9172/metrics` 49 | 50 | To execute a script, use the `name` parameter to the `/probe` endpoint: 51 | 52 | `$ curl http://localhost:9172/probe?name=failure` 53 | 54 | ``` 55 | script_duration_seconds{script="failure"} 2.008337 56 | script_success{script="failure"} 0 57 | ``` 58 | 59 | A regular expression may be specified with the `pattern` paremeter: 60 | 61 | `$ curl http://localhost:9172/probe?pattern=.*` 62 | 63 | ``` 64 | script_duration_seconds{script="timeout"} 1.005727 65 | script_success{script="timeout"} 0 66 | script_duration_seconds{script="failure"} 2.015021 67 | script_success{script="failure"} 0 68 | script_duration_seconds{script="success"} 5.013670 69 | script_success{script="success"} 1 70 | ``` 71 | 72 | ## Design 73 | 74 | YMMV if you're attempting to execute a large number of scripts, and you'd be 75 | better off creating an exporter that can handle your protocol without launching 76 | shell processes for each scrape. 77 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.2.0 2 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/adhocteam/script_exporter 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/prometheus/client_golang v1.5.1 7 | github.com/prometheus/common v0.9.1 8 | gopkg.in/yaml.v2 v2.2.8 9 | ) 10 | 11 | require ( 12 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect 13 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d // indirect 14 | github.com/beorn7/perks v1.0.1 // indirect 15 | github.com/cespare/xxhash/v2 v2.1.1 // indirect 16 | github.com/golang/protobuf v1.3.5 // indirect 17 | github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect 18 | github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect 19 | github.com/prometheus/client_model v0.2.0 // indirect 20 | github.com/prometheus/procfs v0.0.11 // indirect 21 | github.com/sirupsen/logrus v1.4.2 // indirect 22 | golang.org/x/sys v0.4.0 // indirect 23 | gopkg.in/alecthomas/kingpin.v2 v2.2.6 // indirect 24 | ) 25 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 2 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= 3 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 4 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 5 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 6 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E= 7 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= 8 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 9 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 10 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 11 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 12 | github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= 13 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 14 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 16 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 17 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 18 | github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 19 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 20 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 21 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 22 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 23 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 24 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 25 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 26 | github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls= 27 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 28 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 29 | github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= 30 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 31 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 32 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 33 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 34 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 35 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 36 | github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= 37 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 38 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 39 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 40 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 41 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 42 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 43 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 44 | github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= 45 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 46 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 47 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 48 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 49 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 50 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 51 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 52 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 53 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 54 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 55 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 56 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 57 | github.com/prometheus/client_golang v1.5.1 h1:bdHYieyGlH+6OLEk2YQha8THib30KP0/yD0YH9m6xcA= 58 | github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= 59 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 60 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 61 | github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= 62 | github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 63 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 64 | github.com/prometheus/common v0.9.1 h1:KOMtN28tlbam3/7ZKEYKHhKoJZYYj3gMH4uc62x7X7U= 65 | github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= 66 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 67 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 68 | github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= 69 | github.com/prometheus/procfs v0.0.11 h1:DhHlBtkHWPYi8O2y31JkK0TF+DGM+51OopZjH/Ia5qI= 70 | github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= 71 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 72 | github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= 73 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 74 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 75 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 76 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 77 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 78 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 79 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 80 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 81 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 82 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 83 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 84 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 85 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 86 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 87 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 88 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 89 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 90 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 91 | golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 92 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 93 | golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= 94 | golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 95 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 96 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 97 | gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= 98 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 99 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 100 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 101 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 102 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 103 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 104 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 105 | gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 106 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 107 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 108 | -------------------------------------------------------------------------------- /script-exporter.yml: -------------------------------------------------------------------------------- 1 | scripts: 2 | - name: 'success' 3 | script: sleep 5 4 | 5 | - name: 'failure' 6 | script: sleep 2 && exit 1 7 | 8 | - name: 'timeout' 9 | script: sleep 5 10 | timeout: 1 11 | -------------------------------------------------------------------------------- /script_exporter.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "flag" 7 | "fmt" 8 | "gopkg.in/yaml.v2" 9 | "io/ioutil" 10 | "net/http" 11 | "os" 12 | "os/exec" 13 | "regexp" 14 | "time" 15 | 16 | "github.com/prometheus/client_golang/prometheus" 17 | "github.com/prometheus/client_golang/prometheus/promhttp" 18 | "github.com/prometheus/common/log" 19 | "github.com/prometheus/common/version" 20 | ) 21 | 22 | var ( 23 | showVersion = flag.Bool("version", false, "Print version information.") 24 | configFile = flag.String("config.file", "script-exporter.yml", "Script exporter configuration file.") 25 | listenAddress = flag.String("web.listen-address", ":9172", "The address to listen on for HTTP requests.") 26 | metricsPath = flag.String("web.telemetry-path", "/metrics", "Path under which to expose metrics.") 27 | shell = flag.String("config.shell", "/bin/sh", "Shell to execute script") 28 | ) 29 | 30 | type Config struct { 31 | Scripts []*Script `yaml:"scripts"` 32 | } 33 | 34 | type Script struct { 35 | Name string `yaml:"name"` 36 | Content string `yaml:"script"` 37 | Timeout int64 `yaml:"timeout"` 38 | } 39 | 40 | type Measurement struct { 41 | Script *Script 42 | Success int 43 | Duration float64 44 | } 45 | 46 | func runScript(script *Script) error { 47 | ctx, cancel := context.WithTimeout(context.Background(), time.Duration(script.Timeout)*time.Second) 48 | defer cancel() 49 | 50 | bashCmd := exec.CommandContext(ctx, *shell) 51 | 52 | bashIn, err := bashCmd.StdinPipe() 53 | 54 | if err != nil { 55 | return err 56 | } 57 | 58 | if err = bashCmd.Start(); err != nil { 59 | return err 60 | } 61 | 62 | if _, err = bashIn.Write([]byte(script.Content)); err != nil { 63 | return err 64 | } 65 | 66 | bashIn.Close() 67 | 68 | return bashCmd.Wait() 69 | } 70 | 71 | func runScripts(scripts []*Script) []*Measurement { 72 | measurements := make([]*Measurement, 0) 73 | 74 | ch := make(chan *Measurement) 75 | 76 | for _, script := range scripts { 77 | go func(script *Script) { 78 | start := time.Now() 79 | success := 0 80 | err := runScript(script) 81 | duration := time.Since(start).Seconds() 82 | 83 | if err == nil { 84 | log.Debugf("OK: %s (after %fs).", script.Name, duration) 85 | success = 1 86 | } else { 87 | log.Infof("ERROR: %s: %s (failed after %fs).", script.Name, err, duration) 88 | } 89 | 90 | ch <- &Measurement{ 91 | Script: script, 92 | Duration: duration, 93 | Success: success, 94 | } 95 | }(script) 96 | } 97 | 98 | for i := 0; i < len(scripts); i++ { 99 | measurements = append(measurements, <-ch) 100 | } 101 | 102 | return measurements 103 | } 104 | 105 | func scriptFilter(scripts []*Script, name, pattern string) (filteredScripts []*Script, err error) { 106 | if name == "" && pattern == "" { 107 | err = errors.New("`name` or `pattern` required") 108 | return 109 | } 110 | 111 | var patternRegexp *regexp.Regexp 112 | 113 | if pattern != "" { 114 | patternRegexp, err = regexp.Compile(pattern) 115 | 116 | if err != nil { 117 | return 118 | } 119 | } 120 | 121 | for _, script := range scripts { 122 | if script.Name == name || (pattern != "" && patternRegexp.MatchString(script.Name)) { 123 | filteredScripts = append(filteredScripts, script) 124 | } 125 | } 126 | 127 | return 128 | } 129 | 130 | func scriptRunHandler(w http.ResponseWriter, r *http.Request, config *Config) { 131 | params := r.URL.Query() 132 | name := params.Get("name") 133 | pattern := params.Get("pattern") 134 | 135 | scripts, err := scriptFilter(config.Scripts, name, pattern) 136 | 137 | if err != nil { 138 | http.Error(w, err.Error(), 500) 139 | return 140 | } 141 | 142 | measurements := runScripts(scripts) 143 | 144 | for _, measurement := range measurements { 145 | fmt.Fprintf(w, "script_duration_seconds{script=\"%s\"} %f\n", measurement.Script.Name, measurement.Duration) 146 | fmt.Fprintf(w, "script_success{script=\"%s\"} %d\n", measurement.Script.Name, measurement.Success) 147 | } 148 | } 149 | 150 | func init() { 151 | prometheus.MustRegister(version.NewCollector("script_exporter")) 152 | } 153 | 154 | func main() { 155 | flag.Parse() 156 | 157 | if *showVersion { 158 | fmt.Fprintln(os.Stdout, version.Print("script_exporter")) 159 | os.Exit(0) 160 | } 161 | 162 | log.Infoln("Starting script_exporter", version.Info()) 163 | 164 | yamlFile, err := ioutil.ReadFile(*configFile) 165 | 166 | if err != nil { 167 | log.Fatalf("Error reading config file: %s", err) 168 | } 169 | 170 | config := Config{} 171 | 172 | err = yaml.Unmarshal(yamlFile, &config) 173 | 174 | if err != nil { 175 | log.Fatalf("Error parsing config file: %s", err) 176 | } 177 | 178 | log.Infof("Loaded %d script configurations", len(config.Scripts)) 179 | 180 | for _, script := range config.Scripts { 181 | if script.Timeout == 0 { 182 | script.Timeout = 15 183 | } 184 | } 185 | 186 | http.Handle("/metrics", promhttp.Handler()) 187 | 188 | http.HandleFunc("/probe", func(w http.ResponseWriter, r *http.Request) { 189 | scriptRunHandler(w, r, &config) 190 | }) 191 | 192 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 193 | w.Write([]byte(` 194 |