Skip to content

Commit 509ef1e

Browse files
authored
Add ElasticCompatibilityProcessor (#81)
* Add initial compatibility processor * Misc formatting * Add tests * Update E2E test readme
1 parent d9e1437 commit 509ef1e

File tree

11 files changed

+301
-19
lines changed

11 files changed

+301
-19
lines changed

examples/Example.MinimalApi/appsettings.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
"Logging": {
33
"LogLevel": {
44
"Default": "Information",
5-
"Microsoft.AspNetCore": "Warning"
5+
"Microsoft.AspNetCore": "Warning",
6+
"Elastic.OpenTelemetry": "Information"
67
}
78
},
89
"AllowedHosts": "*",
9-
"ServiceName": "elastic-minimal-api-example",
1010
"AspNetCoreInstrumentation": {
1111
"RecordException": "true"
1212
}

src/Elastic.OpenTelemetry/Diagnostics/LoggerMessages.cs

+15-3
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,27 @@
44

55
using System.Diagnostics;
66
using Elastic.OpenTelemetry.Diagnostics.Logging;
7-
using Elastic.OpenTelemetry.Processors;
87
using Microsoft.Extensions.Logging;
98

109
namespace Elastic.OpenTelemetry.Diagnostics;
1110

1211
internal static partial class LoggerMessages
1312
{
14-
[LoggerMessage(EventId = 100, Level = LogLevel.Trace, Message = $"{nameof(TransactionIdProcessor)} added 'transaction.id' tag to Activity.")]
15-
internal static partial void TransactionIdProcessorTagAdded(this ILogger logger);
13+
#pragma warning disable SYSLIB1006 // Multiple logging methods cannot use the same event id within a class
14+
// We explictly reuse the same event ID and this is the same log message, but with different types for the structured data
15+
16+
[LoggerMessage(EventId = 100, Level = LogLevel.Trace, Message = "{ProcessorName} found `{AttributeName}` attribute with value '{AttributeValue}' on the span.")]
17+
internal static partial void FoundTag(this ILogger logger, string processorName, string attributeName, string attributeValue);
18+
19+
[LoggerMessage(EventId = 100, Level = LogLevel.Trace, Message = "{ProcessorName} found `{AttributeName}` attribute with value '{AttributeValue}' on the span.")]
20+
internal static partial void FoundTag(this ILogger logger, string processorName, string attributeName, int attributeValue);
21+
22+
[LoggerMessage(EventId = 101, Level = LogLevel.Trace, Message = "{ProcessorName} set `{AttributeName}` attribute with value '{AttributeValue}' on the span.")]
23+
internal static partial void SetTag(this ILogger logger, string processorName, string attributeName, string attributeValue);
24+
25+
[LoggerMessage(EventId = 101, Level = LogLevel.Trace, Message = "{ProcessorName} set `{AttributeName}` attribute with value '{AttributeValue}' on the span.")]
26+
internal static partial void SetTag(this ILogger logger, string processorName, string attributeName, int attributeValue);
27+
#pragma warning restore SYSLIB1006 // Multiple logging methods cannot use the same event id within a class
1628

1729
[LoggerMessage(EventId = 20, Level = LogLevel.Trace, Message = "Added '{ProcessorTypeName}' processor to '{BuilderTypeName}'.")]
1830
public static partial void LogProcessorAdded(this ILogger logger, string processorTypeName, string builderTypeName);

src/Elastic.OpenTelemetry/Diagnostics/Logging/CompositeLogger.cs

+7-6
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ internal sealed class CompositeLogger(ILogger? additionalLogger) : IDisposable,
1717
{
1818
public FileLogger FileLogger { get; } = new();
1919

20+
private ILogger? _additionalLogger = additionalLogger;
2021
private bool _isDisposed;
2122

2223
public void Dispose()
@@ -39,22 +40,22 @@ public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Except
3940
if (FileLogger.IsEnabled(logLevel))
4041
FileLogger.Log(logLevel, eventId, state, exception, formatter);
4142

42-
if (additionalLogger == null)
43+
if (_additionalLogger == null)
4344
return;
4445

45-
if (additionalLogger.IsEnabled(logLevel))
46-
additionalLogger.Log(logLevel, eventId, state, exception, formatter);
46+
if (_additionalLogger.IsEnabled(logLevel))
47+
_additionalLogger.Log(logLevel, eventId, state, exception, formatter);
4748
}
4849

4950
public bool LogFileEnabled => FileLogger.FileLoggingEnabled;
5051
public string LogFilePath => FileLogger.LogFilePath ?? string.Empty;
5152

52-
public void SetAdditionalLogger(ILogger? logger) => additionalLogger ??= logger;
53+
public void SetAdditionalLogger(ILogger? logger) => _additionalLogger ??= logger;
5354

54-
public bool IsEnabled(LogLevel logLevel) => FileLogger.IsEnabled(logLevel) || (additionalLogger?.IsEnabled(logLevel) ?? false);
55+
public bool IsEnabled(LogLevel logLevel) => FileLogger.IsEnabled(logLevel) || (_additionalLogger?.IsEnabled(logLevel) ?? false);
5556

5657
public IDisposable BeginScope<TState>(TState state) where TState : notnull =>
57-
new CompositeDisposable(FileLogger.BeginScope(state), additionalLogger?.BeginScope(state));
58+
new CompositeDisposable(FileLogger.BeginScope(state), _additionalLogger?.BeginScope(state));
5859

5960
private class CompositeDisposable(params IDisposable?[] disposables) : IDisposable
6061
{

src/Elastic.OpenTelemetry/Diagnostics/Logging/LogState.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ namespace Elastic.OpenTelemetry.Diagnostics.Logging;
1010
internal class LogState : IReadOnlyList<KeyValuePair<string, object?>>
1111
{
1212
private readonly Activity? _activity;
13+
1314
public Activity? Activity
1415
{
1516
get => _activity;
@@ -44,7 +45,7 @@ public string? SpanId
4445
init => _spanId = value;
4546
}
4647

47-
private readonly List<KeyValuePair<string, object?>> _values = new();
48+
private readonly List<KeyValuePair<string, object?>> _values = [];
4849

4950
public IEnumerator<KeyValuePair<string, object?>> GetEnumerator() => _values.GetEnumerator();
5051

src/Elastic.OpenTelemetry/ElasticOpenTelemetryBuilder.cs

+1
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ public ElasticOpenTelemetryBuilder(ElasticOpenTelemetryOptions options)
8686
logging.IncludeScopes = true;
8787
//TODO add processor that adds service.id
8888
});
89+
8990
openTelemetry
9091
.WithLogging(logging =>
9192
{

src/Elastic.OpenTelemetry/Extensions/TracerProviderBuilderExtensions.cs

+8-2
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,14 @@ public static class TracerProviderBuilderExtensions
2020
/// <summary>
2121
/// Include Elastic trace processors to ensure data is enriched and extended.
2222
/// </summary>
23-
public static TracerProviderBuilder AddElasticProcessors(this TracerProviderBuilder builder, ILogger? logger = null) =>
24-
builder.LogAndAddProcessor(new TransactionIdProcessor(logger ?? NullLogger.Instance), logger ?? NullLogger.Instance);
23+
public static TracerProviderBuilder AddElasticProcessors(this TracerProviderBuilder builder, ILogger? logger = null)
24+
{
25+
logger ??= NullLogger.Instance;
26+
27+
return builder
28+
.LogAndAddProcessor(new TransactionIdProcessor(logger), logger)
29+
.LogAndAddProcessor(new ElasticCompatibilityProcessor(logger), logger);
30+
}
2531

2632
private static TracerProviderBuilder LogAndAddProcessor(this TracerProviderBuilder builder, BaseProcessor<Activity> processor, ILogger logger)
2733
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
5+
using System.Diagnostics;
6+
using Elastic.OpenTelemetry.Diagnostics;
7+
using Microsoft.Extensions.Logging;
8+
using OpenTelemetry;
9+
using static Elastic.OpenTelemetry.SemanticConventions.TraceSemanticConventions;
10+
11+
namespace Elastic.OpenTelemetry.Processors;
12+
13+
/// <summary>
14+
/// This processor ensures that the data is compatible with Elastic backends.
15+
/// <para>
16+
/// It checks for the presence of the older semantic conventions and if they are not present, it will
17+
/// add them. This is only necessary for compatibility with older versions of the intake OTel endpoints
18+
/// on Elastic APM. These issues will be fixed centrally in future versions of the intake code.
19+
/// </para>
20+
/// </summary>
21+
/// <param name="logger"></param>
22+
public class ElasticCompatibilityProcessor(ILogger logger) : BaseProcessor<Activity>
23+
{
24+
private readonly ILogger _logger = logger;
25+
26+
/// <inheritdoc />
27+
public override void OnEnd(Activity activity)
28+
{
29+
if (activity.Kind == ActivityKind.Server)
30+
{
31+
// For inbound HTTP requests (server), ASP.NET Core sets the newer semantic conventions in
32+
// the latest versions. For now, we need to ensure the older semantic conventions are also
33+
// included on the spans sent to the Elastic backend as the intake system is currently
34+
// unaware of the newer semantic conventions. We send the older attributes to ensure that
35+
// the UI functions as expected. The http and net host conventions are required to build
36+
// up the URL displayed in the trace sample UI within Kibana. This will be fixed in future
37+
// version of apm-data.
38+
39+
string? httpScheme = null;
40+
string? httpTarget = null;
41+
string? urlScheme = null;
42+
string? urlPath = null;
43+
string? urlQuery = null;
44+
string? netHostName = null;
45+
int? netHostPort = null;
46+
string? serverAddress = null;
47+
int? serverPort = null;
48+
49+
// We loop once, collecting all the attributes we need for the older and newer
50+
// semantic conventions. This is a bit more verbose but ensures we don't iterate
51+
// the tags multiple times.
52+
foreach (var tag in activity.TagObjects)
53+
{
54+
if (tag.Key == HttpScheme)
55+
httpScheme = ProcessStringAttribute(tag);
56+
57+
if (tag.Key == HttpTarget)
58+
httpTarget = ProcessStringAttribute(tag);
59+
60+
if (tag.Key == UrlScheme)
61+
urlScheme = ProcessStringAttribute(tag);
62+
63+
if (tag.Key == UrlPath)
64+
urlPath = ProcessStringAttribute(tag);
65+
66+
if (tag.Key == UrlQuery)
67+
urlQuery = ProcessStringAttribute(tag);
68+
69+
if (tag.Key == NetHostName)
70+
netHostName = ProcessStringAttribute(tag);
71+
72+
if (tag.Key == ServerAddress)
73+
serverAddress = ProcessStringAttribute(tag);
74+
75+
if (tag.Key == NetHostPort)
76+
netHostPort = ProcessIntAttribute(tag);
77+
78+
if (tag.Key == ServerPort)
79+
serverPort = ProcessIntAttribute(tag);
80+
}
81+
82+
// Set the older semantic convention attributes
83+
if (httpScheme is null && urlScheme is not null)
84+
SetStringAttribute(HttpScheme, urlScheme);
85+
86+
if (httpTarget is null && urlPath is not null)
87+
{
88+
var target = urlPath;
89+
90+
if (urlQuery is not null)
91+
target += $"?{urlQuery}";
92+
93+
SetStringAttribute(HttpTarget, target);
94+
}
95+
96+
if (netHostName is null && serverAddress is not null)
97+
SetStringAttribute(NetHostName, serverAddress);
98+
99+
if (netHostPort is null && serverPort is not null)
100+
SetIntAttribute(NetHostPort, serverPort.Value);
101+
}
102+
103+
string? ProcessStringAttribute(KeyValuePair<string, object?> tag)
104+
{
105+
if (tag.Value is string value)
106+
{
107+
_logger.FoundTag(nameof(ElasticCompatibilityProcessor), tag.Key, value);
108+
return value;
109+
}
110+
111+
return null;
112+
}
113+
114+
int? ProcessIntAttribute(KeyValuePair<string, object?> tag)
115+
{
116+
if (tag.Value is int value)
117+
{
118+
_logger.FoundTag(nameof(ElasticCompatibilityProcessor), tag.Key, value);
119+
return value;
120+
}
121+
122+
return null;
123+
}
124+
125+
void SetStringAttribute(string attributeName, string value)
126+
{
127+
_logger.SetTag(nameof(ElasticCompatibilityProcessor), attributeName, value);
128+
activity.SetTag(attributeName, value);
129+
}
130+
131+
void SetIntAttribute(string attributeName, int value)
132+
{
133+
_logger.SetTag(nameof(ElasticCompatibilityProcessor), attributeName, value);
134+
activity.SetTag(attributeName, value);
135+
}
136+
}
137+
}

src/Elastic.OpenTelemetry/Processors/TransactionIdProcessor.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to Elasticsearch B.V under one or more agreements.
22
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
33
// See the LICENSE file in the project root for more information
4+
45
using System.Diagnostics;
56
using Elastic.OpenTelemetry.Diagnostics;
67
using Microsoft.Extensions.Logging;
@@ -30,6 +31,6 @@ public override void OnStart(Activity activity)
3031
return;
3132

3233
activity.SetTag(TransactionIdTagName, _currentTransactionId.Value.Value.ToString());
33-
logger.TransactionIdProcessorTagAdded();
34+
logger.SetTag(nameof(TransactionIdProcessor), TransactionIdTagName, _currentTransactionId.Value.Value.ToString());
3435
}
3536
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
5+
namespace Elastic.OpenTelemetry.SemanticConventions;
6+
7+
internal static class TraceSemanticConventions
8+
{
9+
// HTTP
10+
public const string HttpScheme = "http.scheme";
11+
public const string HttpTarget = "http.target";
12+
13+
// NET
14+
public const string NetHostName = "net.host.name";
15+
public const string NetHostPort = "net.host.port";
16+
17+
// SERVER
18+
public const string ServerAddress = "server.address";
19+
public const string ServerPort = "server.port";
20+
21+
// URL
22+
public const string UrlPath = "url.path";
23+
public const string UrlQuery = "url.query";
24+
public const string UrlScheme = "url.scheme";
25+
}

tests/Elastic.OpenTelemetry.EndToEndTests/README.md

+4-4
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ Requires an already running serverless observability project on cloud.
88
The configuration can be provided either as asp.net secrets or environment variables.
99

1010
```bash
11-
dotnet user-secrets set "E2E:Endpoint" "<url>" --project tests/Elastic.OpenTelemetry.IntegrationTests
12-
dotnet user-secrets set "E2E:Authorization" "<header>" --project tests/Elastic.OpenTelemetry.IntegrationTests
11+
dotnet user-secrets set "E2E:Endpoint" "<url>" --project tests/Elastic.OpenTelemetry.EndToEndTests
12+
dotnet user-secrets set "E2E:Authorization" "<header>" --project tests/Elastic.OpenTelemetry.EndToEndTests
1313
```
1414

1515
The equivalent environment variables are `E2E__ENDPOINT` and `E2E__AUTHORIZATION`. For local development setting
@@ -38,8 +38,8 @@ Once invited and accepted the invited email can be used to login during the auto
3838
These can be provided again as user secrets:
3939

4040
```bash
41-
dotnet user-secrets set "E2E:BrowserEmail" "<email>" --project tests/Elastic.OpenTelemetry.IntegrationTests
42-
dotnet user-secrets set "E2E:BrowserPassword" "<password>" --project tests/Elastic.OpenTelemetry.IntegrationTests
41+
dotnet user-secrets set "E2E:BrowserEmail" "<email>" --project tests/Elastic.OpenTelemetry.EndToEndTests
42+
dotnet user-secrets set "E2E:BrowserPassword" "<password>" --project tests/Elastic.OpenTelemetry.EndToEndTests
4343
```
4444

4545
or environment variables (`E2E__BROWSEREMAIL` and `E2E__BROWSERPASSWORD`).

0 commit comments

Comments
 (0)