Skip to content

Commit 72a0414

Browse files
summary: Support Hybrid Agent mode
feat: The .NET Hybrid Agent provides a future-proof observability solution with integrated support for OpenTelemetry Tracing and Metrics APIs. OpenTelemetry Logs will continue to be supported through the existing Microsoft.Extensions.Logging integration. This release delivers critical OpenTelemetry features including Span Links, Events on Spans, and unified sampling algorithms. It is designed to bridge the gap for .NET teams looking to adopt OpenTelemetry standards while maintaining backwards compatibility with their existing New Relic dashboards.
1 parent 907ae3d commit 72a0414

21 files changed

Lines changed: 850 additions & 62 deletions

src/Agent/NewRelic/Agent/Core/Config/Configuration.cs

Lines changed: 82 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@
1010
namespace NewRelic.Agent.Core.Config
1111
{
1212
using System;
13-
using System.Collections;
14-
using System.Collections.Generic;
15-
using System.ComponentModel;
1613
using System.Diagnostics;
17-
using System.Xml.Schema;
1814
using System.Xml.Serialization;
15+
using System.Collections;
16+
using System.Xml.Schema;
17+
using System.ComponentModel;
18+
using System.Collections.Generic;
1919

2020

2121
[System.CodeDom.Compiler.GeneratedCodeAttribute("Xsd2Code", "3.6.0.20097")]
@@ -6868,16 +6868,20 @@ public partial class configurationOpenTelemetryMetrics
68686868

68696869
private string excludeField;
68706870

6871+
private System.Nullable<int> export_intervalField;
6872+
6873+
private System.Nullable<int> export_timeoutField;
6874+
68716875
/// <summary>
68726876
/// configurationOpenTelemetryMetrics class constructor
68736877
/// </summary>
68746878
public configurationOpenTelemetryMetrics()
68756879
{
6876-
this.enabledField = false;
6880+
this.enabledField = true;
68776881
}
68786882

68796883
[System.Xml.Serialization.XmlAttributeAttribute()]
6880-
[System.ComponentModel.DefaultValueAttribute(false)]
6884+
[System.ComponentModel.DefaultValueAttribute(true)]
68816885
public bool enabled
68826886
{
68836887
get
@@ -6916,6 +6920,78 @@ public string exclude
69166920
}
69176921
}
69186922

6923+
[System.Xml.Serialization.XmlAttributeAttribute()]
6924+
public int export_interval
6925+
{
6926+
get
6927+
{
6928+
if (this.export_intervalField.HasValue)
6929+
{
6930+
return this.export_intervalField.Value;
6931+
}
6932+
else
6933+
{
6934+
return default(int);
6935+
}
6936+
}
6937+
set
6938+
{
6939+
this.export_intervalField = value;
6940+
}
6941+
}
6942+
6943+
[System.Xml.Serialization.XmlIgnoreAttribute()]
6944+
public bool export_intervalSpecified
6945+
{
6946+
get
6947+
{
6948+
return this.export_intervalField.HasValue;
6949+
}
6950+
set
6951+
{
6952+
if (value==false)
6953+
{
6954+
this.export_intervalField = null;
6955+
}
6956+
}
6957+
}
6958+
6959+
[System.Xml.Serialization.XmlAttributeAttribute()]
6960+
public int export_timeout
6961+
{
6962+
get
6963+
{
6964+
if (this.export_timeoutField.HasValue)
6965+
{
6966+
return this.export_timeoutField.Value;
6967+
}
6968+
else
6969+
{
6970+
return default(int);
6971+
}
6972+
}
6973+
set
6974+
{
6975+
this.export_timeoutField = value;
6976+
}
6977+
}
6978+
6979+
[System.Xml.Serialization.XmlIgnoreAttribute()]
6980+
public bool export_timeoutSpecified
6981+
{
6982+
get
6983+
{
6984+
return this.export_timeoutField.HasValue;
6985+
}
6986+
set
6987+
{
6988+
if (value==false)
6989+
{
6990+
this.export_timeoutField = null;
6991+
}
6992+
}
6993+
}
6994+
69196995
#region Clone method
69206996
/// <summary>
69216997
/// Create a clone of this configurationOpenTelemetryMetrics object

src/Agent/NewRelic/Agent/Core/Config/Configuration.xsd

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2302,10 +2302,11 @@
23022302
</xs:annotation>
23032303

23042304
<xs:complexType>
2305-
<xs:attribute name="enabled" type="xs:boolean" default="false">
2305+
<xs:attribute name="enabled" type="xs:boolean" default="true">
23062306
<xs:annotation>
23072307
<xs:documentation>
2308-
Set this to "true" to enable OpenTelemetry metrics collection and export to New Relic.
2308+
This setting controls whether OpenTelemetry metrics collection and export to New Relic is enabled.
2309+
Defaults to "true".
23092310
This feature must be used together with the global openTelemetry enabled setting.
23102311
Environment variable: NEW_RELIC_OPENTELEMETRY_METRICS_ENABLED
23112312
</xs:documentation>
@@ -2333,6 +2334,29 @@
23332334
</xs:documentation>
23342335
</xs:annotation>
23352336
</xs:attribute>
2337+
2338+
<xs:attribute name="export_interval" type="xs:int" use="optional">
2339+
<xs:annotation>
2340+
<xs:documentation>
2341+
The number of milliseconds between each attempt to ship opentelemetry metrics.
2342+
This value must be equal to or greater than the value of export_timeout.
2343+
If export_interval is less than export_timeout, the agent will log a warning and use the default values for both settings.
2344+
Default: 60000 (60 seconds)
2345+
Environment variable: NEW_RELIC_OPENTELEMETRY_METRICS_EXPORT_INTERVAL
2346+
</xs:documentation>
2347+
</xs:annotation>
2348+
</xs:attribute>
2349+
2350+
<xs:attribute name="export_timeout" type="xs:int" use="optional">
2351+
<xs:annotation>
2352+
<xs:documentation>
2353+
The number of milliseconds an export operation is allowed in order to successfully complete.
2354+
This value must be equal to or less than the value of export_interval.
2355+
Default: 10000 (10 seconds)
2356+
Environment variable: NEW_RELIC_OPENTELEMETRY_METRICS_EXPORT_TIMEOUT
2357+
</xs:documentation>
2358+
</xs:annotation>
2359+
</xs:attribute>
23362360
</xs:complexType>
23372361
</xs:element>
23382362
</xs:all>

src/Agent/NewRelic/Agent/Core/Configuration/DefaultConfiguration.cs

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2932,22 +2932,82 @@ public IEnumerable<string> OpenTelemetryMetricsExcludeFilters
29322932
}
29332933
}
29342934

2935-
public int OpenTelemetryOtlpTimeoutSeconds
2935+
2936+
private const int DefaultOtelExportIntervalMs = 60000; // 60,000 ms
2937+
private const int DefaultOtelExportTimeoutMs = 10000; // 10,000 ms
2938+
private int? _otelExportIntervalMs;
2939+
private int? _otelExportTimeoutMs;
2940+
2941+
/// <summary>
2942+
/// Gets the OpenTelemetry metrics export interval in milliseconds, with validation.
2943+
/// </summary>
2944+
public int OpenTelemetryMetricsExportIntervalMs
29362945
{
29372946
get
29382947
{
2939-
var value = EnvironmentOverrides(TryGetAppSettingAsIntWithDefault("OpenTelemetryOtlpTimeoutSeconds", 10), "NEW_RELIC_OPENTELEMETRY_OTLP_TIMEOUT_SECONDS").GetValueOrDefault(10);
2940-
return value > 0 ? value : 10;
2948+
EnsureOtelExportIntervalAndTimeout();
2949+
return _otelExportIntervalMs ?? DefaultOtelExportIntervalMs;
29412950
}
29422951
}
29432952

2944-
public int OpenTelemetryOtlpExportIntervalSeconds
2953+
/// <summary>
2954+
/// Gets the OpenTelemetry metrics export timeout in milliseconds, with validation.
2955+
/// </summary>
2956+
public int OpenTelemetryMetricsExportTimeoutMs
29452957
{
29462958
get
29472959
{
2948-
var value = EnvironmentOverrides(TryGetAppSettingAsIntWithDefault("OpenTelemetryOtlpExportIntervalSeconds", 5), "NEW_RELIC_OPENTELEMETRY_OTLP_EXPORT_INTERVAL_SECONDS").GetValueOrDefault(5);
2949-
return value > 0 ? value : 5;
2960+
EnsureOtelExportIntervalAndTimeout();
2961+
return _otelExportTimeoutMs ?? DefaultOtelExportTimeoutMs;
2962+
}
2963+
}
2964+
2965+
/// <summary>
2966+
/// Ensures export interval and timeout are loaded, validated, and logs a warning if invalid.
2967+
/// Validation rule: interval must be greater than or equal to timeout (interval >= timeout).
2968+
/// If validation fails, both values are reverted to defaults (60,000 ms and 10,000 ms).
2969+
/// </summary>
2970+
private void EnsureOtelExportIntervalAndTimeout()
2971+
{
2972+
if (_otelExportIntervalMs.HasValue && _otelExportTimeoutMs.HasValue)
2973+
return;
2974+
2975+
// Read from XML config first (only if explicitly specified), then appSettings, then apply environment variable overrides, fallback to defaults
2976+
// Priority: Environment Variable > XML Attribute (if specified) > AppSettings > Default
2977+
var xmlIntervalMs = _localConfiguration.openTelemetry?.metrics?.export_intervalSpecified == true
2978+
? _localConfiguration.openTelemetry.metrics.export_interval
2979+
: (int?)null;
2980+
var xmlTimeoutMs = _localConfiguration.openTelemetry?.metrics?.export_timeoutSpecified == true
2981+
? _localConfiguration.openTelemetry.metrics.export_timeout
2982+
: (int?)null;
2983+
2984+
int intervalMs = EnvironmentOverrides(
2985+
xmlIntervalMs ?? TryGetAppSettingAsIntWithDefault("OpenTelemetryMetricsExportInterval", DefaultOtelExportIntervalMs),
2986+
"NEW_RELIC_OPENTELEMETRY_METRICS_EXPORT_INTERVAL"
2987+
).GetValueOrDefault(DefaultOtelExportIntervalMs);
2988+
2989+
int timeoutMs = EnvironmentOverrides(
2990+
xmlTimeoutMs ?? TryGetAppSettingAsIntWithDefault("OpenTelemetryMetricsExportTimeout", DefaultOtelExportTimeoutMs),
2991+
"NEW_RELIC_OPENTELEMETRY_METRICS_EXPORT_TIMEOUT"
2992+
).GetValueOrDefault(DefaultOtelExportTimeoutMs);
2993+
2994+
// Validation: both values must be positive
2995+
if (intervalMs <= 0 || timeoutMs <= 0)
2996+
{
2997+
Log.Warn($"Invalid OpenTelemetry metrics export values: interval={intervalMs} ms, timeout={timeoutMs} ms. Both values must be positive. Reverting to defaults: interval={DefaultOtelExportIntervalMs} ms, timeout={DefaultOtelExportTimeoutMs} ms.");
2998+
intervalMs = DefaultOtelExportIntervalMs;
2999+
timeoutMs = DefaultOtelExportTimeoutMs;
29503000
}
3001+
// Validation: interval must be >= timeout
3002+
else if (intervalMs < timeoutMs)
3003+
{
3004+
Log.Warn($"OpenTelemetry metrics export interval ({intervalMs} ms) is less than export timeout ({timeoutMs} ms). Reverting to defaults: interval={DefaultOtelExportIntervalMs} ms, timeout={DefaultOtelExportTimeoutMs} ms.");
3005+
intervalMs = DefaultOtelExportIntervalMs;
3006+
timeoutMs = DefaultOtelExportTimeoutMs;
3007+
}
3008+
3009+
_otelExportIntervalMs = intervalMs;
3010+
_otelExportTimeoutMs = timeoutMs;
29513011
}
29523012

29533013
/// <summary>

src/Agent/NewRelic/Agent/Core/Configuration/ReportedConfiguration.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -797,11 +797,13 @@ public IReadOnlyDictionary<string, string> GetAppSettings()
797797
[JsonProperty("opentelemetry.metrics.exclude")]
798798
public IEnumerable<string> OpenTelemetryMetricsExcludeFilters => _configuration.OpenTelemetryMetricsExcludeFilters;
799799

800-
[JsonProperty("opentelemetry.otlp.timeout_seconds")]
801-
public int OpenTelemetryOtlpTimeoutSeconds => _configuration.OpenTelemetryOtlpTimeoutSeconds;
802800

803-
[JsonProperty("opentelemetry.otlp.export_interval_seconds")]
804-
public int OpenTelemetryOtlpExportIntervalSeconds => _configuration.OpenTelemetryOtlpExportIntervalSeconds;
801+
[JsonProperty("opentelemetry.metrics.export_interval_ms")]
802+
public int OpenTelemetryMetricsExportIntervalMs => _configuration.OpenTelemetryMetricsExportIntervalMs;
803+
804+
[JsonProperty("opentelemetry.metrics.export_timeout_ms")]
805+
public int OpenTelemetryMetricsExportTimeoutMs => _configuration.OpenTelemetryMetricsExportTimeoutMs;
806+
805807

806808
[JsonProperty("hybrid_http_context_storage.enabled")]
807809
public bool HybridHttpContextStorageEnabled => _configuration.HybridHttpContextStorageEnabled;

src/Agent/NewRelic/Agent/Core/OpenTelemetryBridge/Metrics/DynamicMeterListenerWrapper.cs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public class DynamicMeterListenerWrapper : IMeterListenerWrapper
2222
private volatile bool _disposed;
2323
private object _meterListener;
2424
private Type _meterListenerType;
25+
private Type _ilrepackedMeterListenerType;
2526
private bool _isAvailable;
2627

2728
private MethodInfo _startMethod;
@@ -74,6 +75,13 @@ private void TryInitialize()
7475

7576
_meterListenerType = assembly.GetType("System.Diagnostics.Metrics.MeterListener", throwOnError: true);
7677

78+
// Check if NewRelic assembly has ILRepacked MeterListener type to avoid processing our own bridged meters
79+
if (!TryFindILRepackedMeterListenerType())
80+
{
81+
_isAvailable = false;
82+
return;
83+
}
84+
7785
_startMethod = _meterListenerType.GetMethod("Start") ?? throw new MissingMethodException(_meterListenerType.Name, "Start");
7886
_enableMeasurementEventsMethod = _meterListenerType.GetMethod("EnableMeasurementEvents") ?? throw new MissingMethodException(_meterListenerType.Name, "EnableMeasurementEvents");
7987
_recordObservableInstrumentsMethod = _meterListenerType.GetMethod("RecordObservableInstruments") ?? throw new MissingMethodException(_meterListenerType.Name, "RecordObservableInstruments");
@@ -97,6 +105,72 @@ private void TryInitialize()
97105
}
98106
}
99107

108+
private bool TryFindILRepackedMeterListenerType()
109+
{
110+
try
111+
{
112+
// Find NewRelic agent assemblies (agent core assembly contains ILRepacked types)
113+
// check assemblies starting with "NewRelic.Agent"
114+
var allAssemblies = _assemblyProvider.GetAssemblies().ToList();
115+
Log.Debug($"Searching {allAssemblies.Count} assemblies for ILRepacked MeterListener type");
116+
117+
var newRelicAssemblies = allAssemblies
118+
.Where(a => a.GetName().Name != null &&
119+
a.GetName().Name.StartsWith("NewRelic.Agent", StringComparison.OrdinalIgnoreCase))
120+
.ToList();
121+
122+
Log.Debug($"Found {newRelicAssemblies.Count} NewRelic.Agent assemblies: {string.Join(", ", newRelicAssemblies.Select(a => a.GetName().Name))}");
123+
124+
foreach (var nrAssembly in newRelicAssemblies)
125+
{
126+
// Look for ILRepacked MeterListener type in NewRelic assemblies
127+
var ilrepackedType = nrAssembly.GetType("System.Diagnostics.Metrics.MeterListener", throwOnError: false);
128+
if (ilrepackedType != null)
129+
{
130+
_ilrepackedMeterListenerType = ilrepackedType;
131+
Log.Debug($"Found ILRepacked MeterListener type in assembly: {nrAssembly.GetName().Name}");
132+
return true;
133+
}
134+
}
135+
136+
Log.Debug("No ILRepacked MeterListener type found in any NewRelic.Agent assembly");
137+
}
138+
catch (Exception ex)
139+
{
140+
Log.Debug(ex, "Failed to find ILRepacked MeterListener type. This is expected if not using ILRepack.");
141+
}
142+
return false;
143+
}
144+
145+
public bool IsInstrumentFromILRepackedAssembly(object instrument)
146+
{
147+
if (_ilrepackedMeterListenerType == null)
148+
{
149+
Log.Debug("ILRepacked MeterListener type is null - cannot filter instruments");
150+
return false;
151+
}
152+
153+
if (instrument == null) return false;
154+
155+
try
156+
{
157+
var instrumentAssembly = instrument.GetType().Assembly;
158+
var ilrepackedAssembly = _ilrepackedMeterListenerType.Assembly;
159+
160+
var isFromILRepacked = instrumentAssembly == ilrepackedAssembly;
161+
162+
Log.Debug($"Instrument assembly check: {instrument.GetType().Name} from {instrumentAssembly.GetName().Name}, ILRepacked assembly: {ilrepackedAssembly.GetName().Name}, Match: {isFromILRepacked}");
163+
164+
// Check if instrument is from the same assembly as our ILRepacked MeterListener
165+
return isFromILRepacked;
166+
}
167+
catch (Exception ex)
168+
{
169+
Log.Debug(ex, "Failed to check if instrument is from ILRepacked assembly");
170+
return false;
171+
}
172+
}
173+
100174
public void Start()
101175
{
102176
if (CheckDisposed() || !EnsureInitialized() || _startMethod == null) return;

src/Agent/NewRelic/Agent/Core/OpenTelemetryBridge/Metrics/Interfaces/IMeterListenerWrapper.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,9 @@ public interface IMeterListenerWrapper : IDisposable
4343
/// Sets the measurement callback for the specified numeric type.
4444
/// </summary>
4545
void SetMeasurementCallback<T>(MeasurementCallbackDelegate<T> callback) where T : struct;
46+
47+
/// <summary>
48+
/// Checks if an instrument originates from an ILRepacked NewRelic assembly.
49+
/// </summary>
50+
bool IsInstrumentFromILRepackedAssembly(object instrument);
4651
}

0 commit comments

Comments
 (0)