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

Pass tracecontext to pdf generator #924

Merged
merged 4 commits into from
Nov 27, 2024
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
16 changes: 13 additions & 3 deletions src/Altinn.App.Api/Extensions/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,15 @@ public async Task StartAsync(CancellationToken cancellationToken)
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}

/// <summary>
/// PDF generation works by using a headless browser to render the frontend of an app instance.
/// To make debugging PDF generation failures easier, we want requests originating from the PDF generator to be
/// contained in the root trace (process/next) as children. The frontend will set this header when making requests to the app backend in PDF mode.
/// </summary>
/// <param name="headers">Request headers attached to the span</param>
/// <returns></returns>
private static bool IsPdfGeneratorRequest(IHeaderDictionary headers) => headers.ContainsKey("X-Altinn-IsPdf");

internal sealed class OtelPropagator : TextMapPropagator
{
private readonly TextMapPropagator _inner;
Expand All @@ -359,8 +368,9 @@ public override PropagationContext Extract<T>(
Func<T, string, IEnumerable<string>?> getter
)
{
if (carrier is HttpRequest)
if (carrier is HttpRequest req && !IsPdfGeneratorRequest(req.Headers))
return default;

return _inner.Extract(context, carrier, getter);
}

Expand All @@ -381,7 +391,7 @@ internal sealed class AspNetCorePropagator : DistributedContextPropagator
PropagatorGetterCallback? getter
)
{
if (carrier is IHeaderDictionary)
if (carrier is IHeaderDictionary headers && !IsPdfGeneratorRequest(headers))
return null;

return _inner.ExtractBaggage(carrier, getter);
Expand All @@ -394,7 +404,7 @@ public override void ExtractTraceIdAndState(
out string? traceState
)
{
if (carrier is IHeaderDictionary)
if (carrier is IHeaderDictionary headers && !IsPdfGeneratorRequest(headers))
{
traceId = null;
traceState = null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System.Diagnostics;

namespace Altinn.App.Core.Features;

partial class Telemetry
{
internal Activity? StartGeneratePdfClientActivity()
{
var activity = ActivitySource.StartActivity("PdfGeneratorClient.GeneratePdf");
return activity;
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
using System.Diagnostics;
using System.Text;
using System.Text.Json;
using Altinn.App.Core.Configuration;
using Altinn.App.Core.Features;
using Altinn.App.Core.Internal.Auth;
using Altinn.App.Core.Internal.Pdf;
using Altinn.App.Core.Models.Pdf;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using OpenTelemetry.Context.Propagation;

namespace Altinn.App.Core.Infrastructure.Clients.Pdf;

Expand All @@ -15,40 +19,50 @@ namespace Altinn.App.Core.Infrastructure.Clients.Pdf;
/// </summary>
public class PdfGeneratorClient : IPdfGeneratorClient
{
private static readonly TextMapPropagator _w3cPropagator = new TraceContextPropagator();

private static readonly JsonSerializerOptions _jsonSerializerOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};

private readonly ILogger<PdfGeneratorClient> _logger;
private readonly HttpClient _httpClient;
private readonly PdfGeneratorSettings _pdfGeneratorSettings;
private readonly PlatformSettings _platformSettings;
private readonly IUserTokenProvider _userTokenProvider;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly Telemetry? _telemetry;

/// <summary>
/// Initializes a new instance of the <see cref="PdfGeneratorClient"/> class.
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="httpClient">The HttpClient to use in communication with the PDF generator service.</param>
/// <param name="pdfGeneratorSettings">
/// All generic settings needed for communication with the PDF generator service.
/// </param>
/// <param name="platformSettings">Links to platform services</param>
/// <param name="userTokenProvider">A service able to identify the JWT for currently authenticated user.</param>
/// <param name="httpContextAccessor">http context</param>
/// <param name="telemetry">Telemetry service</param>
public PdfGeneratorClient(
ILogger<PdfGeneratorClient> logger,
HttpClient httpClient,
IOptions<PdfGeneratorSettings> pdfGeneratorSettings,
IOptions<PlatformSettings> platformSettings,
IUserTokenProvider userTokenProvider,
IHttpContextAccessor httpContextAccessor
IHttpContextAccessor httpContextAccessor,
Telemetry? telemetry = null
)
{
_logger = logger;
_httpClient = httpClient;
_userTokenProvider = userTokenProvider;
_pdfGeneratorSettings = pdfGeneratorSettings.Value;
_platformSettings = platformSettings.Value;
_httpContextAccessor = httpContextAccessor;
_telemetry = telemetry;
}

/// <inheritdoc/>
Expand All @@ -60,6 +74,8 @@ public async Task<Stream> GeneratePdf(Uri uri, CancellationToken ct)
/// <inheritdoc/>
public async Task<Stream> GeneratePdf(Uri uri, string? footerContent, CancellationToken ct)
{
using var activity = _telemetry?.StartGeneratePdfClientActivity();

bool hasWaitForSelector = !string.IsNullOrWhiteSpace(_pdfGeneratorSettings.WaitForSelector);
PdfGeneratorRequest generatorRequest = new()
{
Expand All @@ -73,6 +89,33 @@ public async Task<Stream> GeneratePdf(Uri uri, string? footerContent, Cancellati
},
};

if (Activity.Current is { } propagateActivity)
{
// We want the frontend to attach the current trace context to requests
// when making downstream requests back to the app backend.
// This makes it easier to debug issues (such as slow backend requests during PDF generation).
// The frontend expects to see the "traceparent" and "tracestate" values as cookies (as they are easily propagated).
// It will then pass them back to the backend in the "traceparent" and "tracestate" headers as per W3C spec.
_w3cPropagator.Inject(
new PropagationContext(propagateActivity.Context, default),
generatorRequest.Cookies,
(c, k, v) =>
{
if (k != "traceparent" && k != "tracestate")
_logger.LogWarning("Unexpected key '{Key}' when propagating trace context (expected W3C)", k);

c.Add(
new PdfGeneratorCookieOptions
{
Name = $"altinn-telemetry-{k}",
Value = v,
Domain = uri.Host,
}
);
}
);
}

generatorRequest.Cookies.Add(
new PdfGeneratorCookieOptions { Value = _userTokenProvider.GetUserToken(), Domain = uri.Host }
);
Expand Down
9 changes: 9 additions & 0 deletions test/Altinn.App.Api.Tests/Controllers/PdfControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,10 @@ public async Task Request_In_Dev_Should_Generate()
var handler = new Mock<HttpMessageHandler>();
var httpClient = new HttpClient(handler.Object);

var logger = new Mock<ILogger<PdfGeneratorClient>>();

var pdfGeneratorClient = new PdfGeneratorClient(
logger.Object,
httpClient,
_pdfGeneratorSettingsOptions,
_platformSettingsOptions,
Expand Down Expand Up @@ -156,7 +159,10 @@ public async Task Request_In_Dev_Should_Include_Frontend_Version()
var handler = new Mock<HttpMessageHandler>();
var httpClient = new HttpClient(handler.Object);

var logger = new Mock<ILogger<PdfGeneratorClient>>();

var pdfGeneratorClient = new PdfGeneratorClient(
logger.Object,
httpClient,
_pdfGeneratorSettingsOptions,
_platformSettingsOptions,
Expand Down Expand Up @@ -234,7 +240,10 @@ public async Task Request_In_TT02_Should_Ignore_Frontend_Version()
var handler = new Mock<HttpMessageHandler>();
var httpClient = new HttpClient(handler.Object);

var logger = new Mock<ILogger<PdfGeneratorClient>>();

var pdfGeneratorClient = new PdfGeneratorClient(
logger.Object,
httpClient,
_pdfGeneratorSettingsOptions,
_platformSettingsOptions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,10 @@
Status: Error,
Kind: Server
},
{
ActivityName: PdfGeneratorClient.GeneratePdf,
IdFormat: W3C
},
{
ActivityName: PdfService.GenerateAndStorePdf,
Tags: [
Expand Down Expand Up @@ -392,4 +396,4 @@
}
],
Metrics: []
}
}
1 change: 1 addition & 0 deletions test/Altinn.App.Api.Tests/CustomWebApplicationFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ CancellationToken cancellationToken
request.Headers,
(c, k, v) => c.TryAddWithoutValidation(k, v)
);
Assert.Contains(request.Headers, h => h.Key == "traceparent"); // traceparent is mandatory in W3C
}
return base.SendAsync(request, cancellationToken);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
Activities: [
{
ActivityName: GET {org}/{app}/api/v1/applicationmetadata,
Tags: [
{
http.request.method: GET
},
{
http.response.status_code: 200
},
{
http.route: {org}/{app}/api/v1/applicationmetadata
},
{
network.protocol.version: 1.1
},
{
server.address: localhost
},
{
TestId: Guid_1
},
{
url.path: /tdd/contributer-restriction/api/v1/applicationmetadata
},
{
url.scheme: http
},
{
user.authentication.level: 4
},
{
user.authentication.method: Mock
},
{
user.id: 10
},
{
user.name: User10
},
{
user.party.id: Scrubbed
}
],
IdFormat: W3C,
Kind: Server
}
],
Metrics: []
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ public TelemetryEnrichingMiddlewareTests(WebApplicationFactory<Program> factory,

private (TelemetrySink Telemetry, Func<Task> Request) AnalyzeTelemetry(
string token,
bool includeTraceContext = false
bool includeTraceContext = false,
bool includePdfHeader = false
)
{
this.OverrideServicesForThisTest = (services) =>
Expand All @@ -35,6 +36,8 @@ public TelemetryEnrichingMiddlewareTests(WebApplicationFactory<Program> factory,
var telemetry = this.Services.GetRequiredService<TelemetrySink>();

client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
if (includePdfHeader)
client.DefaultRequestHeaders.Add("X-Altinn-IsPdf", "true");

return (telemetry, async () => await client.GetStringAsync($"/{org}/{app}/api/v1/applicationmetadata"));
}
Expand Down Expand Up @@ -110,4 +113,32 @@ public async Task Should_Always_Be_A_Root_Trace()

await telemetry.Snapshot(activity, c => c.ScrubMember(Telemetry.Labels.UserPartyId));
}

[Fact]
public async Task Should_Always_Be_A_Root_Trace_Unless_Pdf()
{
var partyId = Random.Shared.Next();
var principal = PrincipalUtil.GetUserPrincipal(10, partyId, 4);
var token = JwtTokenMock.GenerateToken(principal, new TimeSpan(1, 1, 1));

var (telemetry, request) = AnalyzeTelemetry(token, includeTraceContext: true, includePdfHeader: true);
ActivitySpanId parentSpanId;
using (var parentActivity = telemetry.Object.ActivitySource.StartActivity("TestParentActivity"))
{
Assert.NotNull(parentActivity);
parentSpanId = parentActivity.SpanId;
await request();
}
await telemetry.WaitForServerActivity();

var activities = telemetry.CapturedActivities;
var activity = Assert.Single(activities, a => a.Kind == ActivityKind.Server);
Assert.True(activity.IsAllDataRequested);
Assert.True(activity.Recorded);
Assert.Equal("Microsoft.AspNetCore", activity.Source.Name);
Assert.NotNull(activity.ParentId);
Assert.Equal(parentSpanId, activity.ParentSpanId);

await telemetry.Snapshot(activity, c => c.ScrubMember(Telemetry.Labels.UserPartyId));
}
}
4 changes: 4 additions & 0 deletions test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,9 @@ public async Task ValidRequest_ShouldReturnPdf()
);

var httpClient = new HttpClient(delegatingHandler);
var logger = new Mock<ILogger<PdfGeneratorClient>>();
var pdfGeneratorClient = new PdfGeneratorClient(
logger.Object,
httpClient,
_pdfGeneratorSettingsOptions,
_platformSettingsOptions,
Expand Down Expand Up @@ -114,7 +116,9 @@ public async Task ValidRequest_PdfGenerationFails_ShouldThrowException()
);

var httpClient = new HttpClient(delegatingHandler);
var logger = new Mock<ILogger<PdfGeneratorClient>>();
var pdfGeneratorClient = new PdfGeneratorClient(
logger.Object,
httpClient,
_pdfGeneratorSettingsOptions,
_platformSettingsOptions,
Expand Down
Loading