Skip to content
Draft
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
2 changes: 1 addition & 1 deletion LogInsights/LogInsights.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
<Compile Remove="TextReading\ReadLogByBlock.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Azure.ApplicationInsights.Query" Version="1.0.0" />
<PackageReference Include="Azure.Monitor.Query" Version="1.5.0" />
<PackageReference Include="GitVersion.MsBuild" Version="6.1.0">
<PrivateAssets>All</PrivateAssets>
</PackageReference>
Expand Down
210 changes: 126 additions & 84 deletions LogInsights/LogReader/Implementations/AppInsightsLogReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@
using CsvHelper.Configuration.Attributes;

using LogInsights.Helpers;
using Microsoft.Azure.ApplicationInsights.Query;
using Azure.Core;
using Azure.Identity;

Check failure on line 6 in LogInsights/LogReader/Implementations/AppInsightsLogReader.cs

View workflow job for this annotation

GitHub Actions / build (Release)

The type or namespace name 'Identity' does not exist in the namespace 'Azure' (are you missing an assembly reference?)
using Azure.Monitor.Query;
using Azure.Monitor.Query.Models;
using System.Net.Http;
using System;
using System.Collections.Generic;
using System.Globalization;
Expand All @@ -15,11 +19,32 @@

// ReSharper disable UnusedMember.Local

namespace LogInsights.LogReader
{
namespace LogInsights.LogReader
{
// Custom TokenCredential for Application Insights API key authentication
internal class AppInsightsApiKeyCredential : TokenCredential
{
private readonly string _apiKey;

public AppInsightsApiKeyCredential(string apiKey)
{
_apiKey = apiKey ?? throw new ArgumentNullException(nameof(apiKey));
}

public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
{
// Return the API key as a bearer token - this may need adjustment based on actual Azure.Monitor.Query implementation
return new AccessToken(_apiKey, DateTimeOffset.UtcNow.AddHours(1));
}

public override ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
{
return new ValueTask<AccessToken>(GetToken(requestContext, cancellationToken));
}
}
public class AppInsightsLogReader : LogReader
{
private readonly ApplicationInsightsDataClient _client;
private readonly LogsQueryClient _client;
private readonly AppInsightsLogReaderConnectionStringBuilder _connString;

private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.General) {
Expand Down Expand Up @@ -92,8 +117,13 @@
if ( string.IsNullOrEmpty(apiKey) )
throw new Exception($"Missing {nameof(apiKey)} value.");

_client = new ApplicationInsightsDataClient(new ApiKeyClientCredentials(apiKey));
Display = $"App Insights Reader - {_client.BaseUri.OriginalString}";
// Create LogsQueryClient for Application Insights
// Note: Application Insights now uses Log Analytics workspace under the hood
// API key authentication is handled via custom credential
var credential = new AppInsightsApiKeyCredential(apiKey);
var options = new LogsQueryClientOptions();
_client = new LogsQueryClient(credential, options);
Display = $"App Insights Reader - {appId}";
}
else
{
Expand Down Expand Up @@ -175,84 +205,96 @@
}
}

private async IAsyncEnumerable<_Row> _ReadAppInsightsDataAsync([EnumeratorCancellation] CancellationToken ct)
{
var events = new Events(_client);

var result = await events.Client.Query.ExecuteAsync(
_connString.AppId,
_connString.Query,
!string.IsNullOrEmpty(_connString.TimeSpan) ? _connString.TimeSpan : null,
cancellationToken: ct)
.ConfigureAwait(false);

if (result.Tables.Count < 1)
throw new Exception("Missing result table");

var table = result.Tables[0];

int timestampIdx = -1;
int messageIdx = -1;
int severityIdx = -1;
int itemIdIdx = -1;
int customDimensionsIdx = -1;
int outerMessageIdx = -1;
int detailsIdx = -1;

for (var i = 0; i < table.Columns.Count; i++)
{
var column = table.Columns[i];

switch (column.Name)
{
case "itemId":
itemIdIdx = i;
break;

case "timestamp":
timestampIdx = i;
break;

case "message":
messageIdx = i;
break;

case "severityLevel":
severityIdx = i;
break;

case "customDimensions":
customDimensionsIdx = i;
break;

case "outerMessage":
outerMessageIdx = i;
break;

case "details":
detailsIdx = i;
break;
}
}

if (timestampIdx < 0 ||
messageIdx < 0 ||
itemIdIdx < 0)
throw new Exception("Missing columns");

foreach (var row in table.Rows)
{
yield return new _Row
{
ItemId = (string)row[itemIdIdx],
TimeStamp = (DateTime)row[timestampIdx],
Message = (string)row[messageIdx],
Severity = severityIdx > -1 ? (long)row[severityIdx] : 1L,
CustomDimensions = customDimensionsIdx > -1 ? (string)row[customDimensionsIdx] : "",
Details = detailsIdx > -1 ? (string)row[detailsIdx] : "",
OuterMessage = detailsIdx > -1 ? (string)row[outerMessageIdx] : ""
};
}
private async IAsyncEnumerable<_Row> _ReadAppInsightsDataAsync([EnumeratorCancellation] CancellationToken ct)
{
QueryTimeRange timeRange = QueryTimeRange.All;
if (!string.IsNullOrEmpty(_connString.TimeSpan))
{
// Try to parse the timespan - the old API used formats like "P1D" (ISO 8601 duration)
// The new API expects QueryTimeRange or TimeSpan objects
if (TimeSpan.TryParse(_connString.TimeSpan, out var parsedTimespan))
{
timeRange = new QueryTimeRange(parsedTimespan);
}
else
{
// Try parsing as ISO 8601 duration or use default
timeRange = QueryTimeRange.All;
}
}

var result = await _client.QueryWorkspaceAsync(
_connString.AppId,
_connString.Query,
timeRange,
cancellationToken: ct)
.ConfigureAwait(false);

var table = result.Value.Table;

int timestampIdx = -1;
int messageIdx = -1;
int severityIdx = -1;
int itemIdIdx = -1;
int customDimensionsIdx = -1;
int outerMessageIdx = -1;
int detailsIdx = -1;

for (var i = 0; i < table.Columns.Count; i++)
{
var column = table.Columns[i];

switch (column.Name)
{
case "itemId":
itemIdIdx = i;
break;

case "timestamp":
timestampIdx = i;
break;

case "message":
messageIdx = i;
break;

case "severityLevel":
severityIdx = i;
break;

case "customDimensions":
customDimensionsIdx = i;
break;

case "outerMessage":
outerMessageIdx = i;
break;

case "details":
detailsIdx = i;
break;
}
}

if (timestampIdx < 0 ||
messageIdx < 0 ||
itemIdIdx < 0)
throw new Exception("Missing columns");

foreach (var row in table.Rows)
{
yield return new _Row
{
ItemId = row[itemIdIdx]?.ToString(),
TimeStamp = row[timestampIdx] is DateTimeOffset dto ? dto.DateTime :
DateTime.TryParse(row[timestampIdx]?.ToString(), out var dt) ? dt : DateTime.MinValue,
Message = row[messageIdx]?.ToString(),
Severity = severityIdx > -1 && long.TryParse(row[severityIdx]?.ToString(), out var sev) ? sev : 1L,
CustomDimensions = customDimensionsIdx > -1 ? row[customDimensionsIdx]?.ToString() : "",
Details = detailsIdx > -1 ? row[detailsIdx]?.ToString() : "",
OuterMessage = outerMessageIdx > -1 ? row[outerMessageIdx]?.ToString() : ""
};
}
}

private async IAsyncEnumerable<_Row> _ReadCsvDataAsync()
Expand Down
Loading