Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ENHANCE] UptimeRobot all monitors features #603

Open
didier-segura opened this issue Aug 6, 2024 · 5 comments
Open

[ENHANCE] UptimeRobot all monitors features #603

didier-segura opened this issue Aug 6, 2024 · 5 comments
Labels
kind/enhancement New feature or request kind/help wanted Extra attention is needed

Comments

@didier-segura
Copy link

Can it be possible to add all missing features in a monitor creation ?

  • HTTP method
  • Request headers
  • Request body
  • Add Tags
  • SSL certificate and Domain checks (true / false for each options)
  • Notifications & Integrations

Best regards !

@didier-segura didier-segura added the kind/enhancement New feature or request label Aug 6, 2024
@SheryarButt SheryarButt added the kind/help wanted Extra attention is needed label Aug 28, 2024
@SheryarButt
Copy link
Contributor

Hi @didier-segura
Pull requests are welcomed, feel free to add one

@didier-segura
Copy link
Author

Hi @SheryarButt ,

I will try to PR something, but I can't check if it will works correctly.

@dsegurakaliop
Copy link

dsegurakaliop commented Sep 2, 2024

It seems I can't create a PR.

pkg/monitor/uptime-monitor.go

package uptimerobot

import (
	"encoding/json"
	"errors"
	"fmt"
	Http "net/http"
	"net/url"
	"reflect"
	"strconv"
	"strings"

	endpointmonitorv1alpha1 "github.com/stakater/IngressMonitorController/v2/api/v1alpha1"
	"github.com/stakater/IngressMonitorController/v2/pkg/config"
	"github.com/stakater/IngressMonitorController/v2/pkg/http"
	"github.com/stakater/IngressMonitorController/v2/pkg/models"
)

type UpTimeMonitorService struct {
	apiKey            string
	url               string
	alertContacts     string
	statusPageService UpTimeStatusPageService
}

// Default Interval for status checking
const DefaultInterval = 300

func (monitor *UpTimeMonitorService) Equal(oldMonitor models.Monitor, newMonitor models.Monitor) bool {
	if !(reflect.DeepEqual(monitor.processProviderConfig(oldMonitor, false), monitor.processProviderConfig(newMonitor, false))) {
		log.Info(fmt.Sprintf("There are some new changes in %s monitor", newMonitor.Name))
		return false
	}
	return true
}

func (monitor *UpTimeMonitorService) Setup(p config.Provider) {
	monitor.apiKey = p.ApiKey
	monitor.url = p.ApiURL
	monitor.alertContacts = p.AlertContacts
	monitor.statusPageService = UpTimeStatusPageService{}
	monitor.statusPageService.Setup(p)
}

func (monitor *UpTimeMonitorService) GetByName(name string) (*models.Monitor, error) {
	action := "getMonitors"

	client := http.CreateHttpClient(monitor.url + action)

	body := "api_key=" + monitor.apiKey + "&format=json&logs=1&alert_contacts=1&search=" + name

	response := client.PostUrlEncodedFormBody(body)

	if response.StatusCode == Http.StatusOK {
		var f UptimeMonitorGetMonitorsResponse
		err := json.Unmarshal(response.Bytes, &f)
		if err != nil {
			log.Error(err, "Unable to unmarshal JSON")
		}

		if f.Monitors != nil {
			for _, monitor := range f.Monitors {
				if monitor.FriendlyName == name {
					return UptimeMonitorMonitorToBaseMonitorMapper(monitor), nil
				}
			}
		}

		return nil, nil
	}

	errorString := "GetByName Request failed for name: " + name + ". Status Code: " + strconv.Itoa(response.StatusCode)

	log.Info(errorString)
	return nil, errors.New(errorString)
}

func (monitor *UpTimeMonitorService) GetAllByName(name string) ([]models.Monitor, error) {
	action := "getMonitors"

	client := http.CreateHttpClient(monitor.url + action)

	body := "api_key=" + monitor.apiKey + "&format=json&logs=1" + "&search=" + name

	response := client.PostUrlEncodedFormBody(body)

	if response.StatusCode == 200 {
		var f UptimeMonitorGetMonitorsResponse
		err := json.Unmarshal(response.Bytes, &f)
		if err != nil {
			log.Error(err, "Unable to unmarshal JSON")
		}

		if len(f.Monitors) > 0 {
			return UptimeMonitorMonitorsToBaseMonitorsMapper(f.Monitors), nil
		}
		return nil, nil
	}

	errorString := "GetAllByName Request failed for name: " + name + ". Status Code: " + strconv.Itoa(response.StatusCode)

	log.Info(errorString)
	return nil, errors.New(errorString)
}

func (monitor *UpTimeMonitorService) GetAll() []models.Monitor {

	action := "getMonitors"

	client := http.CreateHttpClient(monitor.url + action)

	body := "api_key=" + monitor.apiKey + "&format=json&logs=1"

	response := client.PostUrlEncodedFormBody(body)

	if response.StatusCode == Http.StatusOK {

		var f UptimeMonitorGetMonitorsResponse
		err := json.Unmarshal(response.Bytes, &f)
		if err != nil {
			log.Error(err, "Unable to unmarshal list monitors response")
		}

		return UptimeMonitorMonitorsToBaseMonitorsMapper(f.Monitors)

	}

	log.Info("GetAllMonitors Request for UptimeRobot failed. Status Code: " + strconv.Itoa(response.StatusCode))
	return nil

}

func (monitor *UpTimeMonitorService) Add(m models.Monitor) {
	action := "newMonitor"

	client := http.CreateHttpClient(monitor.url + action)

	body := monitor.processProviderConfig(m, true)

	response := client.PostUrlEncodedFormBody(body)

	if response.StatusCode == Http.StatusOK {
		var f UptimeMonitorNewMonitorResponse
		err := json.Unmarshal(response.Bytes, &f)
		if err != nil {
			log.Error(err, "Monitor couldn't be added: "+m.Name)
		}

		if f.Stat == "ok" {
			log.Info("Monitor Added: " + m.Name)
			monitor.handleStatusPagesConfig(m, strconv.Itoa(f.Monitor.ID))
		} else {
			log.Info("Monitor couldn't be added: " + m.Name + ". Error: " + f.Error.Message)
		}
	} else {
		log.Info("AddMonitor Request failed. Status Code: " + strconv.Itoa(response.StatusCode))
	}
}

func (monitor *UpTimeMonitorService) Update(m models.Monitor) {
	action := "editMonitor"

	client := http.CreateHttpClient(monitor.url + action)

	body := monitor.processProviderConfig(m, false)

	response := client.PostUrlEncodedFormBody(body)

	if response.StatusCode == Http.StatusOK {
		var f UptimeMonitorStatusMonitorResponse
		err := json.Unmarshal(response.Bytes, &f)
		if err != nil {
			log.Error(err, "Monitor couldn't be updated: "+m.Name)
		}
		if f.Stat == "ok" {
			log.Info("Monitor Updated: " + m.Name)
			monitor.handleStatusPagesConfig(m, strconv.Itoa(f.Monitor.ID))
		} else {
			log.Info("Monitor couldn't be updated: " + m.Name + ". Error: " + f.Error.Message)
		}
	} else {
		log.Info("UpdateMonitor Request failed. Status Code: " + strconv.Itoa(response.StatusCode))
	}
}

func (monitor *UpTimeMonitorService) processProviderConfig(m models.Monitor, createMonitorRequest bool) string {
	var body string

	// if createFunction is true, generate query for create else for update
	if createMonitorRequest {
		body = "api_key=" + monitor.apiKey + "&format=json&url=" + url.QueryEscape(m.URL) + "&friendly_name=" + url.QueryEscape(m.Name)
	} else {
		body = "api_key=" + monitor.apiKey + "&format=json&id=" + m.ID + "&friendly_name=" + m.Name + "&url=" + m.URL
	}

	// Retrieve provider configuration
	providerConfig, _ := m.Config.(*endpointmonitorv1alpha1.UptimeRobotConfig)

	// Type (Required)
	if providerConfig != nil && len(providerConfig.MonitorType) != 0 {
		if strings.Contains(strings.ToLower(providerConfig.MonitorType), "http") {
			body += "&type=1"
		} else if strings.Contains(strings.ToLower(providerConfig.MonitorType), "keyword") {
			body += "&type=2"

			if providerConfig != nil && len(providerConfig.KeywordExists) != 0 {

				if strings.Contains(strings.ToLower(providerConfig.KeywordExists), "yes") {
					body += "&keyword_type=1"
				} else if strings.Contains(strings.ToLower(providerConfig.KeywordExists), "no") {
					body += "&keyword_type=2"
				}
			
			// Keyword Value (Required for keyword monitoring)
			if len(providerConfig.KeywordValue) != 0 {
				body += "&keyword_value=" + url.QueryEscape(providerConfig.KeywordValue)
			} else {
				body += "&keyword_type=1" // By default 1 (check if keyword exists)
			}

			if providerConfig != nil && len(providerConfig.KeywordValue) != 0 {
				body += "&keyword_value=" + providerConfig.KeywordValue
			} else {
				log.Error(nil, "Monitor is of type Keyword but the `keyword-value` is missing")
			}
		}
	} else {
		body += "&type=1" // By default monitor is of type HTTP
	}

	// SubType (Optional for certain types)
	if providerConfig != nil && len(providerConfig.SubType) != 0 {
		body += "&sub_type=" + url.QueryEscape(providerConfig.SubType)
	}

	// Port (Optional for certain types)
	if providerConfig != nil && providerConfig.Port > 0 {
		body += "&port=" + strconv.Itoa(providerConfig.Port)
	}

	// Interval (Optional, in seconds)
	if providerConfig != nil && providerConfig.Interval > 0 {
		body += "&interval=" + strconv.Itoa(providerConfig.Interval)
	} else {
		body += "&interval=" + strconv.Itoa(DefaultInterval)
	}

	// Timeout (Optional, in seconds)
	if providerConfig != nil && providerConfig.Timeout > 0 {
		body += "&timeout=" + strconv.Itoa(providerConfig.Timeout)
	}

	// HTTP Auth (Optional)
	if providerConfig != nil && len(providerConfig.HTTPAuthUsername) != 0 && len(providerConfig.HTTPAuthPassword) != 0 {
		body += "&http_username=" + url.QueryEscape(providerConfig.HTTPAuthUsername)
		body += "&http_password=" + url.QueryEscape(providerConfig.HTTPAuthPassword)
		if providerConfig.HTTPAuthType > 0 {
			body += "&http_auth_type=" + strconv.Itoa(providerConfig.HTTPAuthType)
		}
	}

	// Post Type (Optional)
	if providerConfig != nil && len(providerConfig.PostType) != 0 {
		body += "&post_type=" + url.QueryEscape(providerConfig.PostType)
	}

	// Post Value (Optional)
	if providerConfig != nil && len(providerConfig.PostValue) != 0 {
		body += "&post_value=" + url.QueryEscape(providerConfig.PostValue)
	}

	// HTTP Method (Optional)
	if providerConfig != nil && len(providerConfig.HTTPMethod) != 0 {
		body += "&http_method=" + url.QueryEscape(providerConfig.HTTPMethod)
	}

	// Post Content Type (Optional)
	if providerConfig != nil && len(providerConfig.PostContentType) != 0 {
		body += "&post_content_type=" + url.QueryEscape(providerConfig.PostContentType)
	}

	// Alert Contacts (Optional)
	if providerConfig != nil && len(providerConfig.AlertContacts) != 0 {
		body += "&alert_contacts=" + url.QueryEscape(providerConfig.AlertContacts)
	} else {
		body += "&alert_contacts=" + url.QueryEscape(monitor.alertContacts)
	}

	// Maintenance Windows (Optional)
	if providerConfig != nil && len(providerConfig.MaintenanceWindows) != 0 {
		body += "&mwindows=" + url.QueryEscape(providerConfig.MaintenanceWindows)
	}

	// Custom HTTP Headers (Optional, must be sent as a JSON object)
	if providerConfig != nil && len(providerConfig.CustomHTTPHeaders) != 0 {
		body += "&custom_http_headers=" + url.QueryEscape(providerConfig.CustomHTTPHeaders)
	}

	// Custom HTTP Statuses (Optional, must be sent in specific format)
	if providerConfig != nil && len(providerConfig.CustomHTTPStatuses) != 0 {
		body += "&custom_http_statuses=" + url.QueryEscape(providerConfig.CustomHTTPStatuses)
	}

	// Ignore SSL Errors (Optional)
	if providerConfig != nil && providerConfig.IgnoreSSLErrors > 0 {
		body += "&ignore_ssl_errors=" + strconv.Itoa(providerConfig.IgnoreSSLErrors)
	}

	// Disable Domain Expire Notifications (Optional)
	if providerConfig != nil && providerConfig.DisableDomainExpireNotifications > 0 {
		body += "&disable_domain_expire_notifications=" + strconv.Itoa(providerConfig.DisableDomainExpireNotifications)
	}

	return body
}

func (monitor *UpTimeMonitorService) Remove(m models.Monitor) {
	action := "deleteMonitor"

	client := http.CreateHttpClient(monitor.url + action)

	log.Info(m.ID)
	body := "api_key=" + monitor.apiKey + "&format=json&id=" + m.ID

	response := client.PostUrlEncodedFormBody(body)

	if response.StatusCode == Http.StatusOK {
		var f UptimeMonitorStatusMonitorResponse
		err := json.Unmarshal(response.Bytes, &f)
		if err != nil {
			log.Error(err, "Monitor couldn't be removed: "+m.Name)
		}
		if f.Stat == "ok" {
			log.Info("Monitor Removed: " + m.Name)
		} else {
			log.Info("Monitor couldn't be removed: " + m.Name + ". Error: " + f.Error.Message)
			log.Info(string(body))
		}
	} else {
		log.Info("RemoveMonitor Request failed. Status Code: " + strconv.Itoa(response.StatusCode))
	}
}

func (monitor *UpTimeMonitorService) handleStatusPagesConfig(monitorToAdd models.Monitor, monitorId string) {
	// Retrieve provider configuration
	providerConfig, _ := monitorToAdd.Config.(*endpointmonitorv1alpha1.UptimeRobotConfig)

	if providerConfig != nil && len(providerConfig.StatusPages) != 0 {
		IDs := strings.Split(providerConfig.StatusPages, "-")
		for i := range IDs {
			monitor.updateStatusPages(IDs[i], models.Monitor{ID: monitorId})
		}
	}
}

func (monitor *UpTimeMonitorService) updateStatusPages(statusPages string, monitorToAdd models.Monitor) {
	statusPage := UpTimeStatusPage{ID: statusPages}
	_, err := monitor.statusPageService.AddMonitorToStatusPage(statusPage, monitorToAdd)
	if err != nil {
		log.Info("Monitor couldn't be added to status page: " + err.Error())
	}
}

pkg/monitor/uptime-responses.go

package uptimerobot

type UptimeMonitorGetMonitorsResponse struct {
	Stat       string                  `json:"stat"`
	Pagination UptimeMonitorPagination `json:"pagination"`
	Monitors   []UptimeMonitorMonitor  `json:"monitors"`
}

type UptimeMonitorPagination struct {
	Offset int `json:"offset"`
	Limit  int `json:"limit"`
	Total  int `json:"total"`
}

type UptimeMonitorMonitor struct {
	ID                int                          `json:"id"`
	FriendlyName      string                       `json:"friendly_name"`
	URL               string                       `json:"url"`
	Type              int                          `json:"type"`
	SubType           string                       `json:"sub_type"`
	KeywordType       int                          `json:"keyword_type"`
	KeywordValue      string                       `json:"keyword_value"`
	HTTPUsername      string                       `json:"http_username"`
	HTTPPassword      string                       `json:"http_password"`
	Port              string                       `json:"port"`
	Interval          int                          `json:"interval"`
	Status            int                          `json:"status"`
	CreateDatetime    int                          `json:"create_datetime"`
	Logs              []UptimeMonitorLogs          `json:"logs"`
	AlertContacts     []UptimeMonitorAlertContacts `json:"alert_contacts"`
	SSL               int                          `json:"ssl"`
	Timeout           int                          `json:"timeout"`
	CustomHTTPStatuses string                      `json:"custom_http_statuses"`
	CustomHeader      string                       `json:"custom_header"`
}

type UptimeMonitorAlertContacts struct {
	ID         string `json:"id"`
	Threshold  int    `json:"threshold"`
	Recurrence int    `json:"recurrence"`
}

type UptimeMonitorLogs struct {
	Type     int `json:"type"`
	Datetime int `json:"datetime"`
	Duration int `json:"duration"`
}

type UptimeMonitorNewMonitorResponse struct {
	Stat    string                     `json:"stat"`
	Monitor UptimeMonitorMonitorStatus `json:"monitor"`
	Error   UptimeMonitorError         `json:"error"`
}

type UptimeMonitorError struct {
	Type    string `json:"type"`
	Message string `json:"message"`
}

type UptimeMonitorMonitorStatus struct {
	ID     int `json:"id"`
	Status int `json:"status"`
}

type UptimeMonitorStatusMonitorResponse struct {
	Stat    string             `json:"stat"`
	Error   UptimeMonitorError `json:"error"`
	Monitor struct {
		ID int `json:"id"`
	} `json:"monitor"`
}

type UptimePublicStatusPage struct {
	ID           int    `json:"id"`
	FriendlyName string `json:"friendly_name"`
	Monitors     []int  `json:"monitors"`
	CustomDomain string `json:"custom_domain"`
	Password     string `json:"password"`
	Sort         int    `json:"sort"`
	Status       int    `json:"status"`
}

type UptimeStatusPageResponse struct {
	Stat                   string `json:"stat"`
	UptimePublicStatusPage struct {
		ID int `json:"id"`
	} `json:"psp"`
}

type UptimeStatusPagesResponse struct {
	Stat       string `json:"stat"`
	Pagination struct {
		Offset int `json:"offset"`
		Limit  int `json:"limit"`
		Total  int `json:"total"`
	} `json:"pagination"`
	StatusPages []UptimePublicStatusPage `json:"psps"`
}

I can't test this but these are some options needed to be added.

@MuneebAijaz
Copy link
Contributor

@dsegurakaliop have u tried making a fork on your side of this project, and pushing those changes to a branch, then opening a PR to upstream master branch? You should be able to do that.

@santos-edu
Copy link

+1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
kind/enhancement New feature or request kind/help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests

5 participants