/* * Copyright 2021 gRPC authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ import { isIPv4, isIPv6 } from 'net'; import { ConnectivityState } from './connectivity-state'; import { Status } from './constants'; import { Timestamp } from './generated/google/protobuf/Timestamp'; import { Channel as ChannelMessage } from './generated/grpc/channelz/v1/Channel'; import { ChannelConnectivityState__Output } from './generated/grpc/channelz/v1/ChannelConnectivityState'; import { ChannelRef as ChannelRefMessage } from './generated/grpc/channelz/v1/ChannelRef'; import { ChannelTrace } from './generated/grpc/channelz/v1/ChannelTrace'; import { GetChannelRequest__Output } from './generated/grpc/channelz/v1/GetChannelRequest'; import { GetChannelResponse } from './generated/grpc/channelz/v1/GetChannelResponse'; import { sendUnaryData, ServerUnaryCall } from './server-call'; import { ServerRef as ServerRefMessage } from './generated/grpc/channelz/v1/ServerRef'; import { SocketRef as SocketRefMessage } from './generated/grpc/channelz/v1/SocketRef'; import { isTcpSubchannelAddress, SubchannelAddress, } from './subchannel-address'; import { SubchannelRef as SubchannelRefMessage } from './generated/grpc/channelz/v1/SubchannelRef'; import { GetServerRequest__Output } from './generated/grpc/channelz/v1/GetServerRequest'; import { GetServerResponse } from './generated/grpc/channelz/v1/GetServerResponse'; import { Server as ServerMessage } from './generated/grpc/channelz/v1/Server'; import { GetServersRequest__Output } from './generated/grpc/channelz/v1/GetServersRequest'; import { GetServersResponse } from './generated/grpc/channelz/v1/GetServersResponse'; import { GetTopChannelsRequest__Output } from './generated/grpc/channelz/v1/GetTopChannelsRequest'; import { GetTopChannelsResponse } from './generated/grpc/channelz/v1/GetTopChannelsResponse'; import { GetSubchannelRequest__Output } from './generated/grpc/channelz/v1/GetSubchannelRequest'; import { GetSubchannelResponse } from './generated/grpc/channelz/v1/GetSubchannelResponse'; import { Subchannel as SubchannelMessage } from './generated/grpc/channelz/v1/Subchannel'; import { GetSocketRequest__Output } from './generated/grpc/channelz/v1/GetSocketRequest'; import { GetSocketResponse } from './generated/grpc/channelz/v1/GetSocketResponse'; import { Socket as SocketMessage } from './generated/grpc/channelz/v1/Socket'; import { Address } from './generated/grpc/channelz/v1/Address'; import { Security } from './generated/grpc/channelz/v1/Security'; import { GetServerSocketsRequest__Output } from './generated/grpc/channelz/v1/GetServerSocketsRequest'; import { GetServerSocketsResponse } from './generated/grpc/channelz/v1/GetServerSocketsResponse'; import { ChannelzDefinition, ChannelzHandlers, } from './generated/grpc/channelz/v1/Channelz'; import { ProtoGrpcType as ChannelzProtoGrpcType } from './generated/channelz'; import type { loadSync } from '@grpc/proto-loader'; import { registerAdminService } from './admin'; import { loadPackageDefinition } from './make-client'; export type TraceSeverity = | 'CT_UNKNOWN' | 'CT_INFO' | 'CT_WARNING' | 'CT_ERROR'; export interface ChannelRef { kind: 'channel'; id: number; name: string; } export interface SubchannelRef { kind: 'subchannel'; id: number; name: string; } export interface ServerRef { kind: 'server'; id: number; } export interface SocketRef { kind: 'socket'; id: number; name: string; } function channelRefToMessage(ref: ChannelRef): ChannelRefMessage { return { channel_id: ref.id, name: ref.name, }; } function subchannelRefToMessage(ref: SubchannelRef): SubchannelRefMessage { return { subchannel_id: ref.id, name: ref.name, }; } function serverRefToMessage(ref: ServerRef): ServerRefMessage { return { server_id: ref.id, }; } function socketRefToMessage(ref: SocketRef): SocketRefMessage { return { socket_id: ref.id, name: ref.name, }; } interface TraceEvent { description: string; severity: TraceSeverity; timestamp: Date; childChannel?: ChannelRef; childSubchannel?: SubchannelRef; } /** * The loose upper bound on the number of events that should be retained in a * trace. This may be exceeded by up to a factor of 2. Arbitrarily chosen as a * number that should be large enough to contain the recent relevant * information, but small enough to not use excessive memory. */ const TARGET_RETAINED_TRACES = 32; export class ChannelzTrace { events: TraceEvent[] = []; creationTimestamp: Date; eventsLogged = 0; constructor() { this.creationTimestamp = new Date(); } addTrace( severity: TraceSeverity, description: string, child?: ChannelRef | SubchannelRef ) { const timestamp = new Date(); this.events.push({ description: description, severity: severity, timestamp: timestamp, childChannel: child?.kind === 'channel' ? child : undefined, childSubchannel: child?.kind === 'subchannel' ? child : undefined, }); // Whenever the trace array gets too large, discard the first half if (this.events.length >= TARGET_RETAINED_TRACES * 2) { this.events = this.events.slice(TARGET_RETAINED_TRACES); } this.eventsLogged += 1; } getTraceMessage(): ChannelTrace { return { creation_timestamp: dateToProtoTimestamp(this.creationTimestamp), num_events_logged: this.eventsLogged, events: this.events.map(event => { return { description: event.description, severity: event.severity, timestamp: dateToProtoTimestamp(event.timestamp), channel_ref: event.childChannel ? channelRefToMessage(event.childChannel) : null, subchannel_ref: event.childSubchannel ? subchannelRefToMessage(event.childSubchannel) : null, }; }), }; } } export class ChannelzChildrenTracker { private channelChildren: Map = new Map(); private subchannelChildren: Map< number, { ref: SubchannelRef; count: number } > = new Map(); private socketChildren: Map = new Map(); refChild(child: ChannelRef | SubchannelRef | SocketRef) { switch (child.kind) { case 'channel': { const trackedChild = this.channelChildren.get(child.id) ?? { ref: child, count: 0, }; trackedChild.count += 1; this.channelChildren.set(child.id, trackedChild); break; } case 'subchannel': { const trackedChild = this.subchannelChildren.get(child.id) ?? { ref: child, count: 0, }; trackedChild.count += 1; this.subchannelChildren.set(child.id, trackedChild); break; } case 'socket': { const trackedChild = this.socketChildren.get(child.id) ?? { ref: child, count: 0, }; trackedChild.count += 1; this.socketChildren.set(child.id, trackedChild); break; } } } unrefChild(child: ChannelRef | SubchannelRef | SocketRef) { switch (child.kind) { case 'channel': { const trackedChild = this.channelChildren.get(child.id); if (trackedChild !== undefined) { trackedChild.count -= 1; if (trackedChild.count === 0) { this.channelChildren.delete(child.id); } else { this.channelChildren.set(child.id, trackedChild); } } break; } case 'subchannel': { const trackedChild = this.subchannelChildren.get(child.id); if (trackedChild !== undefined) { trackedChild.count -= 1; if (trackedChild.count === 0) { this.subchannelChildren.delete(child.id); } else { this.subchannelChildren.set(child.id, trackedChild); } } break; } case 'socket': { const trackedChild = this.socketChildren.get(child.id); if (trackedChild !== undefined) { trackedChild.count -= 1; if (trackedChild.count === 0) { this.socketChildren.delete(child.id); } else { this.socketChildren.set(child.id, trackedChild); } } break; } } } getChildLists(): ChannelzChildren { const channels: ChannelRef[] = []; for (const { ref } of this.channelChildren.values()) { channels.push(ref); } const subchannels: SubchannelRef[] = []; for (const { ref } of this.subchannelChildren.values()) { subchannels.push(ref); } const sockets: SocketRef[] = []; for (const { ref } of this.socketChildren.values()) { sockets.push(ref); } return { channels, subchannels, sockets }; } } export class ChannelzCallTracker { callsStarted = 0; callsSucceeded = 0; callsFailed = 0; lastCallStartedTimestamp: Date | null = null; addCallStarted() { this.callsStarted += 1; this.lastCallStartedTimestamp = new Date(); } addCallSucceeded() { this.callsSucceeded += 1; } addCallFailed() { this.callsFailed += 1; } } export interface ChannelzChildren { channels: ChannelRef[]; subchannels: SubchannelRef[]; sockets: SocketRef[]; } export interface ChannelInfo { target: string; state: ConnectivityState; trace: ChannelzTrace; callTracker: ChannelzCallTracker; children: ChannelzChildren; } export type SubchannelInfo = ChannelInfo; export interface ServerInfo { trace: ChannelzTrace; callTracker: ChannelzCallTracker; listenerChildren: ChannelzChildren; sessionChildren: ChannelzChildren; } export interface TlsInfo { cipherSuiteStandardName: string | null; cipherSuiteOtherName: string | null; localCertificate: Buffer | null; remoteCertificate: Buffer | null; } export interface SocketInfo { localAddress: SubchannelAddress | null; remoteAddress: SubchannelAddress | null; security: TlsInfo | null; remoteName: string | null; streamsStarted: number; streamsSucceeded: number; streamsFailed: number; messagesSent: number; messagesReceived: number; keepAlivesSent: number; lastLocalStreamCreatedTimestamp: Date | null; lastRemoteStreamCreatedTimestamp: Date | null; lastMessageSentTimestamp: Date | null; lastMessageReceivedTimestamp: Date | null; localFlowControlWindow: number | null; remoteFlowControlWindow: number | null; } interface ChannelEntry { ref: ChannelRef; getInfo(): ChannelInfo; } interface SubchannelEntry { ref: SubchannelRef; getInfo(): SubchannelInfo; } interface ServerEntry { ref: ServerRef; getInfo(): ServerInfo; } interface SocketEntry { ref: SocketRef; getInfo(): SocketInfo; } let nextId = 1; function getNextId(): number { return nextId++; } const channels: (ChannelEntry | undefined)[] = []; const subchannels: (SubchannelEntry | undefined)[] = []; const servers: (ServerEntry | undefined)[] = []; const sockets: (SocketEntry | undefined)[] = []; export function registerChannelzChannel( name: string, getInfo: () => ChannelInfo, channelzEnabled: boolean ): ChannelRef { const id = getNextId(); const ref: ChannelRef = { id, name, kind: 'channel' }; if (channelzEnabled) { channels[id] = { ref, getInfo }; } return ref; } export function registerChannelzSubchannel( name: string, getInfo: () => SubchannelInfo, channelzEnabled: boolean ): SubchannelRef { const id = getNextId(); const ref: SubchannelRef = { id, name, kind: 'subchannel' }; if (channelzEnabled) { subchannels[id] = { ref, getInfo }; } return ref; } export function registerChannelzServer( getInfo: () => ServerInfo, channelzEnabled: boolean ): ServerRef { const id = getNextId(); const ref: ServerRef = { id, kind: 'server' }; if (channelzEnabled) { servers[id] = { ref, getInfo }; } return ref; } export function registerChannelzSocket( name: string, getInfo: () => SocketInfo, channelzEnabled: boolean ): SocketRef { const id = getNextId(); const ref: SocketRef = { id, name, kind: 'socket' }; if (channelzEnabled) { sockets[id] = { ref, getInfo }; } return ref; } export function unregisterChannelzRef( ref: ChannelRef | SubchannelRef | ServerRef | SocketRef ) { switch (ref.kind) { case 'channel': delete channels[ref.id]; return; case 'subchannel': delete subchannels[ref.id]; return; case 'server': delete servers[ref.id]; return; case 'socket': delete sockets[ref.id]; return; } } /** * Parse a single section of an IPv6 address as two bytes * @param addressSection A hexadecimal string of length up to 4 * @returns The pair of bytes representing this address section */ function parseIPv6Section(addressSection: string): [number, number] { const numberValue = Number.parseInt(addressSection, 16); return [(numberValue / 256) | 0, numberValue % 256]; } /** * Parse a chunk of an IPv6 address string to some number of bytes * @param addressChunk Some number of segments of up to 4 hexadecimal * characters each, joined by colons. * @returns The list of bytes representing this address chunk */ function parseIPv6Chunk(addressChunk: string): number[] { if (addressChunk === '') { return []; } const bytePairs = addressChunk .split(':') .map(section => parseIPv6Section(section)); const result: number[] = []; return result.concat(...bytePairs); } /** * Converts an IPv4 or IPv6 address from string representation to binary * representation * @param ipAddress an IP address in standard IPv4 or IPv6 text format * @returns */ function ipAddressStringToBuffer(ipAddress: string): Buffer | null { if (isIPv4(ipAddress)) { return Buffer.from( Uint8Array.from( ipAddress.split('.').map(segment => Number.parseInt(segment)) ) ); } else if (isIPv6(ipAddress)) { let leftSection: string; let rightSection: string; const doubleColonIndex = ipAddress.indexOf('::'); if (doubleColonIndex === -1) { leftSection = ipAddress; rightSection = ''; } else { leftSection = ipAddress.substring(0, doubleColonIndex); rightSection = ipAddress.substring(doubleColonIndex + 2); } const leftBuffer = Buffer.from(parseIPv6Chunk(leftSection)); const rightBuffer = Buffer.from(parseIPv6Chunk(rightSection)); const middleBuffer = Buffer.alloc( 16 - leftBuffer.length - rightBuffer.length, 0 ); return Buffer.concat([leftBuffer, middleBuffer, rightBuffer]); } else { return null; } } function connectivityStateToMessage( state: ConnectivityState ): ChannelConnectivityState__Output { switch (state) { case ConnectivityState.CONNECTING: return { state: 'CONNECTING', }; case ConnectivityState.IDLE: return { state: 'IDLE', }; case ConnectivityState.READY: return { state: 'READY', }; case ConnectivityState.SHUTDOWN: return { state: 'SHUTDOWN', }; case ConnectivityState.TRANSIENT_FAILURE: return { state: 'TRANSIENT_FAILURE', }; default: return { state: 'UNKNOWN', }; } } function dateToProtoTimestamp(date?: Date | null): Timestamp | null { if (!date) { return null; } const millisSinceEpoch = date.getTime(); return { seconds: (millisSinceEpoch / 1000) | 0, nanos: (millisSinceEpoch % 1000) * 1_000_000, }; } function getChannelMessage(channelEntry: ChannelEntry): ChannelMessage { const resolvedInfo = channelEntry.getInfo(); return { ref: channelRefToMessage(channelEntry.ref), data: { target: resolvedInfo.target, state: connectivityStateToMessage(resolvedInfo.state), calls_started: resolvedInfo.callTracker.callsStarted, calls_succeeded: resolvedInfo.callTracker.callsSucceeded, calls_failed: resolvedInfo.callTracker.callsFailed, last_call_started_timestamp: dateToProtoTimestamp( resolvedInfo.callTracker.lastCallStartedTimestamp ), trace: resolvedInfo.trace.getTraceMessage(), }, channel_ref: resolvedInfo.children.channels.map(ref => channelRefToMessage(ref) ), subchannel_ref: resolvedInfo.children.subchannels.map(ref => subchannelRefToMessage(ref) ), }; } function GetChannel( call: ServerUnaryCall, callback: sendUnaryData ): void { const channelId = Number.parseInt(call.request.channel_id); const channelEntry = channels[channelId]; if (channelEntry === undefined) { callback({ code: Status.NOT_FOUND, details: 'No channel data found for id ' + channelId, }); return; } callback(null, { channel: getChannelMessage(channelEntry) }); } function GetTopChannels( call: ServerUnaryCall, callback: sendUnaryData ): void { const maxResults = Number.parseInt(call.request.max_results); const resultList: ChannelMessage[] = []; let i = Number.parseInt(call.request.start_channel_id); for (; i < channels.length; i++) { const channelEntry = channels[i]; if (channelEntry === undefined) { continue; } resultList.push(getChannelMessage(channelEntry)); if (resultList.length >= maxResults) { break; } } callback(null, { channel: resultList, end: i >= servers.length, }); } function getServerMessage(serverEntry: ServerEntry): ServerMessage { const resolvedInfo = serverEntry.getInfo(); return { ref: serverRefToMessage(serverEntry.ref), data: { calls_started: resolvedInfo.callTracker.callsStarted, calls_succeeded: resolvedInfo.callTracker.callsSucceeded, calls_failed: resolvedInfo.callTracker.callsFailed, last_call_started_timestamp: dateToProtoTimestamp( resolvedInfo.callTracker.lastCallStartedTimestamp ), trace: resolvedInfo.trace.getTraceMessage(), }, listen_socket: resolvedInfo.listenerChildren.sockets.map(ref => socketRefToMessage(ref) ), }; } function GetServer( call: ServerUnaryCall, callback: sendUnaryData ): void { const serverId = Number.parseInt(call.request.server_id); const serverEntry = servers[serverId]; if (serverEntry === undefined) { callback({ code: Status.NOT_FOUND, details: 'No server data found for id ' + serverId, }); return; } callback(null, { server: getServerMessage(serverEntry) }); } function GetServers( call: ServerUnaryCall, callback: sendUnaryData ): void { const maxResults = Number.parseInt(call.request.max_results); const resultList: ServerMessage[] = []; let i = Number.parseInt(call.request.start_server_id); for (; i < servers.length; i++) { const serverEntry = servers[i]; if (serverEntry === undefined) { continue; } resultList.push(getServerMessage(serverEntry)); if (resultList.length >= maxResults) { break; } } callback(null, { server: resultList, end: i >= servers.length, }); } function GetSubchannel( call: ServerUnaryCall, callback: sendUnaryData ): void { const subchannelId = Number.parseInt(call.request.subchannel_id); const subchannelEntry = subchannels[subchannelId]; if (subchannelEntry === undefined) { callback({ code: Status.NOT_FOUND, details: 'No subchannel data found for id ' + subchannelId, }); return; } const resolvedInfo = subchannelEntry.getInfo(); const subchannelMessage: SubchannelMessage = { ref: subchannelRefToMessage(subchannelEntry.ref), data: { target: resolvedInfo.target, state: connectivityStateToMessage(resolvedInfo.state), calls_started: resolvedInfo.callTracker.callsStarted, calls_succeeded: resolvedInfo.callTracker.callsSucceeded, calls_failed: resolvedInfo.callTracker.callsFailed, last_call_started_timestamp: dateToProtoTimestamp( resolvedInfo.callTracker.lastCallStartedTimestamp ), trace: resolvedInfo.trace.getTraceMessage(), }, socket_ref: resolvedInfo.children.sockets.map(ref => socketRefToMessage(ref) ), }; callback(null, { subchannel: subchannelMessage }); } function subchannelAddressToAddressMessage( subchannelAddress: SubchannelAddress ): Address { if (isTcpSubchannelAddress(subchannelAddress)) { return { address: 'tcpip_address', tcpip_address: { ip_address: ipAddressStringToBuffer(subchannelAddress.host) ?? undefined, port: subchannelAddress.port, }, }; } else { return { address: 'uds_address', uds_address: { filename: subchannelAddress.path, }, }; } } function GetSocket( call: ServerUnaryCall, callback: sendUnaryData ): void { const socketId = Number.parseInt(call.request.socket_id); const socketEntry = sockets[socketId]; if (socketEntry === undefined) { callback({ code: Status.NOT_FOUND, details: 'No socket data found for id ' + socketId, }); return; } const resolvedInfo = socketEntry.getInfo(); const securityMessage: Security | null = resolvedInfo.security ? { model: 'tls', tls: { cipher_suite: resolvedInfo.security.cipherSuiteStandardName ? 'standard_name' : 'other_name', standard_name: resolvedInfo.security.cipherSuiteStandardName ?? undefined, other_name: resolvedInfo.security.cipherSuiteOtherName ?? undefined, local_certificate: resolvedInfo.security.localCertificate ?? undefined, remote_certificate: resolvedInfo.security.remoteCertificate ?? undefined, }, } : null; const socketMessage: SocketMessage = { ref: socketRefToMessage(socketEntry.ref), local: resolvedInfo.localAddress ? subchannelAddressToAddressMessage(resolvedInfo.localAddress) : null, remote: resolvedInfo.remoteAddress ? subchannelAddressToAddressMessage(resolvedInfo.remoteAddress) : null, remote_name: resolvedInfo.remoteName ?? undefined, security: securityMessage, data: { keep_alives_sent: resolvedInfo.keepAlivesSent, streams_started: resolvedInfo.streamsStarted, streams_succeeded: resolvedInfo.streamsSucceeded, streams_failed: resolvedInfo.streamsFailed, last_local_stream_created_timestamp: dateToProtoTimestamp( resolvedInfo.lastLocalStreamCreatedTimestamp ), last_remote_stream_created_timestamp: dateToProtoTimestamp( resolvedInfo.lastRemoteStreamCreatedTimestamp ), messages_received: resolvedInfo.messagesReceived, messages_sent: resolvedInfo.messagesSent, last_message_received_timestamp: dateToProtoTimestamp( resolvedInfo.lastMessageReceivedTimestamp ), last_message_sent_timestamp: dateToProtoTimestamp( resolvedInfo.lastMessageSentTimestamp ), local_flow_control_window: resolvedInfo.localFlowControlWindow ? { value: resolvedInfo.localFlowControlWindow } : null, remote_flow_control_window: resolvedInfo.remoteFlowControlWindow ? { value: resolvedInfo.remoteFlowControlWindow } : null, }, }; callback(null, { socket: socketMessage }); } function GetServerSockets( call: ServerUnaryCall< GetServerSocketsRequest__Output, GetServerSocketsResponse >, callback: sendUnaryData ): void { const serverId = Number.parseInt(call.request.server_id); const serverEntry = servers[serverId]; if (serverEntry === undefined) { callback({ code: Status.NOT_FOUND, details: 'No server data found for id ' + serverId, }); return; } const startId = Number.parseInt(call.request.start_socket_id); const maxResults = Number.parseInt(call.request.max_results); const resolvedInfo = serverEntry.getInfo(); // If we wanted to include listener sockets in the result, this line would // instead say // const allSockets = resolvedInfo.listenerChildren.sockets.concat(resolvedInfo.sessionChildren.sockets).sort((ref1, ref2) => ref1.id - ref2.id); const allSockets = resolvedInfo.sessionChildren.sockets.sort( (ref1, ref2) => ref1.id - ref2.id ); const resultList: SocketRefMessage[] = []; let i = 0; for (; i < allSockets.length; i++) { if (allSockets[i].id >= startId) { resultList.push(socketRefToMessage(allSockets[i])); if (resultList.length >= maxResults) { break; } } } callback(null, { socket_ref: resultList, end: i >= allSockets.length, }); } export function getChannelzHandlers(): ChannelzHandlers { return { GetChannel, GetTopChannels, GetServer, GetServers, GetSubchannel, GetSocket, GetServerSockets, }; } let loadedChannelzDefinition: ChannelzDefinition | null = null; export function getChannelzServiceDefinition(): ChannelzDefinition { if (loadedChannelzDefinition) { return loadedChannelzDefinition; } /* The purpose of this complexity is to avoid loading @grpc/proto-loader at * runtime for users who will not use/enable channelz. */ const loaderLoadSync = require('@grpc/proto-loader') .loadSync as typeof loadSync; const loadedProto = loaderLoadSync('channelz.proto', { keepCase: true, longs: String, enums: String, defaults: true, oneofs: true, includeDirs: [`${__dirname}/../../proto`], }); const channelzGrpcObject = loadPackageDefinition( loadedProto ) as unknown as ChannelzProtoGrpcType; loadedChannelzDefinition = channelzGrpcObject.grpc.channelz.v1.Channelz.service; return loadedChannelzDefinition; } export function setup() { registerAdminService(getChannelzServiceDefinition, getChannelzHandlers); }