Skip to content

Commit

Permalink
AzureMonitorExporter - Populate ai.cloud.role and ai.cloud.roleinstan…
Browse files Browse the repository at this point in the history
…ce correctly (#218)

* Populate ai.role.instance and roleinstance correctly

* add more comments
  • Loading branch information
youngbupark authored and wyTrivail committed Jul 13, 2020
1 parent 685a7f4 commit eab70a1
Show file tree
Hide file tree
Showing 2 changed files with 129 additions and 36 deletions.
102 changes: 66 additions & 36 deletions exporter/azuremonitorexporter/traceexporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -642,6 +642,50 @@ func sanitizeWithCallback(sanitizeFunc func() []string, warningCallback func(str
}
}

// populateResourceAttributes populates resource attributes to telemetry envelope.
func (exporter *traceExporter) populateResourceAttributes(
traceData consumerdata.TraceData,
envelope *contracts.Envelope,
) {
// Old trace exporter populates trace resource attributes to Node and Resource Labels.
// https://github.com/open-telemetry/opentelemetry-collector/blob/master/translator/internaldata/resource_to_oc.go#L54
var properties map[string]string
if data, ok := envelope.Data.(*contracts.Data); ok {
switch d := data.BaseData.(type) {
case *contracts.RemoteDependencyData:
properties = d.Properties
case *contracts.RequestData:
properties = d.Properties
}
}

// Extract service.namespace and populate the other resource attributes to properties
cloudRolePrefix := ""
if traceData.Resource != nil && traceData.Resource.Labels != nil && len(traceData.Resource.Labels) > 0 {
for k, v := range traceData.Resource.Labels {
switch k {
case conventions.AttributeServiceNamespace:
cloudRolePrefix = v + "."
default:
if properties != nil {
properties[k] = v
}
}
}
}

if traceData.Node != nil {
// ai.cloud.role is the name of role which represents current service name
if traceData.Node.GetServiceInfo() != nil {
envelope.Tags[contracts.CloudRole] = cloudRolePrefix + traceData.Node.ServiceInfo.GetName()
}
// ai.cloud.roleinstance is the name of the instance where service is running
if traceData.Node.GetIdentifier() != nil {
envelope.Tags[contracts.CloudRoleInstance] = traceData.Node.Identifier.GetHostName()
}
}
}

func (exporter *traceExporter) pushTraceData(
context context.Context,
traceData consumerdata.TraceData,
Expand All @@ -654,42 +698,28 @@ func (exporter *traceExporter) pushTraceData(

for _, wireFormatSpan := range traceData.Spans {
if envelope, err := exporter.spanToEnvelope(exporter.config.InstrumentationKey, wireFormatSpan); err == nil && exporter.transportChannel != nil {

// Attach node level attributes to envelope and data as appropriate
if traceData.Node != nil && traceData.Node.Attributes != nil {
// Augment the envelope and envelope data with node level attributes
// Configure the ai.cloud.role and ai.cloud.roleinstance on the envelope tags itself, then copy the node attributes to the envelope data as well.
// Assumes the node level attribute key/values correspond to:
// https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/data-resource-semantic-conventions.md
if serviceName, ok := traceData.Node.Attributes[conventions.AttributeServiceName]; ok {
if serviceNamespace, success := traceData.Node.Attributes[conventions.AttributeServiceNamespace]; success {
envelope.Tags[contracts.CloudRole] = serviceNamespace + "." + serviceName
} else {
envelope.Tags[contracts.CloudRole] = serviceName
}
}

if serviceInstanceID, ok := traceData.Node.Attributes[conventions.AttributeServiceInstance]; ok {
envelope.Tags[contracts.CloudRoleInstance] = serviceInstanceID
}

// Locate the correct properties map for the envelope
var properties map[string]string
if data, ok := envelope.Data.(*contracts.Data); ok {
if d, success := data.BaseData.(*contracts.RemoteDependencyData); success {
properties = d.Properties
} else if d, success := data.BaseData.(*contracts.RequestData); success {
properties = d.Properties
}

// Copy the node properties
if properties != nil {
for key, value := range traceData.Node.Attributes {
properties[key] = value
}
}
}
}
// The resource attributes needs to be given to populate ai.cloud.role and ai.cloud.roleinstance
// when you create new exporter.
//
// - service.name
// - service.namespace
// - host.hostname
//
// OTLP Exporter example:
// ...
// exp, _ := otlp.NewExporter(otlp.WithInsecure(), otlp.WithAddress("localhost:9090"))
// tp, _ := sdktrace.NewProvider(
// sdktrace.WithSyncer(exp),
// sdktrace.WithConfig(sdktrace.Config{DefaultSampler: sdktrace.AlwaysSample()}),
// sdktrace.WithResourceAttributes(
// key.String(resourcekeys.ServiceKeyName, "your_service"),
// key.String(resourcekeys.ServiceKeyNamespace, "namespace"),
// key.String(resourcekeys.HostKeyHostName, "hostname"),
// ),
// )
// global.SetTraceProvider(tp)
//
exporter.populateResourceAttributes(traceData, envelope)

// This is a fire and forget operation
exporter.transportChannel.Send(envelope)
Expand Down
63 changes: 63 additions & 0 deletions exporter/azuremonitorexporter/traceexporter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ import (
"strconv"
"testing"

"github.com/Microsoft/ApplicationInsights-Go/appinsights/contracts"
commonpb "github.com/census-instrumentation/opencensus-proto/gen-go/agent/common/v1"
resourcepb "github.com/census-instrumentation/opencensus-proto/gen-go/resource/v1"
tracepb "github.com/census-instrumentation/opencensus-proto/gen-go/trace/v1"
timestamp "github.com/golang/protobuf/ptypes/timestamp"
"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -365,6 +368,66 @@ func TestSpanToRemoteDependencyDataDatabaseAttributeSet(t *testing.T) {
validateArbitraryAttributeValuesAsPropertiesOrMeasurements(t, data.Properties, data.Measurements)
}

func TestPopulateResourceAttributes(t *testing.T) {
// create exporter
exporter := &traceExporter{}

// construct fake application insights envelope
envelope := contracts.NewEnvelope()
envelope.Tags = map[string]string{}
data := contracts.NewData()
reqData := contracts.NewRequestData()
reqData.Properties = map[string]string{}
data.BaseData = reqData
envelope.Data = data

// construct test tracedata
traceData := consumerdata.TraceData{
Node: &commonpb.Node{
ServiceInfo: &commonpb.ServiceInfo{
Name: "service",
},
Identifier: &commonpb.ProcessIdentifier{
HostName: "hostname",
},
},
Resource: &resourcepb.Resource{},
Spans: []*tracepb.Span{},
}

t.Run("no attributes", func(t *testing.T) {
traceData.Node.ServiceInfo.Name = ""
traceData.Node.Identifier.HostName = ""
traceData.Resource.Labels = map[string]string{}
exporter.populateResourceAttributes(traceData, envelope)

assert.Equal(t, "", envelope.Tags[contracts.CloudRole])
assert.Equal(t, "", envelope.Tags[contracts.CloudRoleInstance])
})

t.Run("populate ai.cloud.role and ai.cloud.roleinstance", func(t *testing.T) {
traceData.Node.ServiceInfo.Name = "ServiceName"
traceData.Node.Identifier.HostName = "hostname"
exporter.populateResourceAttributes(traceData, envelope)

assert.Equal(t, "ServiceName", envelope.Tags[contracts.CloudRole])
assert.Equal(t, "hostname", envelope.Tags[contracts.CloudRoleInstance])
})

t.Run("populate namespace and custom properties", func(t *testing.T) {
traceData.Node.ServiceInfo.Name = "ServiceName"
traceData.Resource.Labels = map[string]string{
"service.namespace": "namespace",
"service.instance.id": "instanceid",
}
exporter.populateResourceAttributes(traceData, envelope)

assert.Equal(t, "namespace.ServiceName", envelope.Tags[contracts.CloudRole])
props := envelope.Data.(*contracts.Data).BaseData.(*contracts.RequestData).Properties
assert.Equal(t, "instanceid", props["service.instance.id"])
})
}

// Tests the exporter's pushTraceData callback method
func TestExporterPushTraceDataCallback(t *testing.T) {
factory := Factory{}
Expand Down

0 comments on commit eab70a1

Please sign in to comment.