Skip to content

Commit

Permalink
Pass tracecontext to pdf generator
Browse files Browse the repository at this point in the history
  • Loading branch information
martinothamar committed Nov 22, 2024
1 parent eead33f commit fcd922f
Show file tree
Hide file tree
Showing 9 changed files with 167 additions and 6 deletions.
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 @@ -356,6 +356,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 @@ -370,8 +379,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 @@ -392,7 +402,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 @@ -405,7 +415,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
Expand Up @@ -344,7 +344,7 @@ internal static Activity SetProblemDetails(this Activity activity, ProblemDetail
internal static void Errored(this Activity activity, Exception? exception = null, string? error = null)
{
activity.SetStatus(ActivityStatusCode.Error, error);
if(exception is not null)
if (exception is not null)
{
activity.AddException(exception);
}
Expand Down
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 @@ -83,7 +83,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 @@ -163,7 +166,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 @@ -245,7 +251,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
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

0 comments on commit fcd922f

Please sign in to comment.