diff --git a/plugins/outputs/azuremonitor/README.md b/plugins/outputs/azuremonitor/README.md index 92d2ecf70..a6db4d324 100644 --- a/plugins/outputs/azuremonitor/README.md +++ b/plugins/outputs/azuremonitor/README.md @@ -49,7 +49,7 @@ The resourceId used for Azure Monitor metrics. ## specified, the plugin will attempt to retrieve the resource ID ## of the VM via the instance metadata service (optional if running ## on an Azure VM with MSI) -#resourceId = "/subscriptions//resourceGroups//providers/Microsoft.Compute/virtualMachines/" +#resource_id = "/subscriptions//resourceGroups//providers/Microsoft.Compute/virtualMachines/" ## Azure region to publish metrics against. Defaults to eastus. ## Leave blank to automatically query the region via MSI. #region = "useast" diff --git a/plugins/outputs/azuremonitor/azuremetadata.go b/plugins/outputs/azuremonitor/azuremetadata.go index bc1329c56..c1e8b88d6 100644 --- a/plugins/outputs/azuremonitor/azuremetadata.go +++ b/plugins/outputs/azuremonitor/azuremetadata.go @@ -80,24 +80,14 @@ func (m *msiToken) parseTimes() { } } -// ExpiresAt is the time at which the token expires -func (m *msiToken) ExpiresAt() time.Time { - return m.expiresAt -} - // ExpiresInDuration returns the duration until the token expires -func (m *msiToken) ExpiresInDuration() time.Duration { +func (m *msiToken) expiresInDuration() time.Duration { expiresDuration := m.expiresAt.Sub(time.Now().UTC()) return expiresDuration } -// NotBeforeTime returns the time at which the token becomes valid -func (m *msiToken) NotBeforeTime() time.Time { - return m.notBefore -} - // GetMsiToken retrieves a managed service identity token from the specified port on the local VM -func (a *AzureMonitor) getMsiToken(clientID string, resourceID string) (*msiToken, error) { +func (a *AzureMonitor) getMsiToken(clientID string) (*msiToken, error) { // Acquire an MSI token. Documented at: // https://docs.microsoft.com/en-us/azure/active-directory/managed-service-identity/how-to-use-vm-token // @@ -110,13 +100,9 @@ func (a *AzureMonitor) getMsiToken(clientID string, resourceID string) (*msiToke return nil, err } - // Resource ID defaults to https://management.azure.com - if resourceID == "" { - resourceID = "https://management.azure.com" - } - msiParameters := url.Values{} - msiParameters.Add("resource", resourceID) + // Resource ID defaults to https://management.azure.com + msiParameters.Add("resource", defaultMSIResource) msiParameters.Add("api-version", "2018-02-01") // Client id is optional @@ -143,7 +129,7 @@ func (a *AzureMonitor) getMsiToken(clientID string, resourceID string) (*msiToke } if resp.StatusCode >= 300 || resp.StatusCode < 200 { - return nil, fmt.Errorf("Post Error. HTTP response code:%d message:%s, content: %s", + return nil, fmt.Errorf("E! Get Error. %d HTTP response: %s response body: %s", resp.StatusCode, resp.Status, reply) } @@ -156,41 +142,45 @@ func (a *AzureMonitor) getMsiToken(clientID string, resourceID string) (*msiToke return &token, nil } -const ( - vmInstanceMetadataURL = "http://169.254.169.254/metadata/instance?api-version=2017-12-01" - msiInstanceMetadataURL = "http://169.254.169.254/metadata/identity/oauth2/token" -) - // GetInstanceMetadata retrieves metadata about the current Azure VM -func (a *AzureMonitor) GetInstanceMetadata() (*VirtualMachineMetadata, error) { +func (a *AzureMonitor) GetInstanceMetadata() error { req, err := http.NewRequest("GET", vmInstanceMetadataURL, nil) if err != nil { - return nil, fmt.Errorf("Error creating HTTP request") + return fmt.Errorf("Error creating HTTP request") } req.Header.Set("Metadata", "true") resp, err := a.client.Do(req) if err != nil { - return nil, err + return err } defer resp.Body.Close() reply, err := ioutil.ReadAll(resp.Body) if err != nil { - return nil, err + return err } if resp.StatusCode >= 300 || resp.StatusCode < 200 { - return nil, fmt.Errorf("Post Error. HTTP response code:%d message:%s reply:\n%s", + return fmt.Errorf("Post Error. HTTP response code:%d message:%s reply:\n%s", resp.StatusCode, resp.Status, reply) } var metadata VirtualMachineMetadata if err := json.Unmarshal(reply, &metadata); err != nil { - return nil, err + return err } - metadata.AzureResourceID = fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/virtualMachines/%s", - metadata.Compute.SubscriptionID, metadata.Compute.ResourceGroupName, metadata.Compute.Name) - return &metadata, nil + if a.ResourceID == "" { + a.ResourceID = fmt.Sprintf(resourceIDTemplate, + metadata.Compute.SubscriptionID, metadata.Compute.ResourceGroupName, metadata.Compute.Name) + } + + if a.Region == "" { + a.Region = metadata.Compute.Location + } + + a.url = fmt.Sprintf(urlTemplate, a.Region, a.ResourceID) + + return nil } diff --git a/plugins/outputs/azuremonitor/azuremetadata_test.go b/plugins/outputs/azuremonitor/azuremetadata_test.go deleted file mode 100644 index 0611ed2a7..000000000 --- a/plugins/outputs/azuremonitor/azuremetadata_test.go +++ /dev/null @@ -1,44 +0,0 @@ -package azuremonitor - -import ( - "testing" - "time" - - "github.com/stretchr/testify/require" -) - -func TestGetMetadata(t *testing.T) { - azureMetadata := &AzureMonitor{} - metadata, err := azureMetadata.GetInstanceMetadata() - - require.NoError(t, err) - require.NotNil(t, metadata) - require.NotEmpty(t, metadata.AzureResourceID) - require.NotEmpty(t, metadata.Compute.Location) - - // if err != nil { - // t.Logf("could not get metadata: %v\n", err) - // } else { - // t.Logf("resource id \n%s", metadata.AzureResourceID) - // t.Logf("metadata is \n%v", metadata) - // } - - //fmt.Printf("metadata is \n%v", metadata) -} - -func TestGetTOKEN(t *testing.T) { - azureMetadata := &AzureMonitor{} - - resourceID := "https://ingestion.monitor.azure.com/" - token, err := azureMetadata.getMsiToken("", resourceID) - - require.NoError(t, err) - require.NotEmpty(t, token.AccessToken) - require.EqualValues(t, token.Resource, resourceID) - - t.Logf("token is %+v\n", token) - t.Logf("expiry time is %s\n", token.ExpiresAt().Format(time.RFC3339)) - t.Logf("expiry duration is %s\n", token.ExpiresInDuration().String()) - t.Logf("resource is %s\n", token.Resource) - -} diff --git a/plugins/outputs/azuremonitor/azuremonitor.go b/plugins/outputs/azuremonitor/azuremonitor.go index 4bd30bc39..98c5caf53 100644 --- a/plugins/outputs/azuremonitor/azuremonitor.go +++ b/plugins/outputs/azuremonitor/azuremonitor.go @@ -35,7 +35,7 @@ type AzureMonitor struct { AzureClientSecret string `toml:"azure_client_secret"` StringAsDimension bool `toml:"string_as_dimension"` - url string + url string msiToken *msiToken oauthConfig *adal.OAuthConfig adalToken adal.OAuthTokenProvider @@ -51,11 +51,12 @@ type aggregate struct { } const ( - defaultRegion string = "eastus" - - defaultMSIResource string = "https://monitoring.azure.com/" - - urlTemplate string = "https://%s.monitoring.azure.com%s/metrics" + defaultRegion string = "eastus" + defaultMSIResource string = "https://monitoring.azure.com/" + urlTemplate string = "https://%s.monitoring.azure.com%s/metrics" + resourceIDTemplate string = "/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/virtualMachines/%s" + vmInstanceMetadataURL string = "http://169.254.169.254/metadata/instance?api-version=2017-12-01" + msiInstanceMetadataURL string = "http://169.254.169.254/metadata/identity/oauth2/token" ) var sampleConfig = ` @@ -63,7 +64,7 @@ var sampleConfig = ` ## specified, the plugin will attempt to retrieve the resource ID ## of the VM via the instance metadata service (optional if running ## on an Azure VM with MSI) - #resource_id = "/subscriptions//resourceGroups//providers/Microsoft.Compute/virtualMachines/" + #resource_id = "/subscriptions//resourceGroups//providers/Microsoft.Compute/virtualMachines/" ## Azure region to publish metrics against. Defaults to eastus. ## Leave blank to automatically query the region via MSI. #region = "useast" @@ -110,35 +111,27 @@ func (a *AzureMonitor) Connect() error { if a.AzureSubscriptionID == "" && a.AzureTenantID == "" && a.AzureClientID == "" && a.AzureClientSecret == "" { a.useMsi = true } else if a.AzureSubscriptionID == "" || a.AzureTenantID == "" || a.AzureClientID == "" || a.AzureClientSecret == "" { - return fmt.Errorf("Must provide values for azureSubscription, azureTenant, azureClient and azureClientSecret, or leave all blank to default to MSI") + return fmt.Errorf("E! Must provide values for azure_subscription, azure_tenant, azure_client and azure_client_secret, or leave all blank to default to MSI") } if !a.useMsi { // If using direct AD authentication create the AD access client oauthConfig, err := adal.NewOAuthConfig(azure.PublicCloud.ActiveDirectoryEndpoint, a.AzureTenantID) if err != nil { - return fmt.Errorf("Could not initialize AD client: %s", err) + return fmt.Errorf("E! Could not initialize AD client: %s", err) } a.oauthConfig = oauthConfig } - // Validate the resource identifier - metadata, err := a.GetInstanceMetadata() - if err != nil { - return fmt.Errorf("No resource id specified, and Azure Instance metadata service not available. If not running on an Azure VM, provide a value for resourceId") - } - a.ResourceID = metadata.AzureResourceID - - if a.Region == "" { - a.Region = metadata.Compute.Location + // Pull region and resource identifier + err := a.GetInstanceMetadata() + if err != nil && a.ResourceID == "" && a.Region == "" { + return fmt.Errorf("E! No resource id specified, and Azure Instance metadata service not available. If not running on an Azure VM, provide a value for resource_id") } - a.url := fmt.Sprintf(urlTemplate, a.Region, a.ResourceID) - - // Validate credentials err = a.validateCredentials() if err != nil { - return err + return fmt.Errorf("E! Unable to fetch authentication credentials: %v", err) } a.Reset() @@ -149,8 +142,8 @@ func (a *AzureMonitor) Connect() error { func (a *AzureMonitor) validateCredentials() error { if a.useMsi { // Check expiry on the token - if a.msiToken == nil || a.msiToken.ExpiresInDuration() < time.Minute { - msiToken, err := a.getMsiToken(a.AzureClientID, defaultMSIResource) + if a.msiToken == nil || a.msiToken.expiresInDuration() < time.Minute { + msiToken, err := a.getMsiToken(a.AzureClientID) if err != nil { return err } @@ -227,7 +220,7 @@ func (a *AzureMonitor) Write(metrics []telegraf.Metric) error { } if err := a.validateCredentials(); err != nil { - return fmt.Errorf("Error authenticating: %v", err) + return fmt.Errorf("E! Unable to fetch authentication credentials: %v", err) } req, err := http.NewRequest("POST", a.url, bytes.NewBuffer(body)) @@ -249,7 +242,7 @@ func (a *AzureMonitor) Write(metrics []telegraf.Metric) error { if err != nil { reply = nil } - return fmt.Errorf("Post Error. HTTP response code:%d message:%s reply:\n%s", + return fmt.Errorf("E! Get Error. %d HTTP response: %s response body: %s", resp.StatusCode, resp.Status, reply) } diff --git a/plugins/outputs/azuremonitor/azuremonitor_test.go b/plugins/outputs/azuremonitor/azuremonitor_test.go index 7980399ce..34b24cd8c 100644 --- a/plugins/outputs/azuremonitor/azuremonitor_test.go +++ b/plugins/outputs/azuremonitor/azuremonitor_test.go @@ -7,23 +7,6 @@ import ( "github.com/influxdata/telegraf/metric" ) -// func TestDefaultConnectAndWrite(t *testing.T) { -// if testing.Short() { -// t.Skip("Skipping integration test in short mode") -// } - -// // Test with all defaults (MSI+IMS) -// azmon := &AzureMonitor{} - -// // Verify that we can connect to Log Analytics -// err := azmon.Connect() -// require.NoError(t, err) - -// // Verify that we can write a metric to Log Analytics -// err = azmon.Write(testutil.MockMetrics()) -// require.NoError(t, err) -// } - // MockMetrics returns a mock []telegraf.Metric object for using in unit tests // of telegraf output sinks. func getMockMetrics() []telegraf.Metric { @@ -55,35 +38,3 @@ func getTestMetric(value interface{}, name ...string) telegraf.Metric { ) return pt } - -// func TestPostData(t *testing.T) { -// azmon := &AzureMonitor{ -// Region: "eastus", -// } -// err := azmon.Connect() - -// metrics := getMockMetrics() -// t.Logf("mock metrics are %+v\n", metrics) -// // metricsList, err := azmon.add(&metrics[0]) -// for _, m := range metrics { -// azmon.Add(m) -// } - -// jsonBytes, err := json.Marshal(azmon.cache) -// t.Logf("json content is:\n----------\n%s\n----------\n", string(jsonBytes)) - -// req, err := azmon.postData(&jsonBytes) -// if err != nil { -// // t.Logf("Error publishing metrics %s", err) -// t.Logf("url is %+v\n", req.URL) -// // t.Logf("failed request is %+v\n", req) - -// // raw, err := httputil.DumpRequestOut(req, true) -// // if err != nil { -// // t.Logf("Request detail is \n%s\n", string(raw)) -// // } else { -// // t.Logf("could not dump request: %s\n", err) -// // } -// } -// require.NoError(t, err) -// }