diff --git a/.gitignore b/.gitignore index 8f2ea6b..c566138 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ config.yaml .DS_Store +rtsl_exporter diff --git a/dhis2/client.go b/dhis2/client.go index 98d5d8f..72da011 100644 --- a/dhis2/client.go +++ b/dhis2/client.go @@ -1,21 +1,22 @@ package dhis2 import ( - "context" "encoding/base64" "encoding/json" "fmt" - "io/ioutil" + "io" + "net" "net/http" "time" ) -const httpTimeOutSec = 1 +const DefaultConnectionTimeout = 1 * time.Second type Client struct { - Username string - Password string - BaseURL string + Username string + Password string + BaseURL string + ConnectionTimeout time.Duration } type Info struct { @@ -38,16 +39,12 @@ func (c *Client) GetInfo() (*Info, error) { // common method to do request func (c *Client) doRequest(path string, result interface{}) error { - // Create a context with a timeout - ctx, cancel := context.WithTimeout(context.Background(), httpTimeOutSec*time.Second) - defer cancel() - url := fmt.Sprintf("%s%s", c.BaseURL, path) // Create a new request - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + req, err := http.NewRequest("GET", url, nil) if err != nil { - return err + return fmt.Errorf("failed to create request: %w", err) } // Encode credentials @@ -56,21 +53,38 @@ func (c *Client) doRequest(path string, result interface{}) error { // Set Authorization header req.Header.Set("Authorization", "Basic "+credentials) + // Custom Transport with Connect Timeout + transport := &http.Transport{ + DialContext: (&net.Dialer{ + Timeout: c.ConnectionTimeout, + }).DialContext, + } + // Make the request - client := &http.Client{} + client := &http.Client{ + Transport: transport, + } resp, err := client.Do(req) if err != nil { - return err + return fmt.Errorf("request failed: %w", err) } defer resp.Body.Close() + // Check if the status code is not 200 + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + // Read the body - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { - return err + return fmt.Errorf("failed to read response body: %w", err) } // Unmarshal JSON response into result err = json.Unmarshal(body, result) - return err + if err != nil { + return fmt.Errorf("failed to unmarshal response: %w", err) + } + return nil } diff --git a/dhis2/exporter.go b/dhis2/exporter.go index 319e325..acf8e10 100644 --- a/dhis2/exporter.go +++ b/dhis2/exporter.go @@ -2,37 +2,22 @@ package dhis2 import ( "log" - "strings" + "sync" "github.com/prometheus/client_golang/prometheus" ) -// sanitizeName prepares a valid prometheus metric name from a given URL -func sanitizeName(url string) string { - // Remove the protocol part - cleanURL := strings.TrimPrefix(url, "https://") - cleanURL = strings.TrimPrefix(cleanURL, "http://") - - // Replace dots and dashes with underscores - cleanURL = strings.ReplaceAll(cleanURL, "-", "_") - cleanURL = strings.ReplaceAll(cleanURL, ".", "_") - - return cleanURL -} - type Exporter struct { - client *Client - - info *prometheus.GaugeVec + clients []*Client + info *prometheus.GaugeVec } -func NewExporter(client *Client) *Exporter { - dynamicName := "system_info_" + sanitizeName(client.BaseURL) +func NewExporter(clients []*Client) *Exporter { return &Exporter{ - client: client, + clients: clients, info: prometheus.NewGaugeVec(prometheus.GaugeOpts{ Namespace: "dhis2", - Name: dynamicName, + Name: "system_info", Help: "Information about the DHIS2 system", }, []string{"version", "revision", "contextPath", "buildTime"}), } @@ -46,14 +31,25 @@ func (e *Exporter) Describe(ch chan<- *prometheus.Desc) { // Collect is called by the Prometheus registry when collecting metrics. func (e *Exporter) Collect(ch chan<- prometheus.Metric) { - info, err := e.client.GetInfo() - if err != nil { - log.Printf("Failed to get dhis2 system information: %v\n", err) - return // Early return on error to avoid using uninitialized info + var wg sync.WaitGroup + var mu sync.Mutex + + for _, client := range e.clients { + wg.Add(1) + go func(client *Client) { + defer wg.Done() + info, err := client.GetInfo() + if err != nil { + log.Printf("ERROR: Failed to get system information from %s: %v\n", client.BaseURL, err) + return + } + + mu.Lock() + e.info.WithLabelValues(info.Version, info.Revision, client.BaseURL, info.BuildTime).Set(1) + mu.Unlock() + }(client) } - // Set the version and revision as labels; gauge value is less meaningful here, just set to 1 - e.info.WithLabelValues(info.Version, info.Revision, info.ContextPath, info.BuildTime).Set(1) - + wg.Wait() e.info.Collect(ch) } diff --git a/main.go b/main.go index 67b51a5..bd10358 100644 --- a/main.go +++ b/main.go @@ -2,12 +2,13 @@ package main import ( "context" + "io/ioutil" "log" "net/http" "os" "os/signal" - "io/ioutil" "time" + "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/simpledotorg/rtsl_exporter/alphasms" @@ -17,7 +18,7 @@ import ( ) type Config struct { - ALPHASMSAPIKey string `yaml:"alphasms_api_key"` + ALPHASMSAPIKey string `yaml:"alphasms_api_key"` SendGridAccounts []struct { AccountName string `yaml:"account_name"` APIKey string `yaml:"api_key"` @@ -73,6 +74,7 @@ func main() { log.Fatalf("Error reading config file: %v", err) } + // Alphasms if config.ALPHASMSAPIKey == "" { log.Fatalf("ALPHASMS_API_KEY not provided in config file") } @@ -80,15 +82,19 @@ func main() { alphasmsExporter := alphasms.NewExporter(&alphasmsClient) prometheus.MustRegister(alphasmsExporter) + // DHIS2 + dhis2Clients := []*dhis2.Client{} for _, endpoint := range config.DHIS2Endpoints { dhis2Client := dhis2.Client{ - Username: endpoint.Username, - Password: endpoint.Password, - BaseURL: endpoint.BaseURL, + Username: endpoint.Username, + Password: endpoint.Password, + BaseURL: endpoint.BaseURL, + ConnectionTimeout: dhis2.DefaultConnectionTimeout, } - dhis2Exporter := dhis2.NewExporter(&dhis2Client) - prometheus.MustRegister(dhis2Exporter) + dhis2Clients = append(dhis2Clients, &dhis2Client) } + dhis2Exporter := dhis2.NewExporter(dhis2Clients) + prometheus.MustRegister(dhis2Exporter) // Register SendGrid exporters apiKeys := make(map[string]string)