Skip to content

Commit 73624f1

Browse files
author
Lazar Zivadinovic
committed
Initial commit
0 parents  commit 73624f1

File tree

7 files changed

+730
-0
lines changed

7 files changed

+730
-0
lines changed

.gitignore

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Binaries for programs and plugins
2+
*.exe
3+
*.exe~
4+
*.dll
5+
*.so
6+
*.dylib
7+
8+
# IDE
9+
.idea
10+
11+
# Test binary, built with `go test -c`
12+
*.test
13+
14+
# Output of the go coverage tool, specifically when used with LiteIDE
15+
*.out
16+
17+
# Dependency directories (remove the comment below to include it)
18+
# vendor/
19+

Dockerfile

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
FROM golang:alpine as build
2+
WORKDIR /app
3+
COPY *.go .
4+
COPY go.* .
5+
RUN go get
6+
RUN go build -o wsgo
7+
8+
9+
FROM alpine:3
10+
WORKDIR /app
11+
ENV port=9143
12+
ENV host=0.0.0.0
13+
COPY --from=build /app/wsgo .
14+
EXPOSE $port
15+
ENTRYPOINT /app/wsgo --host ${host} --port ${port}

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2023 Lazar Zivadinovic
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Prometheus websocket exporter
2+
Websocket exporter that checks if ws is responding with specific message
3+
4+
Based on https://github.com/sahandhabibi/Websocket-exporter
5+
6+
### Usage
7+
To probe exporter send GET request with target, message and contains parameter, eg:
8+
9+
```bash
10+
curl localhost:9143/probe?target=wss://endpoint&message=something&contains=test
11+
12+
# response should be something like
13+
14+
# HELP websocket_response_time ( Time until we get EOSE in ms; 0 for failed )
15+
# TYPE websocket_response_time gauge
16+
websocket_response_time 63
17+
# HELP websocket_status_code ( 101 is normal status code for ws )
18+
# TYPE websocket_status_code gauge
19+
websocket_status_code 101
20+
# HELP websocket_successful ( 0 = false , 1 = true )
21+
# TYPE websocket_successful gauge
22+
websocket_successful 1
23+
```
24+
25+
26+
TODO:
27+
- Docs
28+
- Test cases if there is no contains or message request (ping pong)

go.mod

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
module websocket-exporter
2+
3+
go 1.20
4+
5+
require (
6+
github.com/gorilla/websocket v1.5.0
7+
github.com/prometheus/client_golang v1.14.0
8+
)
9+
10+
require (
11+
github.com/beorn7/perks v1.0.1 // indirect
12+
github.com/cespare/xxhash/v2 v2.1.2 // indirect
13+
github.com/golang/protobuf v1.5.2 // indirect
14+
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
15+
github.com/prometheus/client_model v0.3.0 // indirect
16+
github.com/prometheus/common v0.37.0 // indirect
17+
github.com/prometheus/procfs v0.8.0 // indirect
18+
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
19+
google.golang.org/protobuf v1.28.1 // indirect
20+
)

go.sum

Lines changed: 480 additions & 0 deletions
Large diffs are not rendered by default.

main.go

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
package main
2+
3+
import (
4+
"flag"
5+
"fmt"
6+
"github.com/gorilla/websocket"
7+
"github.com/prometheus/client_golang/prometheus"
8+
"github.com/prometheus/client_golang/prometheus/promhttp"
9+
"net/http"
10+
"net/url"
11+
"strings"
12+
"time"
13+
)
14+
15+
var (
16+
websocketSuccessful = prometheus.NewGauge(
17+
prometheus.GaugeOpts{
18+
Name: "websocket_successful",
19+
Help: "( 0 = false , 1 = true )",
20+
})
21+
websocketResponseTime = prometheus.NewGauge(
22+
prometheus.GaugeOpts{
23+
Name: "websocket_response_time",
24+
Help: "( Time until we get EOSE in ms; 0 for failed )",
25+
})
26+
websocketStatusCode = prometheus.NewGauge(
27+
prometheus.GaugeOpts{
28+
Name: "websocket_status_code",
29+
Help: "( 101 is normal status code for ws )",
30+
})
31+
32+
respCode float64
33+
)
34+
35+
func probeHandler(w http.ResponseWriter, r *http.Request) {
36+
37+
messages, msOk := r.URL.Query()["message"]
38+
if !msOk || len(messages) != 1 {
39+
http.Error(w, "Specify one 'message' parameter", http.StatusBadRequest)
40+
return
41+
}
42+
sendMessage := []byte(messages[0])
43+
44+
contains, coOk := r.URL.Query()["contains"]
45+
if !coOk || len(contains) != 1 {
46+
http.Error(w, "Specify one 'contains' parameter", http.StatusBadRequest)
47+
return
48+
}
49+
contain := contains[0]
50+
51+
targets, trOk := r.URL.Query()["target"]
52+
if !trOk || len(targets) != 1 {
53+
http.Error(w, "Specify one 'target' parameter", http.StatusBadRequest)
54+
return
55+
}
56+
target := targets[0]
57+
58+
ur, _ := url.Parse(target)
59+
60+
u := url.URL{Scheme: ur.Scheme, Host: ur.Host, Path: ur.Path, RawQuery: ur.RawQuery}
61+
62+
fmt.Printf("Probing %s with message %s and checkign if response contains %s\n", target, sendMessage, contain)
63+
64+
c, resp, errCon := websocket.DefaultDialer.Dial(u.String(), nil)
65+
66+
start := time.Now()
67+
timeout := 5 * time.Second
68+
c.SetReadDeadline(start.Add(timeout))
69+
c.SetWriteDeadline(start.Add(timeout))
70+
71+
if resp != nil {
72+
respCode = float64(resp.StatusCode)
73+
} else {
74+
respCode = 0
75+
}
76+
77+
if (errCon != nil) || (float64(resp.StatusCode) != 101) {
78+
websocketSuccessful.Set(0)
79+
websocketStatusCode.Set(respCode)
80+
websocketResponseTime.Set(0)
81+
} else {
82+
// send ws message
83+
err := c.WriteMessage(websocket.TextMessage, sendMessage)
84+
if err != nil {
85+
websocketSuccessful.Set(0)
86+
websocketStatusCode.Set(respCode)
87+
websocketResponseTime.Set(0)
88+
}
89+
OuterLoop:
90+
for {
91+
select {
92+
case <-time.After(timeout):
93+
websocketSuccessful.Set(0)
94+
websocketStatusCode.Set(respCode)
95+
websocketResponseTime.Set(0)
96+
break OuterLoop
97+
default:
98+
_, message, err := c.ReadMessage()
99+
if err != nil {
100+
websocketSuccessful.Set(0)
101+
websocketStatusCode.Set(respCode)
102+
websocketResponseTime.Set(0)
103+
break OuterLoop
104+
}
105+
if strings.Contains(string(message), contain) {
106+
websocketSuccessful.Set(1)
107+
websocketStatusCode.Set(respCode)
108+
websocketResponseTime.Set(float64(time.Since(start).Milliseconds()))
109+
break OuterLoop
110+
} else {
111+
websocketSuccessful.Set(0)
112+
websocketStatusCode.Set(respCode)
113+
websocketResponseTime.Set(0)
114+
}
115+
}
116+
}
117+
c.Close()
118+
}
119+
120+
reg := prometheus.NewRegistry()
121+
reg.MustRegister(websocketSuccessful)
122+
reg.MustRegister(websocketStatusCode)
123+
reg.MustRegister(websocketResponseTime)
124+
125+
h := promhttp.HandlerFor(reg, promhttp.HandlerOpts{})
126+
h.ServeHTTP(w, r)
127+
}
128+
129+
func main() {
130+
131+
port := flag.Int("port", 9143, "Port Number to listen")
132+
host := flag.String("host", "127.0.0.1", "Host to listen")
133+
134+
flag.Parse()
135+
136+
addr := fmt.Sprint(*host, ":", *port)
137+
http.HandleFunc("/probe", func(w http.ResponseWriter, r *http.Request) {
138+
probeHandler(w, r)
139+
})
140+
141+
fmt.Println("Starting exporter on addr: ", addr)
142+
err := http.ListenAndServe(addr, nil)
143+
if err != nil {
144+
fmt.Println("Error staring server: ", err)
145+
}
146+
147+
}

0 commit comments

Comments
 (0)