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

[Exporter.Geneva] Adding user_events support for logs on Linux - user_events data transport utility #2479

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

#if NET

using Microsoft.LinuxTracepoints.Provider;

namespace OpenTelemetry.Exporter.Geneva.Transports;

internal sealed class UnixUserEventsDataTransport : IDisposable
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a plan to implement IDataTransport later?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not likely, the API to send user_events looks like this: eventBuilder.Write(logsTracepoint). Not like the other data transports which send a byte array.

{
public const string LogsTracepointName = "geneva_logs"; // TODO: find the correct tracepoint name
public const string LogsTracepointNameArgs = $"{LogsTracepointName} u32 protocol;char[8] version;__rel_loc u8[] buffer"; // TODO: find the correct tracepoint args
public const string EventHeaderDynamicProviderName = "MicrosoftOpenTelemetryLogs";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reasoning behind the name MicrosoftOpenTelemetryLogs?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now it's a placeholder. I need to work with the agent to find out the endpoint. The agent is also working in progress so it might be a while to finalize.

public const System.Diagnostics.Tracing.EventLevel LogsTracepointEventLevel = System.Diagnostics.Tracing.EventLevel.Informational; // TODO: find the correct event level
public const ulong LogsTracepointKeyword = 1; // TODO: find the correct keyword

private readonly EventHeaderDynamicProvider eventHeaderDynamicProvider;

private UnixUserEventsDataTransport()
{
this.eventHeaderDynamicProvider = new EventHeaderDynamicProvider(EventHeaderDynamicProviderName);
}

public static UnixUserEventsDataTransport Instance { get; } = new();

public EventHeaderDynamicTracepoint RegisterUserEventProviderForLogs()
{
var logsTracepoint = this.eventHeaderDynamicProvider.Register(LogsTracepointEventLevel, LogsTracepointKeyword);
if (logsTracepoint.RegisterResult != 0)
{
// ENOENT (2): No such file or directory
if (logsTracepoint.RegisterResult == 2)
{
throw new NotSupportedException(
$"Tracepoint registration for 'geneva_logs' failed with result: '{logsTracepoint.RegisterResult}'. Verify your distribution/kernel supports user_events: https://docs.kernel.org/trace/user_events.html.");
}

ExporterEventSource.Log.TransportInformation(
nameof(UnixUserEventsDataTransport),
$"Tracepoint registration operation for 'geneva_logs' returned result '{logsTracepoint.RegisterResult}' which is considered recoverable. Entering running state.");
}

return logsTracepoint;
}

public void Dispose()
{
this.eventHeaderDynamicProvider.Dispose();
}
}

#endif
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@
using System.Globalization;
using System.Text.RegularExpressions;
using Microsoft.LinuxTracepoints.Provider;
using OpenTelemetry.Exporter.Geneva.Transports;
using Xunit;
using Xunit.Abstractions;

namespace OpenTelemetry.Exporter.Geneva.Tests;

[Trait("CategoryName", "Geneva:user_events:metrics")]
[Trait("CategoryName", "Geneva:user_events")]
public class UnixUserEventsDataTransportTests
{
/*
Expand All @@ -26,7 +27,7 @@ public class UnixUserEventsDataTransportTests
* these tests do).
*
* Command:
* sudo dotnet test --configuration Debug --framework net8.0 --filter CategoryName=Geneva:user_events:metrics
* sudo dotnet test --configuration Debug --framework net8.0 --filter CategoryName=Geneva:user_events
*
* How these tests work:
*
Expand All @@ -49,7 +50,7 @@ public UnixUserEventsDataTransportTests(ITestOutputHelper testOutputHelper)
this.testOutputHelper = testOutputHelper;
}

[Fact(Skip = "This would fail on Ubuntu. Skipping for now.")]
[Fact(Skip = "This would fail on Ubuntu. Skipping for now. See issue: #2326.")]
public void UserEvents_Enabled_Success_Linux()
{
EnsureUserEventsEnabled();
Expand Down Expand Up @@ -113,7 +114,7 @@ public void UserEvents_Enabled_Success_Linux()
}
}

[Fact(Skip = "This would fail on Ubuntu. Skipping for now.")]
[Fact(Skip = "This would fail on Ubuntu. Skipping for now. See issue: #2326.")]
public void UserEvents_Disabled_Success_Linux()
{
EnsureUserEventsEnabled();
Expand Down Expand Up @@ -143,6 +144,106 @@ public void UserEvents_Disabled_Success_Linux()
}
}

[Fact(Skip = "This would fail on Ubuntu. Skipping for now. See issue: #2326.")]
public void UserEvents_Logs_Success_Linux()
{
EnsureUserEventsEnabled();

var listener = new PerfTracepointListener(
"MicrosoftOpenTelemetryLogs_L4K1",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is "L4" hardcoded? or its based on severity?
Also, is K1 fixed with no ability to change?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both numbers can change.

Description:

/// Represents a tracepoint registered for a specific provider + level + keyword.
/// The tracepoint name is based on provider name + level + keyword + provider group,
/// e.g. "MyProviderName_L1K1" or "MyProviderName_L1K1Ggroup".

Constructor:

For now I've hard-coded all these values, but at least EventLevel will be dynamic as I make code changes later.

MetricUnixUserEventsDataTransport.MetricsTracepointNameArgs);

var logsTracepoint = UnixUserEventsDataTransport.Instance.RegisterUserEventProviderForLogs();

if (listener.IsEnabled())
{
throw new NotSupportedException($"{MetricUnixUserEventsDataTransport.MetricsTracepointName} is already enabled");
}

try
{
listener.Enable();

Console.WriteLine("------------- ready to write events -------------");
Thread.Sleep(5000);

if (logsTracepoint.IsEnabled)
{
var eventBuilder = CreateEventHeaderDynamicBuilder();

Console.WriteLine("About to write tracepoint:");
eventBuilder.Write(logsTracepoint);
Console.WriteLine("Written tracepoint.");
}

Thread.Sleep(5000);

this.testOutputHelper.WriteLine("Writing events from listener:");
Console.WriteLine("Writing events from listener:");
foreach (var e in listener.Events)
{
this.testOutputHelper.WriteLine(string.Join(", ", e.Select(kvp => $"{kvp.Key}={kvp.Value}")));
Console.WriteLine(string.Join(", ", e.Select(kvp => $"{kvp.Key}={kvp.Value}")));
}

this.testOutputHelper.WriteLine("Total events: " + listener.Events.Count);
Console.WriteLine("Total events: " + listener.Events.Count);
}
finally
{
try
{
listener.Disable();
}
catch
{
}

listener.Dispose();
}
}

private static EventHeaderDynamicBuilder CreateEventHeaderDynamicBuilder()
{
var eb = new EventHeaderDynamicBuilder();
eb.Reset("_");
eb.AddUInt16("__csver__", 1024, Microsoft.LinuxTracepoints.EventHeaderFieldFormat.HexInt);

eb.AddStructWithMetadataPosition("partA", out var partAFieldsCountMetadataPosition);

string rfc3339String = DateTime.UtcNow.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.FFFFFFZ", CultureInfo.InvariantCulture);
eb.AddString16("time", rfc3339String);

byte partAFieldsCount = 1;

eb.SetStructFieldCount(partAFieldsCountMetadataPosition, partAFieldsCount);

// Part B

byte partBFieldsCount = 4; // We at least have three fields in Part B: _typeName, severityText, severityNumber, name
eb.AddStructWithMetadataPosition("PartB", out var partBFieldsCountMetadataPosition);
eb.AddString16("_typeName", "Log");
eb.AddUInt8("severityNumber", 21);

eb.AddString16("severityText", "Critical");
eb.AddString16("name", "CheckoutFailed");

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

eventid and event name too?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was supposed to be a made-up event for testing data transport works, so I didn't make it a conforming event.

eb.SetStructFieldCount(partBFieldsCountMetadataPosition, partBFieldsCount);

// Part C

byte partCFieldsCount = 0;
eb.AddStructWithMetadataPosition("PartC", out var partCFieldsCountMetadataPosition);

eb.AddString16("book_id", "12345");
eb.AddString16("book_name", "The Hitchhiker's Guide to the Galaxy");
partCFieldsCount += 2;

eb.SetStructFieldCount(partCFieldsCountMetadataPosition, partCFieldsCount);

return eb;
}

private static void EnsureUserEventsEnabled()
{
var errors = ConsoleCommand.Run("cat", "/sys/kernel/tracing/user_events_status");
Expand Down Expand Up @@ -184,7 +285,7 @@ private ConsoleCommand(
if (!string.IsNullOrEmpty(args.Data))
{
this.output.Add(args.Data);
Console.WriteLine($"[OUT] {args.Data}");
Console.WriteLine($"{command} {arguments} [OUT] {args.Data}");

onOutputReceived?.Invoke(args.Data);
}
Expand Down
Loading