diff --git a/proto.lock b/proto.lock index 068485f54f..b970e0f612 100644 --- a/proto.lock +++ b/proto.lock @@ -1,1506 +1,5 @@ { "definitions": [ - { - "protopath": "ClientAPI:/:ClientMessageDtos.proto", - "def": { - "enums": [ - { - "name": "OperationResult", - "enum_fields": [ - { - "name": "Success" - }, - { - "name": "PrepareTimeout", - "integer": 1 - }, - { - "name": "CommitTimeout", - "integer": 2 - }, - { - "name": "ForwardTimeout", - "integer": 3 - }, - { - "name": "WrongExpectedVersion", - "integer": 4 - }, - { - "name": "StreamDeleted", - "integer": 5 - }, - { - "name": "InvalidTransaction", - "integer": 6 - }, - { - "name": "AccessDenied", - "integer": 7 - } - ] - }, - { - "name": "ReadEventCompleted.ReadEventResult", - "enum_fields": [ - { - "name": "Success" - }, - { - "name": "NotFound", - "integer": 1 - }, - { - "name": "NoStream", - "integer": 2 - }, - { - "name": "StreamDeleted", - "integer": 3 - }, - { - "name": "Error", - "integer": 4 - }, - { - "name": "AccessDenied", - "integer": 5 - } - ] - }, - { - "name": "ReadStreamEventsCompleted.ReadStreamResult", - "enum_fields": [ - { - "name": "Success" - }, - { - "name": "NoStream", - "integer": 1 - }, - { - "name": "StreamDeleted", - "integer": 2 - }, - { - "name": "NotModified", - "integer": 3 - }, - { - "name": "Error", - "integer": 4 - }, - { - "name": "AccessDenied", - "integer": 5 - } - ] - }, - { - "name": "ReadAllEventsCompleted.ReadAllResult", - "enum_fields": [ - { - "name": "Success" - }, - { - "name": "NotModified", - "integer": 1 - }, - { - "name": "Error", - "integer": 2 - }, - { - "name": "AccessDenied", - "integer": 3 - } - ] - }, - { - "name": "Filter.FilterContext", - "enum_fields": [ - { - "name": "StreamId" - }, - { - "name": "EventType", - "integer": 1 - } - ] - }, - { - "name": "Filter.FilterType", - "enum_fields": [ - { - "name": "Regex" - }, - { - "name": "Prefix", - "integer": 1 - } - ] - }, - { - "name": "FilteredReadAllEventsCompleted.FilteredReadAllResult", - "enum_fields": [ - { - "name": "Success" - }, - { - "name": "NotModified", - "integer": 1 - }, - { - "name": "Error", - "integer": 2 - }, - { - "name": "AccessDenied", - "integer": 3 - } - ] - }, - { - "name": "UpdatePersistentSubscriptionCompleted.UpdatePersistentSubscriptionResult", - "enum_fields": [ - { - "name": "Success" - }, - { - "name": "DoesNotExist", - "integer": 1 - }, - { - "name": "Fail", - "integer": 2 - }, - { - "name": "AccessDenied", - "integer": 3 - } - ] - }, - { - "name": "CreatePersistentSubscriptionCompleted.CreatePersistentSubscriptionResult", - "enum_fields": [ - { - "name": "Success" - }, - { - "name": "AlreadyExists", - "integer": 1 - }, - { - "name": "Fail", - "integer": 2 - }, - { - "name": "AccessDenied", - "integer": 3 - } - ] - }, - { - "name": "DeletePersistentSubscriptionCompleted.DeletePersistentSubscriptionResult", - "enum_fields": [ - { - "name": "Success" - }, - { - "name": "DoesNotExist", - "integer": 1 - }, - { - "name": "Fail", - "integer": 2 - }, - { - "name": "AccessDenied", - "integer": 3 - } - ] - }, - { - "name": "PersistentSubscriptionNakEvents.NakAction", - "enum_fields": [ - { - "name": "Unknown" - }, - { - "name": "Park", - "integer": 1 - }, - { - "name": "Retry", - "integer": 2 - }, - { - "name": "Skip", - "integer": 3 - }, - { - "name": "Stop", - "integer": 4 - } - ] - }, - { - "name": "SubscriptionDropped.SubscriptionDropReason", - "enum_fields": [ - { - "name": "Unsubscribed" - }, - { - "name": "AccessDenied", - "integer": 1 - }, - { - "name": "NotFound", - "integer": 2 - }, - { - "name": "PersistentSubscriptionDeleted", - "integer": 3 - }, - { - "name": "SubscriberMaxCountReached", - "integer": 4 - } - ] - }, - { - "name": "NotHandled.NotHandledReason", - "enum_fields": [ - { - "name": "NotReady" - }, - { - "name": "TooBusy", - "integer": 1 - }, - { - "name": "NotLeader", - "integer": 2 - }, - { - "name": "IsReadOnly", - "integer": 3 - } - ] - }, - { - "name": "ScavengeDatabaseResponse.ScavengeResult", - "enum_fields": [ - { - "name": "Started" - }, - { - "name": "InProgress", - "integer": 1 - }, - { - "name": "Unauthorized", - "integer": 2 - } - ] - } - ], - "messages": [ - { - "name": "NewEvent", - "fields": [ - { - "id": 1, - "name": "event_id", - "type": "bytes" - }, - { - "id": 2, - "name": "event_type", - "type": "string" - }, - { - "id": 3, - "name": "data_content_type", - "type": "int32" - }, - { - "id": 4, - "name": "metadata_content_type", - "type": "int32" - }, - { - "id": 5, - "name": "data", - "type": "bytes" - }, - { - "id": 6, - "name": "metadata", - "type": "bytes" - } - ] - }, - { - "name": "EventRecord", - "fields": [ - { - "id": 1, - "name": "event_stream_id", - "type": "string" - }, - { - "id": 2, - "name": "event_number", - "type": "int64" - }, - { - "id": 3, - "name": "event_id", - "type": "bytes" - }, - { - "id": 4, - "name": "event_type", - "type": "string" - }, - { - "id": 5, - "name": "data_content_type", - "type": "int32" - }, - { - "id": 6, - "name": "metadata_content_type", - "type": "int32" - }, - { - "id": 7, - "name": "data", - "type": "bytes" - }, - { - "id": 8, - "name": "metadata", - "type": "bytes" - }, - { - "id": 9, - "name": "created", - "type": "int64" - }, - { - "id": 10, - "name": "created_epoch", - "type": "int64" - } - ] - }, - { - "name": "ResolvedIndexedEvent", - "fields": [ - { - "id": 1, - "name": "event", - "type": "EventRecord" - }, - { - "id": 2, - "name": "link", - "type": "EventRecord" - } - ] - }, - { - "name": "ResolvedEvent", - "fields": [ - { - "id": 1, - "name": "event", - "type": "EventRecord" - }, - { - "id": 2, - "name": "link", - "type": "EventRecord" - }, - { - "id": 3, - "name": "commit_position", - "type": "int64" - }, - { - "id": 4, - "name": "prepare_position", - "type": "int64" - } - ] - }, - { - "name": "WriteEvents", - "fields": [ - { - "id": 1, - "name": "event_stream_id", - "type": "string" - }, - { - "id": 2, - "name": "expected_version", - "type": "int64" - }, - { - "id": 3, - "name": "events", - "type": "NewEvent", - "is_repeated": true - }, - { - "id": 4, - "name": "require_leader", - "type": "bool" - } - ] - }, - { - "name": "WriteEventsCompleted", - "fields": [ - { - "id": 1, - "name": "result", - "type": "OperationResult" - }, - { - "id": 2, - "name": "message", - "type": "string" - }, - { - "id": 3, - "name": "first_event_number", - "type": "int64" - }, - { - "id": 4, - "name": "last_event_number", - "type": "int64" - }, - { - "id": 5, - "name": "prepare_position", - "type": "int64" - }, - { - "id": 6, - "name": "commit_position", - "type": "int64" - }, - { - "id": 7, - "name": "current_version", - "type": "int64" - } - ] - }, - { - "name": "DeleteStream", - "fields": [ - { - "id": 1, - "name": "event_stream_id", - "type": "string" - }, - { - "id": 2, - "name": "expected_version", - "type": "int64" - }, - { - "id": 3, - "name": "require_leader", - "type": "bool" - }, - { - "id": 4, - "name": "hard_delete", - "type": "bool" - } - ] - }, - { - "name": "DeleteStreamCompleted", - "fields": [ - { - "id": 1, - "name": "result", - "type": "OperationResult" - }, - { - "id": 2, - "name": "message", - "type": "string" - }, - { - "id": 3, - "name": "prepare_position", - "type": "int64" - }, - { - "id": 4, - "name": "commit_position", - "type": "int64" - }, - { - "id": 5, - "name": "current_version", - "type": "int64" - } - ] - }, - { - "name": "TransactionStart", - "fields": [ - { - "id": 1, - "name": "event_stream_id", - "type": "string" - }, - { - "id": 2, - "name": "expected_version", - "type": "int64" - }, - { - "id": 3, - "name": "require_leader", - "type": "bool" - } - ] - }, - { - "name": "TransactionStartCompleted", - "fields": [ - { - "id": 1, - "name": "transaction_id", - "type": "int64" - }, - { - "id": 2, - "name": "result", - "type": "OperationResult" - }, - { - "id": 3, - "name": "message", - "type": "string" - } - ] - }, - { - "name": "TransactionWrite", - "fields": [ - { - "id": 1, - "name": "transaction_id", - "type": "int64" - }, - { - "id": 2, - "name": "events", - "type": "NewEvent", - "is_repeated": true - }, - { - "id": 3, - "name": "require_leader", - "type": "bool" - } - ] - }, - { - "name": "TransactionWriteCompleted", - "fields": [ - { - "id": 1, - "name": "transaction_id", - "type": "int64" - }, - { - "id": 2, - "name": "result", - "type": "OperationResult" - }, - { - "id": 3, - "name": "message", - "type": "string" - } - ] - }, - { - "name": "TransactionCommit", - "fields": [ - { - "id": 1, - "name": "transaction_id", - "type": "int64" - }, - { - "id": 2, - "name": "require_leader", - "type": "bool" - } - ] - }, - { - "name": "TransactionCommitCompleted", - "fields": [ - { - "id": 1, - "name": "transaction_id", - "type": "int64" - }, - { - "id": 2, - "name": "result", - "type": "OperationResult" - }, - { - "id": 3, - "name": "message", - "type": "string" - }, - { - "id": 4, - "name": "first_event_number", - "type": "int64" - }, - { - "id": 5, - "name": "last_event_number", - "type": "int64" - }, - { - "id": 6, - "name": "prepare_position", - "type": "int64" - }, - { - "id": 7, - "name": "commit_position", - "type": "int64" - } - ] - }, - { - "name": "ReadEvent", - "fields": [ - { - "id": 1, - "name": "event_stream_id", - "type": "string" - }, - { - "id": 2, - "name": "event_number", - "type": "int64" - }, - { - "id": 3, - "name": "resolve_link_tos", - "type": "bool" - }, - { - "id": 4, - "name": "require_leader", - "type": "bool" - } - ] - }, - { - "name": "ReadEventCompleted", - "fields": [ - { - "id": 1, - "name": "result", - "type": "ReadEventResult" - }, - { - "id": 2, - "name": "event", - "type": "ResolvedIndexedEvent" - }, - { - "id": 3, - "name": "error", - "type": "string" - } - ] - }, - { - "name": "ReadStreamEvents", - "fields": [ - { - "id": 1, - "name": "event_stream_id", - "type": "string" - }, - { - "id": 2, - "name": "from_event_number", - "type": "int64" - }, - { - "id": 3, - "name": "max_count", - "type": "int32" - }, - { - "id": 4, - "name": "resolve_link_tos", - "type": "bool" - }, - { - "id": 5, - "name": "require_leader", - "type": "bool" - } - ] - }, - { - "name": "ReadStreamEventsCompleted", - "fields": [ - { - "id": 1, - "name": "events", - "type": "ResolvedIndexedEvent", - "is_repeated": true - }, - { - "id": 2, - "name": "result", - "type": "ReadStreamResult" - }, - { - "id": 3, - "name": "next_event_number", - "type": "int64" - }, - { - "id": 4, - "name": "last_event_number", - "type": "int64" - }, - { - "id": 5, - "name": "is_end_of_stream", - "type": "bool" - }, - { - "id": 6, - "name": "last_commit_position", - "type": "int64" - }, - { - "id": 7, - "name": "error", - "type": "string" - } - ] - }, - { - "name": "ReadAllEvents", - "fields": [ - { - "id": 1, - "name": "commit_position", - "type": "int64" - }, - { - "id": 2, - "name": "prepare_position", - "type": "int64" - }, - { - "id": 3, - "name": "max_count", - "type": "int32" - }, - { - "id": 4, - "name": "resolve_link_tos", - "type": "bool" - }, - { - "id": 5, - "name": "require_leader", - "type": "bool" - } - ] - }, - { - "name": "ReadAllEventsCompleted", - "fields": [ - { - "id": 1, - "name": "commit_position", - "type": "int64" - }, - { - "id": 2, - "name": "prepare_position", - "type": "int64" - }, - { - "id": 3, - "name": "events", - "type": "ResolvedEvent", - "is_repeated": true - }, - { - "id": 4, - "name": "next_commit_position", - "type": "int64" - }, - { - "id": 5, - "name": "next_prepare_position", - "type": "int64" - }, - { - "id": 6, - "name": "result", - "type": "ReadAllResult" - }, - { - "id": 7, - "name": "error", - "type": "string" - } - ] - }, - { - "name": "Filter", - "fields": [ - { - "id": 1, - "name": "context", - "type": "FilterContext" - }, - { - "id": 2, - "name": "type", - "type": "FilterType" - }, - { - "id": 3, - "name": "data", - "type": "string", - "is_repeated": true - } - ] - }, - { - "name": "FilteredReadAllEvents", - "fields": [ - { - "id": 1, - "name": "commit_position", - "type": "int64" - }, - { - "id": 2, - "name": "prepare_position", - "type": "int64" - }, - { - "id": 3, - "name": "max_count", - "type": "int32" - }, - { - "id": 4, - "name": "max_search_window", - "type": "int32" - }, - { - "id": 5, - "name": "resolve_link_tos", - "type": "bool" - }, - { - "id": 6, - "name": "require_leader", - "type": "bool" - }, - { - "id": 7, - "name": "filter", - "type": "Filter" - } - ] - }, - { - "name": "FilteredReadAllEventsCompleted", - "fields": [ - { - "id": 1, - "name": "commit_position", - "type": "int64" - }, - { - "id": 2, - "name": "prepare_position", - "type": "int64" - }, - { - "id": 3, - "name": "events", - "type": "ResolvedEvent", - "is_repeated": true - }, - { - "id": 4, - "name": "next_commit_position", - "type": "int64" - }, - { - "id": 5, - "name": "next_prepare_position", - "type": "int64" - }, - { - "id": 6, - "name": "is_end_of_stream", - "type": "bool" - }, - { - "id": 7, - "name": "result", - "type": "FilteredReadAllResult" - }, - { - "id": 8, - "name": "error", - "type": "string" - } - ] - }, - { - "name": "CreatePersistentSubscription", - "fields": [ - { - "id": 1, - "name": "subscription_group_name", - "type": "string" - }, - { - "id": 2, - "name": "event_stream_id", - "type": "string" - }, - { - "id": 3, - "name": "resolve_link_tos", - "type": "bool" - }, - { - "id": 4, - "name": "start_from", - "type": "int64" - }, - { - "id": 5, - "name": "message_timeout_milliseconds", - "type": "int32" - }, - { - "id": 6, - "name": "record_statistics", - "type": "bool" - }, - { - "id": 7, - "name": "live_buffer_size", - "type": "int32" - }, - { - "id": 8, - "name": "read_batch_size", - "type": "int32" - }, - { - "id": 9, - "name": "buffer_size", - "type": "int32" - }, - { - "id": 10, - "name": "max_retry_count", - "type": "int32" - }, - { - "id": 11, - "name": "prefer_round_robin", - "type": "bool" - }, - { - "id": 12, - "name": "checkpoint_after_time", - "type": "int32" - }, - { - "id": 13, - "name": "checkpoint_max_count", - "type": "int32" - }, - { - "id": 14, - "name": "checkpoint_min_count", - "type": "int32" - }, - { - "id": 15, - "name": "subscriber_max_count", - "type": "int32" - }, - { - "id": 16, - "name": "named_consumer_strategy", - "type": "string" - } - ] - }, - { - "name": "DeletePersistentSubscription", - "fields": [ - { - "id": 1, - "name": "subscription_group_name", - "type": "string" - }, - { - "id": 2, - "name": "event_stream_id", - "type": "string" - } - ] - }, - { - "name": "UpdatePersistentSubscription", - "fields": [ - { - "id": 1, - "name": "subscription_group_name", - "type": "string" - }, - { - "id": 2, - "name": "event_stream_id", - "type": "string" - }, - { - "id": 3, - "name": "resolve_link_tos", - "type": "bool" - }, - { - "id": 4, - "name": "start_from", - "type": "int64" - }, - { - "id": 5, - "name": "message_timeout_milliseconds", - "type": "int32" - }, - { - "id": 6, - "name": "record_statistics", - "type": "bool" - }, - { - "id": 7, - "name": "live_buffer_size", - "type": "int32" - }, - { - "id": 8, - "name": "read_batch_size", - "type": "int32" - }, - { - "id": 9, - "name": "buffer_size", - "type": "int32" - }, - { - "id": 10, - "name": "max_retry_count", - "type": "int32" - }, - { - "id": 11, - "name": "prefer_round_robin", - "type": "bool" - }, - { - "id": 12, - "name": "checkpoint_after_time", - "type": "int32" - }, - { - "id": 13, - "name": "checkpoint_max_count", - "type": "int32" - }, - { - "id": 14, - "name": "checkpoint_min_count", - "type": "int32" - }, - { - "id": 15, - "name": "subscriber_max_count", - "type": "int32" - }, - { - "id": 16, - "name": "named_consumer_strategy", - "type": "string" - } - ] - }, - { - "name": "UpdatePersistentSubscriptionCompleted", - "fields": [ - { - "id": 1, - "name": "result", - "type": "UpdatePersistentSubscriptionResult" - }, - { - "id": 2, - "name": "reason", - "type": "string" - } - ] - }, - { - "name": "CreatePersistentSubscriptionCompleted", - "fields": [ - { - "id": 1, - "name": "result", - "type": "CreatePersistentSubscriptionResult" - }, - { - "id": 2, - "name": "reason", - "type": "string" - } - ] - }, - { - "name": "DeletePersistentSubscriptionCompleted", - "fields": [ - { - "id": 1, - "name": "result", - "type": "DeletePersistentSubscriptionResult" - }, - { - "id": 2, - "name": "reason", - "type": "string" - } - ] - }, - { - "name": "ConnectToPersistentSubscription", - "fields": [ - { - "id": 1, - "name": "subscription_id", - "type": "string" - }, - { - "id": 2, - "name": "event_stream_id", - "type": "string" - }, - { - "id": 3, - "name": "allowed_in_flight_messages", - "type": "int32" - } - ] - }, - { - "name": "PersistentSubscriptionAckEvents", - "fields": [ - { - "id": 1, - "name": "subscription_id", - "type": "string" - }, - { - "id": 2, - "name": "processed_event_ids", - "type": "bytes", - "is_repeated": true - } - ] - }, - { - "name": "PersistentSubscriptionNakEvents", - "fields": [ - { - "id": 1, - "name": "subscription_id", - "type": "string" - }, - { - "id": 2, - "name": "processed_event_ids", - "type": "bytes", - "is_repeated": true - }, - { - "id": 3, - "name": "message", - "type": "string" - }, - { - "id": 4, - "name": "action", - "type": "NakAction" - } - ] - }, - { - "name": "PersistentSubscriptionConfirmation", - "fields": [ - { - "id": 1, - "name": "last_commit_position", - "type": "int64" - }, - { - "id": 2, - "name": "subscription_id", - "type": "string" - }, - { - "id": 3, - "name": "last_event_number", - "type": "int64" - } - ] - }, - { - "name": "PersistentSubscriptionStreamEventAppeared", - "fields": [ - { - "id": 1, - "name": "event", - "type": "ResolvedIndexedEvent" - }, - { - "id": 2, - "name": "retryCount", - "type": "int32" - } - ] - }, - { - "name": "SubscribeToStream", - "fields": [ - { - "id": 1, - "name": "event_stream_id", - "type": "string" - }, - { - "id": 2, - "name": "resolve_link_tos", - "type": "bool" - } - ] - }, - { - "name": "FilteredSubscribeToStream", - "fields": [ - { - "id": 1, - "name": "event_stream_id", - "type": "string" - }, - { - "id": 2, - "name": "resolve_link_tos", - "type": "bool" - }, - { - "id": 3, - "name": "filter", - "type": "Filter" - }, - { - "id": 4, - "name": "checkpoint_interval", - "type": "int32" - } - ] - }, - { - "name": "CheckpointReached", - "fields": [ - { - "id": 1, - "name": "commit_position", - "type": "int64" - }, - { - "id": 2, - "name": "prepare_position", - "type": "int64" - } - ] - }, - { - "name": "SubscriptionConfirmation", - "fields": [ - { - "id": 1, - "name": "last_commit_position", - "type": "int64" - }, - { - "id": 2, - "name": "last_event_number", - "type": "int64" - } - ] - }, - { - "name": "StreamEventAppeared", - "fields": [ - { - "id": 1, - "name": "event", - "type": "ResolvedEvent" - } - ] - }, - { - "name": "UnsubscribeFromStream" - }, - { - "name": "SubscriptionDropped", - "fields": [ - { - "id": 1, - "name": "reason", - "type": "SubscriptionDropReason" - } - ] - }, - { - "name": "NotHandled", - "fields": [ - { - "id": 1, - "name": "reason", - "type": "NotHandledReason" - }, - { - "id": 2, - "name": "additional_info", - "type": "bytes" - } - ], - "messages": [ - { - "name": "LeaderInfo", - "fields": [ - { - "id": 1, - "name": "external_tcp_address", - "type": "string" - }, - { - "id": 2, - "name": "external_tcp_port", - "type": "int32" - }, - { - "id": 3, - "name": "http_address", - "type": "string" - }, - { - "id": 4, - "name": "http_port", - "type": "int32" - }, - { - "id": 5, - "name": "external_secure_tcp_address", - "type": "string" - }, - { - "id": 6, - "name": "external_secure_tcp_port", - "type": "int32" - } - ] - } - ] - }, - { - "name": "ScavengeDatabase" - }, - { - "name": "ScavengeDatabaseResponse", - "fields": [ - { - "id": 1, - "name": "result", - "type": "ScavengeResult" - }, - { - "id": 2, - "name": "scavengeId", - "type": "string" - } - ] - }, - { - "name": "IdentifyClient", - "fields": [ - { - "id": 1, - "name": "version", - "type": "int32" - }, - { - "id": 2, - "name": "connection_name", - "type": "string" - } - ] - }, - { - "name": "ClientIdentified" - } - ], - "package": { - "name": "EventStore.Client.Messages" - } - } - }, { "protopath": "Grpc:/:cluster.proto", "def": { @@ -1909,26 +408,6 @@ "name": "http_end_point", "type": "EndPoint" }, - { - "id": 6, - "name": "internal_tcp", - "type": "EndPoint" - }, - { - "id": 7, - "name": "external_tcp", - "type": "EndPoint" - }, - { - "id": 8, - "name": "internal_tcp_uses_tls", - "type": "bool" - }, - { - "id": 9, - "name": "external_tcp_uses_tls", - "type": "bool" - }, { "id": 10, "name": "last_commit_position", @@ -1979,16 +458,30 @@ "name": "advertise_http_port_to_client_as", "type": "uint32" }, - { - "id": 20, - "name": "advertise_tcp_port_to_client_as", - "type": "uint32" - }, { "id": 21, "name": "es_version", "type": "string" + }, + { + "id": 22, + "name": "replication_end_point", + "type": "EndPoint" } + ], + "reserved_ids": [ + 6, + 7, + 8, + 9, + 20 + ], + "reserved_names": [ + "internal_tcp", + "external_tcp", + "internal_tcp_uses_tls", + "external_tcp_uses_tls", + "advertise_tcp_port_to_client_as" ] } ], @@ -2784,21 +1277,19 @@ { "name": "LeaderInfo", "fields": [ - { - "id": 1, - "name": "external_tcp", - "type": "EndPoint" - }, - { - "id": 2, - "name": "is_secure", - "type": "bool" - }, { "id": 3, "name": "http", "type": "EndPoint" } + ], + "reserved_ids": [ + 1, + 2 + ], + "reserved_names": [ + "external_tcp", + "is_secure" ] }, { @@ -3063,21 +1554,21 @@ ] }, { - "name": "TcpStatsReq" + "name": "ConnectionStatsReq" }, { - "name": "TcpStatsResp", + "name": "ConnectionStatsResp", "fields": [ { "id": 1, "name": "connections", - "type": "TcpConnectionStats", + "type": "ConnectionStats", "is_repeated": true } ] }, { - "name": "TcpConnectionStats", + "name": "ConnectionStats", "fields": [ { "id": 1, @@ -3112,22 +1603,32 @@ { "id": 7, "name": "pending_send_bytes", - "type": "int32" + "type": "int64" }, { "id": 8, "name": "pending_received_bytes", - "type": "int32" + "type": "int64" }, { "id": 9, - "name": "is_external_connection", + "name": "is_tls", "type": "bool" }, { "id": 10, - "name": "is_ssl_connection", - "type": "bool" + "name": "protocol", + "type": "string" + }, + { + "id": 11, + "name": "application", + "type": "string" + }, + { + "id": 12, + "name": "connected_at", + "type": "google.protobuf.Timestamp" } ] }, @@ -3202,9 +1703,9 @@ "out_streamed": true }, { - "name": "TcpStats", - "in_type": "TcpStatsReq", - "out_type": "TcpStatsResp" + "name": "ConnectionStats", + "in_type": "ConnectionStatsReq", + "out_type": "ConnectionStatsResp" }, { "name": "ReplicationStats", @@ -3217,6 +1718,9 @@ "imports": [ { "path": "google/protobuf/struct.proto" + }, + { + "path": "google/protobuf/timestamp.proto" } ], "package": { diff --git a/src/EventStore.ClusterNode/Components/Pages/Observability.razor b/src/EventStore.ClusterNode/Components/Pages/Observability.razor index 8d3540afe8..00fd36a002 100644 --- a/src/EventStore.ClusterNode/Components/Pages/Observability.razor +++ b/src/EventStore.ClusterNode/Components/Pages/Observability.razor @@ -10,7 +10,7 @@

Observability

A map of the signals the node already emits.

-

Inspect live queue pressure, processing throughput, health, metrics, replication, TCP, and grouped runtime statistics.

+

Inspect live queue pressure, processing throughput, health, metrics, replication, and grouped runtime statistics.

@@ -20,6 +20,112 @@
+
+
+
+

Network boundary

+

Shared HTTP and gRPC connections

+

Active connections accepted by the node endpoint, including per-second traffic rates and pending bytes.

+
+
+ @NetworkStatusLabel + @NetworkPageStatusLabel + + +
+
+ +
+
+ + + + + + + + + + + + + + + + + @if (NetworkRows.Count == 0) + { + + } + else + { + foreach (var connection in NetworkRows.Take(5)) + { + + + + + + + + + + + + + } + } + +
ConnectionClientTypeRemote endpointSent rateSent currentSent pendingReceived rateReceived currentReceived pending
No active shared-endpoint connections.
@connection.ConnectionId@Display(connection.ClientName)@connection.Application · @Display(connection.Protocol) · @(connection.IsTls ? "TLS" : "Cleartext")@connection.RemoteEndPointWaiting@FormatBytes(connection.TotalBytesSent)@FormatBytes(connection.PendingSendBytes)Waiting@FormatBytes(connection.TotalBytesReceived)@FormatBytes(connection.PendingReceivedBytes)
+
+
+
+ +
+
+

Cluster transport

+

gRPC replication connections

+

Live database replication sessions on the shared HTTP/2 endpoint.

+
+ +
+
+ + + + + + + + + + + + + @if (Page is not null && Page.ReplicationConnections.Count > 0) + { + @foreach (var connection in Page.ReplicationConnections) + { + + + + + + + + + } + } + else + { + + } + +
EndpointConnectionSentReceivedPendingSend queue
@connection.Endpoint@connection.ConnectionId@FormatBytes(connection.TotalBytesSent)@FormatBytes(connection.TotalBytesReceived)@FormatBytes(connection.PendingSendBytes + connection.PendingReceivedBytes)@connection.SendQueueSize.ToString("N0", CultureInfo.InvariantCulture)
No active gRPC replication connections.
+
+
+
+
Dashboard snapshot @@ -87,58 +193,6 @@ }
-
-
-
-

TCP dashboard

-

Realtime connections

-

Shows active connections with per-second sent and received byte rates.

-
-
- @TcpStatusLabel - @TcpPageStatusLabel - - -
-
- -
-
- - - - - - - - - - - - - - - - - @if (TcpRows.Count == 0) - { - - - - } - else - { - foreach (var connection in TcpRows.Take(5)) - { - @RenderTcpRow(connection) - } - } - -
ConnectionClientTypeIP AddressSent rateSent currentSent pendingReceived rateReceived currentReceived pending
@TcpEmptyMessage
-
-
-
- @code { @@ -148,29 +202,21 @@ private string Expanded { get; set; } = ""; private string DashboardPayloadJson => Page?.ClientPayloadJson ?? "{}"; - private IReadOnlyList TcpRows => Page?.TcpConnections ?? Array.Empty(); - private string TcpErrorMessage => !string.IsNullOrWhiteSpace(Page?.TcpMessage) - ? Page.TcpMessage - : Page?.Message ?? ""; - private string TcpStatusLabel { - get { - if (Page is null) - return "Connecting TCP stats..."; - - if (!string.IsNullOrWhiteSpace(TcpErrorMessage)) - return "TCP unavailable"; - - return TcpRows.Count == 0 - ? "TCP live" - : string.Create(CultureInfo.InvariantCulture, $"TCP live · {TcpRows.Count} connection{(TcpRows.Count == 1 ? "" : "s")}"); - } - } - private string TcpPageStatusLabel => TcpRows.Count == 0 + private IReadOnlyList NetworkRows => + Page?.NodeConnections ?? Array.Empty(); + private string NetworkStatusLabel => NetworkRows.Count == 0 + ? "Network live" + : string.Create(CultureInfo.InvariantCulture, $"Network live · {NetworkRows.Count} connection{(NetworkRows.Count == 1 ? "" : "s")}"); + private string NetworkPageStatusLabel => NetworkRows.Count == 0 ? "No pages" - : string.Create(CultureInfo.InvariantCulture, $"Page 1 of {Math.Max(1, (int)Math.Ceiling(TcpRows.Count / 5.0))}"); - private string TcpEmptyMessage => string.IsNullOrWhiteSpace(TcpErrorMessage) - ? "No TCP connections are currently reported." - : TcpErrorMessage; + : string.Create(CultureInfo.InvariantCulture, $"Page 1 of {Math.Max(1, (int)Math.Ceiling(NetworkRows.Count / 5.0))}"); + + private static string FormatBytes(long value) => + value.ToString("N0", CultureInfo.InvariantCulture) + " B"; + private static string Display(string value) => string.IsNullOrWhiteSpace(value) ? "" : value; + private static string ConnectionDetails(NodeConnectionSnapshot connection) => + string.Create(CultureInfo.InvariantCulture, + $"Local {connection.LocalEndPoint}, connected {connection.ConnectedAt:u}"); protected override async Task OnParametersSetAsync() { Page = null; @@ -216,19 +262,6 @@ @row.CurrentLastMessageLabel ; - private RenderFragment RenderTcpRow(TcpConnectionRow connection) => @ - @connection.IdLabel - @connection.ClientLabel - @connection.TypeLabel - @connection.RemoteEndPointLabel - @connection.SentRateLabel - @connection.TotalBytesSentLabel - @connection.PendingSendBytesLabel - @connection.ReceivedRateLabel - @connection.TotalBytesReceivedLabel - @connection.PendingReceivedBytesLabel - ; - private bool IsExpanded(string groupName) => ParseExpandedGroups().Contains(groupName); diff --git a/src/EventStore.ClusterNode/Components/Services/NodeConnectionTracker.cs b/src/EventStore.ClusterNode/Components/Services/NodeConnectionTracker.cs index 8b98cc7262..0a0fdfc005 100644 --- a/src/EventStore.ClusterNode/Components/Services/NodeConnectionTracker.cs +++ b/src/EventStore.ClusterNode/Components/Services/NodeConnectionTracker.cs @@ -6,11 +6,12 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using EventStore.Core.Services.Transport.Grpc; using Microsoft.AspNetCore.Connections; namespace EventStore.ClusterNode.Components.Services; -public sealed class NodeConnectionTracker +public sealed class NodeConnectionTracker : IConnectionStatsProvider { private readonly ConcurrentDictionary _connections = new(); @@ -20,6 +21,8 @@ public IReadOnlyList Snapshot() => .ThenBy(x => x.ConnectionId, StringComparer.Ordinal) .ToArray(); + IReadOnlyList IConnectionStatsProvider.Snapshot() => Snapshot(); + public async Task Track(ConnectionContext context, ConnectionDelegate next, bool isTls) { var state = new NodeConnectionState( @@ -49,7 +52,9 @@ public void ObserveRequest( string userAgent) { if (_connections.TryGetValue(connectionId, out var connection)) + { connection.ObserveRequest(protocol, isGrpc, connectionName, userAgent); + } } } @@ -65,7 +70,19 @@ public sealed record NodeConnectionSnapshot( long TotalBytesSent, long TotalBytesReceived, long PendingSendBytes, - long PendingReceivedBytes); + long PendingReceivedBytes) : ConnectionStatsSnapshot( + ConnectionId, + RemoteEndPoint, + LocalEndPoint, + ClientName, + Application, + Protocol, + IsTls, + ConnectedAt, + TotalBytesSent, + TotalBytesReceived, + PendingSendBytes, + PendingReceivedBytes); internal sealed class NodeConnectionState { @@ -172,7 +189,10 @@ public NodeConnectionSnapshot Snapshot() private static string Merge(string current, string observed) { if (string.IsNullOrWhiteSpace(observed) || current == observed) + { return current; + } + return string.IsNullOrWhiteSpace(current) ? observed : "Mixed"; } } @@ -228,7 +248,9 @@ public override async ValueTask ReadAsync(CancellationToken cancella public override bool TryRead(out ReadResult result) { if (!_inner.TryRead(out result)) + { return false; + } Observe(result); return true; @@ -268,7 +290,10 @@ public override async ValueTask FlushAsync(CancellationToken cancel { var result = await _inner.FlushAsync(cancellationToken); if (!result.IsCanceled) + { _state.Sent(); + } + return result; } diff --git a/src/EventStore.ClusterNode/Components/Services/QueueDashboardService.cs b/src/EventStore.ClusterNode/Components/Services/QueueDashboardService.cs index 6cba5ae36b..84d2642921 100644 --- a/src/EventStore.ClusterNode/Components/Services/QueueDashboardService.cs +++ b/src/EventStore.ClusterNode/Components/Services/QueueDashboardService.cs @@ -18,23 +18,22 @@ public sealed class QueueDashboardService { private static readonly TimeSpan ReadTimeout = TimeSpan.FromSeconds(10); private static readonly Operation StatisticsOperation = new(Operations.Node.Statistics.Read); - private static readonly Operation TcpStatisticsOperation = new(Operations.Node.Statistics.Tcp); private readonly IAuthorizationProvider _authorizationProvider; private readonly IHttpContextAccessor _httpContextAccessor; private readonly IPublisher _monitoringQueue; - private readonly object _tcpGate = new(); - private Dictionary _previousTcpConnections = new(); - private DateTime? _lastTcpRefresh; + private readonly NodeConnectionTracker _nodeConnectionTracker; public QueueDashboardService( IAuthorizationProvider authorizationProvider, IHttpContextAccessor httpContextAccessor, - StandardComponents standardComponents) + StandardComponents standardComponents, + NodeConnectionTracker nodeConnectionTracker) { _authorizationProvider = authorizationProvider; _httpContextAccessor = httpContextAccessor; _monitoringQueue = standardComponents.MonitoringQueue; + _nodeConnectionTracker = nodeConnectionTracker; } public async Task Read(CancellationToken cancellationToken = default) @@ -49,9 +48,13 @@ public async Task Read(CancellationToken cancellationToken = using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); timeout.CancelAfter(ReadTimeout); - var queues = await ReadQueueStats(timeout.Token); - var tcp = await ReadTcpStatsSafe(timeout.Token, cancellationToken); - return QueueDashboardPage.Success(queues, tcp.Rows, tcp.Message); + var queuesTask = ReadQueueStats(timeout.Token); + var replicationConnectionsTask = ReadReplicationStats(timeout.Token); + await Task.WhenAll(queuesTask, replicationConnectionsTask); + return QueueDashboardPage.Success( + await queuesTask, + await replicationConnectionsTask, + _nodeConnectionTracker.Snapshot()); } catch (TimeoutException) { @@ -102,67 +105,17 @@ private async Task> ReadQueueStats(Cancellation return queues; } - private async Task ReadTcpStats(CancellationToken cancellationToken) - { - if (!await HasAccess(TcpStatisticsOperation, cancellationToken)) - { - return new TcpConnectionResult(Array.Empty(), "TCP statistics access was denied."); - } - - var envelope = new TaskCompletionEnvelope(); - _monitoringQueue.Publish(new MonitoringMessage.GetFreshTcpConnectionStats(envelope)); - var completed = await envelope.Task.WaitAsync(ReadTimeout, cancellationToken); - return BuildTcpRows(completed.ConnectionStats ?? []); - } - - private async Task ReadTcpStatsSafe( - CancellationToken timeoutToken, + private async Task> ReadReplicationStats( CancellationToken cancellationToken) { - try - { - return await ReadTcpStats(timeoutToken); - } - catch (TimeoutException) - { - return new TcpConnectionResult(Array.Empty(), "Timed out reading TCP statistics."); - } - catch (OperationCanceledException) - { - if (cancellationToken.IsCancellationRequested) - { - throw; - } - - return new TcpConnectionResult(Array.Empty(), "Timed out reading TCP statistics."); - } - catch (Exception ex) - { - return new TcpConnectionResult( - Array.Empty(), - $"Unable to read TCP statistics: {UiMessages.Friendly(ex)}"); - } - } - - private TcpConnectionResult BuildTcpRows(IReadOnlyList connections) - { - lock (_tcpGate) - { - var now = DateTime.UtcNow; - var elapsedSeconds = _lastTcpRefresh.HasValue - ? Math.Max(1, (now - _lastTcpRefresh.Value).TotalSeconds) - : 1; - - var rows = connections - .Select(x => TcpConnectionRow.From(x, _previousTcpConnections.GetValueOrDefault(x.ConnectionId), elapsedSeconds)) - .OrderBy(x => x.ClientConnectionName, StringComparer.OrdinalIgnoreCase) - .ThenBy(x => x.ConnectionId) - .ToArray(); + var envelope = new TaskCompletionEnvelope(); + _monitoringQueue.Publish(new ReplicationMessage.GetReplicationStats(envelope)); + var completed = await envelope.Task.WaitAsync(ReadTimeout, cancellationToken); - _previousTcpConnections = rows.ToDictionary(x => x.ConnectionId); - _lastTcpRefresh = now; - return new TcpConnectionResult(rows, ""); - } + return completed.ReplicationStats + .Select(ReplicationConnectionRow.From) + .OrderBy(x => x.Endpoint, StringComparer.OrdinalIgnoreCase) + .ToArray(); } } @@ -196,8 +149,8 @@ public static bool TryReadDictionary(object value, out IReadOnlyDictionary Blocks, IReadOnlyList Queues, - IReadOnlyList TcpConnections, - string TcpMessage, + IReadOnlyList ReplicationConnections, + IReadOnlyList NodeConnections, string Message) { private static readonly JsonSerializerOptions PayloadJsonOptions = new(JsonSerializerDefaults.Web); @@ -214,23 +167,28 @@ public sealed record QueueDashboardPage( public string ClientPayloadJson => JsonSerializer.Serialize( new QueueDashboardPayload( Queues.Select(QueuePayload.From).ToArray(), - TcpConnections.Select(TcpConnectionPayload.From).ToArray(), - Message, - TcpMessage), + ReplicationConnections, + NodeConnections, + Message), PayloadJsonOptions); public static QueueDashboardPage Success( IReadOnlyList queues, - IReadOnlyList tcpConnections, - string tcpMessage) => - new(BuildBlocks(queues), queues, tcpConnections, tcpMessage, ""); + IReadOnlyList replicationConnections = null, + IReadOnlyList nodeConnections = null) => + new( + BuildBlocks(queues), + queues, + replicationConnections ?? Array.Empty(), + nodeConnections ?? Array.Empty(), + ""); public static QueueDashboardPage Unavailable(string message) => new( Array.Empty(), Array.Empty(), - Array.Empty(), - message, + Array.Empty(), + Array.Empty(), message); private static IReadOnlyList BuildBlocks(IReadOnlyList queues) @@ -266,15 +224,33 @@ public sealed record QueueDashboardBlock( public bool HasChildren => Children.Count > 0; } -public sealed record TcpConnectionResult( - IReadOnlyList Rows, - string Message); - public sealed record QueueDashboardPayload( IReadOnlyList Queues, - IReadOnlyList TcpConnections, - string Message, - string TcpMessage); + IReadOnlyList ReplicationConnections, + IReadOnlyList NodeConnections, + string Message); + +public sealed record ReplicationConnectionRow( + string SubscriptionId, + string ConnectionId, + string Endpoint, + long TotalBytesSent, + long TotalBytesReceived, + int PendingSendBytes, + int PendingReceivedBytes, + int SendQueueSize) +{ + public static ReplicationConnectionRow From(ReplicationMessage.ReplicationStats stats) => + new( + stats.SubscriptionId.ToString("D"), + stats.ConnectionId.ToString("D"), + stats.SubscriptionEndpoint ?? "", + stats.TotalBytesSent, + stats.TotalBytesReceived, + stats.PendingSendBytes, + stats.PendingReceivedBytes, + stats.SendQueueSize); +} public sealed record QueuePayload( string Kind, @@ -304,95 +280,6 @@ public static QueuePayload From(QueueDashboardRow row) => row.LastProcessedMessage); } -public sealed record TcpConnectionPayload( - Guid ConnectionId, - string ClientConnectionName, - string RemoteEndPoint, - string LocalEndPoint, - long TotalBytesSent, - long TotalBytesReceived, - int PendingSendBytes, - int PendingReceivedBytes, - double SentRate, - double ReceivedRate, - bool IsExternalConnection, - bool IsSslConnection) -{ - public static TcpConnectionPayload From(TcpConnectionRow row) => - new( - row.ConnectionId, - row.ClientConnectionName, - row.RemoteEndPoint, - row.LocalEndPoint, - row.TotalBytesSent, - row.TotalBytesReceived, - row.PendingSendBytes, - row.PendingReceivedBytes, - row.SentRate, - row.ReceivedRate, - row.IsExternalConnection, - row.IsSslConnection); -} - -public sealed record TcpConnectionRow( - Guid ConnectionId, - string ClientConnectionName, - string RemoteEndPoint, - string LocalEndPoint, - long TotalBytesSent, - long TotalBytesReceived, - int PendingSendBytes, - int PendingReceivedBytes, - double SentRate, - double ReceivedRate, - bool IsExternalConnection, - bool IsSslConnection) -{ - public string IdLabel => ConnectionId == Guid.Empty ? "" : ConnectionId.ToString("D"); - public string ClientLabel => DisplayMessage(ClientConnectionName); - public string TypeLabel => $"{(IsExternalConnection ? "External" : "Internal")} {(IsSslConnection ? "TLS" : "TCP")}"; - public string RemoteEndPointLabel => DisplayMessage(RemoteEndPoint); - public string SentRateLabel => FormatByteRate(SentRate); - public string ReceivedRateLabel => FormatByteRate(ReceivedRate); - public string TotalBytesSentLabel => TotalBytesSent.ToString("N0", CultureInfo.InvariantCulture); - public string TotalBytesReceivedLabel => TotalBytesReceived.ToString("N0", CultureInfo.InvariantCulture); - public string PendingSendBytesLabel => PendingSendBytes.ToString("N0", CultureInfo.InvariantCulture); - public string PendingReceivedBytesLabel => PendingReceivedBytes.ToString("N0", CultureInfo.InvariantCulture); - - public static TcpConnectionRow From( - MonitoringMessage.TcpConnectionStats stats, - TcpConnectionRow previous, - double elapsedSeconds) - { - var sentRate = previous is null - ? 0 - : Math.Max(0, (stats.TotalBytesSent - previous.TotalBytesSent) / elapsedSeconds); - var receivedRate = previous is null - ? 0 - : Math.Max(0, (stats.TotalBytesReceived - previous.TotalBytesReceived) / elapsedSeconds); - - return new TcpConnectionRow( - stats.ConnectionId, - stats.ClientConnectionName ?? "", - stats.RemoteEndPoint ?? "", - stats.LocalEndPoint ?? "", - stats.TotalBytesSent, - stats.TotalBytesReceived, - stats.PendingSendBytes, - stats.PendingReceivedBytes, - sentRate, - receivedRate, - stats.IsExternalConnection, - stats.IsSslConnection); - } - - private static string DisplayMessage(string value) => - string.IsNullOrWhiteSpace(value) ? "" : value; - - private static string FormatByteRate(double value) => - $"{Math.Round(value).ToString("N0", CultureInfo.InvariantCulture)} B/s"; -} - public enum QueueDashboardRowKind { Queue, diff --git a/src/EventStore.ClusterNode/Program.cs b/src/EventStore.ClusterNode/Program.cs index 6dbe3b2dbb..e387023c5c 100644 --- a/src/EventStore.ClusterNode/Program.cs +++ b/src/EventStore.ClusterNode/Program.cs @@ -20,6 +20,7 @@ using EventStore.Core.Authentication.OAuth; using EventStore.Core.Certificates; using EventStore.Core.Configuration; +using EventStore.Core.Services.Transport.Grpc; using EventStore.Core.Services.Transport.Http; using EventStore.Plugins.Authentication; using Microsoft.AspNetCore.Builder; @@ -278,6 +279,7 @@ async Task Run(ClusterVNodeHostedService hostedService, ManualResetEventSlim sig var replicationEndpointPolicy = new ReplicationEndpointPolicy( new System.Net.IPEndPoint(options.Interface.ReplicationIp, options.Interface.ReplicationPort)); builder.Services.AddSingleton(nodeConnectionTracker); + builder.Services.AddSingleton(nodeConnectionTracker); builder.WebHost.ConfigureKestrel(server => { server.Limits.Http2.KeepAlivePingDelay = diff --git a/src/EventStore.ClusterNode/metricsconfig.json b/src/EventStore.ClusterNode/metricsconfig.json index cb1edb93ff..f249180912 100644 --- a/src/EventStore.ClusterNode/metricsconfig.json +++ b/src/EventStore.ClusterNode/metricsconfig.json @@ -236,10 +236,6 @@ "Regex": "CoreMessage-System-.*", "Label": "System" }, - { - "Regex": "CoreMessage-Tcp-.*", - "Label": "Tcp" - }, { "Regex": "ProjectionMessage-.*", "Label": "Projections" diff --git a/src/EventStore.ClusterNode/ui-assets/js/queue-dashboard.js b/src/EventStore.ClusterNode/ui-assets/js/queue-dashboard.js index d4602e7bec..4e059393a4 100644 --- a/src/EventStore.ClusterNode/ui-assets/js/queue-dashboard.js +++ b/src/EventStore.ClusterNode/ui-assets/js/queue-dashboard.js @@ -2,7 +2,7 @@ "use strict"; var pollIntervalMs = 1000; - var tcpPageSize = 5; + var networkPageSize = 5; var dashboards = new WeakMap(); function start() { @@ -20,8 +20,10 @@ expanded: parseExpanded(root), blocks: [], queues: [], - tcpConnections: [], - tcpPage: 0, + networkConnections: [], + networkSamples: new Map(), + networkPage: 0, + replicationConnections: [], timer: null, inFlight: false }; @@ -33,10 +35,10 @@ applyPayload(state, initialPayload); root.addEventListener("click", function (event) { - var tcpPager = event.target.closest("[data-tcp-page]"); - if (tcpPager && root.contains(tcpPager)) { + var networkPager = event.target.closest("[data-network-page]"); + if (networkPager && root.contains(networkPager)) { event.preventDefault(); - changeTcpPage(state, tcpPager.getAttribute("data-tcp-page")); + changeNetworkPage(state, networkPager.getAttribute("data-network-page")); return; } @@ -124,21 +126,14 @@ var parsed = parseQueues(payload); state.queues = parsed.queues; state.blocks = parsed.blocks; - state.tcpConnections = parseTcpConnections(payload); + state.networkConnections = parseNetworkConnections(payload, state); + state.replicationConnections = parseReplicationConnections(payload); setStatus( state.root, payload.message ? "Live stats unavailable" : "Live stats", payload.message || "Updated " + formatTime(new Date())); + setNetworkStatus(state.root, payload.message, state.networkConnections.length); render(state); - - if (state.root.querySelector("[data-tcp-table-body]")) { - var tcpMessage = payload.tcpMessage || payload.message || ""; - setTcpStatus( - state.root, - tcpMessage ? "TCP unavailable" : "TCP live", - tcpMessage || state.tcpConnections.length + " connection" + (state.tcpConnections.length === 1 ? "" : "s")); - renderTcpTable(state.root, state.tcpConnections, state); - } } async function refresh(state) { @@ -159,11 +154,11 @@ } catch (error) { state.queues = []; state.blocks = []; - state.tcpConnections = []; + state.networkConnections = []; + state.replicationConnections = []; setStatus(state.root, "Live stats unavailable", friendlyMessage(error)); - setTcpStatus(state.root, "TCP unavailable", friendlyMessage(error)); + setNetworkStatus(state.root, friendlyMessage(error), 0); render(state); - renderTcpTable(state.root, state.tcpConnections, state); } finally { state.inFlight = false; } @@ -208,32 +203,72 @@ }; } - function parseTcpConnections(payload) { - var rows = payload && Array.isArray(payload.tcpConnections) ? payload.tcpConnections : []; - - return rows.map(function (row) { - var id = readFieldString(row, ["connectionId", "ConnectionId"], ""); - var totalBytesSent = readFieldNumber(row, ["totalBytesSent", "TotalBytesSent"]); - var totalBytesReceived = readFieldNumber(row, ["totalBytesReceived", "TotalBytesReceived"]); + function parseNetworkConnections(payload, state) { + var rows = payload && Array.isArray(payload.nodeConnections) ? payload.nodeConnections : []; + var now = Date.now(); + var nextSamples = new Map(); + var connections = rows.map(function (row) { + var id = readString(row.connectionId, ""); + var totalBytesSent = readNumber(row.totalBytesSent); + var totalBytesReceived = readNumber(row.totalBytesReceived); + var previous = state.networkSamples.get(id); + var elapsedSeconds = previous ? Math.max(0.001, (now - previous.observedAt) / 1000) : 0; + var sentRate = previous ? Math.max(0, totalBytesSent - previous.totalBytesSent) / elapsedSeconds : 0; + var receivedRate = previous + ? Math.max(0, totalBytesReceived - previous.totalBytesReceived) / elapsedSeconds + : 0; + + nextSamples.set(id, { + observedAt: now, + totalBytesSent: totalBytesSent, + totalBytesReceived: totalBytesReceived + }); return { id: id, - clientConnectionName: readFieldString(row, ["clientConnectionName", "ClientConnectionName"], ""), - remoteEndPoint: readFieldString(row, ["remoteEndPoint", "RemoteEndPoint"], ""), - localEndPoint: readFieldString(row, ["localEndPoint", "LocalEndPoint"], ""), + clientName: readString(row.clientName, ""), + application: readString(row.application, "Awaiting request"), + protocol: readString(row.protocol, ""), + remoteEndPoint: readString(row.remoteEndPoint, ""), + localEndPoint: readString(row.localEndPoint, ""), + connectedAt: readString(row.connectedAt, ""), + isTls: Boolean(row.isTls), totalBytesSent: totalBytesSent, totalBytesReceived: totalBytesReceived, - pendingSendBytes: readFieldNumber(row, ["pendingSendBytes", "PendingSendBytes"]), - pendingReceivedBytes: readFieldNumber(row, ["pendingReceivedBytes", "PendingReceivedBytes"]), - sentRate: readFieldNumber(row, ["sentRate", "SentRate"]), - receivedRate: readFieldNumber(row, ["receivedRate", "ReceivedRate"]), - isExternalConnection: readFieldBoolean(row, ["isExternalConnection", "IsExternalConnection"]), - isSslConnection: readFieldBoolean(row, ["isSslConnection", "IsSslConnection"]) + pendingSendBytes: readNumber(row.pendingSendBytes), + pendingReceivedBytes: readNumber(row.pendingReceivedBytes), + sentRate: sentRate, + receivedRate: receivedRate }; }).sort(function (left, right) { - return left.clientConnectionName.localeCompare(right.clientConnectionName, undefined, { sensitivity: "base" }) || + return left.clientName.localeCompare(right.clientName, undefined, { sensitivity: "base" }) || left.id.localeCompare(right.id, undefined, { sensitivity: "base" }); }); + + state.networkSamples = nextSamples; + return connections; + } + + function parseReplicationConnections(payload) { + var rows = payload && Array.isArray(payload.replicationConnections) + ? payload.replicationConnections + : []; + + return rows.map(function (row) { + return { + subscriptionId: readString(row.subscriptionId, ""), + connectionId: readString(row.connectionId, ""), + endpoint: readString(row.endpoint, ""), + totalBytesSent: readNumber(row.totalBytesSent), + totalBytesReceived: readNumber(row.totalBytesReceived), + pendingSendBytes: readNumber(row.pendingSendBytes), + pendingReceivedBytes: readNumber(row.pendingReceivedBytes), + sendQueueSize: readNumber(row.sendQueueSize) + }; + }).sort(function (left, right) { + return left.endpoint.localeCompare(right.endpoint, undefined, { sensitivity: "base" }) || + left.connectionId.localeCompare(right.connectionId, undefined, { sensitivity: "base" }); + }); } function buildBlocks(queues) { @@ -311,6 +346,8 @@ renderSpotlightTable(state.root, state.queues); renderQueueTable(state); renderDashboardSnapshot(state.root, state.blocks); + renderNetworkTable(state); + renderReplicationTable(state.root, state.replicationConnections); } function updateMetrics(root, queues) { @@ -451,32 +488,33 @@ node.textContent = lines.join("\n"); } - function renderTcpTable(root, connections, state) { - var tbody = root.querySelector("[data-tcp-table-body]"); + function renderNetworkTable(state) { + var tbody = state.root.querySelector("[data-network-table-body]"); if (!tbody) return; replaceChildren(tbody); - if (connections.length === 0) { + if (state.networkConnections.length === 0) { var empty = element("tr"); - var cell = element("td", "px-5 py-4 text-es-muted"); - cell.colSpan = 10; - cell.textContent = "No TCP connections are currently reported."; - empty.appendChild(cell); + var emptyCell = element("td", "px-5 py-4 text-es-muted"); + emptyCell.colSpan = 10; + emptyCell.textContent = "No active shared-endpoint connections."; + empty.appendChild(emptyCell); tbody.appendChild(empty); - updateTcpPagination(root, state, 0); + updateNetworkPagination(state, 0); return; } - var pageCount = Math.ceil(connections.length / tcpPageSize); - state.tcpPage = Math.min(state.tcpPage, Math.max(0, pageCount - 1)); - var pageStart = state.tcpPage * tcpPageSize; - connections.slice(pageStart, pageStart + tcpPageSize).forEach(function (connection) { + var pageCount = Math.ceil(state.networkConnections.length / networkPageSize); + state.networkPage = Math.min(state.networkPage, pageCount - 1); + var offset = state.networkPage * networkPageSize; + state.networkConnections.slice(offset, offset + networkPageSize).forEach(function (connection) { var row = element("tr", "bg-white/70 text-es-ink"); - appendText(row, "td", connection.id || "", "max-w-[14rem] truncate px-5 py-4 font-mono text-xs text-es-muted"); - appendText(row, "td", displayMessage(connection.clientConnectionName), "px-5 py-4 font-bold text-es-ink"); - appendText(row, "td", tcpTypeLabel(connection), "px-5 py-4 text-es-muted"); - appendText(row, "td", displayMessage(connection.remoteEndPoint), "px-5 py-4 font-mono text-xs text-es-muted"); + appendText(row, "td", displayMessage(connection.id), "max-w-[14rem] truncate px-5 py-4 font-mono text-xs text-es-muted"); + appendText(row, "td", displayMessage(connection.clientName), "max-w-[16rem] truncate px-5 py-4 font-bold text-es-ink"); + appendText(row, "td", networkTypeLabel(connection), "px-5 py-4 text-es-muted"); + var endpointCell = appendText(row, "td", displayMessage(connection.remoteEndPoint), "px-5 py-4 font-mono text-xs text-es-muted"); + endpointCell.title = networkConnectionDetails(connection); appendText(row, "td", formatByteRate(connection.sentRate), "px-5 py-4 text-right font-mono text-es-ink"); appendText(row, "td", formatInteger(connection.totalBytesSent), "px-5 py-4 text-right font-mono text-es-ink"); appendText(row, "td", formatInteger(connection.pendingSendBytes), "px-5 py-4 text-right font-mono text-es-ink"); @@ -485,17 +523,80 @@ appendText(row, "td", formatInteger(connection.pendingReceivedBytes), "px-5 py-4 text-right font-mono text-es-ink"); tbody.appendChild(row); }); - updateTcpPagination(root, state, pageCount); + + updateNetworkPagination(state, pageCount); + } + + function renderReplicationTable(root, connections) { + var tbody = root.querySelector("[data-replication-table-body]"); + if (!tbody) + return; + + replaceChildren(tbody); + if (connections.length === 0) { + var empty = element("tr"); + var emptyCell = element("td", "px-5 py-4 text-es-muted"); + emptyCell.colSpan = 6; + emptyCell.textContent = "No active gRPC replication connections."; + empty.appendChild(emptyCell); + tbody.appendChild(empty); + return; + } + + connections.forEach(function (connection) { + var row = element("tr", "bg-white/70 text-es-ink"); + appendText(row, "td", connection.endpoint, "px-5 py-4 font-bold text-es-ink"); + var connectionCell = appendText(row, "td", displayMessage(connection.connectionId), "px-5 py-4 font-mono text-xs text-es-muted"); + connectionCell.title = "Subscription " + displayMessage(connection.subscriptionId); + appendText(row, "td", formatInteger(connection.totalBytesSent), "px-5 py-4 text-right font-mono text-es-ink"); + appendText(row, "td", formatInteger(connection.totalBytesReceived), "px-5 py-4 text-right font-mono text-es-ink"); + appendText(row, "td", formatInteger(connection.pendingSendBytes + connection.pendingReceivedBytes), "px-5 py-4 text-right font-mono text-es-ink"); + appendText(row, "td", formatInteger(connection.sendQueueSize), "px-5 py-4 text-right font-mono text-es-ink"); + tbody.appendChild(row); + }); } - function changeTcpPage(state, direction) { - var pageCount = Math.ceil(state.tcpConnections.length / tcpPageSize); + function changeNetworkPage(state, direction) { + var pageCount = Math.ceil(state.networkConnections.length / networkPageSize); if (direction === "previous") - state.tcpPage = Math.max(0, state.tcpPage - 1); + state.networkPage = Math.max(0, state.networkPage - 1); else if (direction === "next") - state.tcpPage = Math.min(Math.max(0, pageCount - 1), state.tcpPage + 1); + state.networkPage = Math.min(Math.max(0, pageCount - 1), state.networkPage + 1); + + renderNetworkTable(state); + } + + function updateNetworkPagination(state, pageCount) { + var status = state.root.querySelector("[data-network-page-status]"); + var previous = state.root.querySelector('[data-network-page="previous"]'); + var next = state.root.querySelector('[data-network-page="next"]'); + var hasRows = state.networkConnections.length > 0; + + if (status) + status.textContent = hasRows ? "Page " + (state.networkPage + 1) + " of " + pageCount : "No pages"; + if (previous) + previous.disabled = !hasRows || state.networkPage === 0; + if (next) + next.disabled = !hasRows || state.networkPage >= pageCount - 1; + } + + function setNetworkStatus(root, message, connectionCount) { + var status = root.querySelector("[data-network-status]"); + if (!status) + return; + + status.textContent = message + ? "Network unavailable · " + message + : "Network live · " + connectionCount + " connection" + (connectionCount === 1 ? "" : "s"); + } + + function networkTypeLabel(connection) { + return connection.application + " · " + connection.protocol + " · " + (connection.isTls ? "TLS" : "Cleartext"); + } - renderTcpTable(state.root, state.tcpConnections, state); + function networkConnectionDetails(connection) { + var connected = connection.connectedAt ? new Date(connection.connectedAt).toLocaleString() : "unknown"; + return "Local " + connection.localEndPoint + ", connected " + connected; } function queueTableRow(queue, block, state) { @@ -554,37 +655,6 @@ updatedNode.textContent = updated; } - function setTcpStatus(root, status, detail) { - var statusNode = root.querySelector("[data-tcp-status]"); - if (!statusNode) - return; - - statusNode.textContent = detail ? status + " · " + detail : status; - } - - function updateTcpPagination(root, state, pageCount) { - var status = root.querySelector("[data-tcp-page-status]"); - var previous = root.querySelector('[data-tcp-page="previous"]'); - var next = root.querySelector('[data-tcp-page="next"]'); - var hasRows = state.tcpConnections.length > 0; - - if (status) - status.textContent = hasRows - ? "Page " + (state.tcpPage + 1) + " of " + pageCount - : "No pages"; - - if (previous) - previous.disabled = !hasRows || state.tcpPage === 0; - - if (next) - next.disabled = !hasRows || state.tcpPage >= pageCount - 1; - } - - function tcpTypeLabel(connection) { - return (connection.isExternalConnection ? "External" : "Internal") + " " + - (connection.isSslConnection ? "TLS" : "TCP"); - } - function currentLastMessage(queue) { return queue.kind === "group" ? "n/a" @@ -662,36 +732,6 @@ return Number.isFinite(number) ? number : 0; } - function readFieldString(source, keys, fallback) { - var value = readField(source, keys); - if (value === null || value === undefined) - return fallback; - - var text = String(value); - return text.trim() ? text : fallback; - } - - function readFieldNumber(source, keys) { - return readNumber(readField(source, keys)); - } - - function readFieldBoolean(source, keys) { - var value = readField(source, keys); - return value === true || String(value).toLowerCase() === "true"; - } - - function readField(source, keys) { - if (!source || typeof source !== "object") - return undefined; - - for (var i = 0; i < keys.length; i++) { - if (Object.prototype.hasOwnProperty.call(source, keys[i])) - return source[keys[i]]; - } - - return undefined; - } - function sum(rows, key) { return rows.reduce(function (total, row) { return total + row[key]; diff --git a/src/EventStore.Core.Tests/Regression/GrpcOnlySurfaceParityTests.cs b/src/EventStore.Core.Tests/Regression/GrpcOnlySurfaceParityTests.cs new file mode 100644 index 0000000000..b8bfbcedfa --- /dev/null +++ b/src/EventStore.Core.Tests/Regression/GrpcOnlySurfaceParityTests.cs @@ -0,0 +1,112 @@ +using System; +using System.IO.Pipelines; +using System.Net; +using System.Text.Json; +using System.Threading.Tasks; +using EventStore.ClusterNode.Components.Services; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Http; +using NUnit.Framework; + +namespace EventStore.Core.Tests.Regression; + +[TestFixture] +public class GrpcOnlySurfaceParityTests +{ + [Test] + public void observability_payload_preserves_replication_visibility() + { + var page = QueueDashboardPage.Success(Array.Empty()); + using var payload = JsonDocument.Parse(page.ClientPayloadJson); + + Assert.That(payload.RootElement.TryGetProperty("replicationConnections", out _), Is.True); + Assert.That(payload.RootElement.TryGetProperty("nodeConnections", out _), Is.True); + } + + [Test] + public async Task http_connections_are_visible_only_while_active() + { + var tracker = new NodeConnectionTracker(); + var connection = new DefaultConnectionContext("grpc-connection") + { + LocalEndPoint = new IPEndPoint(IPAddress.Loopback, 2113), + RemoteEndPoint = new IPEndPoint(IPAddress.Loopback, 50123) + }; + var release = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var trafficObserved = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var incoming = new Pipe(); + var outgoing = new Pipe(); + connection.Transport = new TestDuplexPipe(incoming.Reader, outgoing.Writer); + var tracking = tracker.Track(connection, async trackedConnection => + { + tracker.ObserveRequest( + trackedConnection.ConnectionId, + "HTTP/2", + isGrpc: true, + connectionName: "projection-catchup", + userAgent: "grpc-dotnet"); + await trackedConnection.Transport.Output.WriteAsync(new byte[3]); + await incoming.Writer.WriteAsync(new byte[5]); + var read = await trackedConnection.Transport.Input.ReadAsync(); + trackedConnection.Transport.Input.AdvanceTo(read.Buffer.GetPosition(2), read.Buffer.End); + trafficObserved.SetResult(); + await release.Task; + }, isTls: true); + await trafficObserved.Task; + + Assert.That(tracker.Snapshot(), Has.One.Matches(x => + x.ConnectionId == "grpc-connection" && x.IsTls && + x.ClientName == "projection-catchup" && x.Application == "gRPC" && x.Protocol == "HTTP/2" && + x.TotalBytesSent == 3 && x.TotalBytesReceived == 2 && x.PendingReceivedBytes == 3)); + + tracker.ObserveRequest( + connection.ConnectionId, + "HTTP/2", + isGrpc: false, + connectionName: "", + userAgent: "browser"); + Assert.That(tracker.Snapshot(), Has.One.Matches(x => + x.ClientName == "projection-catchup" && x.Application == "HTTP and gRPC")); + + release.SetResult(); + await tracking; + + Assert.That(tracker.Snapshot(), Is.Empty); + } + + [TestCase(2113, "/streams", true)] + [TestCase(2113, "/event_store.replication.Replication/Replicate", false)] + [TestCase(1112, "/streams", false)] + [TestCase(1112, "/event_store.replication.Replication/Replicate", true)] + public void replication_listener_isolated_from_the_public_node_listener( + int localPort, + string path, + bool expected) + { + var policy = new ReplicationEndpointPolicy(new IPEndPoint(IPAddress.Loopback, 1112)); + var context = new DefaultHttpContext(); + context.Connection.LocalIpAddress = IPAddress.Loopback; + context.Connection.LocalPort = localPort; + context.Request.Path = path; + + Assert.That(policy.Allows(context), Is.EqualTo(expected)); + } + + [Test] + public void wildcard_replication_binding_matches_the_resolved_local_address() + { + var policy = new ReplicationEndpointPolicy(new IPEndPoint(IPAddress.Any, 1112)); + var context = new DefaultHttpContext(); + context.Connection.LocalIpAddress = IPAddress.Parse("192.0.2.1"); + context.Connection.LocalPort = 1112; + context.Request.Path = "/event_store.replication.Replication/Replicate"; + + Assert.That(policy.Allows(context), Is.True); + } + + private sealed class TestDuplexPipe(PipeReader input, PipeWriter output) : IDuplexPipe + { + public PipeReader Input { get; } = input; + public PipeWriter Output { get; } = output; + } +} diff --git a/src/EventStore.Core.Tests/Services/Transport/Grpc/MonitoringTests/ConnectionStatsTests.cs b/src/EventStore.Core.Tests/Services/Transport/Grpc/MonitoringTests/ConnectionStatsTests.cs new file mode 100644 index 0000000000..aeeda7f815 --- /dev/null +++ b/src/EventStore.Core.Tests/Services/Transport/Grpc/MonitoringTests/ConnectionStatsTests.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using EventStore.Client.Monitoring; +using EventStore.Core.Bus; +using EventStore.Core.Messaging; +using EventStore.Core.Services.Transport.Grpc; +using Grpc.Core; +using NUnit.Framework; + +namespace EventStore.Core.Tests.Services.Transport.Grpc.MonitoringTests; + +[TestFixture] +public class ConnectionStatsTests +{ + private static readonly DateTimeOffset ConnectedAt = new(2026, 9, 12, 12, 34, 56, TimeSpan.Zero); + private ConnectionStatsResp _response; + + [SetUp] + public async Task SetUp() + { + var provider = new StubConnectionStatsProvider([ + new ConnectionStatsSnapshot( + "connection-1", + "127.0.0.1:50123", + "127.0.0.1:1112", + "projection-catchup", + "gRPC", + "HTTP/2", + true, + ConnectedAt, + 123, + 456, + 7, + 8) + ]); + var serviceType = typeof(Message).Assembly.GetType( + "EventStore.Core.Services.Transport.Grpc.Monitoring", + throwOnError: true); + var service = Activator.CreateInstance( + serviceType!, + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, + binder: null, + args: [new NoOpPublisher(), provider], + culture: null); + + var task = (Task)serviceType!.GetMethod( + nameof(EventStore.Client.Monitoring.Monitoring.MonitoringBase.ConnectionStats))! + .Invoke(service, [new ConnectionStatsReq(), TestServerCallContext.Instance])!; + _response = await task; + } + + [Test] + public void should_map_the_active_connection() + { + var connection = _response.Connections[0]; + + Assert.Multiple(() => + { + Assert.That(connection.ConnectionId, Is.EqualTo("connection-1")); + Assert.That(connection.RemoteEndpoint, Is.EqualTo("127.0.0.1:50123")); + Assert.That(connection.LocalEndpoint, Is.EqualTo("127.0.0.1:1112")); + Assert.That(connection.ClientConnectionName, Is.EqualTo("projection-catchup")); + Assert.That(connection.Application, Is.EqualTo("gRPC")); + Assert.That(connection.Protocol, Is.EqualTo("HTTP/2")); + Assert.That(connection.IsTls, Is.True); + Assert.That(connection.ConnectedAt.ToDateTimeOffset(), Is.EqualTo(ConnectedAt)); + Assert.That(connection.TotalBytesSent, Is.EqualTo(123)); + Assert.That(connection.TotalBytesReceived, Is.EqualTo(456)); + Assert.That(connection.PendingSendBytes, Is.EqualTo(7)); + Assert.That(connection.PendingReceivedBytes, Is.EqualTo(8)); + }); + } + + private sealed class StubConnectionStatsProvider(IReadOnlyList connections) + : IConnectionStatsProvider + { + public IReadOnlyList Snapshot() => connections; + } + + private sealed class NoOpPublisher : IPublisher + { + public void Publish(Message message) + { + } + } + + private sealed class TestServerCallContext : ServerCallContext + { + public static readonly TestServerCallContext Instance = new(); + + private TestServerCallContext() + { + } + + protected override string MethodCore => + nameof(EventStore.Client.Monitoring.Monitoring.MonitoringBase.ConnectionStats); + protected override string HostCore => "localhost"; + protected override string PeerCore => "ipv4:127.0.0.1:0"; + protected override DateTime DeadlineCore => DateTime.MaxValue; + protected override Metadata RequestHeadersCore { get; } = new(); + protected override CancellationToken CancellationTokenCore => CancellationToken.None; + protected override Metadata ResponseTrailersCore { get; } = new(); + protected override Status StatusCore { get; set; } + protected override WriteOptions WriteOptionsCore { get; set; } + protected override AuthContext AuthContextCore => new(null, new Dictionary>()); + protected override IDictionary UserStateCore { get; } = new Dictionary(); + protected override ContextPropagationToken CreatePropagationTokenCore(ContextPropagationOptions options) => + throw new NotSupportedException(); + protected override Task WriteResponseHeadersAsyncCore(Metadata responseHeaders) => Task.CompletedTask; + } +} diff --git a/src/EventStore.Core.Tests/Services/Transport/Grpc/MonitoringTests/TcpStatsTests.cs b/src/EventStore.Core.Tests/Services/Transport/Grpc/MonitoringTests/TcpStatsTests.cs deleted file mode 100644 index f46a618245..0000000000 --- a/src/EventStore.Core.Tests/Services/Transport/Grpc/MonitoringTests/TcpStatsTests.cs +++ /dev/null @@ -1,136 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; -using EventStore.Client.Monitoring; -using EventStore.Core.Bus; -using EventStore.Core.Messages; -using EventStore.Core.Messaging; -using Grpc.Core; -using NUnit.Framework; -using CoreTcpConnectionStats = EventStore.Core.Messages.MonitoringMessage.TcpConnectionStats; - -namespace EventStore.Core.Tests.Services.Transport.Grpc.MonitoringTests; - -[TestFixture] -public class TcpStatsTests -{ - private readonly Guid _connectionId = Guid.Parse("1e1c6d68-3c7c-446f-915e-8bdf8d35e122"); - private TcpStatsResp _response; - private CapturingPublisher _publisher; - - [SetUp] - public async Task SetUp() - { - _publisher = new CapturingPublisher(new List { - new() { - RemoteEndPoint = "127.0.0.1:1113", - LocalEndPoint = "127.0.0.1:2113", - ClientConnectionName = "test-connection", - ConnectionId = _connectionId, - TotalBytesSent = 123, - TotalBytesReceived = 456, - PendingSendBytes = 7, - PendingReceivedBytes = 8, - IsExternalConnection = true, - IsSslConnection = true - }, - new() { - ConnectionId = Guid.Empty - } - }); - var serviceType = typeof(MonitoringMessage).Assembly.GetType( - "EventStore.Core.Services.Transport.Grpc.Monitoring", - throwOnError: true); - var service = Activator.CreateInstance( - serviceType!, - BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, - binder: null, - args: [_publisher], - culture: null); - - var task = (Task)serviceType!.GetMethod(nameof(EventStore.Client.Monitoring.Monitoring.MonitoringBase.TcpStats))! - .Invoke(service, [new TcpStatsReq(), TestServerCallContext.Instance])!; - _response = await task; - } - - [Test] - public void should_request_fresh_tcp_connection_stats() - { - Assert.IsTrue(_publisher.RequestedTcpStats); - } - - [Test] - public void should_return_the_tcp_connection_stats() - { - Assert.AreEqual(2, _response.Connections.Count); - } - - [Test] - public void should_map_all_tcp_connection_fields() - { - var connection = _response.Connections[0]; - - Assert.AreEqual("127.0.0.1:1113", connection.RemoteEndpoint); - Assert.AreEqual("127.0.0.1:2113", connection.LocalEndpoint); - Assert.AreEqual("test-connection", connection.ClientConnectionName); - Assert.AreEqual(_connectionId.ToString("D"), connection.ConnectionId); - Assert.AreEqual(123, connection.TotalBytesSent); - Assert.AreEqual(456, connection.TotalBytesReceived); - Assert.AreEqual(7, connection.PendingSendBytes); - Assert.AreEqual(8, connection.PendingReceivedBytes); - Assert.IsTrue(connection.IsExternalConnection); - Assert.IsTrue(connection.IsSslConnection); - } - - [Test] - public void should_map_null_strings_to_empty_values() - { - var connection = _response.Connections[1]; - - Assert.AreEqual(string.Empty, connection.RemoteEndpoint); - Assert.AreEqual(string.Empty, connection.LocalEndpoint); - Assert.AreEqual(string.Empty, connection.ClientConnectionName); - } - - private sealed class CapturingPublisher(List connectionStats) : IPublisher - { - public bool RequestedTcpStats { get; private set; } - - public void Publish(Message message) - { - if (message is not MonitoringMessage.GetFreshTcpConnectionStats request) - { - throw new InvalidOperationException($"Unexpected message {message.GetType().Name}"); - } - - RequestedTcpStats = true; - request.Envelope.ReplyWith(new MonitoringMessage.GetFreshTcpConnectionStatsCompleted(connectionStats)); - } - } - - private sealed class TestServerCallContext : ServerCallContext - { - public static readonly TestServerCallContext Instance = new(); - - private TestServerCallContext() - { - } - - protected override string MethodCore => nameof(EventStore.Client.Monitoring.Monitoring.MonitoringBase.TcpStats); - protected override string HostCore => "localhost"; - protected override string PeerCore => "ipv4:127.0.0.1:0"; - protected override DateTime DeadlineCore => DateTime.MaxValue; - protected override Metadata RequestHeadersCore { get; } = new(); - protected override CancellationToken CancellationTokenCore => CancellationToken.None; - protected override Metadata ResponseTrailersCore { get; } = new(); - protected override Status StatusCore { get; set; } - protected override WriteOptions WriteOptionsCore { get; set; } - protected override AuthContext AuthContextCore => new(null, new Dictionary>()); - protected override IDictionary UserStateCore { get; } = new Dictionary(); - protected override ContextPropagationToken CreatePropagationTokenCore(ContextPropagationOptions options) => - throw new NotSupportedException(); - protected override Task WriteResponseHeadersAsyncCore(Metadata responseHeaders) => Task.CompletedTask; - } -} diff --git a/src/EventStore.Core/ClusterVNodeStartup.cs b/src/EventStore.Core/ClusterVNodeStartup.cs index d3767fe04d..3d3077c8fe 100644 --- a/src/EventStore.Core/ClusterVNodeStartup.cs +++ b/src/EventStore.Core/ClusterVNodeStartup.cs @@ -235,7 +235,9 @@ public void ConfigureServicesOnly(IServiceCollection services) .AddSingleton(new Elections(_mainQueue, _authorizationProvider, _clusterDns)) .AddSingleton(new ClientGossip(_mainQueue, _authorizationProvider, _trackers.GossipTrackers.ProcessingRequestFromGrpcClient)) - .AddSingleton(new Monitoring(_monitoringQueue)) + .AddSingleton(serviceProvider => new Monitoring( + _monitoringQueue, + serviceProvider.GetService())) .AddSingleton(_nodeInformationProvider) .AddSingleton(new NodeInformation(_nodeInformationProvider, _authorizationProvider)) .AddSingleton(new Redaction(_mainQueue, _authorizationProvider)) diff --git a/src/EventStore.Core/Messages/MonitoringMessage.cs b/src/EventStore.Core/Messages/MonitoringMessage.cs index bde535e546..5616e573cf 100644 --- a/src/EventStore.Core/Messages/MonitoringMessage.cs +++ b/src/EventStore.Core/Messages/MonitoringMessage.cs @@ -205,44 +205,6 @@ public GetFreshStatsCompleted(bool success, Dictionary stats) } } - [DerivedMessage(CoreMessage.Monitoring)] - public partial class GetFreshTcpConnectionStats : Message - { - public readonly IEnvelope Envelope; - - public GetFreshTcpConnectionStats(IEnvelope envelope) - { - Ensure.NotNull(envelope, "envelope"); - - Envelope = envelope; - } - } - - [DerivedMessage(CoreMessage.Monitoring)] - public partial class GetFreshTcpConnectionStatsCompleted : Message - { - public readonly List ConnectionStats; - - public GetFreshTcpConnectionStatsCompleted(List connectionStats) - { - ConnectionStats = connectionStats; - } - } - - public class TcpConnectionStats - { - public string RemoteEndPoint { get; set; } - public string LocalEndPoint { get; set; } - public string ClientConnectionName { get; set; } - public Guid ConnectionId { get; set; } - public long TotalBytesSent { get; set; } - public long TotalBytesReceived { get; set; } - public int PendingSendBytes { get; set; } - public int PendingReceivedBytes { get; set; } - public bool IsExternalConnection { get; set; } - public bool IsSslConnection { get; set; } - } - [DerivedMessage(CoreMessage.Monitoring)] public partial class InternalStatsRequest : Message { diff --git a/src/EventStore.Core/Services/Transport/Grpc/IConnectionStatsProvider.cs b/src/EventStore.Core/Services/Transport/Grpc/IConnectionStatsProvider.cs new file mode 100644 index 0000000000..414f2f70f4 --- /dev/null +++ b/src/EventStore.Core/Services/Transport/Grpc/IConnectionStatsProvider.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; + +namespace EventStore.Core.Services.Transport.Grpc; + +public interface IConnectionStatsProvider +{ + IReadOnlyList Snapshot(); +} + +public record ConnectionStatsSnapshot( + string ConnectionId, + string RemoteEndPoint, + string LocalEndPoint, + string ClientName, + string Application, + string Protocol, + bool IsTls, + DateTimeOffset ConnectedAt, + long TotalBytesSent, + long TotalBytesReceived, + long PendingSendBytes, + long PendingReceivedBytes); + +internal sealed class EmptyConnectionStatsProvider : IConnectionStatsProvider +{ + public static readonly EmptyConnectionStatsProvider Instance = new(); + + public IReadOnlyList Snapshot() => Array.Empty(); +} diff --git a/src/EventStore.Core/Services/Transport/Grpc/Monitoring.cs b/src/EventStore.Core/Services/Transport/Grpc/Monitoring.cs index 1518bf82a3..6dd2a697f1 100644 --- a/src/EventStore.Core/Services/Transport/Grpc/Monitoring.cs +++ b/src/EventStore.Core/Services/Transport/Grpc/Monitoring.cs @@ -4,6 +4,7 @@ using EventStore.Core.Bus; using EventStore.Core.Messages; using EventStore.Core.Messaging; +using Google.Protobuf.WellKnownTypes; using Grpc.Core; namespace EventStore.Core.Services.Transport.Grpc @@ -11,6 +12,7 @@ namespace EventStore.Core.Services.Transport.Grpc internal partial class Monitoring : EventStore.Client.Monitoring.Monitoring.MonitoringBase { private readonly IPublisher _publisher; + private readonly IConnectionStatsProvider _connectionStatsProvider; public override Task Stats(StatsReq request, IServerStreamWriter responseStream, ServerCallContext context) { @@ -84,41 +86,32 @@ async Task StreamStats() } } - public override Task TcpStats(TcpStatsReq request, ServerCallContext context) + public override Task ConnectionStats( + ConnectionStatsReq request, + ServerCallContext context) { - var responseSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var envelope = new CallbackEnvelope(message => + context.CancellationToken.ThrowIfCancellationRequested(); + var response = new ConnectionStatsResp(); + foreach (var connection in _connectionStatsProvider.Snapshot()) { - if (message is not MonitoringMessage.GetFreshTcpConnectionStatsCompleted completed) + response.Connections.Add(new EventStore.Client.Monitoring.ConnectionStats { - responseSource.TrySetException( - UnknownMessage(message)); - return; - } - - var response = new TcpStatsResp(); - foreach (var connection in completed.ConnectionStats) - { - response.Connections.Add(new TcpConnectionStats - { - RemoteEndpoint = connection.RemoteEndPoint ?? string.Empty, - LocalEndpoint = connection.LocalEndPoint ?? string.Empty, - ClientConnectionName = connection.ClientConnectionName ?? string.Empty, - ConnectionId = connection.ConnectionId.ToString("D"), - TotalBytesSent = connection.TotalBytesSent, - TotalBytesReceived = connection.TotalBytesReceived, - PendingSendBytes = connection.PendingSendBytes, - PendingReceivedBytes = connection.PendingReceivedBytes, - IsExternalConnection = connection.IsExternalConnection, - IsSslConnection = connection.IsSslConnection - }); - } - - responseSource.TrySetResult(response); - }); + RemoteEndpoint = connection.RemoteEndPoint ?? string.Empty, + LocalEndpoint = connection.LocalEndPoint ?? string.Empty, + ClientConnectionName = connection.ClientName ?? string.Empty, + ConnectionId = connection.ConnectionId ?? string.Empty, + TotalBytesSent = connection.TotalBytesSent, + TotalBytesReceived = connection.TotalBytesReceived, + PendingSendBytes = connection.PendingSendBytes, + PendingReceivedBytes = connection.PendingReceivedBytes, + IsTls = connection.IsTls, + Protocol = connection.Protocol ?? string.Empty, + Application = connection.Application ?? string.Empty, + ConnectedAt = Timestamp.FromDateTimeOffset(connection.ConnectedAt) + }); + } - _publisher.Publish(new MonitoringMessage.GetFreshTcpConnectionStats(envelope)); - return responseSource.Task.WaitAsync(context.CancellationToken); + return Task.FromResult(response); } public override Task ReplicationStats(ReplicationStatsReq request, ServerCallContext context) @@ -157,9 +150,14 @@ public override Task ReplicationStats(ReplicationStatsReq return responseSource.Task.WaitAsync(context.CancellationToken); } - public Monitoring(IPublisher publisher) + public Monitoring(IPublisher publisher) : this(publisher, null) + { + } + + public Monitoring(IPublisher publisher, IConnectionStatsProvider connectionStatsProvider) { _publisher = publisher; + _connectionStatsProvider = connectionStatsProvider ?? EmptyConnectionStatsProvider.Instance; } private static Exception UnknownMessage(Message message) where T : Message => diff --git a/src/Protos/Grpc/monitoring.proto b/src/Protos/Grpc/monitoring.proto index dc1a31db9e..76ef60d2df 100644 --- a/src/Protos/Grpc/monitoring.proto +++ b/src/Protos/Grpc/monitoring.proto @@ -3,10 +3,11 @@ package event_store.client.monitoring; option java_package = "com.eventstore.dbclient.proto.monitoring"; import "google/protobuf/struct.proto"; +import "google/protobuf/timestamp.proto"; service Monitoring { rpc Stats(StatsReq) returns (stream StatsResp); - rpc TcpStats(TcpStatsReq) returns (TcpStatsResp); + rpc ConnectionStats(ConnectionStatsReq) returns (ConnectionStatsResp); rpc ReplicationStats(ReplicationStatsReq) returns (ReplicationStatsResp); } @@ -22,24 +23,26 @@ message StatsResp { google.protobuf.Value structured_stats = 2; } -message TcpStatsReq { +message ConnectionStatsReq { } -message TcpStatsResp { - repeated TcpConnectionStats connections = 1; +message ConnectionStatsResp { + repeated ConnectionStats connections = 1; } -message TcpConnectionStats { +message ConnectionStats { string remote_endpoint = 1; string local_endpoint = 2; string client_connection_name = 3; string connection_id = 4; int64 total_bytes_sent = 5; int64 total_bytes_received = 6; - int32 pending_send_bytes = 7; - int32 pending_received_bytes = 8; - bool is_external_connection = 9; - bool is_ssl_connection = 10; + int64 pending_send_bytes = 7; + int64 pending_received_bytes = 8; + bool is_tls = 9; + string protocol = 10; + string application = 11; + google.protobuf.Timestamp connected_at = 12; } message ReplicationStatsReq {