Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[geneva] Add support for sending metrics in OTLP format over UDS #2261

Merged
merged 14 commits into from
Oct 30, 2024
21 changes: 13 additions & 8 deletions src/OpenTelemetry.Exporter.Geneva/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,19 @@
* Drop support for .NET 6 as this target is no longer supported.
([#2117](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/2117))

* Added support for exporting metrics via
[user_events](https://docs.kernel.org/trace/user_events.html) on Linux when
OTLP protobuf encoding is enabled via the
`PrivatePreviewEnableOtlpProtobufEncoding=true` connection string switch. With
this, `PrivatePreviewEnableOtlpProtobufEncoding=true` is now supported on both
Widows and Linux. Windows uses ETW as transport, while Linux uses user_events
as transport.
([#2113](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/2113))
* Added support for exporting metrics on Linux when OTLP protobuf encoding is
enabled via the `PrivatePreviewEnableOtlpProtobufEncoding=true` connection
string switch. `PrivatePreviewEnableOtlpProtobufEncoding=true` is now
supported on both Windows and Linux.

* `user_events` transport:
[#2113](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/2113).

* Unix domain socket transport:
[#2261](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/2261).

For configuration details see:
[OtlpProtobufEncoding](./README.md#otlpprotobufencoding).

## 1.9.0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0

using System.Reflection;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using OpenTelemetry.Exporter.Geneva.Metrics;
using OpenTelemetry.Internal;
Expand Down Expand Up @@ -52,14 +53,49 @@ public GenevaMetricExporter(GenevaMetricExporterOptions options)

if (connectionStringBuilder.PrivatePreviewEnableOtlpProtobufEncoding)
{
IMetricDataTransport transport;

if (connectionStringBuilder.Protocol == TransportProtocol.Unix)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
throw new ArgumentException("Unix domain socket should not be used on Windows.");
}

var unixDomainSocketPath = connectionStringBuilder.ParseUnixDomainSocketPath();

transport = new MetricUnixDomainSocketDataTransport(unixDomainSocketPath);
}
else
{
#if NET6_0_OR_GREATER
transport = !RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? MetricUnixUserEventsDataTransport.Instance
: MetricWindowsEventTracingDataTransport.Instance;
#else
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
throw new NotSupportedException("Exporting data in protobuf format is not supported on Linux.");
}

transport = MetricWindowsEventTracingDataTransport.Instance;
#endif
}

connectionStringBuilder.TryGetMetricsAccountAndNamespace(
out var metricsAccount,
out var metricsNamespace);

var otlpProtobufExporter = new OtlpProtobufMetricExporter(
() => { return this.Resource; },
connectionStringBuilder,
transport,
metricsAccount,
metricsNamespace,
options.PrepopulatedMetricDimensions);

this.exporter = otlpProtobufExporter;

this.exportMetrics = otlpProtobufExporter.Export;

this.exporter = otlpProtobufExporter;
}
else
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// SPDX-License-Identifier: Apache-2.0

using System.Diagnostics;
using System.Runtime.InteropServices;
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;

Expand All @@ -18,30 +17,22 @@ internal sealed class OtlpProtobufMetricExporter : IDisposable

public OtlpProtobufMetricExporter(
Func<Resource> getResource,
ConnectionStringBuilder? connectionStringBuilder,
IMetricDataTransport transport,
string? metricsAccount,
string? metricsNamespace,
IReadOnlyDictionary<string, object>? prepopulatedMetricDimensions)
{
Debug.Assert(getResource != null, "getResource was null");
Debug.Assert(transport != null, "transport was null");

this.getResource = getResource!;

#if NET6_0_OR_GREATER
IMetricDataTransport transport = !RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? MetricUnixUserEventsDataTransport.Instance
: MetricWindowsEventTracingDataTransport.Instance;
#else
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
throw new NotSupportedException("Exporting data in protobuf format is not supported on Linux.");
}

var transport = MetricWindowsEventTracingDataTransport.Instance;
#endif

this.otlpProtobufSerializer = new OtlpProtobufSerializer(
transport,
connectionStringBuilder,
prepopulatedMetricDimensions);
transport!,
metricsAccount,
metricsNamespace,
prepopulatedMetricDimensions,
prefixBufferWithUInt32LittleEndianLength: transport is MetricUnixDomainSocketDataTransport);
}

public ExportResult Export(in Batch<Metric> batch)
Expand All @@ -59,5 +50,9 @@ public ExportResult Export(in Batch<Metric> batch)

public void Dispose()
{
if (this.otlpProtobufSerializer.MetricDataTransport is MetricUnixDomainSocketDataTransport udsTransport)
{
udsTransport.Dispose();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

using System.Buffers.Binary;
using System.Diagnostics;
using System.Globalization;
using OpenTelemetry.Metrics;
Expand All @@ -17,6 +18,7 @@ internal sealed class OtlpProtobufSerializer
private readonly Dictionary<string, List<Metric>> scopeMetrics = new();
private readonly string? metricNamespace;
private readonly string? metricAccount;
private readonly bool prefixBufferWithUInt32LittleEndianLength;
private readonly byte[]? prepopulatedNumberDataPointAttributes;
private readonly int prepopulatedNumberDataPointAttributesLength;
private readonly byte[]? prepopulatedHistogramDataPointAttributes;
Expand All @@ -37,12 +39,17 @@ internal sealed class OtlpProtobufSerializer

public OtlpProtobufSerializer(
IMetricDataTransport metricDataTransport,
ConnectionStringBuilder? connectionStringBuilder,
IReadOnlyDictionary<string, object>? prepopulatedMetricDimensions)
string? metricsAccount,
string? metricsNamespace,
IReadOnlyDictionary<string, object>? prepopulatedMetricDimensions,
bool prefixBufferWithUInt32LittleEndianLength = false)
{
Debug.Assert(metricDataTransport != null, "metricDataTransport was null");

this.MetricDataTransport = metricDataTransport!;
this.metricAccount = metricsAccount;
this.metricNamespace = metricsNamespace;
this.prefixBufferWithUInt32LittleEndianLength = prefixBufferWithUInt32LittleEndianLength;

// Taking a arbitrary number here for writing attributes.
byte[] temp = new byte[20000];
Expand All @@ -68,14 +75,6 @@ public OtlpProtobufSerializer(
Array.Copy(temp, this.prepopulatedExponentialHistogramDataPointAttributes, cursor);
this.prepopulatedExponentialHistogramDataPointAttributesLength = cursor;
}

if (connectionStringBuilder?.TryGetMetricsAccountAndNamespace(
out var metricsAccount,
out var metricsNamespace) == true)
{
this.metricAccount = metricsAccount;
this.metricNamespace = metricsNamespace;
}
}

internal static void WriteInstrumentDetails(byte[] buffer, ref int cursor, Metric metric)
Expand Down Expand Up @@ -222,7 +221,7 @@ internal void ClearScopeMetrics()

internal void SerializeResourceMetrics(byte[] buffer, Resource resource)
{
int cursor = 0;
int cursor = this.prefixBufferWithUInt32LittleEndianLength ? 4 : 0;

this.resourceMetricTagAndLengthIndex = cursor;

Expand Down Expand Up @@ -734,11 +733,15 @@ private void WriteIndividualMessageTagsAndLength(byte[] buffer, ref int cursor,

// Write resource metric tag and length
ProtobufSerializerHelper.WriteTagAndLengthPrefix(buffer, ref resourceMetricIndex, cursor - this.resourceMetricValueIndex, FieldNumberConstants.ResourceMetrics_resource, WireType.LEN);

if (this.prefixBufferWithUInt32LittleEndianLength)
{
BinaryPrimitives.WriteUInt32LittleEndian(buffer, (uint)cursor - 4);
}
}

private void SendMetricPoint(byte[] buffer, ref int cursor)
{
// TODO: Extend this for user_events.
this.MetricDataTransport.SendOtlpProtobufEvent(buffer, cursor);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ public void Send(MetricEventType eventType, byte[] body, int size)
this.udsDataTransport.Send(body, size + this.fixedPayloadLength);
}

public void SendOtlpProtobufEvent(byte[] body, int size)
{
this.udsDataTransport.Send(body, size);
}

public void Dispose()
{
if (this.isDisposed)
Expand All @@ -38,9 +43,4 @@ public void Dispose()
this.udsDataTransport.Dispose();
this.isDisposed = true;
}

public void SendOtlpProtobufEvent(byte[] body, int size)
{
throw new NotImplementedException();
}
}
73 changes: 61 additions & 12 deletions src/OpenTelemetry.Exporter.Geneva/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,23 +270,72 @@ For example:

##### OtlpProtobufEncoding

On Windows set `PrivatePreviewEnableOtlpProtobufEncoding=true` on the
`ConnectionString` to opt-in to the experimental feature for changing the
underlying serialization format to binary protobuf following the schema defined
in [OTLP
An experimental feature flag is available to opt-into changing the underlying
serialization format to binary protobuf following the schema defined in [OTLP
specification](https://github.com/open-telemetry/opentelemetry-proto/blob/v1.1.0/opentelemetry/proto/metrics/v1/metrics.proto).

When using OTLP format `Account` and `Namespace` are **NOT** required to be set
CodeBlanch marked this conversation as resolved.
Show resolved Hide resolved
on the `ConnectionString`. The recommended approach is to use OpenTelemetry
Resource instead:

```csharp
using var meterProvider = Sdk.CreateMeterProviderBuilder()
// Other configuration not shown
.ConfigureResource(r => r.AddAttributes(
new Dictionary<string, object>()
{
["_microsoft_metrics_account"] = "MetricsAccountGoesHere",
["_microsoft_metrics_namespace"] = "MetricsNamespaceGoesHere",
}))
.AddGenevaMetricExporter(options =>
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
options.ConnectionString = "PrivatePreviewEnableOtlpProtobufEncoding=true";
}
else
{
// Note: 1.10.0+ version required to use OTLP format on Linux

// Use Unix domain socket mode
options.ConnectionString = "Endpoint=unix:{OTLP UDS Path};PrivatePreviewEnableOtlpProtobufEncoding=true";

// Use user_events mode (preferred but considered experimental)
CodeBlanch marked this conversation as resolved.
Show resolved Hide resolved
// options.ConnectionString = "PrivatePreviewEnableOtlpProtobufEncoding=true";
}
})
.Build();
```

###### Windows

To send metric data over ETW in OTLP format set
`PrivatePreviewEnableOtlpProtobufEncoding=true` on the `ConnectionString`.

###### Linux

As of `1.10.0` `PrivatePreviewEnableOtlpProtobufEncoding=true` is also supported
on Linux. On Linux when using `PrivatePreviewEnableOtlpProtobufEncoding=true` an
`Endpoint` is **NOT** required to be provided on `ConnectionString`. For
example: `Endpoint=unix:Account={MetricAccount};Namespace={MetricNamespace}`.
on Linux.

###### When using unix domain socket

To send metric data over UDS in OTLP format set the `Endpoint` to use the
correct `OtlpSocketPath` path and set
`PrivatePreviewEnableOtlpProtobufEncoding=true` on the `ConnectionString`:
`Endpoint=unix:{OTLP UDS Path};PrivatePreviewEnableOtlpProtobufEncoding=true`.

> [!IMPORTANT]
> When `PrivatePreviewEnableOtlpProtobufEncoding` is enabled on Linux metrics
> are written using
> [user_events](https://docs.kernel.org/trace/user_events.html). `user_events`
> are a newer feature of the Linux kernel and require a distro with the feature
> enabled.
> OTLP over UDS requires a different socket path than TLV over UDS.

###### When using user_events

> [!IMPORTANT]
> [user_events](https://docs.kernel.org/trace/user_events.html) are a newer
> feature of the Linux kernel and require a distro with the feature enabled.

To send metric data over user_events in OTLP format do **NOT** specify an
`Endpoint` and set `PrivatePreviewEnableOtlpProtobufEncoding=true` on the
`ConnectionString`.

#### `MetricExportIntervalMilliseconds` (optional)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,12 +111,12 @@ public void Setup()
this.tlvMetricsExporter = new TlvMetricExporter(connectionStringBuilder, exporterOptions.PrepopulatedMetricDimensions);

// Using test transport here with noop to benchmark just the serialization part.
this.otlpProtobufSerializer = new OtlpProtobufSerializer(new TestTransport(), null, null);
this.otlpProtobufSerializer = new OtlpProtobufSerializer(new TestTransport(), metricsAccount: null, metricsNamespace: null, prepopulatedMetricDimensions: null);

var resourceBuilder = ResourceBuilder.CreateDefault().Clear()
.AddAttributes(new[] { new KeyValuePair<string, object>("TestResourceKey", "TestResourceValue") });
this.resource = resourceBuilder.Build();
this.otlpProtobufMetricExporter = new OtlpProtobufMetricExporter(() => { return this.resource; }, null, null);
this.otlpProtobufMetricExporter = new OtlpProtobufMetricExporter(() => { return this.resource; }, new TestTransport(), metricsAccount: null, metricsNamespace: null, prepopulatedMetricDimensions: null);
this.buffer = new byte[GenevaMetricExporter.BufferSize];

this.counterMetricPointWith3Dimensions = this.GenerateCounterMetricItemWith3Dimensions(out this.counterMetricDataWith3Dimensions);
Expand Down
Loading
Loading