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 }