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
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,12 @@ public TelemetryItem(string name, TelemetryItem telemetryItem, ActivitySpanId ac
}

public TelemetryItem (string name, LogRecord logRecord, AzureMonitorResource? resource, string instrumentationKey, LogContextInfo logContext) :
this(name, FormatUtcTimestamp(logRecord.Timestamp))
this(name, FormatUtcTimestamp(logRecord.Timestamp), logRecord, resource, instrumentationKey, logContext)
{
}

public TelemetryItem(string name, DateTimeOffset envelopeTime, LogRecord logRecord, AzureMonitorResource? resource, string instrumentationKey, LogContextInfo logContext) :
this(name, envelopeTime)
{
if (logRecord.TraceId != default)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ internal static class LogsHelper
private const string AvailabilitySuccessAttributeName = "microsoft.availability.success";
private const string AvailabilityRunLocationAttributeName = "microsoft.availability.runLocation";
private const string AvailabilityMessageAttributeName = "microsoft.availability.message";
private const string AvailabilityTestTimestampAttributeName = "microsoft.availability.testTimestamp";
private const int Version = 2;
private static readonly Action<LogRecordScope, IDictionary<string, string>> s_processScope = (scope, properties) =>
{
Expand Down Expand Up @@ -110,7 +111,16 @@ internal static (List<TelemetryItem> TelemetryItems, TelemetrySchemaTypeCounter
}
else if (availabilityInfo is not null)
{
telemetryItem = new TelemetryItem("Availability", logRecord, resource, instrumentationKey, logContext)
DateTimeOffset envelopeTime = availabilityInfo.Value.TestTimestamp != null
&& DateTimeOffset.TryParse(
availabilityInfo.Value.TestTimestamp,
CultureInfo.InvariantCulture,
System.Globalization.DateTimeStyles.RoundtripKind,
out var parsedTs)
? parsedTs.ToUniversalTime()
: TelemetryItem.FormatUtcTimestamp(logRecord.Timestamp);

telemetryItem = new TelemetryItem("Availability", envelopeTime, logRecord, resource, instrumentationKey, logContext)
{
Data = new MonitorBase
{
Expand Down Expand Up @@ -278,6 +288,7 @@ internal static void ProcessLogRecordProperties(LogRecord logRecord, IDictionary
string? availabilitySuccess = null;
string? availabilityRunLocation = null;
string? availabilityMessage = null;
string? availabilityTestTimestamp = null;
logContext = default;

foreach (KeyValuePair<string, object?> item in logRecord.Attributes ?? Enumerable.Empty<KeyValuePair<string, object?>>())
Expand All @@ -302,6 +313,9 @@ internal static void ProcessLogRecordProperties(LogRecord logRecord, IDictionary
case AvailabilityMessageAttributeName:
availabilityMessage = item.Value?.ToString();
break;
case AvailabilityTestTimestampAttributeName:
availabilityTestTimestamp = item.Value?.ToString();
break;
case ClientIpAttributeName:
logContext.MicrosoftClientIp = item.Value?.ToString().Truncate(SchemaConstants.AvailabilityData_Properties_MaxValueLength);
break;
Expand Down Expand Up @@ -359,7 +373,8 @@ internal static void ProcessLogRecordProperties(LogRecord logRecord, IDictionary
Duration = availabilityDuration!,
Success = bool.TryParse(availabilitySuccess, out var success) ? success : false,
RunLocation = availabilityRunLocation,
Message = availabilityMessage ?? message
Message = availabilityMessage ?? message,
TestTimestamp = availabilityTestTimestamp
};
}

Expand Down Expand Up @@ -441,5 +456,6 @@ internal struct AvailabilityInfo
public bool Success { get; set; }
public string? RunLocation { get; set; }
public string? Message { get; set; }
public string? TestTimestamp { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Azure.Monitor.OpenTelemetry.Exporter.Models;
using Azure.Monitor.OpenTelemetry.Exporter.Tests.CommonTestFramework;
Expand Down Expand Up @@ -306,5 +307,96 @@ public void VerifyIncompleteAvailabilityDataFallsBackToMessage()
var telemetryItem = telemetryItems?.Where(x => x.Name == "Message").Single();
Assert.NotNull(telemetryItem);
}

[Fact]
public void VerifyAvailabilityTelemetryWithTestTimestamp()
{
// SETUP
var uniqueTestId = Guid.NewGuid();
var logCategoryName = $"logCategoryName{uniqueTestId}";
var testTimestamp = "2025-04-19T12:10:59.9930000+00:00";
var expectedTime = DateTimeOffset.Parse(testTimestamp, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind).ToUniversalTime();

List<TelemetryItem>? telemetryItems = null;

var loggerFactory = LoggerFactory.Create(builder =>
{
builder
.AddFilter<OpenTelemetryLoggerProvider>(logCategoryName, LogLevel.Information)
.AddOpenTelemetry(options =>
{
options.SetResourceBuilder(ResourceBuilder.CreateDefault().AddAttributes(testResourceAttributes));
options.AddAzureMonitorLogExporterForTest(out telemetryItems);
});
});

// ACT
var logger = loggerFactory.CreateLogger(logCategoryName);
logger.LogInformation("{microsoft.availability.id} {microsoft.availability.name} {microsoft.availability.duration} {microsoft.availability.success} {microsoft.availability.testTimestamp}",
"test-id-ts", "TimestampTest", "00:00:03", true, testTimestamp);

// CLEANUP
loggerFactory.Dispose();

// ASSERT
Assert.True(telemetryItems?.Any(), "Unit test failed to collect telemetry.");
this.telemetryOutput.Write(telemetryItems);
var telemetryItem = telemetryItems?.Where(x => x.Name == "Availability").Single();
Assert.NotNull(telemetryItem);

// Envelope time should equal the testTimestamp, not the logRecord.Timestamp
Assert.Equal(expectedTime, telemetryItem!.Time);

// testTimestamp attribute must NOT appear in AvailabilityData.Properties
var availabilityData = (AvailabilityData)telemetryItem.Data.BaseData;
Assert.False(availabilityData.Properties.ContainsKey("microsoft.availability.testTimestamp"),
"microsoft.availability.testTimestamp should not be leaked into AvailabilityData.Properties.");
}

[Fact]
public void VerifyAvailabilityTelemetryWithoutTestTimestamp()
{
// SETUP
var uniqueTestId = Guid.NewGuid();
var logCategoryName = $"logCategoryName{uniqueTestId}";

List<TelemetryItem>? telemetryItems = null;

var loggerFactory = LoggerFactory.Create(builder =>
{
builder
.AddFilter<OpenTelemetryLoggerProvider>(logCategoryName, LogLevel.Information)
.AddOpenTelemetry(options =>
{
options.SetResourceBuilder(ResourceBuilder.CreateDefault().AddAttributes(testResourceAttributes));
options.AddAzureMonitorLogExporterForTest(out telemetryItems);
});
});

// ACT
var beforeLog = DateTimeOffset.UtcNow;
var logger = loggerFactory.CreateLogger(logCategoryName);
logger.LogInformation("{microsoft.availability.id} {microsoft.availability.name} {microsoft.availability.duration} {microsoft.availability.success}",
"test-id-nots", "NoTimestampTest", "00:00:01", true);
var afterLog = DateTimeOffset.UtcNow;

// CLEANUP
loggerFactory.Dispose();

// ASSERT
Assert.True(telemetryItems?.Any(), "Unit test failed to collect telemetry.");
this.telemetryOutput.Write(telemetryItems);
var telemetryItem = telemetryItems?.Where(x => x.Name == "Availability").Single();
Assert.NotNull(telemetryItem);

// Envelope time should be derived from logRecord.Timestamp (wall-clock time), not a fixed past time
Assert.True(telemetryItem!.Time >= beforeLog && telemetryItem.Time <= afterLog,
$"Expected envelope time between {beforeLog} and {afterLog}, but got {telemetryItem.Time}.");

// testTimestamp attribute must NOT appear in AvailabilityData.Properties
var availabilityData = (AvailabilityData)telemetryItem.Data.BaseData;
Assert.False(availabilityData.Properties.ContainsKey("microsoft.availability.testTimestamp"),
"microsoft.availability.testTimestamp should not appear in AvailabilityData.Properties when not provided.");
}
}
}
Loading