2018-03-28 02:15:52 +00:00
package azuremonitor
import (
"bytes"
"crypto/tls"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"strconv"
"time"
"github.com/Azure/go-autorest/autorest/adal"
"github.com/Azure/go-autorest/autorest/azure"
"github.com/influxdata/telegraf"
2018-04-29 07:31:24 +00:00
"github.com/influxdata/telegraf/internal"
2018-04-11 23:50:48 +00:00
"github.com/influxdata/telegraf/plugins/outputs"
2018-03-28 02:15:52 +00:00
)
// AzureMonitor allows publishing of metrics to the Azure Monitor custom metrics service
type AzureMonitor struct {
2018-04-29 07:31:24 +00:00
ResourceID string ` toml:"resource_id" `
Region string ` toml:"region" `
Timeout internal . Duration ` toml:"Timeout" `
AzureSubscriptionID string ` toml:"azure_subscription" `
AzureTenantID string ` toml:"azure_tenant" `
AzureClientID string ` toml:"azure_client_id" `
AzureClientSecret string ` toml:"azure_client_secret" `
StringAsDimension bool ` toml:"string_as_dimension" `
useMsi bool ` toml:"use_managed_service_identity" `
2018-03-28 02:15:52 +00:00
metadataService * AzureInstanceMetadata
instanceMetadata * VirtualMachineMetadata
2018-04-29 07:31:24 +00:00
msiToken * msiToken
2018-03-28 02:15:52 +00:00
msiResource string
bearerToken string
expiryWatermark time . Duration
oauthConfig * adal . OAuthConfig
adalToken adal . OAuthTokenProvider
2018-04-11 23:50:48 +00:00
client * http . Client
2018-04-29 07:31:24 +00:00
cache map [ string ] * azureMonitorMetric
2018-04-11 23:50:48 +00:00
period time . Duration
delay time . Duration
periodStart time . Time
periodEnd time . Time
metrics chan telegraf . Metric
shutdown chan struct { }
}
type azureMonitorMetric struct {
Time time . Time ` json:"time" `
Data * azureMonitorData ` json:"data" `
}
type azureMonitorData struct {
BaseData * azureMonitorBaseData ` json:"baseData" `
}
type azureMonitorBaseData struct {
Metric string ` json:"metric" `
Namespace string ` json:"namespace" `
DimensionNames [ ] string ` json:"dimNames" `
Series [ ] * azureMonitorSeries ` json:"series" `
}
type azureMonitorSeries struct {
DimensionValues [ ] string ` json:"dimValues" `
Min float64 ` json:"min" `
Max float64 ` json:"max" `
Sum float64 ` json:"sum" `
Count float64 ` json:"count" `
2018-03-28 02:15:52 +00:00
}
var sampleConfig = `
2018-04-29 07:31:24 +00:00
# # The resource ID against which metric will be logged . If not
# # 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/<subscription-id>/resourceGroups/<resource-group>/providers/Microsoft.Compute/virtualMachines/<vm-name>"
# # Azure region to publish metrics against . Defaults to eastus .
# # Leave blank to automatically query the region via MSI .
# region = "useast"
# # Write HTTP timeout , formatted as a string . If not provided , will default
# # to 5 s . 0 s means no timeout ( not recommended ) .
# timeout = "5s"
# # Whether or not to use managed service identity .
# use_managed_service_identity = true
# # Fill in the following values if using Active Directory Service
# # Principal or User Principal for authentication .
# # Subscription ID
# azure_subscription = ""
# # Tenant ID
# azure_tenant = ""
# # Client ID
# azure_client_id = ""
# # Client secrete
# azure_client_secret = ""
2018-03-28 02:15:52 +00:00
`
const (
2018-04-29 07:31:24 +00:00
defaultRegion = "eastus"
defaultMSIResource = "https://monitoring.azure.com/"
2018-03-28 02:15:52 +00:00
)
// Connect initializes the plugin and validates connectivity
2018-04-11 23:50:48 +00:00
func ( a * AzureMonitor ) Connect ( ) error {
2018-03-28 02:15:52 +00:00
// Set defaults
// If no direct AD values provided, fall back to MSI
2018-04-11 23:50:48 +00:00
if a . AzureSubscriptionID == "" && a . AzureTenantID == "" && a . AzureClientID == "" && a . AzureClientSecret == "" {
a . useMsi = true
} else if a . AzureSubscriptionID == "" || a . AzureTenantID == "" || a . AzureClientID == "" || a . AzureClientSecret == "" {
2018-03-28 02:15:52 +00:00
return fmt . Errorf ( "Must provide values for azureSubscription, azureTenant, azureClient and azureClientSecret, or leave all blank to default to MSI" )
}
2018-04-29 07:31:24 +00:00
if ! a . useMsi {
2018-03-28 02:15:52 +00:00
// If using direct AD authentication create the AD access client
2018-04-11 23:50:48 +00:00
oauthConfig , err := adal . NewOAuthConfig ( azure . PublicCloud . ActiveDirectoryEndpoint , a . AzureTenantID )
2018-03-28 02:15:52 +00:00
if err != nil {
return fmt . Errorf ( "Could not initialize AD client: %s" , err )
}
2018-04-11 23:50:48 +00:00
a . oauthConfig = oauthConfig
2018-03-28 02:15:52 +00:00
}
2018-04-11 23:50:48 +00:00
a . metadataService = & AzureInstanceMetadata { }
2018-03-28 02:15:52 +00:00
// For the metrics API the MSI resource has to be https://ingestion.monitor.azure.com
2018-04-11 23:50:48 +00:00
a . msiResource = "https://monitoring.azure.com/"
2018-03-28 02:15:52 +00:00
// Validate the resource identifier
2018-04-29 07:31:24 +00:00
metadata , err := a . metadataService . 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" )
2018-03-28 02:15:52 +00:00
}
2018-04-29 07:31:24 +00:00
a . ResourceID = metadata . AzureResourceID
2018-03-28 02:15:52 +00:00
2018-04-11 23:50:48 +00:00
if a . Region == "" {
2018-04-29 07:31:24 +00:00
a . Region = metadata . Compute . Location
2018-03-28 02:15:52 +00:00
}
// Validate credentials
2018-04-29 07:31:24 +00:00
err = a . validateCredentials ( )
2018-03-28 02:15:52 +00:00
if err != nil {
return err
}
2018-04-11 23:50:48 +00:00
a . reset ( )
go a . run ( )
2018-03-28 02:15:52 +00:00
return nil
}
2018-04-11 23:50:48 +00:00
func ( a * AzureMonitor ) validateCredentials ( ) error {
2018-03-28 02:15:52 +00:00
// Use managed service identity
2018-04-11 23:50:48 +00:00
if a . useMsi {
2018-03-28 02:15:52 +00:00
// Check expiry on the token
2018-04-11 23:50:48 +00:00
if a . msiToken != nil {
expiryDuration := a . msiToken . ExpiresInDuration ( )
if expiryDuration > a . expiryWatermark {
2018-03-28 02:15:52 +00:00
return nil
}
// Token is about to expire
log . Printf ( "Bearer token expiring in %s; acquiring new token\n" , expiryDuration . String ( ) )
2018-04-11 23:50:48 +00:00
a . msiToken = nil
2018-03-28 02:15:52 +00:00
}
// No token, acquire an MSI token
2018-04-11 23:50:48 +00:00
if a . msiToken == nil {
2018-04-29 07:31:24 +00:00
msiToken , err := a . metadataService . getMsiToken ( a . AzureClientID , a . msiResource )
2018-03-28 02:15:52 +00:00
if err != nil {
return err
}
log . Printf ( "Bearer token acquired; expiring in %s\n" , msiToken . ExpiresInDuration ( ) . String ( ) )
2018-04-11 23:50:48 +00:00
a . msiToken = msiToken
a . bearerToken = msiToken . AccessToken
2018-03-28 02:15:52 +00:00
}
// Otherwise directory acquire a token
} else {
adToken , err := adal . NewServicePrincipalToken (
2018-04-11 23:50:48 +00:00
* ( a . oauthConfig ) , a . AzureClientID , a . AzureClientSecret ,
2018-03-28 02:15:52 +00:00
azure . PublicCloud . ActiveDirectoryEndpoint )
if err != nil {
return fmt . Errorf ( "Could not acquire ADAL token: %s" , err )
}
2018-04-11 23:50:48 +00:00
a . adalToken = adToken
2018-03-28 02:15:52 +00:00
}
return nil
}
2018-04-29 07:31:24 +00:00
// Description provides a description of the plugin
func ( a * AzureMonitor ) Description ( ) string {
return "Configuration for sending aggregate metrics to Azure Monitor"
}
// SampleConfig provides a sample configuration for the plugin
func ( a * AzureMonitor ) SampleConfig ( ) string {
return sampleConfig
}
// Close shuts down an any active connections
func ( a * AzureMonitor ) Close ( ) error {
// Close connection to the URL here
close ( a . shutdown )
return nil
}
// Write writes metrics to the remote endpoint
func ( a * AzureMonitor ) Write ( metrics [ ] telegraf . Metric ) error {
// Assemble basic stats on incoming metrics
for _ , metric := range metrics {
select {
case a . metrics <- metric :
default :
log . Printf ( "metrics buffer is full" )
2018-03-28 02:15:52 +00:00
}
2018-04-29 07:31:24 +00:00
}
return nil
}
2018-03-28 02:15:52 +00:00
2018-04-29 07:31:24 +00:00
func ( a * AzureMonitor ) run ( ) {
// The start of the period is truncated to the nearest minute.
//
// Every metric then gets it's timestamp checked and is dropped if it
// is not within:
//
// start < t < end + truncation + delay
//
// So if we start at now = 00:00.2 with a 10s period and 0.3s delay:
// now = 00:00.2
// start = 00:00
// truncation = 00:00.2
// end = 00:10
// 1st interval: 00:00 - 00:10.5
// 2nd interval: 00:10 - 00:20.5
// etc.
//
now := time . Now ( )
a . periodStart = now . Truncate ( time . Minute )
truncation := now . Sub ( a . periodStart )
a . periodEnd = a . periodStart . Add ( a . period )
time . Sleep ( a . delay )
periodT := time . NewTicker ( a . period )
defer periodT . Stop ( )
for {
select {
case <- a . shutdown :
if len ( a . metrics ) > 0 {
// wait until metrics are flushed before exiting
continue
}
2018-04-11 23:50:48 +00:00
return
2018-04-29 07:31:24 +00:00
case m := <- a . metrics :
if m . Time ( ) . Before ( a . periodStart ) ||
m . Time ( ) . After ( a . periodEnd . Add ( truncation ) . Add ( a . delay ) ) {
// the metric is outside the current aggregation period, so
// skip it.
continue
}
a . add ( m )
case <- periodT . C :
a . periodStart = a . periodEnd
a . periodEnd = a . periodStart . Add ( a . period )
a . push ( )
a . reset ( )
2018-03-28 02:15:52 +00:00
}
2018-04-29 07:31:24 +00:00
}
}
2018-03-28 02:15:52 +00:00
2018-04-29 07:31:24 +00:00
func ( a * AzureMonitor ) reset ( ) {
a . cache = make ( map [ string ] * azureMonitorMetric )
}
func ( a * AzureMonitor ) add ( metric telegraf . Metric ) {
var dimensionNames [ ] string
var dimensionValues [ ] string
for i , tag := range metric . TagList ( ) {
// Azure custom metrics service supports up to 10 dimensions
if i > 10 {
continue
2018-03-28 02:15:52 +00:00
}
2018-04-29 07:31:24 +00:00
dimensionNames = append ( dimensionNames , tag . Key )
dimensionValues = append ( dimensionValues , tag . Value )
}
// Azure Monitoe does not support string value types, so convert string
// fields to dimensions if enabled.
if a . StringAsDimension {
2018-04-11 23:50:48 +00:00
for _ , f := range metric . FieldList ( ) {
2018-04-29 07:31:24 +00:00
switch fv := f . Value . ( type ) {
case string :
dimensionNames = append ( dimensionNames , f . Key )
dimensionValues = append ( dimensionValues , fv )
metric . RemoveField ( f . Key )
2018-04-11 23:50:48 +00:00
}
2018-04-29 07:31:24 +00:00
}
}
for _ , f := range metric . FieldList ( ) {
name := metric . Name ( ) + "_" + f . Key
fv , ok := convert ( f . Value )
if ! ok {
log . Printf ( "unable to convert field %s (type %T) to float type: %v" , f . Key , fv , fv )
continue
}
2018-03-28 02:15:52 +00:00
2018-04-29 07:31:24 +00:00
if azm , ok := a . cache [ name ] ; ! ok {
// hit an uncached metric, create it for first time
a . cache [ name ] = & azureMonitorMetric {
Time : metric . Time ( ) ,
Data : & azureMonitorData {
BaseData : & azureMonitorBaseData {
Metric : name ,
Namespace : "default" ,
DimensionNames : dimensionNames ,
Series : [ ] * azureMonitorSeries {
newAzureMonitorSeries ( dimensionValues , fv ) ,
} ,
} ,
} ,
}
} else {
tmp , i , ok := azm . findSeries ( dimensionValues )
2018-04-11 23:50:48 +00:00
if ! ok {
2018-04-29 07:31:24 +00:00
// add series new series (should be rare)
n := append ( azm . Data . BaseData . Series , newAzureMonitorSeries ( dimensionValues , fv ) )
a . cache [ name ] . Data . BaseData . Series = n
2018-04-11 23:50:48 +00:00
continue
}
//counter compute
n := tmp . Count + 1
tmp . Count = n
//max/min compute
if fv < tmp . Min {
tmp . Min = fv
} else if fv > tmp . Max {
tmp . Max = fv
}
//sum compute
tmp . Sum += fv
//store final data
2018-04-29 07:31:24 +00:00
a . cache [ name ] . Data . BaseData . Series [ i ] = tmp
2018-04-11 23:50:48 +00:00
}
2018-03-28 02:15:52 +00:00
}
}
2018-04-29 07:31:24 +00:00
func ( m * azureMonitorMetric ) findSeries ( dv [ ] string ) ( * azureMonitorSeries , int , bool ) {
if len ( m . Data . BaseData . DimensionNames ) != len ( dv ) {
return nil , 0 , false
}
for i := range m . Data . BaseData . Series {
if m . Data . BaseData . Series [ i ] . equal ( dv ) {
return m . Data . BaseData . Series [ i ] , i , true
2018-04-11 23:50:48 +00:00
}
}
2018-04-29 07:31:24 +00:00
return nil , 0 , false
2018-03-28 02:15:52 +00:00
}
2018-04-29 07:31:24 +00:00
func newAzureMonitorSeries ( dv [ ] string , fv float64 ) * azureMonitorSeries {
2018-04-11 23:50:48 +00:00
return & azureMonitorSeries {
2018-04-29 07:31:24 +00:00
DimensionValues : append ( [ ] string { } , dv ... ) ,
2018-04-11 23:50:48 +00:00
Min : fv ,
Max : fv ,
Sum : fv ,
Count : 1 ,
}
}
2018-04-29 07:31:24 +00:00
func ( s * azureMonitorSeries ) equal ( dv [ ] string ) bool {
if len ( s . DimensionValues ) != len ( dv ) {
return false
}
for i := range dv {
if dv [ i ] != s . DimensionValues [ i ] {
return false
}
}
return true
2018-04-11 23:50:48 +00:00
}
2018-03-28 02:15:52 +00:00
2018-04-11 23:50:48 +00:00
func convert ( in interface { } ) ( float64 , bool ) {
switch v := in . ( type ) {
2018-03-28 02:15:52 +00:00
case int64 :
2018-04-11 23:50:48 +00:00
return float64 ( v ) , true
case uint64 :
return float64 ( v ) , true
2018-03-28 02:15:52 +00:00
case float64 :
2018-04-11 23:50:48 +00:00
return v , true
2018-04-29 07:31:24 +00:00
case bool :
if v {
return 1 , true
}
return 1 , true
2018-04-11 23:50:48 +00:00
case string :
f , err := strconv . ParseFloat ( v , 64 )
if err != nil {
return 0 , false
}
return f , true
2018-03-28 02:15:52 +00:00
default :
2018-04-11 23:50:48 +00:00
return 0 , false
2018-03-28 02:15:52 +00:00
}
}
2018-04-11 23:50:48 +00:00
func ( a * AzureMonitor ) push ( ) {
var body [ ] byte
for _ , metric := range a . cache {
jsonBytes , err := json . Marshal ( & metric )
if err != nil {
log . Printf ( "Error marshalling metrics %s" , err )
return
}
body = append ( body , jsonBytes ... )
body = append ( body , '\n' )
}
_ , err := a . postData ( & body )
if err != nil {
2018-04-29 07:31:24 +00:00
log . Printf ( "Error publishing aggregate metrics %s" , err )
2018-04-11 23:50:48 +00:00
}
return
}
func ( a * AzureMonitor ) postData ( msg * [ ] byte ) ( * http . Request , error ) {
2018-04-29 07:31:24 +00:00
if err := a . validateCredentials ( ) ; err != nil {
return nil , fmt . Errorf ( "Error authenticating: %v" , err )
}
2018-03-28 02:15:52 +00:00
metricsEndpoint := fmt . Sprintf ( "https://%s.monitoring.azure.com%s/metrics" ,
2018-04-11 23:50:48 +00:00
a . Region , a . ResourceID )
2018-03-28 02:15:52 +00:00
req , err := http . NewRequest ( "POST" , metricsEndpoint , bytes . NewBuffer ( * msg ) )
if err != nil {
log . Printf ( "Error creating HTTP request" )
return nil , err
}
2018-04-11 23:50:48 +00:00
req . Header . Set ( "Authorization" , "Bearer " + a . bearerToken )
req . Header . Set ( "Content-Type" , "application/x-ndjson" )
2018-03-28 02:15:52 +00:00
tr := & http . Transport {
TLSClientConfig : & tls . Config { InsecureSkipVerify : true } ,
}
client := http . Client {
Transport : tr ,
2018-04-29 07:31:24 +00:00
Timeout : a . Timeout . Duration ,
2018-03-28 02:15:52 +00:00
}
resp , err := client . Do ( req )
if err != nil {
return req , err
}
defer resp . Body . Close ( )
if resp . StatusCode >= 300 || resp . StatusCode < 200 {
var reply [ ] byte
reply , err = ioutil . ReadAll ( resp . Body )
if err != nil {
reply = nil
}
return req , fmt . Errorf ( "Post Error. HTTP response code:%d message:%s reply:\n%s" ,
resp . StatusCode , resp . Status , reply )
}
return req , nil
}
2018-04-11 23:50:48 +00:00
func init ( ) {
outputs . Add ( "azuremonitor" , func ( ) telegraf . Output {
return & AzureMonitor {
2018-04-29 07:31:24 +00:00
StringAsDimension : true ,
Timeout : internal . Duration { Duration : time . Second * 5 } ,
Region : defaultRegion ,
period : time . Minute ,
delay : time . Second * 5 ,
metrics : make ( chan telegraf . Metric , 100 ) ,
shutdown : make ( chan struct { } ) ,
2018-04-11 23:50:48 +00:00
}
} )
}