blob: b66d370a072333b4d6d725ee14771ecd8da39600 [file]
/*!
*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 config from '../../../config/AgentConfig';
import * as grpc from '@grpc/grpc-js';
import { createLogger, throttled } from '../../../logging';
import { MeterReportServiceClient } from '../../../proto/language-agent/Meter_grpc_pb';
import BootService from '../boot/BootService';
import ServiceManager from '../boot/ServiceManager';
import RuntimeMetricsCollector from './RuntimeMetricsCollector';
import { RuntimeSnapshot } from './RuntimeSampler';
import GRPCChannelManager from '../remote/GRPCChannelManager';
import { GRPCChannelListener } from '../remote/GRPCChannelListener';
import { GRPCChannelStatus } from '../remote/GRPCChannelStatus';
const logger = createLogger(__filename);
const logReportError = throttled(logger, 'error', 30000);
/** Reports Node.js runtime metrics via gRPC MeterReportService (Go/Python-compatible pipeline). */
export default class MeterSender implements BootService, GRPCChannelListener {
private closed = false;
private channelManager?: GRPCChannelManager;
private status = GRPCChannelStatus.DISCONNECT;
private reporterClient?: MeterReportServiceClient;
private readonly buffer: RuntimeSnapshot[] = [];
private collectTimer?: NodeJS.Timeout;
private reportTimer?: NodeJS.Timeout;
private reporting?: Promise<void>;
private collector!: RuntimeMetricsCollector;
prepare(): void {
this.collector = new RuntimeMetricsCollector();
this.channelManager = ServiceManager.INSTANCE.findService(GRPCChannelManager);
this.channelManager?.addChannelListener(this);
}
boot(): void {
if (this.collectTimer || this.reportTimer) {
logger.warn('MeterSender timers already scheduled; skipping duplicate boot.');
return;
}
this.startTimers();
}
onComplete(): void {}
priority(): number {
return 0;
}
statusChanged(status: GRPCChannelStatus): void {
this.status = status;
this.reporterClient = status === GRPCChannelStatus.CONNECTED ? this.createReporterClient() : undefined;
}
private createReporterClient(): MeterReportServiceClient | undefined {
if (!this.channelManager) {
return undefined;
}
return new MeterReportServiceClient(
config.collectorAddress,
grpc.credentials.createInsecure(),
this.channelManager.getClientOptions(),
);
}
private startTimers(): void {
this.collectTimer = setInterval(() => {
if (this.closed) {
return;
}
this.collectSample();
}, config.runtimeMetricsCollectPeriod || 1000) as NodeJS.Timeout;
this.collectTimer.unref();
this.reportTimer = setInterval(() => {
if (this.closed) {
return;
}
void this.reportBufferedMetrics();
}, config.runtimeMetricsReportPeriod || 1000) as NodeJS.Timeout;
this.reportTimer.unref();
}
private collectSample(): void {
const maxBufferSize = config.runtimeMetricsBufferSize || 600;
if (this.buffer.length >= maxBufferSize) {
this.buffer.shift();
}
this.buffer.push(this.collector.sample());
}
private reportBufferedMetrics(): Promise<void> {
if (this.closed) {
return Promise.resolve();
}
if (this.reporting) {
return this.reporting;
}
this.reporting = this.doReportBufferedMetrics().finally(() => {
this.reporting = undefined;
});
return this.reporting;
}
private doReportBufferedMetrics(): Promise<void> {
return new Promise((resolve) => {
try {
if (this.closed) {
resolve();
return;
}
if (this.buffer.length === 0 || this.status !== GRPCChannelStatus.CONNECTED || !this.reporterClient) {
resolve();
return;
}
if (!config.serviceName || !config.serviceInstance) {
resolve();
return;
}
const snapshots = this.buffer.splice(0, this.buffer.length);
const stream = this.reporterClient.collect(
new grpc.Metadata(),
{ deadline: Date.now() + (config.traceTimeout || 10000) },
(error: grpc.ServiceError | null) => {
if (error) {
logReportError('Failed to report runtime meter data', error);
this.reportGrpcError(error);
}
resolve();
},
);
try {
let metadataWritten = false;
const timestamp = Date.now();
for (const snapshot of snapshots) {
for (const meterData of this.collector.toMeterData(snapshot)) {
if (!metadataWritten) {
meterData
.setService(config.serviceName)
.setServiceinstance(config.serviceInstance)
.setTimestamp(timestamp);
metadataWritten = true;
}
stream.write(meterData);
}
}
} finally {
try {
stream.end();
} catch (error) {
logReportError('Failed to end meter collect stream', error);
resolve();
}
}
} catch (error) {
logReportError('Failed to report runtime meter data', error);
this.reportGrpcError(error);
resolve();
}
});
}
private reportGrpcError(error: unknown): void {
if (this.closed) {
return;
}
this.channelManager?.reportError(error);
}
flush(): Promise<any> | null {
if (this.closed) {
return null;
}
this.collectSample();
return this.reportBufferedMetrics();
}
shutdown(): void {
this.closed = true;
if (this.collectTimer) {
clearInterval(this.collectTimer);
this.collectTimer = undefined;
}
if (this.reportTimer) {
clearInterval(this.reportTimer);
this.reportTimer = undefined;
}
this.reporting = undefined;
this.reporterClient = undefined;
this.buffer.length = 0;
this.collector.destroy();
this.channelManager = undefined;
logger.info('MeterSender destroyed and resources cleaned up');
}
}