Skip to content

Commit 274c913

Browse files
authored
Tenant header added (#194)
Added tenant headed to handle multi-tenancy setup - https://grafana.com/docs/loki/latest/operations/multi-tenancy/
1 parent 456dc9e commit 274c913

File tree

4 files changed

+78
-0
lines changed

4 files changed

+78
-0
lines changed

src/Serilog.Sinks.Grafana.Loki/HttpClients/BaseLokiHttpClient.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
using System.Net.Http.Headers;
1212
using System.Text;
13+
using System.Text.RegularExpressions;
1314

1415
namespace Serilog.Sinks.Grafana.Loki.HttpClients;
1516

@@ -24,6 +25,16 @@ public abstract class BaseLokiHttpClient : ILokiHttpClient
2425
/// </summary>
2526
protected readonly HttpClient HttpClient;
2627

28+
/// <summary>
29+
/// Header used for passing tenant ID. See <a href="https://grafana.com/docs/loki/latest/operations/multi-tenancy/">docs</a>.
30+
/// </summary>
31+
private const string TenantHeader = "X-Scope-OrgID";
32+
33+
/// <summary>
34+
/// Regex for Tenant ID validation.
35+
/// </summary>
36+
private static readonly Regex TenantIdValueRegex = new Regex(@"^[a-zA-Z0-9]*$");
37+
2738
/// <summary>
2839
/// Initializes a new instance of the <see cref="BaseLokiHttpClient"/> class.
2940
/// </summary>
@@ -57,6 +68,29 @@ public virtual void SetCredentials(LokiCredentials? credentials)
5768
headers.Authorization = new AuthenticationHeaderValue("Basic", token);
5869
}
5970

71+
/// <inheritdoc/>
72+
public virtual void SetTenant(string? tenant)
73+
{
74+
if (string.IsNullOrEmpty(tenant))
75+
{
76+
return;
77+
}
78+
79+
if (!TenantIdValueRegex.IsMatch(tenant))
80+
{
81+
throw new ArgumentException($"{tenant} argument does not follow rule for Tenant ID", nameof(tenant));
82+
}
83+
84+
var headers = HttpClient.DefaultRequestHeaders;
85+
86+
if (headers.Any(h => h.Key == TenantHeader))
87+
{
88+
return;
89+
}
90+
91+
headers.Add(TenantHeader, tenant);
92+
}
93+
6094
/// <inheritdoc/>
6195
public virtual void Dispose() => HttpClient.Dispose();
6296

src/Serilog.Sinks.Grafana.Loki/ILokiHttpClient.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,12 @@ public interface ILokiHttpClient : IDisposable
3434
/// <see cref="LokiCredentials"/> used for authorization.
3535
/// </param>
3636
void SetCredentials(LokiCredentials? credentials);
37+
38+
/// <summary>
39+
/// Adds tenant header to all requests. See <a href="https://grafana.com/docs/loki/latest/operations/multi-tenancy/">docs</a>.
40+
/// </summary>
41+
/// <param name="tenant">
42+
/// Tenant ID
43+
/// </param>
44+
void SetTenant(string? tenant);
3745
}

src/Serilog.Sinks.Grafana.Loki/LoggerConfigurationLokiExtensions.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ public static class LoggerConfigurationLokiExtensions
4545
/// <param name="credentials">
4646
/// Auth <see cref="LokiCredentials"/>.
4747
/// </param>
48+
/// <param name="tenant">
49+
/// Tenant ID See <a href="https://grafana.com/docs/loki/latest/operations/multi-tenancy/">docs</a>.
50+
/// </param>
4851
/// <param name="restrictedToMinimumLevel">
4952
/// The minimum level for events passed through the sink.
5053
/// Default value is <see cref="LevelAlias.Minimum"/>.
@@ -83,6 +86,7 @@ public static LoggerConfiguration GrafanaLoki(
8386
IEnumerable<LokiLabel>? labels = null,
8487
IEnumerable<string>? propertiesAsLabels = null,
8588
LokiCredentials? credentials = null,
89+
string? tenant = null,
8690
LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum,
8791
int batchPostingLimit = 1000,
8892
int? queueLimit = null,
@@ -104,6 +108,7 @@ public static LoggerConfiguration GrafanaLoki(
104108
httpClient ??= new LokiHttpClient();
105109

106110
httpClient.SetCredentials(credentials);
111+
httpClient.SetTenant(tenant);
107112

108113
var batchFormatter = new LokiBatchFormatter(
109114
reservedPropertyRenamingStrategy,

test/Serilog.Sinks.Grafana.Loki.Tests/HttpClientsTests/BaseLokiHttpClientTests.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,35 @@ public void AuthorizationHeaderShouldNotBeSetWithoutCredentials()
4747

4848
client.Client.DefaultRequestHeaders.Authorization.ShouldBeNull();
4949
}
50+
51+
[Fact]
52+
public void TenantHeaderShouldBeCorrect()
53+
{
54+
var tenantId = "lokitenant";
55+
using var client = new TestLokiHttpClient();
56+
57+
client.SetTenant(tenantId);
58+
59+
var tenantHeaders = client.Client.DefaultRequestHeaders.GetValues("X-Scope-OrgID").ToList();
60+
tenantHeaders.ShouldBeEquivalentTo(new List<string> {"lokitenant"});
61+
}
62+
63+
[Fact]
64+
public void TenantHeaderShouldNotBeSetWithoutTenantId()
65+
{
66+
using var client = new TestLokiHttpClient();
67+
68+
client.SetTenant(null);
69+
70+
client.Client.DefaultRequestHeaders.Contains("X-Scope-OrgID").ShouldBeFalse();
71+
}
72+
73+
[Fact]
74+
public void TenantHeaderShouldThrowAnExceptionOnTenantIdAgainstRule()
75+
{
76+
var tenantId = "non-alphanumerical tenant";
77+
using var client = new TestLokiHttpClient();
78+
79+
Should.Throw<ArgumentException>(() => client.SetTenant(tenantId));
80+
}
5081
}

0 commit comments

Comments
 (0)