Skip to content

Commit 14a926f

Browse files
feat: remote gateway call home option
Signed-off-by: Ricky Moorhouse <[email protected]>
1 parent 1efda8d commit 14a926f

File tree

5 files changed

+181
-5
lines changed

5 files changed

+181
-5
lines changed

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ FROM registry.access.redhat.com/ubi9/ubi:latest AS build
33
WORKDIR /app/
44

55
RUN dnf upgrade --assumeyes
6-
RUN dnf install -y curl jq tar --allowerasing
6+
RUN dnf install -y curl jq tar git --allowerasing
77
COPY . .
88
# Set the Go version dynamically by fetching the latest version
99
RUN GOVERSION=$(egrep "^toolchain " go.mod | awk -Fgo '{print $2}') && \

exporter.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"nets/consumption"
1414
"nets/datapower"
1515
"nets/manager"
16+
1617
"os"
1718
"path/filepath"
1819
"time"
@@ -53,6 +54,11 @@ type Config struct {
5354
Log struct {
5455
Level string `yaml:"level"`
5556
} `yaml:"logging"`
57+
RemoteGateway struct {
58+
Enabled bool `yaml:"enabled"`
59+
ClientID string `yaml:"client_id"`
60+
Url string `yaml:"url"`
61+
} `yaml:"remote_gateway"`
5662
}
5763

5864
type CertReloader struct {
@@ -184,11 +190,16 @@ func main() {
184190
log.Log(alog.INFO, "Enabled datapower net with %s frequency", dp.Frequency)
185191
go dp.BackgroundFishing()
186192
}
193+
// Initialise remote gateway call home
194+
if config.RemoteGateway.Enabled {
195+
RemoteGatewayReport(config.RemoteGateway.ClientID, config.RemoteGateway.Url)
196+
}
187197

188198
mux := http.NewServeMux()
189199
mux.Handle("/metrics", promhttp.Handler())
190200
// Bind dynamic log handler
191201
mux.HandleFunc("/logging", alog.DynamicHandler)
202+
mux.HandleFunc("/remotegw", RemoteGatewayMetricsHandler)
192203
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
193204
fmt.Fprint(w, "ok")
194205
})

go.work.sum

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:Fecb
3030
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
3131
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
3232
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
33-
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
3433
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
3534
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
3635
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@@ -50,6 +49,7 @@ github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65
5049
github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY=
5150
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
5251
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
52+
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
5353
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
5454
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
5555
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=

nets/apiconnect/apiconnect.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ type HealthLabel struct {
3434
var log = alog.UseChannel("apic")
3535

3636
func (a *APIConnect) crdStatusMetrics(group, version, resource string, crdStatus prometheus.GaugeVec) {
37+
log.Log(alog.DEBUG, "Getting status for %s/%s", group, version, resource)
3738
subsystems := nets.GetCustomResourceList(group, version, resource, a.Config.Namespace)
3839

3940
// Check if subsystems is nil to prevent segmentation fault
@@ -42,16 +43,20 @@ func (a *APIConnect) crdStatusMetrics(group, version, resource string, crdStatus
4243
return
4344
}
4445

46+
log.Log(alog.DEBUG, "%v", subsystems)
4547
for _, subsystem := range subsystems.Items {
4648
subsystemName := subsystem.Object["metadata"].(map[string]interface{})["name"].(string)
4749
subsystemNamespace := subsystem.Object["metadata"].(map[string]interface{})["namespace"].(string)
4850
version := subsystem.Object["status"].(map[string]interface{})["versions"].(map[string]interface{})["reconciled"].(string)
4951
conditions := subsystem.Object["status"].(map[string]interface{})["conditions"].([]interface{})
52+
condition := "Unknown"
53+
log.Log(alog.DEBUG, "Getting status for %s/%s", subsystemName, subsystemNamespace)
5054
for i := 0; i < len(conditions); i++ {
5155
conditionType := conditions[i].(map[string]interface{})["type"].(string)
5256
conditionStatus := conditions[i].(map[string]interface{})["status"].(string)
5357
if conditionStatus == "True" {
5458
crdStatus.WithLabelValues(subsystemName, subsystemNamespace, conditionType).Set(1)
59+
condition = conditionType
5560
} else {
5661
crdStatus.WithLabelValues(subsystemName, subsystemNamespace, conditionType).Set(0)
5762
}
@@ -65,9 +70,9 @@ func (a *APIConnect) crdStatusMetrics(group, version, resource string, crdStatus
6570
}
6671
if version != "" {
6772
if a.Config.HealthLabel.Name != "" {
68-
a.healthStatus.WithLabelValues(resource+"_"+subsystemName, version, a.Config.HealthLabel.Value).Set(healthValue)
73+
a.healthStatus.WithLabelValues(resource+"_"+subsystemName, version, condition, subsystemName, subsystemNamespace, a.Config.HealthLabel.Value).Set(healthValue)
6974
} else {
70-
a.healthStatus.WithLabelValues(resource+"_"+subsystemName, version).Set(healthValue)
75+
a.healthStatus.WithLabelValues(resource+"_"+subsystemName, version, condition, subsystemName, subsystemNamespace).Set(healthValue)
7176
}
7277
}
7378
}
@@ -81,7 +86,7 @@ func (a *APIConnect) BackgroundFishing() {
8186
interval := a.Frequency
8287
ticker := time.NewTicker(interval)
8388
var metricName = "health_status"
84-
var metricLabels = []string{"component", "version"}
89+
var metricLabels = []string{"component", "version", "condition", "name", "namespace"}
8590

8691
if a.Config.HealthLabel.Name != "" {
8792
metricLabels = append(metricLabels, a.Config.HealthLabel.Name)

remotegateway.go

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"net/http"
7+
"strings"
8+
"time"
9+
10+
"github.com/IBM/alchemy-logging/src/go/alog"
11+
"github.com/prometheus/client_golang/prometheus"
12+
dto "github.com/prometheus/client_model/go"
13+
)
14+
15+
// allowedMetrics is a list of metrics that are allowed to be exposed
16+
var allowedMetrics = map[string]bool{
17+
"trawler_version_info": true,
18+
"health_status": true,
19+
"datapower_version_info": true,
20+
"apiconnect_gatewaycluster_status": true,
21+
}
22+
23+
func RemoteGatewayMetricsHandler(w http.ResponseWriter, r *http.Request) {
24+
w.Header().Set("Content-Type", "application/json")
25+
w.WriteHeader(http.StatusOK)
26+
fmt.Fprint(w, getMetrics())
27+
}
28+
29+
// Set up a timer to report hourly
30+
func RemoteGatewayReport(ClientId, Url string) {
31+
log.Log(alog.INFO, "Starting hourly reporting for remote gateway")
32+
ticker := time.NewTicker(1 * time.Hour)
33+
// Start the datapower loop
34+
for range ticker.C {
35+
PostMetrics(ClientId, Url)
36+
}
37+
}
38+
39+
// Send metrics for remote gateway back to IBM APIC SaaS
40+
func PostMetrics(ClientId, Url string) {
41+
42+
client := &http.Client{}
43+
44+
client.Transport = &http.Transport{}
45+
log.Log(alog.DEBUG, "Calling %s", Url)
46+
47+
req, err := http.NewRequest("POST", Url, strings.NewReader(getMetrics()))
48+
if err != nil {
49+
log.Log(alog.ERROR, err.Error())
50+
}
51+
req.Header.Add("Accept", "application/json")
52+
req.Header.Add("Content-Type", "application/json")
53+
req.Header.Add("User-Agent", fmt.Sprintf("Trawler/%s", Version))
54+
req.Header.Add("X-IBM-Client-ID", ClientId)
55+
56+
response, err := client.Do(req)
57+
if err != nil {
58+
log.Log(alog.ERROR, err.Error())
59+
}
60+
if response.StatusCode != 200 {
61+
log.Log(alog.ERROR, "unexpected status - got %s, expected 200", response.Status)
62+
}
63+
}
64+
65+
// MetricData represents the structured data for a metric
66+
type MetricData map[string]interface{}
67+
68+
// StatusMetricData represents the structured data for a status metric
69+
type StatusMetricData map[string]interface{}
70+
71+
type StatusOutput struct {
72+
Name string `json:"name"`
73+
Status string `json:"status"`
74+
Namespace string `json:"namespace"`
75+
Version string `json:"version"`
76+
}
77+
type TrawlerDetails struct {
78+
Version string `json:"version"`
79+
BuildTime string `json:"buildtime"`
80+
}
81+
82+
type Payload struct {
83+
Subsystems []StatusOutput `json:"subsystems"`
84+
Trawler TrawlerDetails `json:"trawler"`
85+
}
86+
87+
// getMetrics gathers and formats Prometheus metrics for remote gateway consumption
88+
func getMetrics() string {
89+
metrics, err := prometheus.DefaultGatherer.Gather()
90+
if err != nil {
91+
// Log the error properly instead of just printing
92+
fmt.Println(err)
93+
return "{}"
94+
}
95+
96+
// Process metrics and build the compact representation
97+
gatewayMetrics := processMetrics(metrics)
98+
99+
response := Payload{
100+
Trawler: TrawlerDetails{Version: Version, BuildTime: BuildTime},
101+
Subsystems: gatewayMetrics,
102+
}
103+
jsonData, _ := json.MarshalIndent(response, "", " ")
104+
return string(jsonData)
105+
}
106+
107+
// processMetrics processes the gathered metrics and separates them into status and non-status metrics
108+
func processMetrics(metrics []*dto.MetricFamily) []StatusOutput {
109+
// Map to store status metrics by name and namespace
110+
var gatewayMetrics []StatusOutput
111+
112+
for _, metricFamily := range metrics {
113+
metricName := metricFamily.GetName()
114+
115+
// Skip metrics that are not in the allowlist
116+
if !allowedMetrics[metricName] {
117+
continue
118+
}
119+
120+
for _, metric := range metricFamily.GetMetric() {
121+
// Extract labels from the metric
122+
labels := extractLabels(metric)
123+
124+
// Get the value based on metric type
125+
//value := extractValue(metric)
126+
127+
switch metricName {
128+
case "trawler_version_info":
129+
// Include trawler details
130+
case "health_status":
131+
gatewayMetrics = append(gatewayMetrics, processHealthStatusMetric(labels))
132+
}
133+
}
134+
}
135+
return gatewayMetrics
136+
}
137+
138+
// extractLabels extracts label key-value pairs from a metric
139+
func extractLabels(metric *dto.Metric) map[string]string {
140+
labels := make(map[string]string)
141+
for _, label := range metric.GetLabel() {
142+
labels[label.GetName()] = label.GetValue()
143+
}
144+
return labels
145+
}
146+
147+
// processStatusMetric handles status metrics specifically
148+
func processHealthStatusMetric(labels map[string]string) StatusOutput {
149+
150+
// Initialize the entry if it doesn't exist
151+
statusMetric := StatusOutput{
152+
Status: labels["condition"],
153+
Version: labels["version"],
154+
Name: labels["name"],
155+
Namespace: labels["namespace"],
156+
}
157+
158+
// Append to gateway metrics
159+
return statusMetric
160+
}

0 commit comments

Comments
 (0)