github.com/stakater/IngressMonitorController@v1.0.103/pkg/monitors/appinsights/appinsights-monitor.go (about)

     1  // Package AppInsightsMonitor adds Azure AppInsights webtest support in IngressMonitorController
     2  package appinsights
     3  
     4  import (
     5  	"context"
     6  	"encoding/xml"
     7  	"fmt"
     8  	"net/http"
     9  
    10  	"github.com/Azure/azure-sdk-for-go/services/appinsights/mgmt/2015-05-01/insights"
    11  	insightsAlert "github.com/Azure/azure-sdk-for-go/services/preview/monitor/mgmt/2018-03-01/insights"
    12  	"github.com/Azure/go-autorest/autorest/azure/auth"
    13  	"github.com/kelseyhightower/envconfig"
    14  	log "github.com/sirupsen/logrus"
    15  	"github.com/stakater/IngressMonitorController/pkg/config"
    16  	"github.com/stakater/IngressMonitorController/pkg/models"
    17  )
    18  
    19  const (
    20  	AppInsightsStatusCodeAnnotation   = "appinsights.monitor.stakater.com/statuscode"  // Allowed httpStatusCodes,
    21  	AppInsightsRetryEnabledAnnotation = "appinsights.monitor.stakater.com/retryenable" // Only Boolen values
    22  	AppInsightsFrequency              = "appinsights.monitor.stakater.com/frequency"   // Allowed Values 300,600,900,
    23  	// Default value for annotations
    24  	AppInsightsStatusCodeAnnotationDefaultValue   = http.StatusOK
    25  	AppInsightsRetryEnabledAnnotationDefaultValue = true
    26  	AppInsightsFrequencyDefaultValue              = 300
    27  )
    28  
    29  // Annotation holds appinsights specific annotations provided from ingress object
    30  type Annotation struct {
    31  	isRetryEnabled     bool
    32  	expectedStatusCode int
    33  	frequency          int32
    34  }
    35  
    36  // AppinsightsMonitorService struct contains parameters required by appinsights go client
    37  type AppinsightsMonitorService struct {
    38  	insightsClient   insights.WebTestsClient
    39  	alertrulesClient insightsAlert.AlertRulesClient
    40  	name             string
    41  	location         string
    42  	resourceGroup    string
    43  	geoLocation      []interface{}
    44  	emailAction      []string
    45  	webhookAction    string
    46  	emailToOwners    bool
    47  	subscriptionID   string
    48  	ctx              context.Context
    49  }
    50  
    51  // AzureConfig holds service principle credentials required of auth
    52  type AzureConfig struct {
    53  	Subscription_ID string
    54  	Client_ID       string
    55  	Client_Secret   string
    56  	Tenant_ID       string
    57  }
    58  
    59  type WebTest struct {
    60  	XMLName     xml.Name `xml:"WebTest"`
    61  	Xmlns       string   `xml:"xmlns,attr"`
    62  	Name        string   `xml:"Name,attr"`
    63  	Enabled     bool     `xml:"Enabled,attr"`
    64  	Timeout     string   `xml:"Timeout,attr"`
    65  	Description string   `xml:"Description,attr"`
    66  	StopOnError bool     `xml:"StopOnError,attr"`
    67  	Items       struct {
    68  		Request struct {
    69  			Method                 string `xml:"Method,attr"`
    70  			Version                string `xml:"Version,attr"`
    71  			URL                    string `xml:"Url,attr"`
    72  			ThinkTime              string `xml:"ThinkTime,attr"`
    73  			Timeout                int    `xml:"Timeout,attr"`
    74  			Encoding               string `xml:"Encoding,attr"`
    75  			ExpectedHttpStatusCode int    `xml:"ExpectedHttpStatusCode,attr"`
    76  			ExpectedResponseUrl    string `xml:"ExpectedResponseUrl,attr"`
    77  			IgnoreHttpStatusCode   bool   `xml:"IgnoreHttpStatusCode,attr"`
    78  		} `xml:"Request"`
    79  	} `xml:"Items"`
    80  }
    81  
    82  // NewWebTest() initialize WebTest with default values
    83  func NewWebTest() *WebTest {
    84  	w := WebTest{
    85  		XMLName:     xml.Name{Local: "WebTest"},
    86  		Xmlns:       "http://microsoft.com/schemas/VisualStudio/TeamTest/2010",
    87  		Enabled:     true,
    88  		Timeout:     "120",
    89  		StopOnError: true,
    90  	}
    91  	w.Items.Request.Encoding = "utf-8"
    92  	w.Items.Request.Version = "1.1"
    93  	w.Items.Request.Method = "GET"
    94  	w.Items.Request.IgnoreHttpStatusCode = false
    95  
    96  	return &w
    97  }
    98  
    99  // Setup method will initialize a appinsights's go client
   100  func (aiService *AppinsightsMonitorService) Setup(provider config.Provider) {
   101  
   102  	log.Println("AppInsights Monitor's Setup has been called. Initializing AppInsights Client..")
   103  
   104  	var azConfig AzureConfig
   105  	err := envconfig.Process("AZURE", &azConfig)
   106  	if err != nil {
   107  		log.Fatalf("Error fetching environment variable: %s", err.Error())
   108  	}
   109  
   110  	aiService.ctx = context.Background()
   111  	aiService.name = provider.AppInsightsConfig.Name
   112  	aiService.location = provider.AppInsightsConfig.Location
   113  	aiService.resourceGroup = provider.AppInsightsConfig.ResourceGroup
   114  	aiService.geoLocation = provider.AppInsightsConfig.GeoLocation
   115  	aiService.emailAction = provider.AppInsightsConfig.EmailAction.CustomEmails
   116  	aiService.emailToOwners = provider.AppInsightsConfig.EmailAction.SendToServiceOwners
   117  	aiService.webhookAction = provider.AppInsightsConfig.WebhookAction.ServiceURI
   118  	aiService.subscriptionID = azConfig.Subscription_ID
   119  
   120  	// Generate clientConfig based on Azure Credentials (Service Principle)
   121  	clientConfig := auth.NewClientCredentialsConfig(azConfig.Client_ID, azConfig.Client_Secret, azConfig.Tenant_ID)
   122  
   123  	// initialize appinsights client
   124  	err = aiService.insightsClient.AddToUserAgent("appInsightsMonitor")
   125  	if err != nil {
   126  		log.Fatal("Error adding UserAgent in AppInsights Client")
   127  	}
   128  
   129  	aiService.insightsClient = insights.NewWebTestsClient(azConfig.Subscription_ID)
   130  	if err != nil {
   131  		log.Fatal("Error initializing AppInsights Client")
   132  	}
   133  
   134  	aiService.insightsClient.Authorizer, err = clientConfig.Authorizer()
   135  	if err != nil {
   136  		log.Fatal("Error initializing AppInsights Client")
   137  	}
   138  
   139  	log.Println("AppInsights Insights Client has been initialized")
   140  
   141  	// initialize monitoring alertrule client only if Email Action or Webhook Action is specified.
   142  	if aiService.isAlertEnabled() {
   143  		aiService.alertrulesClient = insightsAlert.NewAlertRulesClient(azConfig.Subscription_ID)
   144  		aiService.alertrulesClient.Authorizer, err = clientConfig.Authorizer()
   145  		if err != nil {
   146  			log.Fatal("Error initializing AppInsights Alertrules Client")
   147  		}
   148  		log.Println("AppInsights Alertrules Client has been initialized")
   149  	}
   150  
   151  	log.Println("AppInsights Monitor has been initialized")
   152  }
   153  
   154  // GetAll function will return all monitors (appinsights webtest) object in an array
   155  // GetAll for AppInsights returns all webtest for specific component in a resource group.
   156  func (aiService *AppinsightsMonitorService) GetAll() []models.Monitor {
   157  
   158  	log.Println("AppInsight monitor's GetAll method has been called")
   159  
   160  	var monitors []models.Monitor
   161  
   162  	webtests, err := aiService.insightsClient.ListByComponent(aiService.ctx, aiService.name, aiService.resourceGroup)
   163  	if err != nil {
   164  		if webtests.Response().StatusCode == http.StatusNotFound {
   165  			return monitors
   166  		}
   167  		return monitors
   168  	}
   169  	for _, webtest := range webtests.Values() {
   170  
   171  		newMonitor := models.Monitor{
   172  			Name: *webtest.Name,
   173  			URL:  getURL(*webtest.Configuration.WebTest),
   174  			ID:   *webtest.ID,
   175  		}
   176  		monitors = append(monitors, newMonitor)
   177  	}
   178  
   179  	return monitors
   180  
   181  }
   182  
   183  // GetByName function will return a  monitors (appinsights webtest) object based on the name provided
   184  // GetAll for AppInsights returns a webtest for specific resource group.
   185  func (aiService *AppinsightsMonitorService) GetByName(monitorName string) (*models.Monitor, error) {
   186  
   187  	log.Println("AppInsights Monitor's GetByName method has been called")
   188  	webtest, err := aiService.insightsClient.Get(aiService.ctx, aiService.resourceGroup, monitorName)
   189  	if err != nil {
   190  		if webtest.Response.StatusCode == http.StatusNotFound {
   191  			return nil, fmt.Errorf("Application Insights WebTest %s was not found in Resource Group %s", monitorName, aiService.resourceGroup)
   192  		}
   193  		return nil, fmt.Errorf("Error retrieving Application Insights WebTests %s (Resource Group %s): %v", monitorName, aiService.resourceGroup, err)
   194  	}
   195  	return &models.Monitor{
   196  		Name: *webtest.Name,
   197  		URL:  getURL(*webtest.Configuration.WebTest),
   198  		ID:   *webtest.ID,
   199  	}, nil
   200  
   201  }
   202  
   203  // Add function method will add a monitor
   204  func (aiService *AppinsightsMonitorService) Add(monitor models.Monitor) {
   205  
   206  	log.Info("AppInsights Monitor's Add method has been called")
   207  	log.Printf("Adding Application Insights WebTest '%s' from '%s'", monitor.Name, aiService.name)
   208  	webtest := aiService.createWebTest(monitor)
   209  	_, err := aiService.insightsClient.CreateOrUpdate(aiService.ctx, aiService.resourceGroup, monitor.Name, webtest)
   210  	if err != nil {
   211  		log.Errorf("Error adding Application Insights WebTests %s (Resource Group %s): %v", monitor.Name, aiService.resourceGroup, err)
   212  	} else {
   213  		log.Printf("Successfully added Application Insights WebTest %s (Resource Group %s)", monitor.Name, aiService.resourceGroup)
   214  		if aiService.isAlertEnabled() {
   215  			log.Printf("Adding alert rule for WebTest '%s' from '%s'", monitor.Name, aiService.name)
   216  			alertName := fmt.Sprintf("%s-alert", monitor.Name)
   217  			webtestAlert := aiService.createAlertRuleResource(monitor)
   218  			_, err := aiService.alertrulesClient.CreateOrUpdate(aiService.ctx, aiService.resourceGroup, alertName, webtestAlert)
   219  			if err != nil {
   220  				log.Errorf("Error adding alert rule for WebTests %s (Resource Group %s): %v", monitor.Name, aiService.resourceGroup, err)
   221  			}
   222  			log.Printf("Successfully added Alert rule for WebTest %s (Resource Group %s)", monitor.Name, aiService.resourceGroup)
   223  		}
   224  	}
   225  
   226  }
   227  
   228  // Update method will update a monitor
   229  func (aiService *AppinsightsMonitorService) Update(monitor models.Monitor) {
   230  
   231  	log.Println("AppInsights Monitor's Update method has been called")
   232  	log.Printf("Updating Application Insights WebTest '%s' from '%s'", monitor.Name, aiService.name)
   233  
   234  	webtest := aiService.createWebTest(monitor)
   235  	_, err := aiService.insightsClient.CreateOrUpdate(aiService.ctx, aiService.resourceGroup, monitor.Name, webtest)
   236  	if err != nil {
   237  		log.Errorf("Error updating Application Insights WebTests %s (Resource Group %s): %v", monitor.Name, aiService.resourceGroup, err)
   238  	} else {
   239  		log.Printf("Successfully updated Application Insights WebTest %s (Resource Group %s)", monitor.Name, aiService.resourceGroup)
   240  		if aiService.isAlertEnabled() {
   241  			log.Printf("Updating alert rule for WebTest '%s' from '%s'", monitor.Name, aiService.name)
   242  			alertName := fmt.Sprintf("%s-alert", monitor.Name)
   243  			webtestAlert := aiService.createAlertRuleResource(monitor)
   244  			_, err := aiService.alertrulesClient.CreateOrUpdate(aiService.ctx, aiService.resourceGroup, alertName, webtestAlert)
   245  			if err != nil {
   246  				log.Errorf("Error updating alert rule for WebTests %s (Resource Group %s): %v", monitor.Name, aiService.resourceGroup, err)
   247  			}
   248  			log.Printf("Successfully updating Alert rule for WebTest %s (Resource Group %s)", monitor.Name, aiService.resourceGroup)
   249  		}
   250  	}
   251  }
   252  
   253  // Remove method will remove a monitor
   254  func (aiService *AppinsightsMonitorService) Remove(monitor models.Monitor) {
   255  
   256  	log.Println("AppInsights Monitor's Remove method has been called")
   257  	log.Printf("Deleting Application Insights WebTest '%s' from '%s'", monitor.Name, aiService.name)
   258  	r, err := aiService.insightsClient.Delete(aiService.ctx, aiService.resourceGroup, monitor.Name)
   259  	if err != nil {
   260  		if r.Response.StatusCode == http.StatusNotFound {
   261  			log.Errorf("Application Insights WebTest %s was not found in Resource Group %s", monitor.Name, aiService.resourceGroup)
   262  		}
   263  		log.Errorf("Error deleting Application Insights WebTests %s (Resource Group %s): %v", monitor.Name, aiService.resourceGroup, err)
   264  	} else {
   265  		log.Printf("Successfully removed Application Insights WebTest %s (Resource Group %s)", monitor.Name, aiService.resourceGroup)
   266  		if aiService.isAlertEnabled() {
   267  			log.Printf("Deleting alert rule for WebTest '%s' from '%s'", monitor.Name, aiService.name)
   268  			alertName := fmt.Sprintf("%s-alert", monitor.Name)
   269  			r, err := aiService.alertrulesClient.Delete(aiService.ctx, aiService.resourceGroup, alertName)
   270  			if err != nil {
   271  				if r.Response.StatusCode == http.StatusNotFound {
   272  					log.Errorf("WebTest Alert rule %s was not found in Resource Group %s", alertName, aiService.resourceGroup)
   273  				}
   274  				log.Errorf("Error deleting alert rule for WebTests %s (Resource Group %s): %v", alertName, aiService.resourceGroup, err)
   275  			}
   276  			log.Printf("Successfully removed Alert rule for WebTest %s (Resource Group %s)", monitor.Name, aiService.resourceGroup)
   277  		}
   278  	}
   279  }
   280  
   281  // createWebTest forms xml configuration for Appinsights WebTest
   282  func (aiService *AppinsightsMonitorService) createWebTest(monitor models.Monitor) insights.WebTest {
   283  
   284  	isEnabled := true
   285  	webtest := NewWebTest()
   286  	annotations := getAnnotation(monitor)
   287  
   288  	webtest.Description = fmt.Sprintf("%s webtest is created by Ingress Monitor controller", monitor.Name)
   289  	webtest.Items.Request.URL = monitor.URL
   290  	webtest.Items.Request.ExpectedHttpStatusCode = annotations.expectedStatusCode
   291  
   292  	xmlByte, err := xml.Marshal(webtest)
   293  	if err != nil {
   294  		log.Error("Error encoding XML WebTest Configuration")
   295  	}
   296  	webtestConfig := string(xmlByte)
   297  	return insights.WebTest{
   298  		Name:     &monitor.Name,
   299  		Location: &aiService.location,
   300  		Kind:     insights.Ping, // forcing type of webtest to 'ping',this could be as replace with provider configuration
   301  		WebTestProperties: &insights.WebTestProperties{
   302  			SyntheticMonitorID: &monitor.Name,
   303  			WebTestName:        &monitor.Name,
   304  			WebTestKind:        insights.Ping,
   305  			RetryEnabled:       &annotations.isRetryEnabled,
   306  			Enabled:            &isEnabled,
   307  			Frequency:          &annotations.frequency,
   308  			Locations:          getGeoLocation(aiService.geoLocation),
   309  			Configuration: &insights.WebTestPropertiesConfiguration{
   310  				WebTest: &webtestConfig,
   311  			},
   312  		},
   313  		Tags: aiService.getTags("webtest", monitor.Name),
   314  	}
   315  
   316  }
   317  
   318  // createWebTestAlert forms xml configuration for Appinsights WebTest
   319  func (aiService *AppinsightsMonitorService) createAlertRuleResource(monitor models.Monitor) insightsAlert.AlertRuleResource {
   320  
   321  	isEnabled := aiService.isAlertEnabled()
   322  	failedLocationCount := int32(1)
   323  	period := "PT5M"
   324  	alertName := fmt.Sprintf("%s-alert", monitor.Name)
   325  	description := fmt.Sprintf("%s alert is created using Ingress Monitor Controller", alertName)
   326  	resourceUri := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/microsoft.insights/webtests/%s", aiService.subscriptionID, aiService.resourceGroup, monitor.Name)
   327  
   328  	actions := make([]insightsAlert.BasicRuleAction, 0, 2)
   329  
   330  	if len(aiService.emailAction) > 0 {
   331  		actions = append(actions, insightsAlert.RuleEmailAction{
   332  			SendToServiceOwners: &aiService.emailToOwners,
   333  			CustomEmails:        &(aiService.emailAction),
   334  		})
   335  	}
   336  
   337  	if aiService.webhookAction != "" {
   338  		actions = append(actions, insightsAlert.RuleWebhookAction{
   339  			ServiceURI: &aiService.webhookAction,
   340  		})
   341  	}
   342  
   343  	alertRule := insightsAlert.AlertRule{
   344  		Name:        &alertName,
   345  		IsEnabled:   &isEnabled,
   346  		Description: &description,
   347  		Condition: &insightsAlert.LocationThresholdRuleCondition{
   348  			DataSource: insightsAlert.RuleMetricDataSource{
   349  				ResourceURI: &resourceUri,
   350  				OdataType:   insightsAlert.OdataTypeMicrosoftAzureManagementInsightsModelsRuleMetricDataSource,
   351  				MetricName:  &alertName,
   352  			},
   353  			FailedLocationCount: &failedLocationCount,
   354  			WindowSize:          &period,
   355  			OdataType:           insightsAlert.OdataTypeMicrosoftAzureManagementInsightsModelsLocationThresholdRuleCondition,
   356  		},
   357  		Actions: &actions,
   358  	}
   359  
   360  	return insightsAlert.AlertRuleResource{
   361  		Name:      &monitor.Name,
   362  		Location:  &aiService.location,
   363  		AlertRule: &alertRule,
   364  		ID:        &resourceUri,
   365  		Tags:      aiService.getTags("alert", monitor.Name),
   366  	}
   367  }