Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 10 additions & 8 deletions src/NATS.Client.Core/Internal/Telemetry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,18 +139,19 @@ public static TagList BuildMetricTags(INatsConnection? connection, string operat
if (replyTo is not null)
len++;

var serverPort = conn.ServerInfo.Port.ToString();
var serverPort = conn.BoxedServerPort; // boxed once per ServerInfo change, reused for server.port and network.peer.port
var serverHost = conn.ServerHost; // grabbed once per ServerInfo change
tags = new KeyValuePair<string, object?>[len];
tags[0] = new KeyValuePair<string, object?>(Constants.SystemKey, Constants.SystemVal);
tags[1] = new KeyValuePair<string, object?>(Constants.OpKey, Constants.OpPub);
tags[2] = new KeyValuePair<string, object?>(Constants.DestName, subject);

tags[3] = new KeyValuePair<string, object?>(Constants.ClientId, conn.ServerInfo.ClientId.ToString());
tags[4] = new KeyValuePair<string, object?>(Constants.ServerAddress, conn.ServerInfo.Host);
tags[3] = new KeyValuePair<string, object?>(Constants.ClientId, conn.ClientId);
tags[4] = new KeyValuePair<string, object?>(Constants.ServerAddress, serverHost);
tags[5] = new KeyValuePair<string, object?>(Constants.ServerPort, serverPort);
tags[6] = new KeyValuePair<string, object?>(Constants.NetworkProtoName, "nats");
tags[7] = new KeyValuePair<string, object?>(Constants.NetworkTransport, "tcp");
tags[8] = new KeyValuePair<string, object?>(Constants.NetworkPeerAddress, conn.ServerInfo.Host);
tags[8] = new KeyValuePair<string, object?>(Constants.NetworkPeerAddress, serverHost);
tags[9] = new KeyValuePair<string, object?>(Constants.NetworkPeerPort, serverPort);
tags[10] = new KeyValuePair<string, object?>(Constants.NetworkLocalAddress, conn.ServerInfo.ClientIp);

Expand Down Expand Up @@ -238,7 +239,8 @@ public static void AddTraceContextHeaders(Activity? activity, ref NatsHeaders? h
KeyValuePair<string, object?>[] tags;
if (connection is NatsConnection { ServerInfo: not null } conn)
{
var serverPort = conn.ServerInfo.Port.ToString();
var serverPort = conn.BoxedServerPort; // boxed once per ServerInfo change, reused for server.port and network.peer.port
var serverHost = conn.ServerHost; // grabbed once per ServerInfo change

var len = 17;
if (replyTo is not null)
Expand All @@ -256,12 +258,12 @@ public static void AddTraceContextHeaders(Activity? activity, ref NatsHeaders? h
tags[6] = new KeyValuePair<string, object?>(Constants.DestPubName, subject);
tags[7] = new KeyValuePair<string, object?>(Constants.MsgBodySize, bodySize.ToString());
tags[8] = new KeyValuePair<string, object?>(Constants.MsgTotalSize, size.ToString());
tags[9] = new KeyValuePair<string, object?>(Constants.ClientId, conn.ServerInfo.ClientId.ToString());
tags[10] = new KeyValuePair<string, object?>(Constants.ServerAddress, conn.ServerInfo.Host);
tags[9] = new KeyValuePair<string, object?>(Constants.ClientId, conn.ClientId);
tags[10] = new KeyValuePair<string, object?>(Constants.ServerAddress, serverHost);
tags[11] = new KeyValuePair<string, object?>(Constants.ServerPort, serverPort);
tags[12] = new KeyValuePair<string, object?>(Constants.NetworkProtoName, "nats");
tags[13] = new KeyValuePair<string, object?>(Constants.NetworkTransport, "tcp");
tags[14] = new KeyValuePair<string, object?>(Constants.NetworkPeerAddress, conn.ServerInfo.Host);
tags[14] = new KeyValuePair<string, object?>(Constants.NetworkPeerAddress, serverHost);
tags[15] = new KeyValuePair<string, object?>(Constants.NetworkPeerPort, serverPort);
tags[16] = new KeyValuePair<string, object?>(Constants.NetworkLocalAddress, conn.ServerInfo.ClientIp);

Expand Down
33 changes: 26 additions & 7 deletions src/NATS.Client.Core/NatsConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ public partial class NatsConnection : INatsConnection

private ServerInfo? _writableServerInfo;
private KeyValuePair<string, object?>[]? _metricTagsPrefix;
private object? _boxedServerPort;
private string? _serverHost;
private string? _clientId;
private int _pongCount;
private int _connectionState;
private int _isDisposed;
Expand Down Expand Up @@ -164,34 +167,50 @@ internal ServerInfo? WritableServerInfo
}

KeyValuePair<string, object?>[]? prefix = null;
object? port = null;
string? host = null;
string? clientId = null;
if (value is not null)
{
// ServerInfo.Host/Port is the server's bind address (often 0.0.0.0); use the
// address the client actually dialled for OTel server.address/server.port.
var connectUri = _currentConnectUri;
var host = connectUri?.Host ?? value.Host;
var port = connectUri?.Port ?? value.Port;
host = connectUri?.Host ?? value.Host;
port = connectUri?.Port ?? value.Port;
clientId = value.ClientId.ToString();
prefix = new[]
{
new KeyValuePair<string, object?>(Telemetry.Constants.SystemKey, Telemetry.Constants.SystemVal),
new KeyValuePair<string, object?>(Telemetry.Constants.ServerAddress, host),
new KeyValuePair<string, object?>(Telemetry.Constants.ServerPort, (object)port),
new KeyValuePair<string, object?>(Telemetry.Constants.ServerPort, port),
new KeyValuePair<string, object?>(Telemetry.Constants.NetworkProtoName, "nats"),
new KeyValuePair<string, object?>(Telemetry.Constants.NetworkTransport, "tcp"),
};
}

// Publish prefix before ServerInfo so any reader observing the new ServerInfo
// is guaranteed to also observe the matching prefix. Today no single reader
// reads both, but #1156 will unify the trace and metric tag sources and start
// relying on this ordering.
// Cache the trace-tag host, boxed port and client id once per ServerInfo change so the
// per-message trace path can reuse them instead of re-boxing the port and re-allocating
// the client id string on every message. Host and port come from the connect URI, so the
// trace and metric tags now share the same source.
Volatile.Write(ref _boxedServerPort, port);
Volatile.Write(ref _serverHost, host);
Volatile.Write(ref _clientId, clientId);

// Publish the cached fields before ServerInfo so any reader observing the new ServerInfo
// is guaranteed to also observe the matching tags.
Volatile.Write(ref _metricTagsPrefix, prefix);
Interlocked.Exchange(ref _writableServerInfo, value);
}
}

internal KeyValuePair<string, object?>[]? MetricTagsPrefix => Volatile.Read(ref _metricTagsPrefix);

internal object? BoxedServerPort => Volatile.Read(ref _boxedServerPort);

internal string? ServerHost => Volatile.Read(ref _serverHost);

internal string? ClientId => Volatile.Read(ref _clientId);

internal bool IsDisposed
{
get => Interlocked.CompareExchange(ref _isDisposed, 0, 0) == 1;
Expand Down
30 changes: 28 additions & 2 deletions tests/NATS.Net.OpenTelemetry.Tests/OpenTelemetryTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ public async Task Publish_subscribe_activities()

await sub.Msgs.ReadAsync(cts.Token);

AssertActivityData("foo", tracker.Started);
var expectedHost = new Uri(server.Url).Host;
var expectedClientId = nats.ServerInfo!.ClientId.ToString();
AssertActivityData("foo", tracker.Started, expectedHost, expectedClientId);
tracker.AssertAllStopped();
}

Expand Down Expand Up @@ -424,7 +426,7 @@ public async Task Direct_request_reply_receive_activity_is_disposed()
await reg;
}

private void AssertActivityData(string subject, IReadOnlyList<Activity> activityList)
private void AssertActivityData(string subject, IReadOnlyList<Activity> activityList, string expectedHost, string expectedClientId)
{
var activities = activityList.ToArray();
Assert.NotEmpty(activities);
Expand Down Expand Up @@ -457,6 +459,23 @@ private void AssertActivityData(string subject, IReadOnlyList<Activity> activity
// Verify network.protocol.version is no longer present
Assert.Null(sendActivity.GetTagItem("network.protocol.version"));
Assert.Null(receiveActivity.GetTagItem("network.protocol.version"));

// server.port and network.peer.port are integers per OTel semconv, not strings
AssertIntTag(sendActivity, "server.port");
AssertIntTag(sendActivity, "network.peer.port");
AssertIntTag(receiveActivity, "server.port");
AssertIntTag(receiveActivity, "network.peer.port");

// server.address/network.peer.address come from the connect URI, not ServerInfo.Host
// (the server bind address, often 0.0.0.0)
Assert.Equal(expectedHost, sendActivity.GetTagItem("server.address"));
Assert.Equal(expectedHost, sendActivity.GetTagItem("network.peer.address"));
Assert.Equal(expectedHost, receiveActivity.GetTagItem("server.address"));
Assert.Equal(expectedHost, receiveActivity.GetTagItem("network.peer.address"));

// messaging.client_id is the server-assigned client id, cached per ServerInfo change
Assert.Equal(expectedClientId, sendActivity.GetTagItem("messaging.client_id"));
Assert.Equal(expectedClientId, receiveActivity.GetTagItem("messaging.client_id"));
}

private void AssertStringTagNotNullOrEmpty(Activity activity, string name)
Expand All @@ -466,6 +485,13 @@ private void AssertStringTagNotNullOrEmpty(Activity activity, string name)
Assert.False(string.IsNullOrEmpty(tag));
}

private void AssertIntTag(Activity activity, string name)
{
var tag = activity.GetTagItem(name);
Assert.NotNull(tag);
Assert.IsType<int>(tag);
}

private sealed class ActivityTracker : IDisposable
{
private readonly List<Activity> _started = new();
Expand Down
Loading