Skip to content

Commit 623fda0

Browse files
authored
Introduced NLog.Extensions.AzureEventGrid (#131)
1 parent 18f8d77 commit 623fda0

File tree

13 files changed

+587
-8
lines changed

13 files changed

+587
-8
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
| **NLog.Extensions.AzureBlobStorage** | [![NuGet](https://img.shields.io/nuget/v/NLog.Extensions.AzureBlobStorage.svg)](https://www.nuget.org/packages/NLog.Extensions.AzureBlobStorage/) | Azure Blob Storage | [![](https://img.shields.io/badge/Readme-Docs-blue)](src/NLog.Extensions.AzureBlobStorage/README.md) |
88
| **NLog.Extensions.AzureDataTables** | [![NuGet](https://img.shields.io/nuget/v/NLog.Extensions.AzureDataTables.svg)](https://www.nuget.org/packages/NLog.Extensions.AzureDataTables/) | Azure Table Storage or Azure CosmosDb Tables | [![](https://img.shields.io/badge/Readme-Docs-blue)](src/NLog.Extensions.AzureDataTables/README.md) |
99
| **NLog.Extensions.AzureEventHub** | [![NuGet](https://img.shields.io/nuget/v/NLog.Extensions.AzureEventHub.svg)](https://www.nuget.org/packages/NLog.Extensions.AzureEventHub/) | Azure EventHubs | [![](https://img.shields.io/badge/Readme-Docs-blue)](src/NLog.Extensions.AzureEventHub/README.md) |
10+
| **NLog.Extensions.AzureEventGrid** | [![NuGet](https://img.shields.io/nuget/v/NLog.Extensions.AzureEventGrid.svg)](https://www.nuget.org/packages/NLog.Extensions.AzureEventGrid/) | Azure Event Grid | [![](https://img.shields.io/badge/Readme-Docs-blue)](src/NLog.Extensions.AzureEventGrid/README.md) |
1011
| **NLog.Extensions.AzureQueueStorage** | [![NuGet](https://img.shields.io/nuget/v/NLog.Extensions.AzureQueueStorage.svg)](https://www.nuget.org/packages/NLog.Extensions.AzureQueueStorage/) | Azure Queue Storage | [![](https://img.shields.io/badge/Readme-Docs-blue)](src/NLog.Extensions.AzureQueueStorage/README.md) |
1112
| **NLog.Extensions.AzureServiceBus** | [![NuGet](https://img.shields.io/nuget/v/NLog.Extensions.AzureServiceBus.svg)](https://www.nuget.org/packages/NLog.Extensions.AzureServiceBus/) | Azure Service Bus | [![](https://img.shields.io/badge/Readme-Docs-blue)](src/NLog.Extensions.AzureServiceBus/README.md) |
1213
| **NLog.Extensions.AzureAccessToken** | [![NuGet](https://img.shields.io/nuget/v/NLog.Extensions.AzureAccessToken.svg)](https://www.nuget.org/packages/NLog.Extensions.AzureAccessToken/) | Azure App Authentication Access Token for Managed Identity | [![](https://img.shields.io/badge/Readme-Docs-blue)](src/NLog.Extensions.AzureAccessToken/README.md) |
@@ -24,6 +25,7 @@ and so [NLog.Extensions.AzureCosmosTable](https://www.nuget.org/packages/NLog.Ex
2425
<add assembly="NLog.Extensions.AzureDataTables" />
2526
<add assembly="NLog.Extensions.AzureQueueStorage" />
2627
<add assembly="NLog.Extensions.AzureEventHub" />
28+
<add assembly="NLog.Extensions.AzureEventGrid" />
2729
<add assembly="NLog.Extensions.AzureServiceBus" />
2830
<add assembly="NLog.Extensions.AzureAccessToken" />
2931
</extensions>

appveyor.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ build_script:
77
- cmd: msbuild /t:Pack src\NLog.Extensions.AzureBlobStorage /p:Configuration=Release /p:IncludeSymbols=true /p:SymbolPackageFormat=snupkg /p:ContinuousIntegrationBuild=true /p:EmbedUntrackedSources=true /p:PublishRepositoryUrl=true /verbosity:minimal
88
- cmd: msbuild /t:Pack src\NLog.Extensions.AzureDataTables /p:Configuration=Release /p:IncludeSymbols=true /p:SymbolPackageFormat=snupkg /p:ContinuousIntegrationBuild=true /p:EmbedUntrackedSources=true /p:PublishRepositoryUrl=true /verbosity:minimal
99
- cmd: msbuild /t:Pack src\NLog.Extensions.AzureQueueStorage /p:Configuration=Release /p:IncludeSymbols=true /p:SymbolPackageFormat=snupkg /p:ContinuousIntegrationBuild=true /p:EmbedUntrackedSources=true /p:PublishRepositoryUrl=true /verbosity:minimal
10+
- cmd: msbuild /t:Pack src\NLog.Extensions.AzureEventGrid /p:Configuration=Release /p:IncludeSymbols=true /p:SymbolPackageFormat=snupkg /p:ContinuousIntegrationBuild=true /p:EmbedUntrackedSources=true /p:PublishRepositoryUrl=true /verbosity:minimal
1011
- cmd: msbuild /t:Pack src\NLog.Extensions.AzureEventHub /p:Configuration=Release /p:IncludeSymbols=true /p:SymbolPackageFormat=snupkg /p:ContinuousIntegrationBuild=true /p:EmbedUntrackedSources=true /p:PublishRepositoryUrl=true /verbosity:minimal
1112
- cmd: msbuild /t:Pack src\NLog.Extensions.AzureServiceBus /p:Configuration=Release /p:IncludeSymbols=true /p:SymbolPackageFormat=snupkg /p:ContinuousIntegrationBuild=true /p:EmbedUntrackedSources=true /p:PublishRepositoryUrl=true /verbosity:minimal
1213
- cmd: msbuild /t:Pack src\NLog.Extensions.AzureAccessToken /p:Configuration=Release /p:IncludeSymbols=true /p:SymbolPackageFormat=snupkg /p:ContinuousIntegrationBuild=true /p:EmbedUntrackedSources=true /p:PublishRepositoryUrl=true /verbosity:minimal
@@ -15,6 +16,7 @@ test_script:
1516
- cmd: dotnet test test\NLog.Extensions.AzureBlobStorage.Tests /p:Configuration=Release --verbosity minimal
1617
- cmd: dotnet test test\NLog.Extensions.AzureDataTables.Tests /p:Configuration=Release --verbosity minimal
1718
- cmd: dotnet test test\NLog.Extensions.AzureQueueStorage.Tests /p:Configuration=Release --verbosity minimal
19+
- cmd: dotnet test test\NLog.Extensions.AzureEventGrid.Tests /p:Configuration=Release --verbosity minimal
1820
- cmd: dotnet test test\NLog.Extensions.AzureEventHub.Tests /p:Configuration=Release --verbosity minimal
1921
- cmd: dotnet test test\NLog.Extensions.AzureServiceBus.Tests /p:Configuration=Release --verbosity minimal
2022
- cmd: dotnet test test\NLog.Extensions.AzureAccessToken.Tests /p:Configuration=Release --verbosity minimal
Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Text;
4+
using System.Threading.Tasks;
5+
using System.Threading;
6+
using Azure;
7+
using Azure.Messaging;
8+
using Azure.Messaging.EventGrid;
9+
using NLog.Common;
10+
using NLog.Config;
11+
using NLog.Layouts;
12+
using NLog.Extensions.AzureStorage;
13+
14+
namespace NLog.Targets
15+
{
16+
/// <summary>
17+
/// Azure Event Grid NLog Target
18+
/// </summary>
19+
[Target("AzureEventGrid")]
20+
public class EventGridTarget : AsyncTaskTarget
21+
{
22+
private readonly IEventGridService _eventGridService;
23+
private readonly char[] _reusableEncodingBuffer = new char[32 * 1024]; // Avoid large-object-heap
24+
25+
/// <summary>
26+
/// The topic endpoint. For example, "https://TOPIC-NAME.REGION-NAME-1.eventgrid.azure.net/api/events"
27+
/// </summary>
28+
public Layout Topic { get; set; }
29+
30+
/// <summary>
31+
/// A resource path relative to the topic path.
32+
/// </summary>
33+
public Layout GridEventSubject { get; set; }
34+
35+
/// <summary>
36+
/// Identifies the context in which an event happened. The combination of id and source must be unique for each distinct event.
37+
/// </summary>
38+
public Layout CloudEventSource { get; set; }
39+
40+
/// <summary>
41+
/// The type of the event that occurred. For example, "Contoso.Items.ItemReceived".
42+
/// </summary>
43+
public Layout EventType { get; set; }
44+
45+
/// <summary>
46+
/// Content type of the payload. A content type different from "application/json" should be specified if payload is not JSON.
47+
/// </summary>
48+
public Layout ContentType { get; set; }
49+
50+
/// <summary>
51+
/// The schema version of the data object.
52+
/// </summary>
53+
public Layout DataSchema { get; set; }
54+
55+
/// <summary>
56+
/// Input for AzureKeyCredential for EventGridPublisherClient constructor
57+
/// </summary>
58+
public Layout AccessKey { get; set; }
59+
60+
/// <summary>
61+
/// Alternative to AccessKey. Input for <see cref="Azure.Identity.DefaultAzureCredential"/>
62+
/// </summary>
63+
public Layout TenantIdentity { get; set; }
64+
65+
/// <summary>
66+
/// Alternative to AccessKey. Input for <see cref="Azure.Identity.DefaultAzureCredential"/>
67+
/// </summary>
68+
public Layout ResourceIdentity { get; set; }
69+
70+
/// <summary>
71+
/// Alternative to AccessKey. Input for <see cref="Azure.Identity.DefaultAzureCredential"/>
72+
/// </summary>
73+
public Layout ClientIdentity { get; set; }
74+
75+
/// <summary>
76+
/// Gets a list of message properties aka. custom CloudEvent Extension Attributes
77+
/// </summary>
78+
[ArrayParameter(typeof(TargetPropertyWithContext), "messageproperty")]
79+
public IList<TargetPropertyWithContext> MessageProperties { get => ContextProperties; }
80+
81+
public EventGridTarget()
82+
: this(new EventGridService())
83+
{
84+
}
85+
86+
internal EventGridTarget(IEventGridService eventGridService)
87+
{
88+
_eventGridService = eventGridService;
89+
}
90+
91+
/// <inheritdoc />
92+
protected override void InitializeTarget()
93+
{
94+
base.InitializeTarget();
95+
96+
for (int i = 0; i < MessageProperties.Count; ++i)
97+
{
98+
var messageProperty = MessageProperties[i];
99+
if (!IsValidExtensionAttribue(messageProperty.Name))
100+
{
101+
var messagePropertyName = (messageProperty.Name ?? string.Empty).Trim().ToLowerInvariant();
102+
if (!IsValidExtensionAttribue(messagePropertyName))
103+
{
104+
messagePropertyName = string.Join(string.Empty, System.Linq.Enumerable.Where(messagePropertyName, chr => !IsInvalidExtensionAttribueChar(chr)));
105+
}
106+
107+
InternalLogger.Debug("AzureEventGridTarget(Name={0}): Fixing MessageProperty-Name from '{1}' to '{2}'", Name, messageProperty.Name, messagePropertyName);
108+
messageProperty.Name = messagePropertyName;
109+
}
110+
}
111+
112+
string topic = string.Empty;
113+
string tenantIdentity = string.Empty;
114+
string resourceIdentity = string.Empty;
115+
string clientIdentity = string.Empty;
116+
string accessKey = string.Empty;
117+
118+
var defaultLogEvent = LogEventInfo.CreateNullEvent();
119+
120+
try
121+
{
122+
topic = Topic?.Render(defaultLogEvent);
123+
tenantIdentity = TenantIdentity?.Render(defaultLogEvent);
124+
resourceIdentity = ResourceIdentity?.Render(defaultLogEvent);
125+
clientIdentity = ClientIdentity?.Render(defaultLogEvent);
126+
accessKey = AccessKey?.Render(defaultLogEvent);
127+
128+
_eventGridService.Connect(topic, tenantIdentity, resourceIdentity, clientIdentity, accessKey);
129+
InternalLogger.Debug("AzureEventGridTarget(Name={0}): Initialized", Name);
130+
}
131+
catch (Exception ex)
132+
{
133+
InternalLogger.Error(ex, "AzureEventGridTarget(Name={0}): Failed to create EventGridPublisherClient with Topic={1}.", Name, topic);
134+
throw;
135+
}
136+
}
137+
138+
protected override Task WriteAsyncTask(LogEventInfo logEvent, CancellationToken cancellationToken)
139+
{
140+
try
141+
{
142+
if (CloudEventSource is null)
143+
{
144+
var gridEvent = CreateGridEvent(logEvent);
145+
return _eventGridService.SendEventAsync(gridEvent, cancellationToken);
146+
}
147+
else
148+
{
149+
var cloudEvent = CreateCloudEvent(logEvent);
150+
return _eventGridService.SendEventAsync(cloudEvent, cancellationToken);
151+
}
152+
}
153+
catch (Exception ex)
154+
{
155+
InternalLogger.Error(ex, "AzureEventGridTarget(Name={0}): Failed sending logevent to Topic={1}", Name, _eventGridService?.Topic);
156+
throw;
157+
}
158+
}
159+
160+
private sealed class EventGridService : IEventGridService
161+
{
162+
EventGridPublisherClient _client;
163+
164+
public string Topic { get; private set; }
165+
166+
public void Connect(string topic, string tenantIdentity, string resourceIdentifier, string clientIdentity, string accessKey)
167+
{
168+
Topic = topic;
169+
170+
if (!string.IsNullOrWhiteSpace(accessKey))
171+
{
172+
_client = new EventGridPublisherClient(new Uri(topic), new AzureKeyCredential(accessKey));
173+
}
174+
else
175+
{
176+
var tokenCredentials = AzureCredentialHelpers.CreateTokenCredentials(clientIdentity, tenantIdentity, resourceIdentifier);
177+
_client = new EventGridPublisherClient(new Uri(topic), tokenCredentials);
178+
}
179+
}
180+
181+
public Task SendEventAsync(EventGridEvent gridEvent, CancellationToken cancellationToken)
182+
{
183+
return _client.SendEventAsync(gridEvent, cancellationToken);
184+
}
185+
186+
public Task SendEventAsync(CloudEvent cloudEvent, CancellationToken cancellationToken)
187+
{
188+
return _client.SendEventAsync(cloudEvent, cancellationToken);
189+
}
190+
}
191+
192+
private CloudEvent CreateCloudEvent(LogEventInfo logEvent)
193+
{
194+
var eventDataBody = RenderLogEvent(Layout, logEvent) ?? string.Empty;
195+
var eventSource = RenderLogEvent(CloudEventSource, logEvent) ?? string.Empty;
196+
var eventType = RenderLogEvent(EventType, logEvent) ?? string.Empty;
197+
var eventDataSchema = RenderLogEvent(DataSchema, logEvent) ?? string.Empty;
198+
var eventContentType = RenderLogEvent(ContentType, logEvent) ?? string.Empty;
199+
var cloudEvent = new CloudEvent(eventSource, eventType, new BinaryData(EncodeToUTF8(eventDataBody)), eventContentType, CloudEventDataFormat.Binary);
200+
cloudEvent.Time = logEvent.TimeStamp.ToUniversalTime();
201+
202+
if (!string.IsNullOrEmpty(eventDataSchema))
203+
{
204+
cloudEvent.DataSchema = eventDataSchema;
205+
}
206+
207+
for (int i = 0; i < MessageProperties.Count; ++i)
208+
{
209+
var messageProperty = MessageProperties[i];
210+
if (string.IsNullOrEmpty(messageProperty.Name))
211+
continue;
212+
213+
var propertyValue = RenderLogEvent(messageProperty.Layout, logEvent);
214+
if (string.IsNullOrEmpty(propertyValue) && !messageProperty.IncludeEmptyValue)
215+
continue;
216+
217+
cloudEvent.ExtensionAttributes.Add(messageProperty.Name, propertyValue);
218+
}
219+
220+
return cloudEvent;
221+
}
222+
223+
private EventGridEvent CreateGridEvent(LogEventInfo logEvent)
224+
{
225+
var eventDataBody = RenderLogEvent(Layout, logEvent) ?? string.Empty;
226+
var eventSubject = RenderLogEvent(GridEventSubject, logEvent) ?? string.Empty;
227+
var eventType = RenderLogEvent(EventType, logEvent) ?? string.Empty;
228+
var eventDataSchema = RenderLogEvent(DataSchema, logEvent) ?? string.Empty;
229+
var gridEvent = new EventGridEvent(eventSubject, eventType, eventDataSchema, new BinaryData(EncodeToUTF8(eventDataBody)));
230+
gridEvent.EventTime = logEvent.TimeStamp.ToUniversalTime();
231+
return gridEvent;
232+
}
233+
234+
private byte[] EncodeToUTF8(string eventDataBody)
235+
{
236+
if (eventDataBody.Length < _reusableEncodingBuffer.Length)
237+
{
238+
lock (_reusableEncodingBuffer)
239+
{
240+
eventDataBody.CopyTo(0, _reusableEncodingBuffer, 0, eventDataBody.Length);
241+
return Encoding.UTF8.GetBytes(_reusableEncodingBuffer, 0, eventDataBody.Length);
242+
}
243+
}
244+
else
245+
{
246+
return Encoding.UTF8.GetBytes(eventDataBody); // Calls string.ToCharArray()
247+
}
248+
}
249+
250+
private static bool IsValidExtensionAttribue(string propertyValue)
251+
{
252+
for (int i = 0; i < propertyValue.Length; ++i)
253+
{
254+
char chr = propertyValue[i];
255+
if (IsInvalidExtensionAttribueChar(chr))
256+
{
257+
return false;
258+
}
259+
}
260+
return true;
261+
}
262+
263+
private static bool IsInvalidExtensionAttribueChar(char chr)
264+
{
265+
return (chr < 'a' || chr > 'z') && (chr < '0' || chr > '9');
266+
}
267+
}
268+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Text;
4+
using Azure.Messaging.EventGrid;
5+
using System.Threading.Tasks;
6+
using Azure.Messaging;
7+
using System.Threading;
8+
9+
namespace NLog.Extensions.AzureStorage
10+
{
11+
internal interface IEventGridService
12+
{
13+
string Topic { get; }
14+
15+
void Connect(string topic, string tenantIdentity, string resourceIdentifier, string clientIdentity, string accessKey);
16+
17+
Task SendEventAsync(EventGridEvent gridEvent, CancellationToken cancellationToken);
18+
19+
Task SendEventAsync(CloudEvent cloudEvent, CancellationToken cancellationToken);
20+
}
21+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFrameworks>netstandard2.0</TargetFrameworks>
5+
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
6+
7+
<Version>4.0.0</Version>
8+
9+
<Description>NLog EventGridTarget for writing to Azure Event Grid</Description>
10+
<Authors>jdetmar</Authors>
11+
<CurrentYear>$([System.DateTime]::Now.ToString(yyyy))</CurrentYear>
12+
<Copyright>Copyright (c) $(CurrentYear) - jdetmar</Copyright>
13+
14+
<PackageTags>NLog;azure;EventGrid;Event;Grid;CloudEvents;log;logging</PackageTags>
15+
<PackageIcon>logo64.png</PackageIcon>
16+
<PackageProjectUrl>https://github.com/JDetmar/NLog.Extensions.AzureStorage</PackageProjectUrl>
17+
<RepositoryType>git</RepositoryType>
18+
<RepositoryUrl>https://github.com/JDetmar/NLog.Extensions.AzureStorage.git</RepositoryUrl>
19+
<PackageLicenseExpression>MIT</PackageLicenseExpression>
20+
<PackageReleaseNotes>
21+
- Initial Release
22+
23+
Docs: https://github.com/JDetmar/NLog.Extensions.AzureStorage/blob/master/src/NLog.Extensions.AzureEventGrid/README.md
24+
</PackageReleaseNotes>
25+
</PropertyGroup>
26+
27+
<ItemGroup>
28+
<Compile Include="..\NLog.Extensions.AzureStorage\AzureCredentialHelper.cs" Link="AzureCredentialHelper.cs" />
29+
</ItemGroup>
30+
31+
<ItemGroup>
32+
<None Include="../../logo64.png" Link="logo64.png" Pack="true" PackagePath="" Visible="false" />
33+
</ItemGroup>
34+
35+
<ItemGroup>
36+
<PackageReference Include="Azure.Messaging.EventGrid" Version="4.13.0" />
37+
<PackageReference Include="Azure.Identity" Version="1.8.2" />
38+
<PackageReference Include="NLog" Version="4.7.15" />
39+
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
40+
</ItemGroup>
41+
42+
43+
</Project>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
using System.Reflection;
2+
using System.Runtime.CompilerServices;
3+
using System.Runtime.InteropServices;
4+
5+
[assembly: ComVisible(false)]
6+
7+
[assembly: InternalsVisibleTo("NLog.Extensions.AzureEventGrid.Tests")]

0 commit comments

Comments
 (0)