Skip to content

Commit

Permalink
Add standard metrics custom processor and request duration metric (#3…
Browse files Browse the repository at this point in the history
…3955)

* draft

* compositeprocessor

* add test

* revert

* formatting

* rmv using

* rmv ?

* resolve PR comments

* add meter

* fix test

* rmv using

* format

* rmv using
  • Loading branch information
vishweshbankwar committed Feb 8, 2023
1 parent 95de9f7 commit 6615941
Show file tree
Hide file tree
Showing 7 changed files with 238 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using Azure.Core;
using Azure.Monitor.OpenTelemetry.Exporter.Internals;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using OpenTelemetry.Metrics;
Expand Down Expand Up @@ -42,6 +43,8 @@ public static class AzureMonitorExporterMetricExtensions
builder.ConfigureServices(services => services.Configure(finalOptionsName, configure));
}

builder.AddMeter(StandardMetricConstants.StandardMetricMeterName);

return builder.AddReader(sp =>
{
var exporterOptions = sp.GetRequiredService<IOptionsMonitor<AzureMonitorExporterOptions>>().Get(finalOptionsName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
// Licensed under the MIT License.

using System;
using System.Diagnostics;
using Azure.Core;
using Azure.Monitor.OpenTelemetry.Exporter.Internals;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using OpenTelemetry;
Expand Down Expand Up @@ -57,7 +59,11 @@ public static class AzureMonitorExporterTraceExtensions
configure(exporterOptions);
}
return new BatchActivityExportProcessor(new AzureMonitorTraceExporter(exporterOptions, credential));
return new CompositeProcessor<Activity>(new BaseProcessor<Activity>[]
{
new StandardMetricsExtractionProcessor(),
new BatchActivityExportProcessor(new AzureMonitorTraceExporter(exporterOptions, credential))
});
});
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,15 @@ internal partial class MetricDataPoint
{
public MetricDataPoint(Metric metric, MetricPoint metricPoint)
{
Name = metric.Name;
Namespace = metric.MeterName;
if (StandardMetricsExtractionProcessor.s_standardMetricNameMapping.TryGetValue(metric.Name, out var metricName))
{
Name = metricName;
}
else
{
Name = metric.Name;
Namespace = metric.MeterName;
}

switch (metric.MetricType)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,7 @@ public RequestData(int version, Activity activity, ref TagEnumerationState monit
?.ToString().Truncate(SchemaConstants.RequestData_ResponseCode_MaxLength)
?? "0";

if (monitorTags.activityType == OperationType.Http && int.TryParse(ResponseCode, out int statusCode))
{
bool isSuccessStatusCode = statusCode != 0 && statusCode < 400;
Success = activity.Status != ActivityStatusCode.Error && isSuccessStatusCode;
}
else
{
Success = activity.Status != ActivityStatusCode.Error;
}
Success = isSuccess(activity, ResponseCode, monitorTags.activityType);

Url = url.Truncate(SchemaConstants.RequestData_Url_MaxLength);
Properties = new ChangeTrackingDictionary<string, string>();
Expand All @@ -52,5 +44,18 @@ public RequestData(int version, Activity activity, ref TagEnumerationState monit
TraceHelper.AddActivityLinksToProperties(activity.Links, ref monitorTags.UnMappedTags);
TraceHelper.AddPropertiesToTelemetry(Properties, ref monitorTags.UnMappedTags);
}

internal static bool isSuccess(Activity activity, string responseCode, OperationType operationType)
{
if (operationType == OperationType.Http && int.TryParse(responseCode, out int statusCode))
{
bool isSuccessStatusCode = statusCode != 0 && statusCode < 400;
return activity.Status != ActivityStatusCode.Error && isSuccessStatusCode;
}
else
{
return activity.Status != ActivityStatusCode.Error;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

namespace Azure.Monitor.OpenTelemetry.Exporter.Internals
{
internal class StandardMetricConstants
{
internal const string StandardMetricMeterName = "StandardMetricMeter";
internal const string RequestDurationInstrumentName = "RequestDurationStandardMetric";

// request duration keys and values
internal const string RequestDurationMetricIdValue = "requests/duration";
internal const string RequestSuccessKey = "Request.Success";
internal const string RequestResultCodeKey = "request/resultCode";

//common keys
internal const string IsSyntheticKey = "operation/synthetic";
internal const string IsAutoCollectedKey = "_MS.IsAutocollected";
internal const string CloudRoleNameKey = "cloud/roleName";
internal const string CloudRoleInstanceKey = "cloud/roleInstance";
internal const string MetricIdKey = "_MS.MetricId";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

#nullable disable // TODO: remove and fix errors

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.Metrics;
using Azure.Monitor.OpenTelemetry.Exporter.Models;
using OpenTelemetry;

namespace Azure.Monitor.OpenTelemetry.Exporter.Internals
{
internal class StandardMetricsExtractionProcessor : BaseProcessor<Activity>
{
private bool _disposed;
private AzureMonitorResource _resource;
private readonly Meter _meter;
private readonly Histogram<double> _requestDuration;

internal static readonly IReadOnlyDictionary<string, string> s_standardMetricNameMapping = new Dictionary<string, string>()
{
[StandardMetricConstants.RequestDurationInstrumentName] = StandardMetricConstants.RequestDurationMetricIdValue,
};

internal AzureMonitorResource StandardMetricResource => _resource ??= ParentProvider.GetResource().UpdateRoleNameAndInstance();

internal StandardMetricsExtractionProcessor()
{
_meter = new Meter(StandardMetricConstants.StandardMetricMeterName);
_requestDuration = _meter.CreateHistogram<double>(StandardMetricConstants.RequestDurationInstrumentName);
}

public override void OnEnd(Activity activity)
{
if (activity.Kind == ActivityKind.Server)
{
if (_requestDuration.Enabled)
{
activity.SetTag("_MS.ProcessedByMetricExtractors", "(Name: X,Ver:'1.1')");
ReportRequestDurationMetric(activity, SemanticConventions.AttributeHttpStatusCode);
}
}

// TODO: other activity kinds
}

private void ReportRequestDurationMetric(Activity activity, string statusCodeAttribute)
{
string statusCodeAttributeValue = null;
foreach (var tag in activity.EnumerateTagObjects())
{
if (tag.Key == statusCodeAttribute)
{
statusCodeAttributeValue = tag.Value.ToString();
break;
}
}

TagList tags = default;
tags.Add(new KeyValuePair<string, object>(StandardMetricConstants.RequestResultCodeKey, statusCodeAttributeValue));
tags.Add(new KeyValuePair<string, object>(StandardMetricConstants.MetricIdKey, StandardMetricConstants.RequestDurationMetricIdValue));
tags.Add(new KeyValuePair<string, object>(StandardMetricConstants.IsAutoCollectedKey, "True"));
tags.Add(new KeyValuePair<string, object>(StandardMetricConstants.IsSyntheticKey, "False"));
tags.Add(new KeyValuePair<string, object>(StandardMetricConstants.CloudRoleInstanceKey, StandardMetricResource.RoleInstance));
tags.Add(new KeyValuePair<string, object>(StandardMetricConstants.CloudRoleNameKey, StandardMetricResource.RoleName));
tags.Add(new KeyValuePair<string, object>(StandardMetricConstants.RequestSuccessKey, RequestData.isSuccess(activity, statusCodeAttributeValue, OperationType.Http)));

// Report metric
_requestDuration.Record(activity.Duration.TotalMilliseconds, tags);
}

protected override void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
try
{
_meter?.Dispose();
}
catch (Exception)
{
}
}

_disposed = true;
}

base.Dispose(disposing);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Azure.Monitor.OpenTelemetry.Exporter.Internals;
using Azure.Monitor.OpenTelemetry.Exporter.Models;
using OpenTelemetry.Metrics;
using OpenTelemetry;
using Xunit;
using Azure.Monitor.OpenTelemetry.Exporter.Tests.CommonTestFramework;
using OpenTelemetry.Trace;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Linq;
using Microsoft.Identity.Client.Platforms.Features.DesktopOs.Kerberos;
using System;
using System.Threading;

namespace Azure.Monitor.OpenTelemetry.Exporter.Tests
{
public class StandardMetricTests
{
[Fact]
public void ValidateRequestDurationMetric()
{
var activitySource = new ActivitySource(nameof(StandardMetricTests.ValidateRequestDurationMetric));
var traceTelemetryItems = new ConcurrentBag<TelemetryItem>();
var metricTelemetryItems = new ConcurrentBag<TelemetryItem>();

var standardMetricCustomProcessor = new StandardMetricsExtractionProcessor();

using var tracerProvider = Sdk.CreateTracerProviderBuilder()
.SetSampler(new AlwaysOnSampler())
.AddSource(nameof(StandardMetricTests.ValidateRequestDurationMetric))
.AddProcessor(standardMetricCustomProcessor)
.AddProcessor(new BatchActivityExportProcessor(new AzureMonitorTraceExporter(new MockTransmitter(traceTelemetryItems))))
.Build();

using var meterProvider = Sdk.CreateMeterProviderBuilder()
.AddMeter(StandardMetricConstants.StandardMetricMeterName)
.AddReader(new PeriodicExportingMetricReader(new AzureMonitorMetricExporter(new MockTransmitter(metricTelemetryItems)))
{ TemporalityPreference = MetricReaderTemporalityPreference.Delta })
.Build();

using (var activity = activitySource.StartActivity("Test", ActivityKind.Server))
{
activity?.SetTag(SemanticConventions.AttributeHttpStatusCode, 200);
}

tracerProvider?.ForceFlush();

WaitForActivityExport(traceTelemetryItems);

meterProvider?.ForceFlush();

Assert.Single(metricTelemetryItems);

var metricTelemetry = metricTelemetryItems.Single();
Assert.Equal("MetricData", metricTelemetry.Data.BaseType);
var metricData = (MetricsData)metricTelemetry.Data.BaseData;
Assert.True(metricData.Properties.TryGetValue(StandardMetricConstants.RequestSuccessKey, out var isSuccess));
Assert.Equal("True", isSuccess);
Assert.True(metricData.Properties.TryGetValue(StandardMetricConstants.RequestResultCodeKey, out var resultCode));
Assert.Equal("200", resultCode);
Assert.True(metricData.Properties.TryGetValue(StandardMetricConstants.IsAutoCollectedKey, out var isAutoCollectedFlag));
Assert.Equal("True", isAutoCollectedFlag);
Assert.True(metricData.Properties.TryGetValue(StandardMetricConstants.IsSyntheticKey, out var isSynthetic));
Assert.Equal("False", isSynthetic);
Assert.True(metricData.Properties.TryGetValue(StandardMetricConstants.CloudRoleInstanceKey, out _));
Assert.True(metricData.Properties.TryGetValue(StandardMetricConstants.CloudRoleNameKey, out _));
Assert.True(metricData.Properties.TryGetValue(StandardMetricConstants.MetricIdKey, out var metricId));
Assert.Equal(StandardMetricConstants.RequestDurationMetricIdValue, metricId);
}

private void WaitForActivityExport(ConcurrentBag<TelemetryItem> traceTelemetryItems)
{
var result = SpinWait.SpinUntil(
condition: () =>
{
Thread.Sleep(10);
return traceTelemetryItems.Any();
},
timeout: TimeSpan.FromSeconds(10));

Assert.True(result, $"{nameof(WaitForActivityExport)} failed.");
}
}
}

0 comments on commit 6615941

Please sign in to comment.