Skip to content

Commit

Permalink
[geneva] Add support for sending metrics in OTLP format over UDS (#2261)
Browse files Browse the repository at this point in the history
Co-authored-by: Cijo Thomas <cithomas@microsoft.com>
  • Loading branch information
CodeBlanch and cijothomas authored Oct 30, 2024
1 parent 503e0b1 commit 0351a0b
Show file tree
Hide file tree
Showing 10 changed files with 274 additions and 104 deletions.
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
42 changes: 39 additions & 3 deletions src/OpenTelemetry.Exporter.Geneva/Metrics/GenevaMetricExporter.cs
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 protobuf encoding `Account` and `Namespace` are **NOT** required
to be set 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 protobuf encoding on Linux
// Use Unix domain socket mode
options.ConnectionString = "Endpoint=unix:{OTLP UDS Path};PrivatePreviewEnableOtlpProtobufEncoding=true";

// Use user_events mode (preferred but considered experimental as this is a new capability in Linux kernel)
// options.ConnectionString = "PrivatePreviewEnableOtlpProtobufEncoding=true";
}
})
.Build();
```

###### Windows

To send metric data over ETW using OTLP protobuf encoding 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 using OTLP protobuf encoding 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 using OTLP protobuf encoding 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

0 comments on commit 0351a0b

Please sign in to comment.