Skip to content

Commit 385a57a

Browse files
eskebabEspen Gulbrandsen
andauthored
Feature/658 log entries (#672)
* Add service layer and repository layer for trace log * Add tracelogservice tests * Add log entry to storage controller endpoint * Make GetTestClient generic * Add transfer object for webhook postback log entry * Fix tests * Add activity endpoints to trace log service * Fix comments * Add traceLogService injections for tests * Fix LogEntryDto * Update traceLogService to reflect log entry dto * Add controller tests * Add tracelog tests * Fix usings order and format * Add parameters to log error * Add better documentation to CreateWebhookResponseEntry method * Add invalid GUID test * Create explicit dto parameter checks * Create explicit dto parameter checks * Add tests for good coverage * Add EventsClient test for post log * Add separate validation check for log entry dto * Add invalid subscription activity to enum * Do cleanup * Add constants class * Add eventsClient dependency to WebhookService * Add cloudEventType check for logging correct activity * Fix warnings * Fix issues * Fix condition coverage traceLogService * Fix warnings * Add changes based on feedback * Remove try catch from logs action * Add try catch to logWebhookHttpStatusCode in function project * Add changes based on feedback * Move logs to its own controller * Add schema filter example for log entry * Add changes based on feedback * Add changes based on feedback --------- Co-authored-by: Espen Gulbrandsen <espen.gulbrandsen@digdir.no>
1 parent a02951f commit 385a57a

29 files changed

+665
-91
lines changed

src/Events.Functions/Clients/EventsClient.cs

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@
33
using System.Net.Http;
44
using System.Security.Cryptography.X509Certificates;
55
using System.Text;
6+
using System.Text.Json;
67
using System.Threading.Tasks;
78

89
using Altinn.Common.AccessTokenClient.Services;
910
using Altinn.Platform.Events.Functions.Clients.Interfaces;
1011
using Altinn.Platform.Events.Functions.Configuration;
1112
using Altinn.Platform.Events.Functions.Extensions;
13+
using Altinn.Platform.Events.Functions.Models;
1214
using Altinn.Platform.Events.Functions.Services.Interfaces;
1315

1416
using CloudNative.CloudEvents;
@@ -116,19 +118,48 @@ public async Task ValidateSubscription(int subscriptionId)
116118

117119
if (response.StatusCode == HttpStatusCode.NotFound)
118120
{
119-
_logger.LogError("Attempting to validate non existing subscription {subscriptionId}", subscriptionId);
121+
_logger.LogError("Attempting to validate non existing subscription {SubscriptionId}", subscriptionId);
120122
return;
121123
}
122-
124+
123125
if (!response.IsSuccessStatusCode)
124126
{
125127
_logger.LogError(
126-
$"// Validate subscription with id {subscriptionId} failed with status code {response.StatusCode}");
128+
"Validate subscription with id {SubscriptionId} failed with status code {StatusCode}", subscriptionId, response.StatusCode);
127129
throw new HttpRequestException(
128130
$"// Validate subscription with id {subscriptionId} failed with status code {response.StatusCode}");
129131
}
130132
}
131133

134+
/// <inheritdoc/>
135+
public async Task LogWebhookHttpStatusCode(CloudEventEnvelope cloudEventEnvelope, HttpStatusCode statusCode)
136+
{
137+
try
138+
{
139+
var endpoint = "storage/events/logs";
140+
141+
var logEntryData = new LogEntryDto
142+
{
143+
CloudEventId = cloudEventEnvelope.CloudEvent.Id,
144+
CloudEventType = cloudEventEnvelope.CloudEvent.Type,
145+
CloudEventResource = cloudEventEnvelope.CloudEvent["resource"]?.ToString(),
146+
Consumer = cloudEventEnvelope.Consumer,
147+
Endpoint = cloudEventEnvelope.Endpoint,
148+
SubscriptionId = cloudEventEnvelope.SubscriptionId,
149+
StatusCode = statusCode,
150+
};
151+
152+
StringContent httpContent = new(JsonSerializer.Serialize(logEntryData), Encoding.UTF8, "application/json");
153+
var accessToken = await GenerateAccessToken();
154+
155+
await _client.PostAsync(endpoint, httpContent, accessToken);
156+
}
157+
catch (Exception e)
158+
{
159+
_logger.LogError(e, "Failed to log trace log webhook status code for cloud event id {CloudEventId}", cloudEventEnvelope.CloudEvent.Id);
160+
}
161+
}
162+
132163
private async Task<(bool Success, HttpStatusCode StatusCode)> PostCloudEventToEndpoint(CloudEvent cloudEvent, string endpoint)
133164
{
134165
StringContent httpContent = new(cloudEvent.Serialize(), Encoding.UTF8, "application/cloudevents+json");

src/Events.Functions/Clients/Interfaces/IEventsClient.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
using System.Threading.Tasks;
2+
3+
using Altinn.Platform.Events.Functions.Models;
4+
25
using CloudNative.CloudEvents;
36

47
namespace Altinn.Platform.Events.Functions.Clients.Interfaces
@@ -15,6 +18,12 @@ public interface IEventsClient
1518
/// <returns>CloudEvent as stored in the database</returns>
1619
Task SaveCloudEvent(CloudEvent cloudEvent);
1720

21+
/// <summary>
22+
/// Posts an event with the given statusCode
23+
/// </summary>
24+
/// <returns></returns>
25+
Task LogWebhookHttpStatusCode(CloudEventEnvelope cloudEventEnvelope, System.Net.HttpStatusCode statusCode);
26+
1827
/// <summary>
1928
/// Send cloudEvent for outbound processing.
2029
/// </summary>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace Altinn.Platform.Events.Functions.Constants;
2+
3+
/// <summary>
4+
/// Shared constants for Events.Functions project
5+
/// </summary>
6+
public static class EventConstants
7+
{
8+
/// <summary>
9+
/// The cloud event type for subscription validation
10+
/// </summary>
11+
public const string ValidationType = "platform.events.validatesubscription";
12+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
using System;
2+
using System.Net;
3+
4+
namespace Altinn.Platform.Events.Functions.Models
5+
{
6+
/// <summary>
7+
/// Data transfer object for posting a log event after receiving a webhook response.
8+
/// </summary>
9+
public record LogEntryDto
10+
{
11+
/// <summary>
12+
/// The cloud event id associated with the logged event
13+
/// </summary>
14+
public string CloudEventId { get; set; }
15+
16+
/// <summary>
17+
/// The resource associated with the cloud event
18+
/// </summary>
19+
public string CloudEventResource { get; set; }
20+
21+
/// <summary>
22+
/// The type associated with the logged event
23+
/// </summary>
24+
public string CloudEventType { get; set; }
25+
26+
/// <summary>
27+
/// The subscription id associated with the post action.
28+
/// </summary>
29+
public int SubscriptionId { get; set; }
30+
31+
/// <summary>
32+
/// The consumer of the event
33+
/// </summary>
34+
public string Consumer { get; set; }
35+
36+
/// <summary>
37+
/// The consumers webhook endpoint
38+
/// </summary>
39+
public Uri Endpoint { get; set; }
40+
41+
/// <summary>
42+
/// The staus code returned from the subscriber endpoint
43+
/// </summary>
44+
public HttpStatusCode StatusCode { get; set; }
45+
}
46+
}
Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
using System.Threading.Tasks;
2+
23
using Altinn.Platform.Events.Functions.Models;
34

4-
namespace Altinn.Platform.Events.Functions.Services.Interfaces
5+
namespace Altinn.Platform.Events.Functions.Services.Interfaces;
6+
7+
/// <summary>
8+
/// Interface to send content to webhooks
9+
/// </summary>
10+
public interface IWebhookService
511
{
612
/// <summary>
7-
/// Interface to send content to webhooks
13+
/// Send cloudevent to webhook
814
/// </summary>
9-
public interface IWebhookService
10-
{
11-
/// <summary>
12-
/// Send cloudevent to webhook
13-
/// </summary>
14-
/// <param name="envelope">CloudEventEnvelope, includes content and uri</param>
15-
Task Send(CloudEventEnvelope envelope);
16-
}
15+
/// <param name="envelope">CloudEventEnvelope, includes content and uri</param>
16+
Task Send(CloudEventEnvelope envelope);
1717
}

src/Events.Functions/Services/WebhookService.cs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
using System.Text;
44
using System.Threading.Tasks;
55

6+
using Altinn.Platform.Events.Functions.Clients.Interfaces;
67
using Altinn.Platform.Events.Functions.Configuration;
78
using Altinn.Platform.Events.Functions.Extensions;
89
using Altinn.Platform.Events.Functions.Models;
910
using Altinn.Platform.Events.Functions.Models.Payloads;
1011
using Altinn.Platform.Events.Functions.Services.Interfaces;
11-
1212
using Microsoft.Extensions.Logging;
1313
using Microsoft.Extensions.Options;
1414

@@ -20,18 +20,19 @@ namespace Altinn.Platform.Events.Functions.Services
2020
public class WebhookService : IWebhookService
2121
{
2222
private readonly HttpClient _client;
23+
private readonly IEventsClient _eventsClient;
2324
private readonly ILogger _logger;
2425
private readonly string _slackUri = "hooks.slack.com";
2526

2627
/// <summary>
2728
/// Initializes a new instance of the <see cref="WebhookService"/> class.
2829
/// </summary>
2930
public WebhookService(
30-
HttpClient client, IOptions<EventsOutboundSettings> eventOutboundSettings, ILogger<WebhookService> logger)
31+
HttpClient client, IEventsClient eventsClient, IOptions<EventsOutboundSettings> eventOutboundSettings, ILogger<WebhookService> logger)
3132
{
3233
_client = client;
34+
_eventsClient = eventsClient;
3335
_logger = logger;
34-
3536
_client.Timeout = TimeSpan.FromSeconds(eventOutboundSettings.Value.RequestTimeout);
3637
}
3738

@@ -44,17 +45,21 @@ public async Task Send(CloudEventEnvelope envelope)
4445
try
4546
{
4647
HttpResponseMessage response = await _client.PostAsync(envelope.Endpoint, httpContent);
48+
49+
// log response from webhook to Events
50+
await _eventsClient.LogWebhookHttpStatusCode(envelope, response.StatusCode);
51+
4752
if (!response.IsSuccessStatusCode)
4853
{
4954
string reason = await response.Content.ReadAsStringAsync();
50-
_logger.LogError($"// WebhookService // Send // Failed to send cloud event id {envelope.CloudEvent.Id}, subscriptionId: {envelope.SubscriptionId}. \nReason: {reason} \nResponse: {response}");
55+
_logger.LogError("WebhookService send failed to send cloud event id {CloudEventId} {SubscriptionId} {Reason} {Response}", envelope.CloudEvent.Id, envelope.SubscriptionId, reason, response);
5156

5257
throw new HttpRequestException(reason);
5358
}
5459
}
5560
catch (Exception e)
5661
{
57-
_logger.LogError(e, $"// Send to webhook with subscriptionId: {envelope.SubscriptionId} failed with error message {e.Message}");
62+
_logger.LogError(e, "Send to webhook with {SubscriptionId} failed with error message {Message}", envelope.SubscriptionId, e.Message);
5863
throw;
5964
}
6065
}

src/Events.Functions/SubscriptionValidation.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ internal CloudEventEnvelope CreateValidateEvent(Subscription subscription)
6363
{
6464
Id = Guid.NewGuid().ToString(),
6565
Source = new Uri(_platformSettings.ApiEventsEndpoint + "subscriptions/" + subscription.Id),
66-
Type = "platform.events.validatesubscription",
66+
Type = Constants.EventConstants.ValidationType,
6767
}
6868
};
6969

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
using Altinn.Platform.Events.Configuration;
4+
using Altinn.Platform.Events.Models;
5+
using Altinn.Platform.Events.Services.Interfaces;
6+
7+
using Microsoft.AspNetCore.Authorization;
8+
using Microsoft.AspNetCore.Http;
9+
using Microsoft.AspNetCore.Mvc;
10+
using Swashbuckle.AspNetCore.Annotations;
11+
12+
namespace Altinn.Platform.Events.Controllers
13+
{
14+
/// <summary>
15+
/// Controller for logging event operations to persistence
16+
/// </summary>
17+
/// <param name="traceLogService">Service for logging event operations to persistence</param>
18+
[Route("events/api/v1/storage/events/logs")]
19+
[ApiController]
20+
[SwaggerTag("Private API")]
21+
public class LogsController(ITraceLogService traceLogService) : ControllerBase
22+
{
23+
private readonly ITraceLogService _traceLogService = traceLogService;
24+
25+
/// <summary>
26+
/// Create a new trace log for cloud event with a status code.
27+
/// </summary>
28+
/// <param name="logEntry">The event wrapper associated with the event for logging <see cref="CloudEventEnvelope"/></param>
29+
/// <returns></returns>
30+
[Authorize(Policy = AuthorizationConstants.POLICY_PLATFORM_ACCESS)]
31+
[HttpPost]
32+
[Consumes("application/json")]
33+
[SwaggerResponse(201, Type = typeof(Guid))]
34+
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
35+
[ProducesResponseType(StatusCodes.Status400BadRequest)]
36+
public async Task<IActionResult> Logs([FromBody] LogEntryDto logEntry)
37+
{
38+
var result = await _traceLogService.CreateWebhookResponseEntry(logEntry);
39+
40+
if (string.IsNullOrEmpty(result))
41+
{
42+
return BadRequest();
43+
}
44+
45+
return Created();
46+
}
47+
}
48+
}

src/Events/Controllers/StorageController.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
using Microsoft.AspNetCore.Http;
1010
using Microsoft.AspNetCore.Mvc;
1111
using Microsoft.Extensions.Logging;
12-
1312
using Swashbuckle.AspNetCore.Annotations;
1413

1514
namespace Altinn.Platform.Events.Controllers

src/Events/Models/LogEntryDto.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#nullable enable
2-
32
using System;
43
using System.Net;
4+
55
using CloudNative.CloudEvents;
66

77
namespace Altinn.Platform.Events.Models

0 commit comments

Comments
 (0)