Skip to content

Commit a7bda2d

Browse files
authored
add manifest information to diagnostics (#1811)
1 parent c41e8fe commit a7bda2d

File tree

4 files changed

+169
-1
lines changed

4 files changed

+169
-1
lines changed
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using NServiceBus;
5+
using NServiceBus.Features;
6+
using NServiceBus.Settings;
7+
8+
class ManifestOutput : Feature
9+
{
10+
public ManifestOutput()
11+
{
12+
EnableByDefault();
13+
Defaults(s => s.Set(new PersistenceManifest { Prefix = s.Get<string>("NServiceBus.Routing.EndpointName") }));
14+
}
15+
16+
[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Used exclusively for serialization")]
17+
internal class PersistenceManifest
18+
{
19+
Lazy<OutboxManifest> outboxManifest;
20+
Lazy<SagaManifest[]> sagaManifests;
21+
Lazy<SubscriptionManifest> subscriptionManifest;
22+
23+
public string Prefix { get; init; }
24+
public string Dialect { get; set; }
25+
public OutboxManifest Outbox => outboxManifest?.Value;
26+
public SagaManifest[] Sagas => sagaManifests?.Value;
27+
public SubscriptionManifest SqlSubscriptions => subscriptionManifest?.Value;
28+
public string[] StorageTypes { get; set; }
29+
30+
//initialising lazy so that any reflection/calculations don't slow down endpoint startup
31+
public void SetOutbox(Func<OutboxManifest> lazyOutbox) => outboxManifest = new Lazy<OutboxManifest>(lazyOutbox);
32+
public void SetSagas(Func<SagaManifest[]> lazySagas) => sagaManifests = new Lazy<SagaManifest[]>(lazySagas);
33+
public void SetSqlSubscriptions(Func<SubscriptionManifest> lazySubscription) => subscriptionManifest = new Lazy<SubscriptionManifest>(lazySubscription);
34+
35+
//NOTE Some values are hardcoded so if the outbox script (Create_MsSqlServer.sql in ScriptBuilder/Outbox) changes this needs to be updated
36+
public record OutboxManifest
37+
{
38+
public string TableName { get; init; }
39+
public string PrimaryKey => "MessageId";
40+
public IndexProperty[] Indexes => [
41+
new() { Name = "Index_DispatchedAt", Columns = "DispatchedAt" },
42+
new() { Name = "Index_Dispatched", Columns = "Dispatched" }
43+
];
44+
//object to allow for serialization of multiple subtypes. They will never be deserialized
45+
public object[] TableColumns => [
46+
new VarcharSchemaProperty { Name = "MessageId", Length = "200", Mandatory = true },
47+
new BitSchemaProperty { Name = "Dispatched", Mandatory = true, Default = false },
48+
new SchemaProperty { Name = "DispatchedAt", Type = "datetime", Mandatory = false },
49+
new VarcharSchemaProperty { Name = "PersistenceVersion", Length = "23", Mandatory = true },
50+
new VarcharSchemaProperty { Name = "Operations", Length = "max", Mandatory = true }
51+
];
52+
}
53+
54+
//NOTE Some values are hardcoded so if the saga script (MsSqlServerSagaScriptWriter.cs in ScriptBuilder/Saga) changes this needs to be updated
55+
public record SagaManifest
56+
{
57+
public string Name { get; init; }
58+
public string PrimaryKey => "Id";
59+
public string TableName { get; init; }
60+
public IndexProperty[] Indexes { get; init; }
61+
//object to allow for serialization of multiple subtypes. They will never be deserialized
62+
public object[] TableColumns => [
63+
new SchemaProperty { Name = "Id", Type = "guid", Mandatory = true },
64+
new VarcharSchemaProperty { Name = "Metadata", Length = "max", Mandatory = true },
65+
new VarcharSchemaProperty { Name = "Data", Length = "max", Mandatory = true },
66+
new VarcharSchemaProperty { Name = "PersistenceVersion", Length = "23", Mandatory = true },
67+
new VarcharSchemaProperty { Name = "SagaTypeVersion", Length = "23", Mandatory = true },
68+
new SchemaProperty { Name = "Concurrency", Type = "integer", Mandatory = true }
69+
];
70+
}
71+
72+
//NOTE Some values are hardcoded so if the subscription script (Create_MsSqlServer.sq in ScriptBuilder/Subscriptions) changes this needs to be updated
73+
public record SubscriptionManifest
74+
{
75+
public string TableName { get; init; }
76+
public string PrimaryKey => "Subscriber, MessageType";
77+
public IndexProperty[] Indexes => [];
78+
//object to allow for serialization of multiple subtypes. They will never be deserialized
79+
public object[] TableColumns => [
80+
new VarcharSchemaProperty { Name = "Subscriber", Length = "200", Mandatory = true },
81+
new VarcharSchemaProperty { Name = "Endpoint", Length = "200", Mandatory = true },
82+
new VarcharSchemaProperty { Name = "MessageType", Length = "200", Mandatory = true },
83+
new VarcharSchemaProperty { Name = "PersistenceVersion", Length = "23", Mandatory = true }
84+
];
85+
}
86+
87+
public record SchemaProperty
88+
{
89+
public string Name { get; init; }
90+
public virtual string Type { get; set; }
91+
public bool Mandatory { get; init; }
92+
}
93+
94+
public record VarcharSchemaProperty : SchemaProperty
95+
{
96+
public override string Type => "string";
97+
public string Length { get; init; }
98+
}
99+
100+
public record BitSchemaProperty : SchemaProperty
101+
{
102+
public override string Type => "boolean";
103+
public bool Default { get; init; }
104+
}
105+
106+
public record IndexProperty
107+
{
108+
public string Name { get; init; }
109+
public string Columns { get; init; }
110+
}
111+
}
112+
113+
protected override void Setup(FeatureConfigurationContext context)
114+
{
115+
var settings = context.Settings;
116+
if (!settings.HasSetting("SqlPersistence.SqlDialect"))
117+
{
118+
return;
119+
}
120+
121+
settings.AddStartupDiagnosticsSection("Manifest-Persistence", () => GenerateManifest(context.Settings));
122+
}
123+
124+
static PersistenceManifest GenerateManifest(IReadOnlySettings settings)
125+
{
126+
var manifest = settings.Get<PersistenceManifest>();
127+
manifest.Dialect = settings.GetSqlDialect().Name;
128+
129+
if (settings.TryGet("ResultingSupportedStorages", out List<Type> supportedStorageTypes))
130+
{
131+
manifest.StorageTypes = supportedStorageTypes.Select(storageType => storageType.Name).ToArray();
132+
}
133+
134+
return manifest;
135+
}
136+
}

src/SqlPersistence/Outbox/SqlOutboxFeature.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ class SqlOutboxFeature : Feature
1414
});
1515
DependsOn<Outbox>();
1616
DependsOn<SqlStorageSessionFeature>();
17+
DependsOnOptionally<ManifestOutput>();
1718
}
1819

1920
protected override void Setup(FeatureConfigurationContext context)
@@ -85,6 +86,11 @@ ISqlOutboxTransaction transactionFactory()
8586

8687
context.RegisterStartupTask(b =>
8788
new OutboxCleaner(outboxPersister.RemoveEntriesOlderThan, b.GetRequiredService<CriticalError>().Raise, timeToKeepDeduplicationData, frequencyToRunCleanup, new AsyncTimer()));
89+
90+
if (settings.TryGet<ManifestOutput.PersistenceManifest>(out var manifest))
91+
{
92+
manifest.SetOutbox(() => new ManifestOutput.PersistenceManifest.OutboxManifest { TableName = sqlDialect.GetOutboxTableName($"{manifest.Prefix}_") });
93+
}
8894
}
8995

9096
internal const string TimeToKeepDeduplicationData = "Persistence.Sql.Outbox.TimeToKeepDeduplicationData";

src/SqlPersistence/Saga/SqlSagaFeature.cs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using Microsoft.Extensions.DependencyInjection;
1+
using System.Linq;
2+
using Microsoft.Extensions.DependencyInjection;
23
using Newtonsoft.Json;
34
using NServiceBus;
45
using NServiceBus.Features;
@@ -18,6 +19,7 @@ class SqlSagaFeature : Feature
1819
});
1920
DependsOn<Sagas>();
2021
DependsOn<SqlStorageSessionFeature>();
22+
DependsOnOptionally<ManifestOutput>();
2123
}
2224

2325
protected override void Setup(FeatureConfigurationContext context)
@@ -42,6 +44,21 @@ protected override void Setup(FeatureConfigurationContext context)
4244
CustomSagaWriter = customSagaWriter != null,
4345
CustomSagaReader = customSagaReader != null
4446
});
47+
48+
if (settings.TryGet<ManifestOutput.PersistenceManifest>(out var manifest))
49+
{
50+
manifest.SetSagas(() => settings.Get<SagaMetadataCollection>().Select(
51+
saga => GetSagaTableSchema(saga.Name, saga.EntityName, saga.TryGetCorrelationProperty(out var correlationProperty) ? correlationProperty.Name : null)).ToArray());
52+
53+
ManifestOutput.PersistenceManifest.SagaManifest GetSagaTableSchema(string sagaName, string entityName, string correlationProperty) => new()
54+
{
55+
Name = sagaName,
56+
TableName = sqlDialect.GetSagaTableName($"{manifest.Prefix}_", entityName),
57+
Indexes = !string.IsNullOrEmpty(correlationProperty)
58+
? [new() { Name = $"Index_Correlation_{correlationProperty}", Columns = correlationProperty }]
59+
: []
60+
};
61+
}
4562
}
4663

4764
static SagaInfoCache BuildSagaInfoCache(SqlDialect sqlDialect, IReadOnlySettings settings)

src/SqlPersistence/Subscription/SqlSubscriptionFeature.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ class SqlSubscriptionFeature : Feature
99
SqlSubscriptionFeature()
1010
{
1111
DependsOn("NServiceBus.Features.MessageDrivenSubscriptions");
12+
DependsOnOptionally<ManifestOutput>();
1213
}
1314

1415
protected override void Setup(FeatureConfigurationContext context)
@@ -31,5 +32,13 @@ protected override void Setup(FeatureConfigurationContext context)
3132
});
3233

3334
context.Services.AddSingleton(typeof(ISubscriptionStorage), persister);
35+
36+
if (settings.TryGet<ManifestOutput.PersistenceManifest>(out var manifest))
37+
{
38+
manifest.SetSqlSubscriptions(() => new ManifestOutput.PersistenceManifest.SubscriptionManifest
39+
{
40+
TableName = sqlDialect.GetSubscriptionTableName($"{manifest.Prefix}_")
41+
});
42+
}
3443
}
3544
}

0 commit comments

Comments
 (0)