diff --git a/exporters/dsexporter/dsexporter.go b/exporters/dsexporter/dsexporter.go index 38232ba8..ab26c104 100644 --- a/exporters/dsexporter/dsexporter.go +++ b/exporters/dsexporter/dsexporter.go @@ -1,24 +1,35 @@ package main import ( + "context" + "encoding/json" "fmt" "net/http" + "os" + "strings" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" ) +const service = "https://console-openshift-console.apps.stone-prd-rh01.pg1f.p1.openshiftapps.com/k8s/ns/appstudio-grafana/services" +const check = "prometheus-appstudio-ds" +var kubeconfig *string + type CustomCollector struct { - requestCounter prometheus.Counter + konfluxProbeSuccess *prometheus.GaugeVec } // Creating a new instance of CustomCollector. func NewCustomCollector() *CustomCollector { return &CustomCollector{ - requestCounter: prometheus.NewCounter(prometheus.CounterOpts{ - Name: "request_count", - Help: "Number of requests handled by the handler", - }), + konfluxProbeSuccess: prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Name: "konflux_probe_success", + Help: "Availability of konflux service/component", + }, + []string{"service", "check"}), } } @@ -26,14 +37,79 @@ func NewCustomCollector() *CustomCollector { // When Prometheus scrapes the /metrics endpoint of the exporter, // it first calls the Describe method to get a description of all the metrics. func (e *CustomCollector) Describe(ch chan<- *prometheus.Desc) { - e.requestCounter.Describe(ch) + e.konfluxProbeSuccess.Describe(ch) } // Collect method sends the current values of the metrics to Prometheus. // After Prometheus understands what metrics are available (using the `Describe` method), // it then calls the `Collect` method to actually get the values of those metrics. func (e *CustomCollector) Collect(ch chan<- prometheus.Metric) { - e.requestCounter.Collect(ch) + var availability float64 + + availability = IsDataSourceExist(GetDataSources(GetGrafanaResource()), check) + e.konfluxProbeSuccess.WithLabelValues(service, check).Set(availability) + e.konfluxProbeSuccess.Collect(ch) +} + +// get the grafna resource as a map +func GetGrafanaResource() map[string]interface{} { + clientset := NewKubeClient() + + data, err := clientset.RESTClient(). + Get(). + AbsPath("/apis/grafana.integreatly.org/v1beta1"). + Namespace("appstudio-grafana"). + Resource("grafanas"). + Name("grafana-oauth"). + DoRaw(context.TODO()) + var grafanaResoruce map[string]interface{} + err = json.Unmarshal(data, &grafanaResoruce) + if err != nil { + fmt.Printf("Error getting resource: %v\n", err) + os.Exit(1) + } + + return grafanaResoruce +} + +// get datasources from grafana resource +func GetDataSources(grafanaResource map[string]interface{}) []string { + // return empty string slice if datasources are not defined + if grafanaResource["status"].(map[string]any)["datasources"] == nil { + return make([]string, 0) + } + datasourcesIfc := grafanaResource["status"].(map[string]any)["datasources"].([]interface{}) + datasources := make([]string, len(datasourcesIfc)) + for i, v := range datasourcesIfc { + datasources[i] = v.(string) + } + return datasources +} + +// check if datasource exists, return 1 if yes, 0 if not +func IsDataSourceExist(datasources []string, dsToCheck string) float64 { + for _, datasource := range datasources { + if strings.Contains(datasource, dsToCheck) { + fmt.Println("Datasource", datasource, "exists") + return 1 + } + } + fmt.Println("Datasource", dsToCheck, "does not exist") + return 0 +} + +func NewKubeClient() *kubernetes.Clientset { + // creates the in-cluster config + config, err := rest.InClusterConfig() + if err != nil { + panic(err.Error()) + } + // creates the clientset + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + panic(err.Error()) + } + return clientset } // Using a separate pedantic registry ensures that only your custom metric is exposed on the "/metrics" endpoint, @@ -50,6 +126,7 @@ func main() { Registry: reg, }, )) + fmt.Println("Server is listening on http://localhost:8090/metrics") http.ListenAndServe(":8090", nil) } diff --git a/exporters/dsexporter/dsexporter_test.go b/exporters/dsexporter/dsexporter_test.go index 43e59759..6885e27b 100644 --- a/exporters/dsexporter/dsexporter_test.go +++ b/exporters/dsexporter/dsexporter_test.go @@ -1,46 +1,83 @@ package main import ( + "fmt" "testing" + "net/http" + "net/http/httptest" - "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/assert" + "github.com/prometheus/client_golang/prometheus/promhttp" ) -func TestCustomCollector(t *testing.T) { - exporter := NewCustomCollector() - prometheus.MustRegister(exporter) - - // Simulate collecting metrics and check the exported metric value. - metrics := prometheus.DefaultGatherer - metricFamilies, err := metrics.Gather() - assert.NoError(t, err) - - var RequestCountValue float64 - for _, mf := range metricFamilies { - if mf.GetName() == "request_count" { - RequestCountValue = mf.GetMetric()[0].GetCounter().GetValue() - break - } +func TestAvailabilityHandler(t *testing.T) { + mockExporter := NewCustomCollector() + + req, err := http.NewRequest("GET", "/?service=test.com&check=test", nil) + if err != nil { + t.Fatal(err) + } + + rec := httptest.NewRecorder() + availabilityHandler(rec, req, mockExporter) + + if status := rec.Code; status != http.StatusOK { + t.Errorf("Service unavailable") } - // Check whether the exported metric value is initially 0. - assert.Equal(t, float64(0), RequestCountValue) + expected := fmt.Sprintf("Service: test.com, Check Name: test, Availability:",) + assert.Contains(t, rec.Body.String(), expected) +} + +func TestcheckIfServiceUpPass(t *testing.T) { + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + defer mockServer.Close() + + serviceUrl := mockServer.URL - // Increment the requestCounter by calling the Inc method. - exporter.requestCounter.Inc() + available := checkIfServiceUp(serviceUrl) + + if available == 0 { + t.Errorf("Service not accessible") + } +} - // Collecting metrics again - metricFamilies, err = metrics.Gather() - assert.NoError(t, err) +func TestcheckIfServiceUpFail(t *testing.T) { + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNoContent) + })) + defer mockServer.Close() - for _, mf := range metricFamilies { - if mf.GetName() == "request_count" { - RequestCountValue = mf.GetMetric()[0].GetCounter().GetValue() - break + serviceUrl := mockServer.URL + + available := checkIfServiceUp(serviceUrl) + + if available == 0 { + t.Errorf("Service not accessible") + } +} + +func TestCustomCollector(t *testing.T) { + + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + if r.URL.Path == "/metrics" { + promhttp.Handler().ServeHTTP(w, r) + return } + + })) + + defer mockServer.Close() + + resp, err := http.Get(mockServer.URL + "/metrics") + if err != nil { + t.Fatal(err) } - // Check whether the exported metric value is now 1 after incrementing. - assert.Equal(t, float64(1), RequestCountValue) + if resp.StatusCode != http.StatusOK { + t.Errorf("Service unavailable") + } } diff --git a/go.mod b/go.mod index 7fc3a777..0fecba13 100644 --- a/go.mod +++ b/go.mod @@ -14,8 +14,9 @@ require ( github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect github.com/prometheus/common v0.44.0 // indirect github.com/prometheus/procfs v0.11.1 // indirect - github.com/stretchr/testify v1.8.4 // indirect + github.com/stretchr/testify v1.8.4 golang.org/x/sys v0.11.0 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/client-go v0.19.0 // indirect )