diff --git a/docker-compose.yml b/docker-compose.yml index a80c99256..219efc290 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,11 +29,11 @@ services: - shared.env environment: - EVENTSTORE_GOSSIP_SEED=172.30.240.12:2113,172.30.240.13:2113 - - EVENTSTORE_INT_IP=172.30.240.11 + - EVENTSTORE_REPLICATION_IP=172.30.240.11 - EVENTSTORE_CERTIFICATE_FILE=/etc/eventstore/certs/node1/node.crt - EVENTSTORE_CERTIFICATE_PRIVATE_KEY_FILE=/etc/eventstore/certs/node1/node.key - EVENTSTORE_ADVERTISE_HOST_TO_CLIENT_AS=127.0.0.1 - - EVENTSTORE_ADVERTISE_HTTP_PORT_TO_CLIENT_AS=2111 + - EVENTSTORE_ADVERTISE_NODE_PORT_TO_CLIENT_AS=2111 ports: - 2111:2113 networks: @@ -51,11 +51,11 @@ services: - shared.env environment: - EVENTSTORE_GOSSIP_SEED=172.30.240.11:2113,172.30.240.13:2113 - - EVENTSTORE_INT_IP=172.30.240.12 + - EVENTSTORE_REPLICATION_IP=172.30.240.12 - EVENTSTORE_CERTIFICATE_FILE=/etc/eventstore/certs/node2/node.crt - EVENTSTORE_CERTIFICATE_PRIVATE_KEY_FILE=/etc/eventstore/certs/node2/node.key - EVENTSTORE_ADVERTISE_HOST_TO_CLIENT_AS=127.0.0.1 - - EVENTSTORE_ADVERTISE_HTTP_PORT_TO_CLIENT_AS=2112 + - EVENTSTORE_ADVERTISE_NODE_PORT_TO_CLIENT_AS=2112 ports: - 2112:2113 networks: @@ -73,11 +73,11 @@ services: - shared.env environment: - EVENTSTORE_GOSSIP_SEED=172.30.240.11:2113,172.30.240.12:2113 - - EVENTSTORE_INT_IP=172.30.240.13 + - EVENTSTORE_REPLICATION_IP=172.30.240.13 - EVENTSTORE_CERTIFICATE_FILE=/etc/eventstore/certs/node3/node.crt - EVENTSTORE_CERTIFICATE_PRIVATE_KEY_FILE=/etc/eventstore/certs/node3/node.key - EVENTSTORE_ADVERTISE_HOST_TO_CLIENT_AS=127.0.0.1 - - EVENTSTORE_ADVERTISE_HTTP_PORT_TO_CLIENT_AS=2113 + - EVENTSTORE_ADVERTISE_NODE_PORT_TO_CLIENT_AS=2113 ports: - 2113:2113 networks: diff --git a/samples/server/docker-compose.yaml b/samples/server/docker-compose.yaml index 962833cd1..5e92554fb 100644 --- a/samples/server/docker-compose.yaml +++ b/samples/server/docker-compose.yaml @@ -7,7 +7,7 @@ services: - EVENTSTORE_CLUSTER_SIZE=1 - EVENTSTORE_RUN_PROJECTIONS=All - EVENTSTORE_START_STANDARD_PROJECTIONS=true - - EVENTSTORE_HTTP_PORT=2113 + - EVENTSTORE_NODE_PORT=2113 - EVENTSTORE_INSECURE=true - EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP=true ports: diff --git a/shared.env b/shared.env index 175e11562..164471459 100644 --- a/shared.env +++ b/shared.env @@ -1,7 +1,7 @@ EVENTSTORE_CLUSTER_SIZE=3 EVENTSTORE_RUN_PROJECTIONS=All -EVENTSTORE_INT_TCP_PORT=1112 -EVENTSTORE_HTTP_PORT=2113 +EVENTSTORE_REPLICATION_PORT=1112 +EVENTSTORE_NODE_PORT=2113 EVENTSTORE_TRUSTED_ROOT_CERTIFICATES_PATH=/etc/eventstore/certs/ca EVENTSTORE_DISCOVER_VIA_DNS=false -EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP=true \ No newline at end of file +EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP=true diff --git a/src/EventStore.Core.Tests/Helpers/MiniClusterNode.cs b/src/EventStore.Core.Tests/Helpers/MiniClusterNode.cs index ae42b7089..3bc5365a7 100644 --- a/src/EventStore.Core.Tests/Helpers/MiniClusterNode.cs +++ b/src/EventStore.Core.Tests/Helpers/MiniClusterNode.cs @@ -127,7 +127,6 @@ public MiniClusterNode(string pathname, int debugIndex, IPEndPoint internalTcp, NodeIp = ExternalTcpEndPoint.Address, ReplicationPort = InternalTcpEndPoint.Port, NodePort = HttpEndPoint.Port, - DisableInternalTcpTls = false, ReplicationHeartbeatTimeout = 2_000, ReplicationHeartbeatInterval = 2_000, EnableAtomPubOverHttp = true, diff --git a/src/EventStore.Core.Tests/Helpers/MiniNode.cs b/src/EventStore.Core.Tests/Helpers/MiniNode.cs index a91b02264..7967ef996 100644 --- a/src/EventStore.Core.Tests/Helpers/MiniNode.cs +++ b/src/EventStore.Core.Tests/Helpers/MiniNode.cs @@ -163,9 +163,9 @@ public MiniNode(string pathname, .Build()), }.Secure(new X509Certificate2Collection(ssl_connections.GetRootCertificate()), ssl_connections.GetServerCertificate()) - .WithInternalSecureTcpOn(IntTcpEndPoint) - .WithExternalSecureTcpOn(TcpEndPoint) - .WithHttpOn(HttpEndPoint); + .WithReplicationEndpointOn(IntTcpEndPoint) + .WithExternalTcpOn(TcpEndPoint) + .WithNodeEndpointOn(HttpEndPoint); var inMemConf = new ConfigurationBuilder() .AddInMemoryCollection(new KeyValuePair[] { @@ -178,7 +178,7 @@ public MiniNode(string pathname, }).Build(); if (advertisedExtHostAddress != null) - options = options.AdvertiseHttpHostAs(new DnsEndPoint(advertisedExtHostAddress, advertisedHttpPort)); + options = options.AdvertiseNodeAs(new DnsEndPoint(advertisedExtHostAddress, advertisedHttpPort)); options = inMemDb ? options.RunInMemory() diff --git a/src/EventStore.Core.XUnit.Tests/Configuration/ClusterNodeOptionsTests/when_building/with_cluster_node_and_custom_settings.cs b/src/EventStore.Core.XUnit.Tests/Configuration/ClusterNodeOptionsTests/when_building/with_cluster_node_and_custom_settings.cs index 9133ca29b..6c3b3ce7f 100644 --- a/src/EventStore.Core.XUnit.Tests/Configuration/ClusterNodeOptionsTests/when_building/with_cluster_node_and_custom_settings.cs +++ b/src/EventStore.Core.XUnit.Tests/Configuration/ClusterNodeOptionsTests/when_building/with_cluster_node_and_custom_settings.cs @@ -131,9 +131,9 @@ protected override ClusterVNodeOptions WithOptions(ClusterVNodeOptions options) return options .Insecure() .WithExternalTcpOn(new IPEndPoint(IPAddress.Loopback, 11130)) - .WithInternalTcpOn(new IPEndPoint(IPAddress.Loopback, 11120)) + .WithReplicationEndpointOn(new IPEndPoint(IPAddress.Loopback, 11120)) .AdvertiseExternalHostAs(new DnsEndPoint("196.168.1.1", 11131)) - .AdvertiseHttpHostAs(new DnsEndPoint("196.168.1.1", 21130)); + .AdvertiseNodeAs(new DnsEndPoint("196.168.1.1", 21130)); } [Test] @@ -158,7 +158,7 @@ protected override ClusterVNodeOptions WithOptions(ClusterVNodeOptions options) { return options .Insecure() - .WithInternalTcpOn(new IPEndPoint(IPAddress.Any, 11120)) + .WithReplicationEndpointOn(new IPEndPoint(IPAddress.Any, 11120)) .WithExternalTcpOn(new IPEndPoint(IPAddress.Any, 11130)) .AdvertiseExternalHostAs(new DnsEndPoint("10.0.0.1", 11131)); } @@ -186,9 +186,9 @@ protected override ClusterVNodeOptions WithOptions(ClusterVNodeOptions options) { return options .Insecure() - .WithHttpOn(new IPEndPoint(IPAddress.Any, 21130)) + .WithNodeEndpointOn(new IPEndPoint(IPAddress.Any, 21130)) .WithExternalTcpOn(new IPEndPoint(IPAddress.Any, 11130)) - .WithInternalTcpOn(new IPEndPoint(IPAddress.Loopback, 11120)); + .WithReplicationEndpointOn(new IPEndPoint(IPAddress.Loopback, 11120)); } [Test] @@ -214,11 +214,11 @@ protected override ClusterVNodeOptions WithOptions(ClusterVNodeOptions options) { return options .Insecure() - .WithHttpOn(new IPEndPoint(IPAddress.Loopback, 21130)) + .WithNodeEndpointOn(new IPEndPoint(IPAddress.Loopback, 21130)) .WithExternalTcpOn(new IPEndPoint(IPAddress.Loopback, 11130)) - .WithInternalTcpOn(new IPEndPoint(IPAddress.Any, 11120)) + .WithReplicationEndpointOn(new IPEndPoint(IPAddress.Any, 11120)) .AdvertiseExternalHostAs(new DnsEndPoint("10.0.0.1", 11131)) - .AdvertiseHttpHostAs(new DnsEndPoint("10.0.0.1", 21131)); + .AdvertiseNodeAs(new DnsEndPoint("10.0.0.1", 21131)); } [Test] diff --git a/src/EventStore.Core.XUnit.Tests/Configuration/ClusterNodeOptionsTests/when_building/with_default_settings.cs b/src/EventStore.Core.XUnit.Tests/Configuration/ClusterNodeOptionsTests/when_building/with_default_settings.cs index 4d3ae98e8..fe4d2a8e5 100644 --- a/src/EventStore.Core.XUnit.Tests/Configuration/ClusterNodeOptionsTests/when_building/with_default_settings.cs +++ b/src/EventStore.Core.XUnit.Tests/Configuration/ClusterNodeOptionsTests/when_building/with_default_settings.cs @@ -39,12 +39,6 @@ public void should_have_default_endpoints() Assert.AreEqual(new IPEndPoint(IPAddress.Loopback, 2113), _node.NodeInfo.HttpEndPoint); } - [Test] - public void should_use_tls() - { - Assert.IsFalse(_options.Interface.DisableInternalTcpTls); - } - [Test] public void should_set_command_line_args_to_default_values() { @@ -110,12 +104,6 @@ public void should_have_default_secure_endpoints() Assert.AreEqual(httpEndPoint.ToDnsEndPoint(), _node.GossipAdvertiseInfo.HttpEndPoint); } - [Test] - public void should_use_tls() - { - Assert.IsFalse(_options.Interface.DisableInternalTcpTls); - } - protected override ClusterVNodeOptions WithOptions(ClusterVNodeOptions options) { return options; diff --git a/src/EventStore.Core.XUnit.Tests/Configuration/ClusterNodeOptionsTests/when_building/with_secure_tcp.cs b/src/EventStore.Core.XUnit.Tests/Configuration/ClusterNodeOptionsTests/when_building/with_secure_tcp.cs index 7a35a9961..4f6b05512 100644 --- a/src/EventStore.Core.XUnit.Tests/Configuration/ClusterNodeOptionsTests/when_building/with_secure_tcp.cs +++ b/src/EventStore.Core.XUnit.Tests/Configuration/ClusterNodeOptionsTests/when_building/with_secure_tcp.cs @@ -21,7 +21,7 @@ public class with_ssl_enabled_and_using_a_security_certificate_from_file(_options, LogFormatHelper.LogFormatFactory, diff --git a/src/EventStore.Core.XUnit.Tests/Configuration/ClusterNodeOptionsTests/when_building/with_single_node_and_custom_settings.cs b/src/EventStore.Core.XUnit.Tests/Configuration/ClusterNodeOptionsTests/when_building/with_single_node_and_custom_settings.cs index 4d36f54e2..3f9173147 100644 --- a/src/EventStore.Core.XUnit.Tests/Configuration/ClusterNodeOptionsTests/when_building/with_single_node_and_custom_settings.cs +++ b/src/EventStore.Core.XUnit.Tests/Configuration/ClusterNodeOptionsTests/when_building/with_single_node_and_custom_settings.cs @@ -57,9 +57,9 @@ public class with_custom_ip_endpoints : SingleNodeScenari protected override ClusterVNodeOptions WithOptions(ClusterVNodeOptions options) { return options - .WithHttpOn(_httpEndPoint) - .WithExternalSecureTcpOn(_externalTcp) - .WithInternalSecureTcpOn(_internalTcp); + .WithNodeEndpointOn(_httpEndPoint) + .WithExternalTcpOn(_externalTcp) + .WithReplicationEndpointOn(_internalTcp); } [Test] @@ -154,12 +154,12 @@ public class with_custom_advertise_as : SingleNodeScenari protected override ClusterVNodeOptions WithOptions(ClusterVNodeOptions options) { return options - .WithHttpOn(_httpEndpoint) - .WithExternalSecureTcpOn(_extTcpEndpoint) - .WithInternalSecureTcpOn(_intTcpEndpoint) + .WithNodeEndpointOn(_httpEndpoint) + .WithExternalTcpOn(_extTcpEndpoint) + .WithReplicationEndpointOn(_intTcpEndpoint) .AdvertiseInternalHostAs(new DnsEndPoint($"{InternalIp}.com", _intTcpEndpoint.Port + 1000)) .AdvertiseExternalHostAs(new DnsEndPoint($"{ExternalIp}.com", _extTcpEndpoint.Port + 1000)) - .AdvertiseHttpHostAs(new DnsEndPoint($"{ExternalIp}.com", _httpEndpoint.Port + 1000)); + .AdvertiseNodeAs(new DnsEndPoint($"{ExternalIp}.com", _httpEndpoint.Port + 1000)); } [Test] diff --git a/src/EventStore.Core.XUnit.Tests/Configuration/ClusterVNodeOptionsTests.cs b/src/EventStore.Core.XUnit.Tests/Configuration/ClusterVNodeOptionsTests.cs index 96c37358d..ddcedd7e5 100644 --- a/src/EventStore.Core.XUnit.Tests/Configuration/ClusterVNodeOptionsTests.cs +++ b/src/EventStore.Core.XUnit.Tests/Configuration/ClusterVNodeOptionsTests.cs @@ -1,6 +1,7 @@ #nullable enable using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; @@ -169,6 +170,24 @@ public void can_set_gossip_seed_values() options.Cluster.GossipSeed.Should().BeEquivalentTo(endpoints); } + [Fact] + public void can_set_gossip_seed_values_via_array() + { + var config = new ConfigurationBuilder() + .AddInMemoryCollection([ + new KeyValuePair("EventStore:GossipSeed:0", "127.0.0.1:1113"), + new KeyValuePair("EventStore:GossipSeed:1", "some-host:1114"), + ]) + .Build(); + + var options = ClusterVNodeOptions.FromConfiguration(config); + + options.Cluster.GossipSeed.Should().BeEquivalentTo(new EndPoint[] + { + new IPEndPoint(IPAddress.Loopback, 1113), new DnsEndPoint("some-host", 1114), + }); + } + [Theory] [InlineData("127.0.0.1", "You must specify the ports in the gossip seed.")] [InlineData("127.0.0.1:3.1415", "Invalid format for gossip seed port: 3.1415.")] @@ -188,6 +207,18 @@ public void reports_gossip_seed_errors(string gossipSeed, string expectedError) ex.Message); } + [Fact] + public void can_set_node_ip() + { + var config = new ConfigurationBuilder() + .AddEventStoreEnvironmentVariables(("EVENTSTORE_NODE_IP", "192.168.0.1")) + .Build(); + + var options = ClusterVNodeOptions.FromConfiguration(config); + + options.Interface.NodeIp.Should().Be(IPAddress.Parse("192.168.0.1")); + } + [Theory] [InlineData("127.0.0.1.0", "An invalid IP address was specified.")] public void reports_ip_address_errors(string nodeIp, string expectedError) diff --git a/src/EventStore.Core.XUnit.Tests/Telemetry/TelemetryServiceTests.cs b/src/EventStore.Core.XUnit.Tests/Telemetry/TelemetryServiceTests.cs index 8e797257a..1193feeb5 100644 --- a/src/EventStore.Core.XUnit.Tests/Telemetry/TelemetryServiceTests.cs +++ b/src/EventStore.Core.XUnit.Tests/Telemetry/TelemetryServiceTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Net; using System.Runtime.InteropServices; using System.Text.Json.Nodes; @@ -14,6 +15,7 @@ using EventStore.Core.TransactionLog.Checkpoint; using EventStore.Core.TransactionLog.Chunks; using EventStore.Plugins; +using Microsoft.Extensions.Configuration; using Xunit; using static EventStore.Plugins.Diagnostics.PluginDiagnosticsDataCollectionMode; @@ -37,6 +39,7 @@ public TelemetryServiceTests() { task.Wait(); } + var channel = Channel.CreateUnbounded(); _channelReader = channel.Reader; _sink = new InMemoryTelemetrySink(); @@ -46,6 +49,9 @@ public TelemetryServiceTests() _sut = new TelemetryService( _db.Manager, new ClusterVNodeOptions().WithPlugableComponent(_plugin), + new ConfigurationBuilder() + .AddInMemoryCollection( + new Dictionary() { { "EventStore:Telemetry:CloudIdentifier", "abc" }, }).Build(), new EnvelopePublisher(new ChannelEnvelope(channel)), _sink, new InMemoryCheckpoint(0), @@ -67,26 +73,26 @@ private static MemberInfo CreateMemberInfo(Guid instanceId, VNodeState state, bo static int random() => Random.Shared.Next(65000); var memberInfo = MemberInfo.ForVNode( - instanceId: instanceId, - timeStamp: DateTime.Now, - state: state, - isAlive: true, - internalTcpEndPoint: default, - internalSecureTcpEndPoint: new DnsEndPoint("myhost", random()), - externalTcpEndPoint: default, - externalSecureTcpEndPoint: new DnsEndPoint("myhost", random()), - httpEndPoint: new DnsEndPoint("myhost", random()), - advertiseHostToClientAs: "advertiseHostToClientAs", - advertiseHttpPortToClientAs: random(), - advertiseTcpPortToClientAs: random(), - lastCommitPosition: random(), - writerCheckpoint: random(), - chaserCheckpoint: random(), - epochPosition: random(), - epochNumber: random(), - epochId: Guid.NewGuid(), - nodePriority: random(), - isReadOnlyReplica: isReadOnly); + instanceId: instanceId, + timeStamp: DateTime.Now, + state: state, + isAlive: true, + internalTcpEndPoint: default, + internalSecureTcpEndPoint: new DnsEndPoint("myhost", random()), + externalTcpEndPoint: default, + externalSecureTcpEndPoint: new DnsEndPoint("myhost", random()), + httpEndPoint: new DnsEndPoint("myhost", random()), + advertiseHostToClientAs: "advertiseHostToClientAs", + advertiseHttpPortToClientAs: random(), + advertiseTcpPortToClientAs: random(), + lastCommitPosition: random(), + writerCheckpoint: random(), + chaserCheckpoint: random(), + epochPosition: random(), + epochNumber: random(), + epochId: Guid.NewGuid(), + nodePriority: random(), + isReadOnlyReplica: isReadOnly); return memberInfo; } @@ -113,10 +119,7 @@ public async Task can_collect_and_flush_telemetry() var request = Assert.IsType(await _channelReader.ReadAsync()); request.Envelope.ReplyWith(new TelemetryMessage.Response( "foo", - new JsonObject - { - ["bar"] = 42, - })); + new JsonObject { ["bar"] = 42, })); // receive schedule of flush and trigger it schedule = Assert.IsType(await _channelReader.ReadAsync()); @@ -131,17 +134,23 @@ public async Task can_collect_and_flush_telemetry() Assert.NotNull(_sink.Data["foo"]); Assert.Equal(new JsonObject { ["bar"] = 42 }.ToString(), _sink.Data["foo"].ToString()); - Assert.NotNull(_sink.Data["plugins"]); + Assert.NotNull(_sink.Data["fakeComponent"]); Assert.Equal(""" - { - "fakeComponent": { - "foo": "bar" - } - } - """, - _sink.Data["plugins"].ToString()); + { + "baz": "qux" + } + """, + _sink.Data["fakeComponent"].ToString()); Assert.Equal(_sink.Data["environment"]!["os"]!.ToString(), RuntimeInformation.OSDescription); + + Assert.NotNull(_sink.Data["telemetry"]); + Assert.Equal(""" + { + "cloudIdentifier": "abc" + } + """, + _sink.Data["telemetry"].ToString()); } [Fact] @@ -173,10 +182,7 @@ public async Task check_for_leaderid_and_epochid() var request = Assert.IsType(await _channelReader.ReadAsync()); request.Envelope.ReplyWith(new TelemetryMessage.Response( "foo", - new JsonObject - { - ["bar"] = 42, - })); + new JsonObject { ["bar"] = 42, })); // receive schedule of flush and trigger it schedule = Assert.IsType(await _channelReader.ReadAsync()); @@ -190,30 +196,27 @@ public async Task check_for_leaderid_and_epochid() // check sink has received the data Assert.NotNull(_sink.Data); Assert.Equal(Guid.Parse(_sink.Data["cluster"]["leaderId"].ToString()), _electionsDoneMessage.Leader.InstanceId); - Assert.Equal(Int32.Parse(_sink.Data["database"]["epochNumber"].ToString()), _electionsDoneMessage.ProposalNumber); + Assert.Equal(Int32.Parse(_sink.Data["database"]["epochNumber"].ToString()), + _electionsDoneMessage.ProposalNumber); Assert.Equal(Guid.Parse(_sink.Data["cluster"]["leaderId"].ToString()), _leaderFoundMessage.Leader.InstanceId); - Assert.Equal(Int32.Parse(_sink.Data["database"]["epochNumber"].ToString()), _leaderFoundMessage.Leader.EpochNumber); + Assert.Equal(Int32.Parse(_sink.Data["database"]["epochNumber"].ToString()), + _leaderFoundMessage.Leader.EpochNumber); Assert.Equal(Guid.Parse(_sink.Data["cluster"]["leaderId"].ToString()), _replicaStateMessage.Leader.InstanceId); - Assert.Equal(Int32.Parse(_sink.Data["database"]["epochNumber"].ToString()), _replicaStateMessage.Leader.EpochNumber); + Assert.Equal(Int32.Parse(_sink.Data["database"]["epochNumber"].ToString()), + _replicaStateMessage.Leader.EpochNumber); - Assert.NotNull(_sink.Data["plugins"]); + Assert.NotNull(_sink.Data["fakeComponent"]); } - class FakePlugableComponent(string name = "fakeComponent") : Plugin(name) + class FakePlugableComponent(string name = "FakeComponent") : Plugin(name) { public void PublishSomeTelemetry() { - PublishDiagnosticsData(new() - { - ["enabled"] = Enabled - }, Snapshot); - - PublishDiagnosticsData(new() - { - ["foo"] = "bar" - }, Snapshot); + PublishDiagnosticsData(new() { ["enabled"] = Enabled }, Snapshot); + + PublishDiagnosticsData(new() { ["Baz"] = "qux" }, Snapshot); } } } diff --git a/src/EventStore.Core/Authentication/PassthroughAuthentication/PassthroughAuthenticationProvider.cs b/src/EventStore.Core/Authentication/PassthroughAuthentication/PassthroughAuthenticationProvider.cs index 68096ab3c..cb3951b97 100644 --- a/src/EventStore.Core/Authentication/PassthroughAuthentication/PassthroughAuthenticationProvider.cs +++ b/src/EventStore.Core/Authentication/PassthroughAuthentication/PassthroughAuthenticationProvider.cs @@ -6,7 +6,9 @@ namespace EventStore.Core.Authentication.PassthroughAuthentication; -public class PassthroughAuthenticationProvider() : AuthenticationProviderBase(name: "insecure") { +public class PassthroughAuthenticationProvider() + : AuthenticationProviderBase(name: "insecure", diagnosticsName: "PassthroughAuthentication") +{ public override void Authenticate(AuthenticationRequest authenticationRequest) => authenticationRequest.Authenticated(SystemAccounts.System); diff --git a/src/EventStore.Core/ClusterVNode.cs b/src/EventStore.Core/ClusterVNode.cs index b01091bab..99ec89a3f 100644 --- a/src/EventStore.Core/ClusterVNode.cs +++ b/src/EventStore.Core/ClusterVNode.cs @@ -1469,6 +1469,7 @@ GossipAdvertiseInfo GetGossipAdvertiseInfo() var telemetryService = new TelemetryService( Db.Manager, modifiedOptions, + configuration, _mainQueue, new TelemetrySink(options.Application.TelemetryOptout), Db.Config.WriterCheckpoint.AsReadOnly(), diff --git a/src/EventStore.Core/Configuration/ClusterVNodeOptions.Framework.cs b/src/EventStore.Core/Configuration/ClusterVNodeOptions.Framework.cs index d4c20c64f..d7c1b7793 100644 --- a/src/EventStore.Core/Configuration/ClusterVNodeOptions.Framework.cs +++ b/src/EventStore.Core/Configuration/ClusterVNodeOptions.Framework.cs @@ -93,6 +93,23 @@ public static IReadOnlyDictionary GetLoadedOptions(IConfig var sourceDisplayName = GetSourceDisplayName(option.Value.Key, provider); var isDefault = provider.GetType() == typeof(EventStoreDefaultValuesConfigurationProvider); + // Handle array-style options; currently only GossipSeed uses this + if (sourceDisplayName is "" && value is null) + { + var parentPath = option.Value.Key; + var childValues = new List(); + + foreach (var childKey in provider.GetChildKeys([], parentPath)) + { + var absoluteChildKey = parentPath + ":" + childKey; + if (!provider.TryGet(absoluteChildKey, out var childValue) || childValue is null) continue; + childValues.Add(childValue); + sourceDisplayName = GetSourceDisplayName(absoluteChildKey, provider); + } + + value = string.Join(", ", childValues); + } + loadedOptions[option.Value.Key] = new( metadata: option.Value, title: title, diff --git a/src/EventStore.Core/Configuration/ClusterVNodeOptions.cs b/src/EventStore.Core/Configuration/ClusterVNodeOptions.cs index 9de7e49f9..e491e6220 100644 --- a/src/EventStore.Core/Configuration/ClusterVNodeOptions.cs +++ b/src/EventStore.Core/Configuration/ClusterVNodeOptions.cs @@ -24,786 +24,661 @@ using Serilog; using Quickenshtein; -namespace EventStore.Core { - [PublicAPI] - public partial record ClusterVNodeOptions { - public ClusterVNodeOptions() => FileStreamExtensions.ConfigureFlush(Database.UnsafeDisableFlushToDisk); - - public IConfigurationRoot? ConfigurationRoot { get; init; } - [OptionGroup] public ApplicationOptions Application { get; init; } = new(); - [OptionGroup] public DevModeOptions DevMode { get; init; } = new(); - [OptionGroup] public DefaultUserOptions DefaultUser { get; init; } = new(); - [OptionGroup] public LoggingOptions Logging { get; init; } = new(); - [OptionGroup] public AuthOptions Auth { get; init; } = new(); - [OptionGroup] public CertificateOptions Certificate { get; init; } = new(); - [OptionGroup] public CertificateFileOptions CertificateFile { get; init; } = new(); - [OptionGroup] public CertificateStoreOptions CertificateStore { get; init; } = new(); - [OptionGroup] public ClusterOptions Cluster { get; init; } = new(); - [OptionGroup] public DatabaseOptions Database { get; init; } = new(); - [OptionGroup] public GrpcOptions Grpc { get; init; } = new(); - [OptionGroup] public InterfaceOptions Interface { get; init; } = new(); - [OptionGroup] public ProjectionOptions Projection { get; init; } = new(); - public UnknownOptions Unknown { get; init; } = new([]); - - public byte IndexBitnessVersion { get; init; } = Index.PTableVersions.IndexV4; - - public X509Certificate2? ServerCertificate { get; init; } - public X509Certificate2Collection? TrustedRootCertificates { get; init; } - public IReadOnlyList PlugableComponents { get; init; } = []; - - public IReadOnlyList Subsystems => PlugableComponents.OfType().ToArray(); - - public bool UnknownOptionsDetected => Unknown.Options.Any(); - - public static ClusterVNodeOptions FromConfiguration(IConfigurationRoot configurationRoot) { - var configuration = configurationRoot.GetRequiredSection("EventStore"); - - // required because of a bug in the configuration system that - // is not reading the attribute from the property itself - TypeDescriptor.AddAttributes(typeof(EndPoint[]), new TypeConverterAttribute(typeof(GossipSeedConverter))); - TypeDescriptor.AddAttributes(typeof(IPAddress), new TypeConverterAttribute(typeof(IPAddressConverter))); - - // with full keys we would not even need to do all these binds, just a single one - // configurationRoot.BindOptions(); - - var options = new ClusterVNodeOptions { - Application = configuration.BindOptions(), - DevMode = configuration.BindOptions(), - DefaultUser = configuration.BindOptions(), - Logging = configuration.BindOptions(), - Auth = configuration.BindOptions(), - Certificate = configuration.BindOptions(), - CertificateFile = configuration.BindOptions(), - CertificateStore = configuration.BindOptions(), - Cluster = configuration.BindOptions(), - Database = configuration.BindOptions(), - Grpc = configuration.BindOptions(), - Interface = configuration.BindOptions(), - Projection = configuration.BindOptions(), - - Unknown = UnknownOptions.FromConfiguration(configuration), - ConfigurationRoot = configurationRoot, - LoadedOptions = GetLoadedOptions(configurationRoot) - }; - - return options; - } - - [Description("Default User Options")] - public record DefaultUserOptions { - [Description("Admin Default password"), Sensitive, EnvironmentOnly("The Admin user password can only be set using Environment Variables")] - public string DefaultAdminPassword { get; init; } = "changeit"; +namespace EventStore.Core; + +[PublicAPI] +public partial record ClusterVNodeOptions +{ + public ClusterVNodeOptions() => FileStreamExtensions.ConfigureFlush(Database.UnsafeDisableFlushToDisk); + + public IConfigurationRoot? ConfigurationRoot { get; init; } + [OptionGroup] public ApplicationOptions Application { get; init; } = new(); + [OptionGroup] public DevModeOptions DevMode { get; init; } = new(); + [OptionGroup] public DefaultUserOptions DefaultUser { get; init; } = new(); + [OptionGroup] public LoggingOptions Logging { get; init; } = new(); + [OptionGroup] public AuthOptions Auth { get; init; } = new(); + [OptionGroup] public CertificateOptions Certificate { get; init; } = new(); + [OptionGroup] public CertificateFileOptions CertificateFile { get; init; } = new(); + [OptionGroup] public CertificateStoreOptions CertificateStore { get; init; } = new(); + [OptionGroup] public ClusterOptions Cluster { get; init; } = new(); + [OptionGroup] public DatabaseOptions Database { get; init; } = new(); + [OptionGroup] public GrpcOptions Grpc { get; init; } = new(); + [OptionGroup] public InterfaceOptions Interface { get; init; } = new(); + [OptionGroup] public ProjectionOptions Projection { get; init; } = new(); + public UnknownOptions Unknown { get; init; } = new([]); + + public byte IndexBitnessVersion { get; init; } = Index.PTableVersions.IndexV4; + + public X509Certificate2? ServerCertificate { get; init; } + public X509Certificate2Collection? TrustedRootCertificates { get; init; } + public IReadOnlyList PlugableComponents { get; init; } = []; + + public IReadOnlyList Subsystems => PlugableComponents.OfType().ToArray(); + + public bool UnknownOptionsDetected => Unknown.Options.Any(); + + public static ClusterVNodeOptions FromConfiguration(IConfigurationRoot configurationRoot) + { + var configuration = configurationRoot.GetRequiredSection("EventStore"); + + // required because of a bug in the configuration system that + // is not reading the attribute from the property itself + TypeDescriptor.AddAttributes(typeof(EndPoint[]), new TypeConverterAttribute(typeof(GossipSeedConverter))); + TypeDescriptor.AddAttributes(typeof(EndPoint), new TypeConverterAttribute(typeof(GossipEndPointConverter))); + TypeDescriptor.AddAttributes(typeof(IPAddress), new TypeConverterAttribute(typeof(IPAddressConverter))); + + // with full keys we would not even need to do all these binds, just a single one + // configurationRoot.BindOptions(); + + var options = new ClusterVNodeOptions + { + Application = configuration.BindOptions(), + DevMode = configuration.BindOptions(), + DefaultUser = configuration.BindOptions(), + Logging = configuration.BindOptions(), + Auth = configuration.BindOptions(), + Certificate = configuration.BindOptions(), + CertificateFile = configuration.BindOptions(), + CertificateStore = configuration.BindOptions(), + Cluster = configuration.BindOptions(), + Database = configuration.BindOptions(), + Grpc = configuration.BindOptions(), + Interface = configuration.BindOptions(), + Projection = configuration.BindOptions(), + Unknown = UnknownOptions.FromConfiguration(configuration), + ConfigurationRoot = configurationRoot, + LoadedOptions = GetLoadedOptions(configurationRoot) + }; + + return options; + } - [Description("Ops Default password"), Sensitive, EnvironmentOnly("The Ops user password can only be set using Environment Variables")] - public string DefaultOpsPassword { get; init; } = "changeit"; - } + [Description("Default User Options")] + public record DefaultUserOptions + { + [Description("Admin Default password"), Sensitive, + EnvironmentOnly("The Admin user password can only be set using Environment Variables")] + public string DefaultAdminPassword { get; init; } = "changeit"; - [Description("Dev Mode Options")] - public record DevModeOptions { - [Description("Runs EventStoreDB in dev mode. This will create and add dev certificates to your certificate store, enable atompub over http, and run standard projections.")] - public bool Dev { get; init; } = false; + [Description("Ops Default password"), Sensitive, + EnvironmentOnly("The Ops user password can only be set using Environment Variables")] + public string DefaultOpsPassword { get; init; } = "changeit"; + } - [Description("Removes any dev certificates installed on this computer without starting EventStoreDB.")] - public bool RemoveDevCerts { get; init; } = false; - } + [Description("Dev Mode Options")] + public record DevModeOptions + { + [Description( + "Runs EventStoreDB in dev mode. This will create and add dev certificates to your certificate store, enable atompub over http, and run standard projections.")] + public bool Dev { get; init; } = false; - [Description("Application Options")] - public record ApplicationOptions { - [Description("Show help.")] public bool Help { get; init; } = false; + [Description("Removes any dev certificates installed on this computer without starting EventStoreDB.")] + public bool RemoveDevCerts { get; init; } = false; + } - [Description("Show version.")] public bool Version { get; init; } = false; + [Description("Application Options")] + public record ApplicationOptions + { + [Description("Show help.")] public bool Help { get; init; } = false; - [Description("Configuration files.")] - public string Config { get; init; } = - Path.Combine(Locations.DefaultConfigurationDirectory, DefaultFiles.DefaultConfigFile); + [Description("Show version.")] public bool Version { get; init; } = false; - [Description("Print effective configuration to console and then exit.")] - public bool WhatIf { get; init; } = false; + [Description("Configuration files.")] + public string Config { get; init; } = + Path.Combine(Locations.DefaultConfigurationDirectory, DefaultFiles.DefaultConfigFile); - [Description("Allows EventStoreDB to run with unknown configuration options present.")] - public bool AllowUnknownOptions { get; init; } = false; + [Description("Print effective configuration to console and then exit.")] + public bool WhatIf { get; init; } = false; - [Description("Disable HTTP caching.")] public bool DisableHttpCaching { get; init; } = false; + [Description("Allows EventStoreDB to run with unknown configuration options present.")] + public bool AllowUnknownOptions { get; init; } = false; - [Description("The number of seconds between statistics gathers."), - Unit("s")] - public int StatsPeriodSec { get; init; } = 30; + [Description("Disable HTTP caching.")] public bool DisableHttpCaching { get; init; } = false; - [Description("The number of threads to use for pool of worker services. Set to '0' to scale automatically (Default)")] - public int WorkerThreads { get; init; } = 0; + [Description("The number of seconds between statistics gathers."), + Unit("s")] + public int StatsPeriodSec { get; init; } = 30; - [Description("Enables the tracking of various histograms in the backend, " + - "typically only used for debugging, etc.")] - [Deprecated("The EnableHistograms setting has been deprecated as of version 24.10.0 and currently has no effect. " + - "Please contact EventStore if this feature is of interest to you.")] - public bool EnableHistograms { get; init; } = false; + [Description( + "The number of threads to use for pool of worker services. Set to '0' to scale automatically (Default)")] + public int WorkerThreads { get; init; } = 0; - [Description("Log Http Requests and Responses before processing them.")] - public bool LogHttpRequests { get; init; } = false; + [Description("Enables the tracking of various histograms in the backend, " + + "typically only used for debugging, etc.")] + [Deprecated( + "The EnableHistograms setting has been deprecated as of version 24.10.0 and currently has no effect. " + + "Please contact EventStore if this feature is of interest to you.")] + public bool EnableHistograms { get; init; } = false; - [Description("Log the failed authentication attempts.")] - public bool LogFailedAuthenticationAttempts { get; init; } = false; + [Description("Log Http Requests and Responses before processing them.")] + public bool LogHttpRequests { get; init; } = false; - [Description("Skip Index Scan on Reads. This skips the index scan which was used " + - "to stop reading duplicates.")] - public bool SkipIndexScanOnReads { get; init; } = false; + [Description("Log the failed authentication attempts.")] + public bool LogFailedAuthenticationAttempts { get; init; } = false; - [Description("The maximum size of appends, in bytes. May not exceed 16MB.")] - public int MaxAppendSize { get; init; } = 1_024 * 1_024; + [Description("Skip Index Scan on Reads. This skips the index scan which was used " + + "to stop reading duplicates.")] + public bool SkipIndexScanOnReads { get; init; } = false; - [Description("Disable Authentication, Authorization and TLS on all TCP/HTTP interfaces.")] - public bool Insecure { get; init; } = false; + [Description("The maximum size of appends, in bytes. May not exceed 16MB.")] + public int MaxAppendSize { get; init; } = 1_024 * 1_024; - [Description("Allow anonymous access to HTTP API endpoints.")] - public bool AllowAnonymousEndpointAccess { get; init; } = false; + [Description("Disable Authentication, Authorization and TLS on all TCP/HTTP interfaces.")] + public bool Insecure { get; init; } = false; - [Description("Allow anonymous access to streams.")] - public bool AllowAnonymousStreamAccess { get; init; } = false; + [Description("Allow anonymous access to HTTP API endpoints.")] + public bool AllowAnonymousEndpointAccess { get; init; } = false; - [Description("Overrides anonymous access for the gossip endpoint. If set to true, the gossip endpoint will accept anonymous access. " + - $"Otherwise anonymous access will be dis/allowed based on the value of the '{nameof(AllowAnonymousEndpointAccess)}' option")] - public bool OverrideAnonymousEndpointAccessForGossip { get; init; } = true; + [Description("Allow anonymous access to streams.")] + public bool AllowAnonymousStreamAccess { get; init; } = false; - [Description("Disable telemetry data collection."), EnvironmentOnly("You can only opt-out of telemetry using Environment Variables")] - public bool TelemetryOptout { get; init; } = false; - } + [Description( + "Overrides anonymous access for the gossip endpoint. If set to true, the gossip endpoint will accept anonymous access. " + + $"Otherwise anonymous access will be dis/allowed based on the value of the '{nameof(AllowAnonymousEndpointAccess)}' option")] + public bool OverrideAnonymousEndpointAccessForGossip { get; init; } = true; - [Description("Logging Options")] - public record LoggingOptions { - [Description("Path where to keep log files.")] - public string Log { get; init; } = Locations.DefaultLogDirectory; + [Description("Disable telemetry data collection."), + EnvironmentOnly("You can only opt-out of telemetry using Environment Variables")] + public bool TelemetryOptout { get; init; } = false; + } - [Description("The name of the log configuration file.")] - public string LogConfig { get; init; } = "logconfig.json"; + [Description("Logging Options")] + public record LoggingOptions + { + [Description("Path where to keep log files.")] + public string Log { get; init; } = Locations.DefaultLogDirectory; - [Description("Sets the minimum log level. For more granular settings, please edit logconfig.json.")] - public LogLevel LogLevel { get; init; } = LogLevel.Default; + [Description("The name of the log configuration file.")] + public string LogConfig { get; init; } = "logconfig.json"; - [Description("Which format (plain, json) to use when writing to the console.")] - public LogConsoleFormat LogConsoleFormat { get; init; } = LogConsoleFormat.Plain; + [Description("Sets the minimum log level. For more granular settings, please edit logconfig.json.")] + public LogLevel LogLevel { get; init; } = LogLevel.Default; - [Description("Maximum size of each log file.")] - public int LogFileSize { get; init; } = 1024 * 1024 * 1024; + [Description("Which format (plain, json) to use when writing to the console.")] + public LogConsoleFormat LogConsoleFormat { get; init; } = LogConsoleFormat.Plain; - [Description("How often to rotate logs.")] - public RollingInterval LogFileInterval { get; init; } = RollingInterval.Day; + [Description("Maximum size of each log file.")] + public int LogFileSize { get; init; } = 1024 * 1024 * 1024; - [Description("How many log files to hold on to.")] - public int LogFileRetentionCount { get; init; } = 31; + [Description("How often to rotate logs.")] + public RollingInterval LogFileInterval { get; init; } = RollingInterval.Day; - [Description("Disable log to disk.")] - public bool DisableLogFile { get; init; } = false; - } + [Description("How many log files to hold on to.")] + public int LogFileRetentionCount { get; init; } = 31; - [Description("Authentication/Authorization Options")] - public record AuthOptions { - [Description("The type of authorization to use.")] - public string AuthorizationType { get; init; } = "internal"; + [Description("Disable log to disk.")] public bool DisableLogFile { get; init; } = false; + } - [Description("Path to the configuration file for authorization configuration (if applicable).")] - public string? AuthorizationConfig { get; init; } + [Description("Authentication/Authorization Options")] + public record AuthOptions + { + [Description("The type of authorization to use.")] + public string AuthorizationType { get; init; } = "internal"; - [Description("The type of Authentication to use.")] - public string AuthenticationType { get; init; } = "internal"; + [Description("Path to the configuration file for authorization configuration (if applicable).")] + public string? AuthorizationConfig { get; init; } - [Description("Path to the configuration file for Authentication configuration (if applicable).")] - public string? AuthenticationConfig { get; init; } + [Description("The type of Authentication to use.")] + public string AuthenticationType { get; init; } = "internal"; - [Description("Disables first level authorization checks on all HTTP endpoints. " + - "This option can be enabled for backwards compatibility with EventStore 5.0.1 or earlier.")] - public bool DisableFirstLevelHttpAuthorization { get; init; } = false; - } + [Description("Path to the configuration file for Authentication configuration (if applicable).")] + public string? AuthenticationConfig { get; init; } - [Description("Certificate Options (from file)")] - public record CertificateFileOptions { - [Description("The path to a PKCS #12 (.p12/.pfx) or an X.509 (.pem, .crt, .cer, .der) certificate file. " + - "If you have intermediate certificates, they should be bundled together in a PEM or PKCS #12 file containing the node's certificate followed by the intermediate certificates.")] - public string? CertificateFile { get; init; } - - [Description("The path to the certificate private key file (.key) if an X.509 (.pem, .crt, .cer, .der) " + - "certificate file is provided.")] - public string? CertificatePrivateKeyFile { get; init; } + [Description("Disables first level authorization checks on all HTTP endpoints. " + + "This option can be enabled for backwards compatibility with EventStore 5.0.1 or earlier.")] + public bool DisableFirstLevelHttpAuthorization { get; init; } = false; + } - [Description("The password to the certificate if a PKCS #12 (.p12/.pfx) certificate file is provided."), - Sensitive] - public string? CertificatePassword { get; init; } + [Description("Certificate Options (from file)")] + public record CertificateFileOptions + { + [Description("The path to a PKCS #12 (.p12/.pfx) or an X.509 (.pem, .crt, .cer, .der) certificate file. " + + "If you have intermediate certificates, they should be bundled together in a PEM or PKCS #12 file containing the node's certificate followed by the intermediate certificates.")] + public string? CertificateFile { get; init; } + + [Description("The path to the certificate private key file (.key) if an X.509 (.pem, .crt, .cer, .der) " + + "certificate file is provided.")] + public string? CertificatePrivateKeyFile { get; init; } + + [Description("The password to the certificate if a PKCS #12 (.p12/.pfx) certificate file is provided."), + Sensitive] + public string? CertificatePassword { get; init; } + + [Description( + "The password to the certificate private key file if an encrypted PKCS #8 private key file is provided."), + Sensitive] + public string? CertificatePrivateKeyPassword { get; init; } + } - [Description("The password to the certificate private key file if an encrypted PKCS #8 private key file is provided."), - Sensitive] - public string? CertificatePrivateKeyPassword { get; init; } - } + [Description("Certificate Options")] + public record CertificateOptions + { + [Description("The path to a directory which contains trusted X.509 (.pem, .crt, .cer, .der) " + + "root certificate files.")] + public string? TrustedRootCertificatesPath { get; init; } = + Locations.DefaultTrustedRootCertificateDirectory; + + [Description( + "The pattern the CN (Common Name) of a connecting EventStoreDB node must match to be authenticated. A wildcard FQDN can be specified if using wildcard certificates or if the CN is not the same on all nodes. Leave empty to automatically use the CN of this node's certificate.")] + public string CertificateReservedNodeCommonName { get; init; } = string.Empty; + } - [Description("Certificate Options")] - public record CertificateOptions { - [Description("The path to a directory which contains trusted X.509 (.pem, .crt, .cer, .der) " + - "root certificate files.")] - public string? TrustedRootCertificatesPath { get; init; } = - Locations.DefaultTrustedRootCertificateDirectory; + [Description("Certificate Options (from store)")] + public record CertificateStoreOptions + { + [Description("The certificate store location name.")] + public string CertificateStoreLocation { get; init; } = string.Empty; - [Description("The pattern the CN (Common Name) of a connecting EventStoreDB node must match to be authenticated. A wildcard FQDN can be specified if using wildcard certificates or if the CN is not the same on all nodes. Leave empty to automatically use the CN of this node's certificate.")] - public string CertificateReservedNodeCommonName { get; init; } = string.Empty; - } + [Description("The certificate store name.")] + public string CertificateStoreName { get; init; } = string.Empty; - [Description("Certificate Options (from store)")] - public record CertificateStoreOptions { - [Description("The certificate store location name.")] - public string CertificateStoreLocation { get; init; } = string.Empty; + [Description("The certificate store subject name.")] + public string CertificateSubjectName { get; init; } = string.Empty; - [Description("The certificate store name.")] - public string CertificateStoreName { get; init; } = string.Empty; + [Description("The certificate fingerprint/thumbprint.")] + public string CertificateThumbprint { get; init; } = string.Empty; - [Description("The certificate store subject name.")] - public string CertificateSubjectName { get; init; } = string.Empty; + [Description("The name of the certificate store that contains the trusted root certificate.")] + public string TrustedRootCertificateStoreName { get; init; } = string.Empty; - [Description("The certificate fingerprint/thumbprint.")] - public string CertificateThumbprint { get; init; } = string.Empty; + [Description("The certificate store location that contains the trusted root certificate.")] + public string TrustedRootCertificateStoreLocation { get; init; } = string.Empty; - [Description("The name of the certificate store that contains the trusted root certificate.")] - public string TrustedRootCertificateStoreName { get; init; } = string.Empty; + [Description("The trusted root certificate subject name.")] + public string TrustedRootCertificateSubjectName { get; init; } = string.Empty; - [Description("The certificate store location that contains the trusted root certificate.")] - public string TrustedRootCertificateStoreLocation { get; init; } = string.Empty; + [Description("The trusted root certificate fingerprint/thumbprint.")] + public string TrustedRootCertificateThumbprint { get; init; } = string.Empty; + } - [Description("The trusted root certificate subject name.")] - public string TrustedRootCertificateSubjectName { get; init; } = string.Empty; + [Description("Cluster Options")] + public record ClusterOptions + { + [Description( + "The maximum number of entries to keep in the stream info cache. Set to '0' to scale automatically (Default)")] + public int StreamInfoCacheCapacity { get; init; } = 0; - [Description("The trusted root certificate fingerprint/thumbprint.")] - public string TrustedRootCertificateThumbprint { get; init; } = string.Empty; - } + [Description("The number of nodes in the cluster.")] + public int ClusterSize { get; init; } = 1; - [Description("Cluster Options")] - public record ClusterOptions { - [Description("The maximum number of entries to keep in the stream info cache. Set to '0' to scale automatically (Default)")] - public int StreamInfoCacheCapacity { get; init; } = 0; + [Description("The node priority used during leader election.")] + public int NodePriority { get; init; } = 0; - [Description("The number of nodes in the cluster.")] - public int ClusterSize { get; init; } = 1; + [Description("Whether to use DNS lookup to discover other cluster nodes.")] + public bool DiscoverViaDns { get; init; } = true; - [Description("The node priority used during leader election.")] - public int NodePriority { get; init; } = 0; + [Description("DNS name from which other nodes can be discovered.")] + public string ClusterDns { get; init; } = "fake.dns"; - [Description("Whether to use DNS lookup to discover other cluster nodes.")] - public bool DiscoverViaDns { get; init; } = true; + [Description("The port on which cluster nodes' managers are running.")] + public int ClusterGossipPort { get; init; } = 2113; - [Description("DNS name from which other nodes can be discovered.")] - public string ClusterDns { get; init; } = "fake.dns"; + [Description("Endpoints for other cluster nodes from which to seed gossip.")] + public EndPoint[] GossipSeed { get; init; } = []; - [Description("The port on which cluster nodes' managers are running.")] - public int ClusterGossipPort { get; init; } = 2113; + [Description("The interval, in ms, nodes should try to gossip with each other."), + Unit("ms")] + public int GossipIntervalMs { get; init; } = 2_000; - [Description("Endpoints for other cluster nodes from which to seed gossip.")] - public EndPoint[] GossipSeed { get; init; } = []; + [Description("The amount of drift, in ms, between clocks on nodes allowed before gossip is rejected."), + Unit("ms")] + public int GossipAllowedDifferenceMs { get; init; } = 60_000; - [Description("The interval, in ms, nodes should try to gossip with each other."), - Unit("ms")] - public int GossipIntervalMs { get; init; } = 2_000; + [Description("The timeout, in ms, on gossip to another node."), + Unit("ms")] + public int GossipTimeoutMs { get; init; } = 2_500; - [Description("The amount of drift, in ms, between clocks on nodes allowed before gossip is rejected."), - Unit("ms")] - public int GossipAllowedDifferenceMs { get; init; } = 60_000; + [Description("Sets this node as a read only replica that is not allowed to participate in elections " + + "or accept writes from clients.")] + public bool ReadOnlyReplica { get; init; } = false; - [Description("The timeout, in ms, on gossip to another node."), - Unit("ms")] - public int GossipTimeoutMs { get; init; } = 2_500; + [Description("Sets this node as an Archiver node. Requires ReadOnlyReplica to be true. Experimental.")] + public bool Archiver { get; init; } = false; - [Description("Sets this node as a read only replica that is not allowed to participate in elections " + - "or accept writes from clients.")] - public bool ReadOnlyReplica { get; init; } = false; + [Description("Allow more nodes than the cluster size to join the cluster as clones. " + + "(UNSAFE: can cause data loss if a clone is promoted as leader)")] + public bool UnsafeAllowSurplusNodes { get; init; } = false; - [Description("Sets this node as an Archiver node. Requires ReadOnlyReplica to be true. Experimental.")] - public bool Archiver { get; init; } = false; + [Description("The number of seconds a dead node will remain in the gossip before being pruned."), + Unit("s")] + public int DeadMemberRemovalPeriodSec { get; init; } = 1_800; - [Description("Allow more nodes than the cluster size to join the cluster as clones. " + - "(UNSAFE: can cause data loss if a clone is promoted as leader)")] - public bool UnsafeAllowSurplusNodes { get; init; } = false; + [Description("The timeout, in milliseconds, on election messages to other nodes."), + Unit("ms")] + public int LeaderElectionTimeoutMs { get; init; } = 1_000; - [Description("The number of seconds a dead node will remain in the gossip before being pruned."), - Unit("s")] - public int DeadMemberRemovalPeriodSec { get; init; } = 1_800; + public int QuorumSize => ClusterSize == 1 ? 1 : ClusterSize / 2 + 1; + } - [Description("The timeout, in milliseconds, on election messages to other nodes."), - Unit("ms")] - public int LeaderElectionTimeoutMs { get; init; } = 1_000; + [Description("Database Options")] + public record DatabaseOptions + { + [Description("The minimum flush delay in milliseconds."), + Unit("ms")] + public double MinFlushDelayMs { get; init; } = TFConsts.MinFlushDelayMs.TotalMilliseconds; - public int QuorumSize => ClusterSize == 1 ? 1 : ClusterSize / 2 + 1; - } + [Description("Disables the merging of chunks when scavenge is running.")] + public bool DisableScavengeMerging { get; init; } = false; - [Description("Database Options")] - public record DatabaseOptions { - [Description("The minimum flush delay in milliseconds."), - Unit("ms")] - public double MinFlushDelayMs { get; init; } = TFConsts.MinFlushDelayMs.TotalMilliseconds; + [Description("The number of days to keep scavenge history."), + Unit("d")] + public int ScavengeHistoryMaxAge { get; init; } = 30; - [Description("Disables the merging of chunks when scavenge is running.")] - public bool DisableScavengeMerging { get; init; } = false; + [Description("The number of chunks to cache in unmanaged memory.")] + public int CachedChunks { get; init; } = -1; - [Description("The number of days to keep scavenge history."), - Unit("d")] - public int ScavengeHistoryMaxAge { get; init; } = 30; + [Description("The amount of unmanaged memory to use for caching chunks in bytes.")] + public long ChunksCacheSize { get; init; } = TFConsts.ChunksCacheSize; - [Description("The number of chunks to cache in unmanaged memory.")] - public int CachedChunks { get; init; } = -1; + [Description("Adjusts the maximum size of a mem table.")] + public int MaxMemTableSize { get; init; } = 1_000_000; - [Description("The amount of unmanaged memory to use for caching chunks in bytes.")] - public long ChunksCacheSize { get; init; } = TFConsts.ChunksCacheSize; + [Description("The number of events to read per candidate in the case of a hash collision.")] + public int HashCollisionReadLimit { get; init; } = 100; - [Description("Adjusts the maximum size of a mem table.")] - public int MaxMemTableSize { get; init; } = 1_000_000; + [Description("The path the db should be loaded/saved to.")] + public string Db { get; init; } = Locations.DefaultDataDirectory; - [Description("The number of events to read per candidate in the case of a hash collision.")] - public int HashCollisionReadLimit { get; init; } = 100; + [Description("The path the index should be loaded/saved to.")] + public string? Index { get; init; } = null; - [Description("The path the db should be loaded/saved to.")] - public string Db { get; init; } = Locations.DefaultDataDirectory; + [Description("The type of transformation to apply to the database.")] + public string Transform { get; init; } = "identity"; - [Description("The path the index should be loaded/saved to.")] - public string? Index { get; init; } = null; + [Description("Keep everything in memory, no directories or files are created.")] + public bool MemDb { get; init; } = false; - [Description("The type of transformation to apply to the database.")] - public string Transform { get; init; } = "identity"; + [Description("Creates a Bloom filter file for each new index file to speed up index reads.")] + public bool UseIndexBloomFilters { get; init; } = true; - [Description("Keep everything in memory, no directories or files are created.")] - public bool MemDb { get; init; } = false; + [Description("The maximum number of entries to keep in each index cache.")] + public int IndexCacheSize { get; init; } = 0; - [Description("Creates a Bloom filter file for each new index file to speed up index reads.")] - public bool UseIndexBloomFilters { get; init; } = true; + [Description("Bypasses the checking of file hashes of database during startup " + + "(allows for faster startup).")] + public bool SkipDbVerify { get; init; } = false; - [Description("The maximum number of entries to keep in each index cache.")] - public int IndexCacheSize { get; init; } = 0; + [Description("Enables Write Through when writing to the file system, this bypasses filesystem caches.")] + public bool WriteThrough { get; init; } = false; - [Description("Bypasses the checking of file hashes of database during startup " + - "(allows for faster startup).")] - public bool SkipDbVerify { get; init; } = false; + [Description("Enables Unbuffered/DirectIO when writing to the file system, this bypasses filesystem " + + "caches.")] + [Deprecated("The Unbuffered setting has been deprecated as of version 24.6.0 and currently has no effect. " + + "Please contact EventStore if this feature is of interest to you.")] + public bool Unbuffered { get; init; } = false; - [Description("Enables Write Through when writing to the file system, this bypasses filesystem caches.")] - public bool WriteThrough { get; init; } = false; + [Description("The initial number of readers to start when opening a TFChunk.")] + [Deprecated( + "The ChunkInitialReaderCount parameter has been deprecated as of version 24.6.0 and currently has no effect.")] + public int ChunkInitialReaderCount { get; init; } = 5; - [Description("Enables Unbuffered/DirectIO when writing to the file system, this bypasses filesystem " + - "caches.")] - [Deprecated("The Unbuffered setting has been deprecated as of version 24.6.0 and currently has no effect. " + - "Please contact EventStore if this feature is of interest to you.")] - public bool Unbuffered { get; init; } = false; + [Description("Prepare timeout (in milliseconds)."), + Unit("ms")] + public int PrepareTimeoutMs { get; init; } = 2_000; - [Description("The initial number of readers to start when opening a TFChunk.")] - [Deprecated("The ChunkInitialReaderCount parameter has been deprecated as of version 24.6.0 and currently has no effect.")] - public int ChunkInitialReaderCount { get; init; } = 5; + [Description("Commit timeout (in milliseconds)."), + Unit("ms")] + public int CommitTimeoutMs { get; init; } = 2_000; - [Description("Prepare timeout (in milliseconds)."), - Unit("ms")] - public int PrepareTimeoutMs { get; init; } = 2_000; + [Description("Write timeout (in milliseconds)."), + Unit("ms")] + public int WriteTimeoutMs { get; init; } = 2_000; - [Description("Commit timeout (in milliseconds)."), - Unit("ms")] - public int CommitTimeoutMs { get; init; } = 2_000; + [Description("Disable flushing to disk. (UNSAFE: on power off)")] + public bool UnsafeDisableFlushToDisk { get; init; } - [Description("Write timeout (in milliseconds)."), - Unit("ms")] - public int WriteTimeoutMs { get; init; } = 2_000; + [Description("Disables Hard Deletes. (UNSAFE: use to remove hard deletes)")] + [Deprecated("This setting is unsafe and not recommended")] + public bool UnsafeIgnoreHardDelete { get; init; } = false; - [Description("Disable flushing to disk. (UNSAFE: on power off)")] - public bool UnsafeDisableFlushToDisk { get; init; } + [Description("Bypasses the checking of file hashes of indexes during startup and after index merges " + + "(allows for faster startup and less disk pressure after merges).")] + public bool SkipIndexVerify { get; init; } = false; - [Description("Disables Hard Deletes. (UNSAFE: use to remove hard deletes)")] - [Deprecated("This setting is unsafe and not recommended")] - public bool UnsafeIgnoreHardDelete { get; init; } = false; + [Description("Sets the depth to cache for the mid point cache in index.")] + public int IndexCacheDepth { get; init; } = 16; - [Description("Bypasses the checking of file hashes of indexes during startup and after index merges " + - "(allows for faster startup and less disk pressure after merges).")] - public bool SkipIndexVerify { get; init; } = false; + [Description("Change the way the DB files are opened to reduce their stickiness in the system file cache.")] + public bool ReduceFileCachePressure { get; init; } = false; - [Description("Sets the depth to cache for the mid point cache in index.")] - public int IndexCacheDepth { get; init; } = 16; + [Description("Number of threads to be used to initialize the database. " + + "Will be capped at host processor count.")] + public int InitializationThreads { get; init; } = 1; - [Description("Makes index merges faster and reduces disk pressure during merges.")] - [Deprecated("This setting is ignored by the new scavenge algorithm and will be removed in future versions.")] - public bool OptimizeIndexMerge { get; init; } = false; + [Description( + "The number of reader threads to use for processing reads. Set to '0' to scale automatically (Default)")] + public int ReaderThreadsCount { get; init; } = 0; - [Description("Always keeps the newer chunks from a scavenge operation.")] - [Deprecated("This setting is ignored by the new scavenge algorithm and will be removed in future versions.")] - public bool AlwaysKeepScavenged { get; init; } = false; + [Description("During large Index Merge operations, writes may be slowed down. Set this to the maximum " + + "index file level for which automatic merges should happen. Merging indexes above this level " + + "should be done manually.")] + public int MaxAutoMergeIndexLevel { get; init; } = int.MaxValue; - [Description("Change the way the DB files are opened to reduce their stickiness in the system file cache.")] - public bool ReduceFileCachePressure { get; init; } = false; + [Description("Set this option to write statistics to the database.")] + public bool WriteStatsToDb + { + get => (StatsStorage.Stream & StatsStorage) != 0; + init => StatsStorage = + value + ? StatsStorage.StreamAndFile + : StatsStorage.File; // TODO SS: not sure if we should do this here + } - [Description("Number of threads to be used to initialize the database. " + - "Will be capped at host processor count.")] - public int InitializationThreads { get; init; } = 1; + [Description("When truncate.chk is set, the database will be truncated on startup. " + + "This is a safety check to ensure large amounts of data truncation does not happen " + + "accidentally. This value should be set in the low 10,000s for allow for " + + "standard cluster recovery operations. -1 is no max.")] + public long MaxTruncation { get; init; } = 256 * 1_024 * 1_024; - [Description("The number of reader threads to use for processing reads. Set to '0' to scale automatically (Default)")] - public int ReaderThreadsCount { get; init; } = 0; + public int ChunkSize { get; init; } = TFConsts.ChunkSize; - [Description("During large Index Merge operations, writes may be slowed down. Set this to the maximum " + - "index file level for which automatic merges should happen. Merging indexes above this level " + - "should be done manually.")] - public int MaxAutoMergeIndexLevel { get; init; } = int.MaxValue; + public StatsStorage StatsStorage { get; init; } = StatsStorage.File; - [Description("Set this option to write statistics to the database.")] - public bool WriteStatsToDb { - get => (StatsStorage.Stream & StatsStorage) != 0; - init => StatsStorage = - value - ? StatsStorage.StreamAndFile - : StatsStorage.File; // TODO SS: not sure if we should do this here - } + [Description("The log format version to use for storing the event log. " + + "V3 is currently in development and should only be used for testing purposes.")] + public DbLogFormat DbLogFormat { get; init; } = DbLogFormat.V2; - [Description("When truncate.chk is set, the database will be truncated on startup. " + - "This is a safety check to ensure large amounts of data truncation does not happen " + - "accidentally. This value should be set in the low 10,000s for allow for " + - "standard cluster recovery operations. -1 is no max.")] - public long MaxTruncation { get; init; } = 256 * 1_024 * 1_024; + [Description("The amount of memory & disk space, in bytes, to use for the stream existence filter. " + + "This should be set to roughly the maximum number of streams you expect to have in your database, " + + "i.e if you expect to have a max of 500 million streams, use a value of 500 megabytes. " + + "The value you select should also fit entirely in memory to avoid any performance degradation. " + + "Use 0 to disable the filter. Resizing the filter will cause a full rebuild.")] + public long StreamExistenceFilterSize { get; init; } = Opts.StreamExistenceFilterSizeDefault; - public int ChunkSize { get; init; } = TFConsts.ChunkSize; + [Description("The page size of the scavenge database.")] + public int ScavengeBackendPageSize { get; init; } = Opts.ScavengeBackendPageSizeDefault; - public StatsStorage StatsStorage { get; init; } = StatsStorage.File; + [Description("The amount of memory to use for backend caching in bytes.")] + public long ScavengeBackendCacheSize { get; init; } = Opts.ScavengeBackendCacheSizeDefault; - [Description("The log format version to use for storing the event log. " + - "V3 is currently in development and should only be used for testing purposes.")] - public DbLogFormat DbLogFormat { get; init; } = DbLogFormat.V2; + [Description("The number of stream hashes to remember when checking for collisions.")] + public int ScavengeHashUsersCacheCapacity { get; init; } = Opts.ScavengeHashUsersCacheCapacityDefault; + } - [Description("The amount of memory & disk space, in bytes, to use for the stream existence filter. " + - "This should be set to roughly the maximum number of streams you expect to have in your database, " + - "i.e if you expect to have a max of 500 million streams, use a value of 500 megabytes. " + - "The value you select should also fit entirely in memory to avoid any performance degradation. " + - "Use 0 to disable the filter. Resizing the filter will cause a full rebuild.")] - public long StreamExistenceFilterSize { get; init; } = Opts.StreamExistenceFilterSizeDefault; + [Description("gRPC Options")] + public record GrpcOptions + { + [Description("Controls the period (in milliseconds) after which a keepalive ping " + + "is sent on the transport."), + Unit("ms")] + public int KeepAliveInterval { get; init; } = 10_000; + + [Description("Controls the amount of time (in milliseconds) the sender of the keepalive ping waits " + + "for an acknowledgement. If it does not receive an acknowledgment within this time, " + + "it will close the connection."), + Unit("ms")] + public int KeepAliveTimeout { get; init; } = 10_000; + + internal static GrpcOptions FromConfiguration(IConfiguration configurationRoot) => new() + { + KeepAliveInterval = configurationRoot.GetValue(nameof(KeepAliveInterval)), + KeepAliveTimeout = configurationRoot.GetValue(nameof(KeepAliveTimeout)) + }; + } - [Description("The page size of the scavenge database.")] - public int ScavengeBackendPageSize { get; init; } = Opts.ScavengeBackendPageSizeDefault; + [Description("Interface Options")] + public record InterfaceOptions + { + [Description("The IP Address used by internal replication between nodes in the cluster.")] + public IPAddress ReplicationIp { get; init; } = IPAddress.Loopback; - [Description("The amount of memory to use for backend caching in bytes.")] - public long ScavengeBackendCacheSize { get; init; } = Opts.ScavengeBackendCacheSizeDefault; + [Description("The IP Address for the node.")] + public IPAddress NodeIp { get; init; } = IPAddress.Loopback; - [Description("The number of stream hashes to remember when checking for collisions.")] - public int ScavengeHashUsersCacheCapacity { get; init; } = Opts.ScavengeHashUsersCacheCapacityDefault; - } + [Description("The Port to run the HTTP server on.")] + public int NodePort { get; init; } = 2113; - [Description("gRPC Options")] - public record GrpcOptions { - [Description("Controls the period (in milliseconds) after which a keepalive ping " + - "is sent on the transport."), - Unit("ms")] - public int KeepAliveInterval { get; init; } = 10_000; - - [Description("Controls the amount of time (in milliseconds) the sender of the keepalive ping waits " + - "for an acknowledgement. If it does not receive an acknowledgment within this time, " + - "it will close the connection."), - Unit("ms")] - public int KeepAliveTimeout { get; init; } = 10_000; - - internal static GrpcOptions FromConfiguration(IConfiguration configurationRoot) => new() { - KeepAliveInterval = configurationRoot.GetValue(nameof(KeepAliveInterval)), - KeepAliveTimeout = configurationRoot.GetValue(nameof(KeepAliveTimeout)) - }; - } + [Description("The TCP port used by internal replication between nodes in the cluster.")] + public int ReplicationPort { get; init; } = 1112; - [Description("Interface Options")] - public record InterfaceOptions { -#pragma warning disable 0618 - [Description("Internal IP Address."), - Deprecated( - "The IntIp parameter has been deprecated as of version 23.10.0. It is recommended to use the ReplicationIp parameter instead.")] - [Obsolete("IntIp is deprecated, use ReplicationIp instead")] - public IPAddress IntIp { get; init; } = IPAddress.Loopback; - - private readonly IPAddress _replicationIp = IPAddress.Loopback; - [Description("The IP Address used by internal replication between nodes in the cluster.")] - public IPAddress ReplicationIp { - get { - return _replicationIp.Equals(IPAddress.Loopback) ? IntIp : _replicationIp; - } - init { _replicationIp = value; } - } + [Description("Advertise the Node's host name to other nodes and external clients as.")] + public string? NodeHostAdvertiseAs { get; init; } = null; - [Description("External IP Address."), - Deprecated( - "The ExtIp parameter has been deprecated as of version 23.10.0. It is recommended to use the NodeIp parameter instead.")] - [Obsolete("ExtIp is deprecated, use NodeIp instead")] - public IPAddress ExtIp { get; init; } = IPAddress.Loopback; - - private readonly IPAddress _nodeIp = IPAddress.Loopback; - [Description("The IP Address for the node.")] - public IPAddress NodeIp { - get { - return _nodeIp.Equals(IPAddress.Loopback) ? ExtIp : _nodeIp; - } - init { _nodeIp = value; } - } + [Description("Advertise the Replication host name to other nodes in the cluster as.")] + public string? ReplicationHostAdvertiseAs { get; init; } = null; - [Description("The port to run the HTTP server on."), - Deprecated( - "The HttpPort parameter has been deprecated as of version 23.10.0. It is recommended to use the NodePort parameter instead.")] - [Obsolete("HttpPort is deprecated, use NodePort instead")] - public int HttpPort { get; init; } = 2113; - - private readonly int _nodePort = 2113; - - [Description("The Port to run the HTTP server on.")] - public int NodePort { - get { - return _nodePort == 2113 ? HttpPort : _nodePort; - } - init { - _nodePort = value; - } - } + [Description("Advertise Host in Gossip to Client As.")] + public string? AdvertiseHostToClientAs { get; init; } = null; - [Description("Internal TCP Port."), - Deprecated( - "The IntTcpPort parameter has been deprecated as of version 23.10.0. It is recommended to use the ReplicationPort parameter instead.")] - [Obsolete("IntTcpPort is deprecated, use ReplicationPort instead")] - public int IntTcpPort { get; init; } = 1112; - - private readonly int _replicationPort = 1112; - [Description("The TCP port used by internal replication between nodes in the cluster.")] - public int ReplicationPort { - get { - return _replicationPort == 1112 ? IntTcpPort : _replicationPort; - } - init { - _replicationPort = value; - } - } + [Description("Advertise Node Port in Gossip to Client As.")] + public int AdvertiseNodePortToClientAs { get; init; } = 0; - [Description("Advertise the Node's host name to other nodes and external clients as.")] - public string? NodeHostAdvertiseAs { get; init; } = null; - - [Description("Advertise Internal Tcp Address As."), - Deprecated( - "The IntHostAdvertiseAs parameter has been deprecated as of version 23.10.0. It is recommended to use the ReplicationHostAdvertiseAs parameter instead.")] - [Obsolete("IntHostAdvertiseAs is deprecated, use ReplicationHostAdvertiseAs instead")] - public string? IntHostAdvertiseAs { get; init; } = null; - - private readonly string? _replicationHostAdvertiseAs = null; - [Description("Advertise the Replication host name to other nodes in the cluster as.")] - public string? ReplicationHostAdvertiseAs { - get { - return _replicationHostAdvertiseAs ?? IntHostAdvertiseAs; - } - init { - _replicationHostAdvertiseAs = value; - } - } + [Description("Advertise Http Port As.")] + public int NodePortAdvertiseAs { get; init; } = 0; - [Description("Advertise Host in Gossip to Client As.")] - public string? AdvertiseHostToClientAs { get; init; } = null; - - [Description("Advertise HTTP Port in Gossip to Client As."), - Deprecated( - "The AdvertiseHttpPortToClientAs parameter has been deprecated as of version 23.10.0. It is recommended to use the AdvertiseNodePortToClientAs parameter instead.")] - [Obsolete("AdvertiseHttpPortToClientAs is deprecated, use AdvertiseNodePortToClientAs instead")] - public int AdvertiseHttpPortToClientAs { get; init; } = 0; - - private readonly int _advertiseNodePortToClientAs = 0; - - [Description("Advertise Node Port in Gossip to Client As.")] - public int AdvertiseNodePortToClientAs { - get { - return _advertiseNodePortToClientAs == 0 - ? AdvertiseHttpPortToClientAs - : _advertiseNodePortToClientAs; - } - init { - _advertiseNodePortToClientAs = value; - } - } + [Description("Advertise Replication Tcp Port As.")] + public int ReplicationTcpPortAdvertiseAs { get; init; } = 0; - [Description("Advertise Http Port As."), - Deprecated( - "The HttpPortAdvertiseAs parameter has been deprecated as of version 23.10.0. It is recommended to use the NodePortAdvertiseAs parameter instead.")] - [Obsolete("HttpPortAdvertiseAs is deprecated, use NodePortAdvertiseAs instead")] - public int HttpPortAdvertiseAs { get; init; } = 0; - - private readonly int _nodePortAdvertiseAs = 0; - [Description("Advertise Http Port As.")] - public int NodePortAdvertiseAs { - get { - return _nodePortAdvertiseAs == 0 ? HttpPortAdvertiseAs : _nodePortAdvertiseAs; - } - init { - _nodePortAdvertiseAs = value; - } - } + [Description("Heartbeat timeout for Replication TCP sockets."), + Unit("ms")] + public int ReplicationHeartbeatTimeout { get; init; } = 700; - [Description("Advertise Internal Tcp Port As."), - Deprecated( - "The IntTcpPortAdvertiseAs parameter has been deprecated as of version 23.10.0. It is recommended to use the ReplicationTcpPortAdvertiseAs parameter instead.")] - [Obsolete("IntTcpPortAdvertiseAs is deprecated, use ReplicationTcpPortAdvertiseAs instead")] - public int IntTcpPortAdvertiseAs { get; init; } = 0; - - private readonly int _replicationTcpPortAdvertiseAs = 0; - [Description("Advertise Replication Tcp Port As.")] - public int ReplicationTcpPortAdvertiseAs { - get { - return _replicationTcpPortAdvertiseAs == 0 ? IntTcpPortAdvertiseAs : _replicationTcpPortAdvertiseAs; - } - init { - _replicationTcpPortAdvertiseAs = value; - } - } + [Description("Heartbeat interval for Replication TCP sockets."), + Unit("ms")] + public int ReplicationHeartbeatInterval { get; init; } = 700; - [Description("Heartbeat timeout for internal TCP sockets."), - Unit("ms"), - Deprecated( - "The IntTcpHeartbeatTimeout parameter has been deprecated as of version 23.10.0. It is recommended to use the ReplicationHeartbeatTimeout parameter instead.")] - [Obsolete("IntTcpHeartbeatTimeout is deprecated, use ReplicationHeartbeatTimeout instead")] - public int IntTcpHeartbeatTimeout { get; init; } = 700; - - private readonly int _replicationHeartbeatTimeout = 700; - [Description("Heartbeat timeout for Replication TCP sockets."), - Unit("ms")] - public int ReplicationHeartbeatTimeout { - get { - return _replicationHeartbeatTimeout == 700 ? IntTcpHeartbeatTimeout : _replicationHeartbeatTimeout; - } - init { - _replicationHeartbeatTimeout = value; - } - } + [Description("Whether to allow local connections via a UNIX domain socket.")] + public bool EnableUnixSocket { get; init; } = false; - [Description("Heartbeat interval for internal TCP sockets."), - Unit("ms"), - Deprecated( - "The IntTcpHeartbeatInterval parameter has been deprecated as of version 23.10.0. It is recommended to use the ReplicationHeartbeatInterval parameter instead.")] - [Obsolete("IntTcpHeartbeatInterval is deprecated, use ReplicationHeartbeatInterval instead")] - public int IntTcpHeartbeatInterval { get; init; } = 700; - - private readonly int _replicationHeartbeatInterval = 700; - [Description("Heartbeat interval for Replication TCP sockets."), - Unit("ms")] - public int ReplicationHeartbeatInterval { - get { - return _replicationHeartbeatInterval == 700 - ? IntTcpHeartbeatInterval - : _replicationHeartbeatInterval; - } - init { - _replicationHeartbeatInterval = value; - } - } + [Description("The maximum number of pending send bytes allowed before a connection is closed.")] + public int ConnectionPendingSendBytesThreshold { get; init; } = 10 * 1_024 * 1_024; - [Description("Whether to allow local connections via a UNIX domain socket.")] - public bool EnableUnixSocket { get; init; } = false; + [Description("The maximum number of pending connection operations allowed before a connection is closed.")] + public int ConnectionQueueSizeThreshold { get; init; } = 50_000; - [Description("When enabled, tells a single node to run gossip as if it is a cluster."), - Deprecated("The '" + nameof(GossipOnSingleNode) + "' option has been deprecated as of version 21.2.")] - public bool? GossipOnSingleNode { get; init; } = null; + [Description("Disables the admin ui on the HTTP endpoint.")] + public bool DisableAdminUi { get; init; } = false; - [Description("The maximum number of pending send bytes allowed before a connection is closed.")] - public int ConnectionPendingSendBytesThreshold { get; init; } = 10 * 1_024 * 1_024; + [Description("Disables statistics requests on the HTTP endpoint.")] + public bool DisableStatsOnHttp { get; init; } = false; - [Description("The maximum number of pending connection operations allowed before a connection is closed.")] - public int ConnectionQueueSizeThreshold { get; init; } = 50_000; + [Description("Disables gossip requests on the HTTP endpoint.")] + public bool DisableGossipOnHttp { get; init; } = false; - [Description("Disables the admin ui on the HTTP endpoint.")] - public bool DisableAdminUi { get; init; } = false; + [Description("Enables trusted authentication by an intermediary in the HTTP.")] + public bool EnableTrustedAuth { get; init; } = false; - [Description("Disables statistics requests on the HTTP endpoint.")] - public bool DisableStatsOnHttp { get; init; } = false; + [Description("Enable AtomPub over HTTP Interface."), + Deprecated( + "AtomPub over HTTP Interface has been deprecated as of version 20.6.0. It is recommended to use gRPC instead")] + public bool EnableAtomPubOverHttp { get; init; } = false; + } - [Description("Disables gossip requests on the HTTP endpoint.")] - public bool DisableGossipOnHttp { get; init; } = false; + [Description("Projection Options")] + public record ProjectionOptions + { + public const int DefaultProjectionExecutionTimeout = 250; - [Description("Enables trusted authentication by an intermediary in the HTTP.")] - public bool EnableTrustedAuth { get; init; } = false; + [Description("Enables the running of projections. System runs built-in projections, " + + "All runs user projections.")] + public ProjectionType RunProjections { get; init; } - [Description("Whether to disable secure internal TCP communication."), - Deprecated("The '" + nameof(DisableInternalTcpTls) + - "' option has been deprecated as of version 20.6.1 and currently has no effect. Please use the '" + - nameof(Application.Insecure) + "' option instead.")] - public bool DisableInternalTcpTls { get; init; } = false; + [Description("Start the built in system projections.")] + public bool StartStandardProjections { get; init; } = false; - [Description("Enable AtomPub over HTTP Interface."), - Deprecated("AtomPub over HTTP Interface has been deprecated as of version 20.6.0. It is recommended to use gRPC instead")] - public bool EnableAtomPubOverHttp { get; init; } = false; -#pragma warning restore 0618 - } + [Description("The number of threads to use for projections.")] + public int ProjectionThreads { get; init; } = 3; - [Description("Projection Options")] - public record ProjectionOptions { - public const int DefaultProjectionExecutionTimeout = 250; - [Description("Enables the running of projections. System runs built-in projections, " + - "All runs user projections.")] - public ProjectionType RunProjections { get; init; } + [Description("The number of minutes a query can be idle before it expires."), + Unit("m")] + public int ProjectionsQueryExpiry { get; init; } = 5; - [Description("Start the built in system projections.")] - public bool StartStandardProjections { get; init; } = false; + [Description("Fault the projection if the Event number that was expected in the stream differs " + + "from what is received. This may happen if events have been deleted or expired.")] + public bool FaultOutOfOrderProjections { get; init; } = false; - [Description("The number of threads to use for projections.")] - public int ProjectionThreads { get; init; } = 3; + [Description("The time in milliseconds allowed for the compilation phase of user projections"), + Unit("ms")] + public int ProjectionCompilationTimeout { get; set; } = 500; - [Description("The number of minutes a query can be idle before it expires."), - Unit("m")] - public int ProjectionsQueryExpiry { get; init; } = 5; + [Description( + "The maximum execution time in milliseconds for executing a handler in a user projection. It can be overridden for a specific projection by setting ProjectionExecutionTimeout config for that projection"), + Unit("ms")] + public int ProjectionExecutionTimeout { get; set; } = DefaultProjectionExecutionTimeout; + } - [Description("Fault the projection if the Event number that was expected in the stream differs " + - "from what is received. This may happen if events have been deleted or expired.")] - public bool FaultOutOfOrderProjections { get; init; } = false; + public record UnknownOptions(IReadOnlyList<(string, string)> Options) + { + /// + /// Identifies unknown options in the configuration and provides suggestions for known options. + /// + public static UnknownOptions FromConfiguration(IConfiguration configuration) + { + var knownKeys = Metadata + .SelectMany(x => x.Options) + .Select(x => x.Key) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + var unknownKeys = FindUnknownKeys(configuration, knownKeys); + + var result = unknownKeys + .Select(unknownKey => CreateUnknownOptionResult(knownKeys, unknownKey)) + .ToList(); + + return new(result); + + static IEnumerable FindUnknownKeys(IConfiguration configuration, + IReadOnlySet knownKeys) + { + var unknownKeys = configuration + .AsEnumerable() + .Select(kvp => kvp.Key) + .Where(key => key != EventStoreConfigurationKeys.Prefix + && !knownKeys.Contains(EventStoreConfigurationKeys.Normalize(key))) + .ToList(); - [Description("The time in milliseconds allowed for the compilation phase of user projections"), - Unit("ms")] - public int ProjectionCompilationTimeout { get; set; } = 500; + var unknownSections = FindUnknownSections(unknownKeys); - [Description("The maximum execution time in milliseconds for executing a handler in a user projection. It can be overridden for a specific projection by setting ProjectionExecutionTimeout config for that projection"), - Unit("ms")] - public int ProjectionExecutionTimeout { get; set; } = DefaultProjectionExecutionTimeout; - } + // only report top level unknown keys. plugins, metrics, etc will use nested keys. + // in the future we may report unknown keys in nested sections but it is out of scope for now. + return unknownKeys + .Where(key => !unknownSections.Any(key.StartsWith)); + } - public record UnknownOptions(IReadOnlyList<(string, string)> Options) { - /// - /// Identifies unknown options in the configuration and provides suggestions for known options. - /// - public static UnknownOptions FromConfiguration(IConfiguration configuration) { - var knownKeys = Metadata - .SelectMany(x => x.Options) - .Select(x => x.Key) - .ToHashSet(StringComparer.OrdinalIgnoreCase); + static HashSet FindUnknownSections(IEnumerable keys) + { + // if it has more than 2 sections, we found a value with an unknown section + var hashSet = new HashSet(); - var unknownKeys = FindUnknownKeys(configuration, knownKeys); + foreach (var key in keys.Where(key => key.Split(":").Length > 2)) + hashSet.Add(key[..key.LastIndexOf(':')]); - var result = unknownKeys - .Select(unknownKey => CreateUnknownOptionResult(knownKeys, unknownKey)) - .ToList(); + return hashSet; + } - return new(result); - - static IEnumerable FindUnknownKeys(IConfiguration configuration, - IReadOnlySet knownKeys) { - var unknownKeys = configuration - .AsEnumerable() - .Select(kvp => kvp.Key) - .Where(key => key != EventStoreConfigurationKeys.Prefix - && !knownKeys.Contains(EventStoreConfigurationKeys.Normalize(key))) - .ToList(); - - var unknownSections = FindUnknownSections(unknownKeys); - - return unknownKeys - .Where(key => !unknownSections.Any(key.StartsWith)); - } - - static HashSet FindUnknownSections(IEnumerable keys) { - // if it has more than 2 sections, we found a value with an unknown section - var hashSet = new HashSet(); - - foreach (var key in keys.Where(key => key.Split(":").Length > 2)) - hashSet.Add(key[..key.LastIndexOf(':')]); - - return hashSet; - } - - static (string UnknownKey, string SuggestedKey) CreateUnknownOptionResult(IEnumerable knownKeys, - string unknownKey, int distanceThreshold = 5) { - var suggestion = knownKeys - .Select(key => (AllowedKey: key, Distance: Levenshtein.GetDistance(unknownKey, key))) - .MinBy(x => x.Distance); - - return ( - UnknownKey: EventStoreConfigurationKeys.StripConfigurationPrefix(unknownKey), - SuggestedKey: suggestion.Distance > distanceThreshold - ? "" - : EventStoreConfigurationKeys.StripConfigurationPrefix(suggestion.AllowedKey) - ); - } + static (string UnknownKey, string SuggestedKey) CreateUnknownOptionResult(IEnumerable knownKeys, + string unknownKey, int distanceThreshold = 5) + { + var suggestion = knownKeys + .Select(key => (AllowedKey: key, Distance: Levenshtein.GetDistance(unknownKey, key))) + .MinBy(x => x.Distance); + + return ( + UnknownKey: EventStoreConfigurationKeys.StripConfigurationPrefix(unknownKey), + SuggestedKey: suggestion.Distance > distanceThreshold + ? "" + : EventStoreConfigurationKeys.StripConfigurationPrefix(suggestion.AllowedKey) + ); } } } diff --git a/src/EventStore.Core/Configuration/ClusterVNodeOptionsExtensions.cs b/src/EventStore.Core/Configuration/ClusterVNodeOptionsExtensions.cs index a1c03fc9e..07b8e4b65 100644 --- a/src/EventStore.Core/Configuration/ClusterVNodeOptionsExtensions.cs +++ b/src/EventStore.Core/Configuration/ClusterVNodeOptionsExtensions.cs @@ -9,301 +9,266 @@ using EventStore.Plugins; using Serilog; -namespace EventStore.Core { - public static class ClusterVNodeOptionsExtensions { - public static ClusterVNodeOptions Reload(this ClusterVNodeOptions options) => - options.ConfigurationRoot == null - ? options - : ClusterVNodeOptions.FromConfiguration(options.ConfigurationRoot); +namespace EventStore.Core; - public static ClusterVNodeOptions WithPlugableComponent(this ClusterVNodeOptions options, IPlugableComponent plugableComponent) => - options with { PlugableComponents = [..options.PlugableComponents, plugableComponent] }; +public static class ClusterVNodeOptionsExtensions +{ + public static ClusterVNodeOptions Reload(this ClusterVNodeOptions options) => + options.ConfigurationRoot == null + ? options + : ClusterVNodeOptions.FromConfiguration(options.ConfigurationRoot); - public static ClusterVNodeOptions InCluster(this ClusterVNodeOptions options, int clusterSize) => options with { - Cluster = options.Cluster with { - ClusterSize = clusterSize <= 1 - ? throw new ArgumentOutOfRangeException(nameof(clusterSize), clusterSize, - $"{nameof(clusterSize)} must be greater than 1.") - : clusterSize - } - }; - - /// - /// Returns a builder set to run in memory only - /// - /// The - /// A with the options set - public static ClusterVNodeOptions RunInMemory(this ClusterVNodeOptions options) => options with { - Database = options.Database with { - MemDb = true, - Db = new ClusterVNodeOptions().Database.Db - } - }; - - /// - /// Returns a builder set to write database files to the specified path - /// - /// The - /// The path on disk in which to write the database files - /// A with the options set - public static ClusterVNodeOptions RunOnDisk(this ClusterVNodeOptions options, string path) => options with { - Database = options.Database with { - MemDb = false, - Db = path - } - }; - - /// - /// Runs the node in insecure mode. - /// - /// The - /// A with the options set - public static ClusterVNodeOptions Insecure(this ClusterVNodeOptions options) => options with { - Application = options.Application with { - Insecure = true - }, - ServerCertificate = null, - TrustedRootCertificates = null - }; + public static ClusterVNodeOptions WithPlugableComponent(this ClusterVNodeOptions options, + IPlugableComponent plugableComponent) => + options with { PlugableComponents = [..options.PlugableComponents, plugableComponent] }; - /// - /// Runs the node in secure mode. - /// - /// The - /// A containing trusted root - /// A for the server - /// A with the options set - public static ClusterVNodeOptions Secure(this ClusterVNodeOptions options, - X509Certificate2Collection trustedRootCertificates, X509Certificate2 serverCertificate) => options with { - Application = options.Application with { - Insecure = false, - }, - ServerCertificate = serverCertificate, - TrustedRootCertificates = trustedRootCertificates - }; + public static ClusterVNodeOptions InCluster(this ClusterVNodeOptions options, int clusterSize) => options with + { + Cluster = options.Cluster with + { + ClusterSize = clusterSize <= 1 + ? throw new ArgumentOutOfRangeException(nameof(clusterSize), clusterSize, + $"{nameof(clusterSize)} must be greater than 1.") + : clusterSize + } + }; - /// - /// Sets gossip seeds to the specified value and turns off dns discovery - /// - /// The - /// The list of gossip seeds - /// A with the options set - public static ClusterVNodeOptions WithGossipSeeds(this ClusterVNodeOptions options, EndPoint[] gossipSeeds) => - options with { - Cluster = options.Cluster with { - GossipSeed = gossipSeeds, - DiscoverViaDns = false, - ClusterDns = string.Empty - } - }; + /// + /// Returns a builder set to run in memory only + /// + /// The + /// A with the options set + public static ClusterVNodeOptions RunInMemory(this ClusterVNodeOptions options) => options with + { + Database = options.Database with { MemDb = true, Db = new ClusterVNodeOptions().Database.Db } + }; - /// - /// Sets the external tcp endpoint to the specified value - /// - /// The - /// The external secure endpoint to use - /// A with the options set - public static ClusterVNodeOptions WithExternalSecureTcpOn( - this ClusterVNodeOptions options, IPEndPoint endPoint) => - options with { - Interface = options.Interface with { - NodeIp = endPoint.Address, - } - }; + /// + /// Returns a builder set to write database files to the specified path + /// + /// The + /// The path on disk in which to write the database files + /// A with the options set + public static ClusterVNodeOptions RunOnDisk(this ClusterVNodeOptions options, string path) => options with + { + Database = options.Database with { MemDb = false, Db = path } + }; - /// - /// Sets the internal secure tcp endpoint to the specified value - /// - /// The - /// The internal secure endpoint to use - /// A with the options set - public static ClusterVNodeOptions WithInternalSecureTcpOn( - this ClusterVNodeOptions options, IPEndPoint endPoint) => - options with { - Interface = options.Interface with { - ReplicationIp = endPoint.Address, - DisableInternalTcpTls = false, - ReplicationPort = endPoint.Port - } - }; + /// + /// Runs the node in insecure mode. + /// + /// The + /// A with the options set + public static ClusterVNodeOptions Insecure(this ClusterVNodeOptions options) => options with + { + Application = options.Application with { Insecure = true }, + ServerCertificate = null, + TrustedRootCertificates = null + }; - /// - /// Sets the external tcp endpoint to the specified value - /// - /// The - /// The external endpoint to use - /// A with the options set - public static ClusterVNodeOptions WithExternalTcpOn( - this ClusterVNodeOptions options, IPEndPoint endPoint) => - options with { - Interface = options.Interface with { - NodeIp = endPoint.Address, - } - }; + /// + /// Runs the node in secure mode. + /// + /// The + /// A containing trusted root + /// A for the server + /// A with the options set + public static ClusterVNodeOptions Secure(this ClusterVNodeOptions options, + X509Certificate2Collection trustedRootCertificates, X509Certificate2 serverCertificate) => options with + { + Application = options.Application with { Insecure = false, }, + ServerCertificate = serverCertificate, + TrustedRootCertificates = trustedRootCertificates + }; - /// - /// Sets the internal tcp endpoint to the specified value - /// - /// The - /// The internal endpoint to use - /// A with the options set - public static ClusterVNodeOptions WithInternalTcpOn( - this ClusterVNodeOptions options, IPEndPoint endPoint) => - options with { - Interface = options.Interface with { - ReplicationIp = endPoint.Address, - ReplicationPort = endPoint.Port, - DisableInternalTcpTls = true - } - }; + /// + /// Sets gossip seeds to the specified value and turns off dns discovery + /// + /// The + /// The list of gossip seeds + /// A with the options set + public static ClusterVNodeOptions WithGossipSeeds(this ClusterVNodeOptions options, EndPoint[] gossipSeeds) => + options with + { + Cluster = options.Cluster with + { + GossipSeed = gossipSeeds, DiscoverViaDns = false, ClusterDns = string.Empty + } + }; - /// - /// Sets the http endpoint to the specified value - /// - /// The - /// The http endpoint to use - /// A with the options set - public static ClusterVNodeOptions WithHttpOn( - this ClusterVNodeOptions options, IPEndPoint endPoint) => - options with { - Interface = options.Interface with { - NodeIp = endPoint.Address, - NodePort = endPoint.Port - } - }; + /// + /// Sets the external tcp endpoint to the specified value + /// + /// The + /// The external endpoint to use + /// A with the options set + public static ClusterVNodeOptions WithExternalTcpOn( + this ClusterVNodeOptions options, IPEndPoint endPoint) => + options with { Interface = options.Interface with { NodeIp = endPoint.Address, } }; - /// - /// Sets up the External Host that would be advertised - /// - /// The - /// The advertised host - /// A with the options set - public static ClusterVNodeOptions - AdvertiseExternalHostAs(this ClusterVNodeOptions options, EndPoint endPoint) => - options with { - Interface = options.Interface with { - NodeHostAdvertiseAs = endPoint.GetHost(), - } - }; + /// + /// Sets the internal tcp endpoint to the specified value + /// + /// The + /// The internal endpoint to use + /// A with the options set + public static ClusterVNodeOptions WithReplicationEndpointOn( + this ClusterVNodeOptions options, IPEndPoint endPoint) => + options with + { + Interface = options.Interface with { ReplicationIp = endPoint.Address, ReplicationPort = endPoint.Port } + }; - /// - /// Sets up the Internal Host that would be advertised - /// - /// The - /// The advertised host - /// A with the options set - public static ClusterVNodeOptions - AdvertiseInternalHostAs(this ClusterVNodeOptions options, EndPoint endPoint) => - options with { - Interface = options.Interface with { - ReplicationHostAdvertiseAs = endPoint.GetHost(), - ReplicationTcpPortAdvertiseAs = endPoint.GetPort() - } - }; + /// + /// Sets the http endpoint to the specified value + /// + /// The + /// The http endpoint to use + /// A with the options set + public static ClusterVNodeOptions WithNodeEndpointOn( + this ClusterVNodeOptions options, IPEndPoint endPoint) => + options with { Interface = options.Interface with { NodeIp = endPoint.Address, NodePort = endPoint.Port } }; - /// - /// - /// The - /// The advertised host - /// A with the options set - public static ClusterVNodeOptions AdvertiseHttpHostAs(this ClusterVNodeOptions options, EndPoint endPoint) => - options with { - Interface = options.Interface with { - NodeHostAdvertiseAs = endPoint.GetHost(), - NodePortAdvertiseAs = endPoint.GetPort() - } - }; + /// + /// Sets up the External Host that would be advertised + /// + /// The + /// The advertised host + /// A with the options set + public static ClusterVNodeOptions + AdvertiseExternalHostAs(this ClusterVNodeOptions options, EndPoint endPoint) => + options with { Interface = options.Interface with { NodeHostAdvertiseAs = endPoint.GetHost(), } }; - /// - /// - /// - /// - /// - public static (X509Certificate2 certificate, X509Certificate2Collection intermediates) LoadNodeCertificate( - this ClusterVNodeOptions options) { - if (options.ServerCertificate != null) { - //used by test code paths only - return (options.ServerCertificate!, null); + /// + /// Sets up the Internal Host that would be advertised + /// + /// The + /// The advertised host + /// A with the options set + public static ClusterVNodeOptions + AdvertiseInternalHostAs(this ClusterVNodeOptions options, EndPoint endPoint) => + options with + { + Interface = options.Interface with + { + ReplicationHostAdvertiseAs = endPoint.GetHost(), + ReplicationTcpPortAdvertiseAs = endPoint.GetPort() } + }; - - if (!string.IsNullOrWhiteSpace(options.CertificateStore.CertificateStoreLocation)) { - var location = - CertificateUtils.GetCertificateStoreLocation(options.CertificateStore.CertificateStoreLocation); - var name = CertificateUtils.GetCertificateStoreName(options.CertificateStore.CertificateStoreName); - return (CertificateUtils.LoadFromStore(location, name, options.CertificateStore.CertificateSubjectName, - options.CertificateStore.CertificateThumbprint), null); + /// + /// + /// The + /// The advertised host + /// A with the options set + public static ClusterVNodeOptions AdvertiseNodeAs(this ClusterVNodeOptions options, EndPoint endPoint) => + options with + { + Interface = options.Interface with + { + NodeHostAdvertiseAs = endPoint.GetHost(), NodePortAdvertiseAs = endPoint.GetPort() } + }; - if (!string.IsNullOrWhiteSpace(options.CertificateStore.CertificateStoreName)) { - var name = CertificateUtils.GetCertificateStoreName(options.CertificateStore.CertificateStoreName); - return ( - CertificateUtils.LoadFromStore(name, options.CertificateStore.CertificateSubjectName, - options.CertificateStore.CertificateThumbprint), null); - } + /// + /// + /// + /// + /// + public static (X509Certificate2 certificate, X509Certificate2Collection intermediates) LoadNodeCertificate( + this ClusterVNodeOptions options) + { + if (options.ServerCertificate != null) + { + //used by test code paths only + return (options.ServerCertificate!, null); + } - if (options.CertificateFile.CertificateFile.IsNotEmptyString()) { - Log.Information("Loading the node's certificate(s) from file: {path}", - options.CertificateFile.CertificateFile); - return CertificateUtils.LoadFromFile(options.CertificateFile.CertificateFile, - options.CertificateFile.CertificatePrivateKeyFile, options.CertificateFile.CertificatePassword, - options.CertificateFile.CertificatePrivateKeyPassword); - } - throw new InvalidConfigurationException( - "A certificate is required unless insecure mode (--insecure) is set."); + if (!string.IsNullOrWhiteSpace(options.CertificateStore.CertificateStoreLocation)) + { + var location = + CertificateUtils.GetCertificateStoreLocation(options.CertificateStore.CertificateStoreLocation); + var name = CertificateUtils.GetCertificateStoreName(options.CertificateStore.CertificateStoreName); + return (CertificateUtils.LoadFromStore(location, name, options.CertificateStore.CertificateSubjectName, + options.CertificateStore.CertificateThumbprint), null); } - /// - /// Loads an from the options set. - /// If either TrustedRootCertificateStoreLocation or TrustedRootCertificateStoreName is set, - /// then the certificates will only be loaded from the certificate store. - /// Otherwise, the certificates will be loaded from the path specified by TrustedRootCertificatesPath. - /// - /// - /// - /// - public static X509Certificate2Collection LoadTrustedRootCertificates(this ClusterVNodeOptions options) { - if (options.TrustedRootCertificates != null) return options.TrustedRootCertificates; - var trustedRootCerts = new X509Certificate2Collection(); + if (!string.IsNullOrWhiteSpace(options.CertificateStore.CertificateStoreName)) + { + var name = CertificateUtils.GetCertificateStoreName(options.CertificateStore.CertificateStoreName); + return ( + CertificateUtils.LoadFromStore(name, options.CertificateStore.CertificateSubjectName, + options.CertificateStore.CertificateThumbprint), null); + } - if (!string.IsNullOrWhiteSpace(options.CertificateStore.TrustedRootCertificateStoreLocation)) { - var location = - CertificateUtils.GetCertificateStoreLocation(options.CertificateStore - .TrustedRootCertificateStoreLocation); - var name = CertificateUtils.GetCertificateStoreName(options.CertificateStore - .TrustedRootCertificateStoreName); - trustedRootCerts.Add(CertificateUtils.LoadFromStore(location, name, - options.CertificateStore.TrustedRootCertificateSubjectName, - options.CertificateStore.TrustedRootCertificateThumbprint)); - return trustedRootCerts; - } + if (options.CertificateFile.CertificateFile.IsNotEmptyString()) + { + Log.Information("Loading the node's certificate(s) from file: {path}", + options.CertificateFile.CertificateFile); + return CertificateUtils.LoadFromFile(options.CertificateFile.CertificateFile, + options.CertificateFile.CertificatePrivateKeyFile, options.CertificateFile.CertificatePassword, + options.CertificateFile.CertificatePrivateKeyPassword); + } - if (!string.IsNullOrWhiteSpace(options.CertificateStore.TrustedRootCertificateStoreName)) { - var name = CertificateUtils.GetCertificateStoreName(options.CertificateStore - .TrustedRootCertificateStoreName); - trustedRootCerts.Add(CertificateUtils.LoadFromStore(name, - options.CertificateStore.TrustedRootCertificateSubjectName, - options.CertificateStore.TrustedRootCertificateThumbprint)); - return trustedRootCerts; - } + throw new InvalidConfigurationException( + "A certificate is required unless insecure mode (--insecure) is set."); + } - if (string.IsNullOrEmpty(options.Certificate.TrustedRootCertificatesPath)) { - throw new InvalidConfigurationException( - $"{nameof(options.Certificate.TrustedRootCertificatesPath)} must be specified unless insecure mode (--insecure) is set."); - } + /// + /// Loads an from the options set. + /// If either TrustedRootCertificateStoreLocation or TrustedRootCertificateStoreName is set, + /// then the certificates will only be loaded from the certificate store. + /// Otherwise, the certificates will be loaded from the path specified by TrustedRootCertificatesPath. + /// + /// + /// + /// + public static X509Certificate2Collection LoadTrustedRootCertificates(this ClusterVNodeOptions options) + { + if (options.TrustedRootCertificates != null) return options.TrustedRootCertificates; + var trustedRootCerts = new X509Certificate2Collection(); - Log.Information("Loading trusted root certificates."); - foreach (var (fileName, cert) in CertificateUtils - .LoadAllCertificates(options.Certificate.TrustedRootCertificatesPath)) { - trustedRootCerts.Add(cert); - Log.Information("Loading trusted root certificate file: {file}", fileName); - } + if (!string.IsNullOrWhiteSpace(options.CertificateStore.TrustedRootCertificateStoreLocation)) + { + var location = + CertificateUtils.GetCertificateStoreLocation(options.CertificateStore + .TrustedRootCertificateStoreLocation); + var name = CertificateUtils.GetCertificateStoreName(options.CertificateStore + .TrustedRootCertificateStoreName); + trustedRootCerts.Add(CertificateUtils.LoadFromStore(location, name, + options.CertificateStore.TrustedRootCertificateSubjectName, + options.CertificateStore.TrustedRootCertificateThumbprint)); + return trustedRootCerts; + } - if (trustedRootCerts.Count == 0) - throw new InvalidConfigurationException( - $"No trusted root certificate files were loaded from the specified path: {options.Certificate.TrustedRootCertificatesPath}"); + if (!string.IsNullOrWhiteSpace(options.CertificateStore.TrustedRootCertificateStoreName)) + { + var name = CertificateUtils.GetCertificateStoreName(options.CertificateStore + .TrustedRootCertificateStoreName); + trustedRootCerts.Add(CertificateUtils.LoadFromStore(name, + options.CertificateStore.TrustedRootCertificateSubjectName, + options.CertificateStore.TrustedRootCertificateThumbprint)); return trustedRootCerts; } + + if (string.IsNullOrEmpty(options.Certificate.TrustedRootCertificatesPath)) + { + throw new InvalidConfigurationException( + $"{nameof(options.Certificate.TrustedRootCertificatesPath)} must be specified unless insecure mode (--insecure) is set."); + } + + Log.Information("Loading trusted root certificates."); + foreach (var (fileName, cert) in CertificateUtils + .LoadAllCertificates(options.Certificate.TrustedRootCertificatesPath)) + { + trustedRootCerts.Add(cert); + Log.Information("Loading trusted root certificate file: {file}", fileName); + } + + if (trustedRootCerts.Count == 0) + throw new InvalidConfigurationException( + $"No trusted root certificate files were loaded from the specified path: {options.Certificate.TrustedRootCertificatesPath}"); + return trustedRootCerts; } } diff --git a/src/EventStore.Core/Telemetry/TelemetryService.cs b/src/EventStore.Core/Telemetry/TelemetryService.cs index f437c1807..2203bb61a 100644 --- a/src/EventStore.Core/Telemetry/TelemetryService.cs +++ b/src/EventStore.Core/Telemetry/TelemetryService.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; using System.Text.Json; @@ -19,6 +20,7 @@ using EventStore.Core.TransactionLog.Chunks; using EventStore.Core.TransactionLog.LogRecords; using EventStore.Plugins.Diagnostics; +using Microsoft.Extensions.Configuration; using Serilog; using static EventStore.Plugins.Diagnostics.PluginDiagnosticsDataCollectionMode; @@ -38,6 +40,7 @@ public sealed class TelemetryService : private static readonly TimeSpan FlushDelay = TimeSpan.FromSeconds(10); private readonly ClusterVNodeOptions _nodeOptions; + private readonly IConfiguration _configuration; private readonly Task _task; private readonly IPublisher _publisher; private readonly IReadOnlyCheckpoint _writerCheckpoint; @@ -55,6 +58,7 @@ public sealed class TelemetryService : public TelemetryService( TFChunkManager manager, ClusterVNodeOptions nodeOptions, + IConfiguration configuration, IPublisher publisher, ITelemetrySink sink, IReadOnlyCheckpoint writerCheckpoint, @@ -63,6 +67,7 @@ Guid nodeId { _manager = manager; _nodeOptions = nodeOptions; + _configuration = configuration; _publisher = publisher; _writerCheckpoint = writerCheckpoint; _nodeId = nodeId; @@ -248,8 +253,9 @@ private async ValueTask Handle(TelemetryMessage.Request message, CancellationTok { try { - var payload = JsonSerializer.SerializeToNode(evt.Data); - message.Envelope.ReplyWith(new TelemetryMessage.Response("plugins", evt.Source, payload)); + var payload = JsonSerializer.SerializeToNode( + evt.Data.ToDictionary(kvp => LowerFirstLetter(kvp.Key), kvp => kvp.Value)); + message.Envelope.ReplyWith(new TelemetryMessage.Response(LowerFirstLetter(evt.Source), payload)); } catch (Exception ex) { @@ -259,6 +265,23 @@ private async ValueTask Handle(TelemetryMessage.Request message, CancellationTok _publisher.Publish( new GossipMessage.ReadGossip(new CallbackEnvelope(resp => OnGossipReceived(message.Envelope, resp)))); + { + var extraTelemetry = _configuration.GetSection("EventStore:Telemetry").Get>() ?? + []; + var payload = + JsonSerializer.SerializeToNode(extraTelemetry.ToDictionary(kvp => LowerFirstLetter(kvp.Key), + kvp => kvp.Value)); + message.Envelope.ReplyWith(new TelemetryMessage.Response( + "telemetry", payload)); + } + } + + private static string LowerFirstLetter(string x) + { + if (string.IsNullOrEmpty(x) || char.IsLower(x[0])) + return x; + + return $"{char.ToLower(x[0])}{x[1..]}"; } private static void OnGossipReceived(IEnvelope envelope, Message message) diff --git a/src/EventStore.SystemRuntime/Diagnostics/DriveStats.cs b/src/EventStore.SystemRuntime/Diagnostics/DriveStats.cs index d6c81fa52..5210edb67 100644 --- a/src/EventStore.SystemRuntime/Diagnostics/DriveStats.cs +++ b/src/EventStore.SystemRuntime/Diagnostics/DriveStats.cs @@ -1,14 +1,38 @@ // ReSharper disable CheckNamespace +using Serilog; + namespace System.Diagnostics; -public static class DriveStats +public class DriveStats { + public static readonly ILogger Log = Serilog.Log.ForContext(); + public static DriveData GetDriveInfo(string path) { - var info = new DriveInfo(Directory.GetDirectoryRoot(path)); - var data = new DriveData(info.Name, info.TotalSize, info.AvailableFreeSpace); - return data; + try + { + var info = new DriveInfo(Path.GetFullPath(path)); + var target = info.Name; + var diskName = ""; + + foreach (var candidate in DriveInfo.GetDrives()) + { + if (target.StartsWith(candidate.Name, StringComparison.InvariantCultureIgnoreCase) && + candidate.Name.StartsWith(diskName, StringComparison.InvariantCultureIgnoreCase)) + { + diskName = candidate.Name; + } + } + + return new DriveData(diskName, info.TotalSize, info.AvailableFreeSpace); + + } + catch (Exception ex) + { + Log.Warning(ex, "Failed to retrieve drive stats for {Path}", path); + return new DriveData("Unknown", 0, 0); + } } }