Skip to content

Commit fb1a4be

Browse files
[HA Metrics] Capture and bridge Tags and scope defined on meters incl… (#3330)
1 parent 49342d5 commit fb1a4be

File tree

2 files changed

+366
-10
lines changed

2 files changed

+366
-10
lines changed

src/Agent/NewRelic/Agent/Core/OpenTelemetryBridge/MeterListenerBridge.cs

Lines changed: 98 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -654,15 +654,42 @@ private static object GetStateForInstrument(object instrument)
654654
return observableResult;
655655
}
656656

657-
var createInstrumentDelegate = (Func<Meter, string, string, string, object>)_createInstrumentDelegates.GetOrAdd(instrumentType, CreateBridgedInstrumentDelegate);
657+
var createInstrumentDelegate = (Func<Meter, string, string, string, IEnumerable<KeyValuePair<string, object>>, object>)_createInstrumentDelegates.GetOrAdd(instrumentType, CreateBridgedInstrumentDelegate);
658658

659659
if (createInstrumentDelegate == null)
660660
{
661661
return null;
662662
}
663663

664-
// TODO: Tags are not avaiilable on older instruments, need a way to dynamically get them for newer versions
665-
var result = createInstrumentDelegate.Invoke(meter, dynamicInstrument.Name, dynamicInstrument.Unit, dynamicInstrument.Description);
664+
// Try to get Tags from the instrument if available (DiagnosticSource 8.0+)
665+
IEnumerable<KeyValuePair<string, object>> instrumentTags = null;
666+
var tagsProperty = instrumentType.GetProperty("Tags");
667+
if (tagsProperty != null)
668+
{
669+
try
670+
{
671+
var tagsValue = tagsProperty.GetValue(instrument);
672+
if (tagsValue != null)
673+
{
674+
var tagsList = new List<KeyValuePair<string, object>>();
675+
foreach (var tag in (dynamic)tagsValue)
676+
{
677+
tagsList.Add(new KeyValuePair<string, object>(tag.Key, tag.Value ?? string.Empty));
678+
}
679+
if (tagsList.Count > 0)
680+
{
681+
instrumentTags = tagsList;
682+
Log.Finest($"Bridged {tagsList.Count} tags from instrument '{dynamicInstrument.Name}'");
683+
}
684+
}
685+
}
686+
catch (Exception ex)
687+
{
688+
Log.Debug(ex, $"Failed to bridge Tags from instrument '{dynamicInstrument.Name}'. Continuing without tags.");
689+
}
690+
}
691+
692+
var result = createInstrumentDelegate.Invoke(meter, dynamicInstrument.Name, dynamicInstrument.Unit, dynamicInstrument.Description, instrumentTags);
666693
return result;
667694

668695
Meter CreateBridgedMeterFromInstrument(string _)
@@ -856,7 +883,7 @@ private static Func<object, Measurement<T>> GetMethodToBridgeMeasurement<T>(Type
856883
return lambda.Compile();
857884
}
858885

859-
private static Func<Meter, string, string, string, object> CreateBridgedInstrumentDelegate(Type originalInstrumentType)
886+
private static Func<Meter, string, string, string, IEnumerable<KeyValuePair<string, object>>, object> CreateBridgedInstrumentDelegate(Type originalInstrumentType)
860887
{
861888
var genericType = originalInstrumentType.GetGenericArguments().FirstOrDefault();
862889
if (genericType == null)
@@ -878,7 +905,6 @@ private static Func<Meter, string, string, string, object> CreateBridgedInstrume
878905
return null;
879906
}
880907

881-
// TODO: If we upgrade .NET or see test failures related to this reflection, revisit this logic.
882908
// Reflection logic for Meter.Create* methods:
883909
// 1. Try to match the latest public OTel signature (4 parameters: name, unit, description, tags)
884910
// 2. Fallback to 3-parameter version (name, unit, description) for backward compatibility
@@ -899,8 +925,8 @@ private static Func<Meter, string, string, string, object> CreateBridgedInstrume
899925
if (methodInfo != null)
900926
{
901927
methodInfo = methodInfo.MakeGenericMethod(genericType);
902-
// Wrap in a delegate that ignores the 4th argument (tags) for now (pass null)
903-
return (meter, name, unit, description) => methodInfo.Invoke(meter, new object[] { name, unit, description, null });
928+
// Use the 4-parameter overload and pass through the tags
929+
return (meter, name, unit, description, tags) => methodInfo.Invoke(meter, new object[] { name, unit, description, tags });
904930
}
905931

906932
// Fallback: Try 3-parameter overload (name, unit, description)
@@ -918,7 +944,8 @@ private static Func<Meter, string, string, string, object> CreateBridgedInstrume
918944
}
919945

920946
methodInfo = methodInfo.MakeGenericMethod(genericType);
921-
return (meter, name, unit, description) => methodInfo.Invoke(meter, new object[] { name, unit, description });
947+
// Fallback to 3-parameter version, ignoring tags
948+
return (meter, name, unit, description, tags) => methodInfo.Invoke(meter, new object[] { name, unit, description });
922949
}
923950
catch (Exception ex)
924951
{
@@ -978,10 +1005,71 @@ private static Instrument CreateBridgedObservableUpDownCounter<T>(Meter meter, s
9781005

9791006
private static Meter CreateBridgedMeter(object meter)
9801007
{
1008+
//https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.metrics.meter?view=net-10.0
9811009
dynamic dynamicMeter = meter;
9821010

983-
// TODO: Tags and Scope are not available on older meters, need a way to dynamically get them for newer versions
984-
return new Meter(dynamicMeter.Name, dynamicMeter.Version);
1011+
var meterType = meter.GetType();
1012+
var name = (string)dynamicMeter.Name;
1013+
var version = (string)dynamicMeter.Version;
1014+
1015+
// Try to get Tags property if it exists (available in DiagnosticSource 8.0+)
1016+
IEnumerable<KeyValuePair<string, object>> tags = null;
1017+
var tagsProperty = meterType.GetProperty("Tags");
1018+
if (tagsProperty != null)
1019+
{
1020+
try
1021+
{
1022+
var tagsValue = tagsProperty.GetValue(meter);
1023+
if (tagsValue != null)
1024+
{
1025+
// Tags is IEnumerable<KeyValuePair<string, object?>>?, need to convert to non-nullable
1026+
var tagsList = new List<KeyValuePair<string, object>>();
1027+
foreach (var tag in (dynamic)tagsValue)
1028+
{
1029+
tagsList.Add(new KeyValuePair<string, object>(tag.Key, tag.Value ?? string.Empty));
1030+
}
1031+
if (tagsList.Count > 0)
1032+
{
1033+
tags = tagsList;
1034+
Log.Finest($"Bridged {tagsList.Count} tags from meter '{name}'");
1035+
}
1036+
}
1037+
}
1038+
catch (Exception ex)
1039+
{
1040+
Log.Debug(ex, $"Failed to bridge Tags from meter '{name}'. Continuing without tags.");
1041+
}
1042+
}
1043+
1044+
// Try to get Scope property if it exists (available in DiagnosticSource 8.0+)
1045+
object scope = null;
1046+
var scopeProperty = meterType.GetProperty("Scope");
1047+
if (scopeProperty != null)
1048+
{
1049+
try
1050+
{
1051+
scope = scopeProperty.GetValue(meter);
1052+
if (scope != null)
1053+
{
1054+
Log.Finest($"Bridged scope from meter '{name}': {scope}");
1055+
}
1056+
}
1057+
catch (Exception ex)
1058+
{
1059+
Log.Debug(ex, $"Failed to bridge Scope from meter '{name}'. Continuing without scope.");
1060+
}
1061+
}
1062+
1063+
// Create bridged meter with available properties
1064+
// Use the 4-parameter constructor if tags or scope are available, otherwise fall back to 2-parameter constructor
1065+
if (tags != null || scope != null)
1066+
{
1067+
return new Meter(name, version, tags, scope);
1068+
}
1069+
else
1070+
{
1071+
return new Meter(name, version);
1072+
}
9851073
}
9861074

9871075
private void OnMeasurementRecorded<T>(object instrument, T measurement, ReadOnlySpan<KeyValuePair<string, object>> tags, object state)

0 commit comments

Comments
 (0)