cloudrun-malware-scanner/metrics.ts (276 lines of code) (raw):

/* * Copyright 2021 Google LLC * * 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 * * https://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 * as process from 'node:process'; import { MeterProvider, PeriodicExportingMetricReader, } from '@opentelemetry/sdk-metrics'; import {Resource} from '@opentelemetry/resources'; import {MetricExporter as GcpMetricExporter} from '@google-cloud/opentelemetry-cloud-monitoring-exporter'; import {GcpDetectorSync} from '@google-cloud/opentelemetry-resource-util'; import * as Semconv from '@opentelemetry/semantic-conventions'; import {version as packageVersion} from './package.json'; import {logger} from './logger.js'; import * as OpenTelemetryApi from '@opentelemetry/api'; const METRIC_TYPE_ROOT = 'malware-scanning/'; interface CounterAttributes { [x: string]: string; } const RESOURCE_ATTRIBUTES: CounterAttributes = { [Semconv.SEMRESATTRS_SERVICE_NAMESPACE]: 'googlecloudplatform', [Semconv.SEMRESATTRS_SERVICE_NAME]: 'gcs-malware-scanning', [Semconv.SEMRESATTRS_SERVICE_VERSION]: packageVersion, }; const COUNTERS_PREFIX = RESOURCE_ATTRIBUTES[Semconv.SEMRESATTRS_SERVICE_NAMESPACE] + '/' + RESOURCE_ATTRIBUTES[Semconv.SEMRESATTRS_SERVICE_NAME] + '/'; enum COUNTER_NAMES { cleanFiles = 'clean-files', infectedFiles = 'infected-files', ignoredFiles = 'ignored-files', scansFailed = 'scans-failed', bytesScanned = 'bytes-scanned', scanDuration = 'scan-duration', cvdUpdates = 'cvd-mirror-updates', } const COUNTER_ATTRIBUTE_NAMES = { sourceBucket: 'source_bucket', destinationBucket: 'destination_bucket', clamVersion: 'clam_version', cloudRunRevision: 'cloud_run_revision', cvdUpdateStatus: 'cvd_update_status', ignoredReason: 'ignored_reason', ignoredRegex: 'ignored_regex', }; interface Counter { cumulative?: OpenTelemetryApi.Counter; histogram?: OpenTelemetryApi.Histogram; } const COUNTERS: Map<COUNTER_NAMES, Counter> = new Map(); const METRIC_EXPORT_INTERVAL = parseInt( process.env.EXPORT_INTERVAL || '20000', 10, ); class DiagToPinoLogger implements OpenTelemetryApi.DiagLogger { suppressErrors: boolean; constructor() { // In some cases where errors may be expected, we want to be able to supress // them. this.suppressErrors = false; } /* eslint-disable @typescript-eslint/no-explicit-any */ verbose(message: string, ...args: any[]) { logger.trace('otel: ' + message, args); } debug(message: string, ...args: any[]) { logger.debug('otel: ' + message, args); } info(message: string, ...args: any[]) { logger.info('otel: ' + message, args); } warn(message: string, ...args: any[]) { logger.warn('otel: ' + message, args); } error(message: string, ...args: any[]) { if (!this.suppressErrors) { logger.error('otel: ' + message, args); } } /* eslint-enable */ } OpenTelemetryApi.default.diag.setLogger(new DiagToPinoLogger(), { logLevel: OpenTelemetryApi.DiagLogLevel.INFO, suppressOverrideMessage: true, }); function writeScanFailedMetric(sourceBucket?: string) { const attrs: CounterAttributes = { [COUNTER_ATTRIBUTE_NAMES.cloudRunRevision]: process.env.K_REVISION || 'no-revision', }; if (sourceBucket) { attrs[COUNTER_ATTRIBUTE_NAMES.sourceBucket] = sourceBucket; attrs[COUNTER_ATTRIBUTE_NAMES.destinationBucket] = sourceBucket; } COUNTERS.get(COUNTER_NAMES.scansFailed)?.cumulative?.add(1, attrs); } function writeScanCleanMetric( sourceBucket: string, destinationBucket: string, fileSize: number, scanDuration: number, clamVersion: string, ) { writeScanCompletedMetric_( COUNTER_NAMES.cleanFiles, sourceBucket, destinationBucket, fileSize, scanDuration, clamVersion, ); } function writeScanIgnoredMetric( sourceBucket: string, destinationBucket: string, fileSize: number, ignoredReason: string, ignoredRegex?: string, ) { const additionalAttrs: CounterAttributes = {}; if (ignoredReason) { additionalAttrs[COUNTER_ATTRIBUTE_NAMES.ignoredReason] = ignoredReason; } if (ignoredRegex) { additionalAttrs[COUNTER_ATTRIBUTE_NAMES.ignoredRegex] = ignoredRegex; } writeScanCompletedMetric_( COUNTER_NAMES.ignoredFiles, sourceBucket, destinationBucket, fileSize, 0, null, additionalAttrs, ); } function writeScanInfectedMetric( sourceBucket: string, destinationBucket: string, fileSize: number, scanDuration: number, clamVersion: string, ) { writeScanCompletedMetric_( COUNTER_NAMES.infectedFiles, sourceBucket, destinationBucket, fileSize, scanDuration, clamVersion, ); } function writeScanCompletedMetric_( counterName: COUNTER_NAMES, sourceBucket: string, destinationBucket: string, fileSize: number, scanDuration: number | null, clamVersion: string | null, additionalAttrs: CounterAttributes = {}, ) { const attrs: CounterAttributes = { ...additionalAttrs, [COUNTER_ATTRIBUTE_NAMES.sourceBucket]: sourceBucket, [COUNTER_ATTRIBUTE_NAMES.destinationBucket]: destinationBucket, [COUNTER_ATTRIBUTE_NAMES.cloudRunRevision]: process.env.K_REVISION || 'no-revision', }; if (clamVersion) { attrs[COUNTER_ATTRIBUTE_NAMES.clamVersion] = clamVersion; } const counter = COUNTERS.get(counterName); if (!counter?.cumulative) { throw new Error('Unknown counter: ' + counterName); } counter.cumulative.add(1, attrs); COUNTERS.get(COUNTER_NAMES.bytesScanned)?.cumulative?.add(fileSize, attrs); if (scanDuration) { COUNTERS.get(COUNTER_NAMES.scanDuration)?.histogram?.record( scanDuration, attrs, ); } } function writeCvdMirrorUpdatedMetric(success: boolean, isUpdated: boolean) { COUNTERS.get(COUNTER_NAMES.cvdUpdates)?.cumulative?.add(1, { [COUNTER_ATTRIBUTE_NAMES.cloudRunRevision]: process.env.K_REVISION || 'no-revision', [COUNTER_ATTRIBUTE_NAMES.cvdUpdateStatus]: success ? isUpdated ? 'SUCCESS_UPDATED' : 'SUCCESS_NO_UPDATES' : 'FAILURE', }); } function initMetrics(projectId: string) { if (!projectId) { throw Error('Unable to proceed without a Project ID'); } logger.debug('initializing metrics'); const resources = new GcpDetectorSync() .detect() .merge(new Resource(RESOURCE_ATTRIBUTES)); const meterProvider = new MeterProvider({ resource: resources, readers: [ new PeriodicExportingMetricReader({ exportIntervalMillis: METRIC_EXPORT_INTERVAL, exportTimeoutMillis: METRIC_EXPORT_INTERVAL, exporter: new GcpMetricExporter({prefix: 'workload.googleapis.com'}), }), ], }); const meter = meterProvider.getMeter(COUNTERS_PREFIX); // Create cumulative counters [ { name: COUNTER_NAMES.cleanFiles, description: 'Number of files scanned that were found to be clean of malware at the time of scan', }, { name: COUNTER_NAMES.ignoredFiles, description: 'Number of files that were ignored and not scanned', }, { name: COUNTER_NAMES.infectedFiles, description: 'Number of files scanned that were found to contain malware at the time of scan', }, { name: COUNTER_NAMES.scansFailed, description: 'Number of malware scan requests which failed', }, { name: COUNTER_NAMES.cvdUpdates, description: 'Number of CVD mirror update checks performed with their status', }, { name: COUNTER_NAMES.bytesScanned, description: 'Total number of bytes scanned', unit: 'By', }, ].forEach((counter) => COUNTERS.set(counter.name, { cumulative: meter.createCounter(COUNTERS_PREFIX + counter.name, { description: counter.description, unit: counter.unit, }), }), ); COUNTERS.set(COUNTER_NAMES.scanDuration, { histogram: meter.createHistogram( COUNTERS_PREFIX + COUNTER_NAMES.scanDuration, { description: 'Duration spent scanning files', unit: 'ms', advice: { explicitBucketBoundaries: [ 0, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 50000, 100000, 200000, ], }, }, ), }); // Sanity check on COUNTERS length if (COUNTERS.size !== Object.keys(COUNTER_NAMES).length) { throw new Error('Code Error: not all counters initialized'); } logger.info( `Metrics initialized for ${METRIC_TYPE_ROOT} on project ${projectId}`, ); } export { writeScanFailedMetric as writeScanFailed, writeScanCleanMetric as writeScanClean, writeScanIgnoredMetric as writeScanIgnored, writeScanInfectedMetric as writeScanInfected, writeCvdMirrorUpdatedMetric as writeCvdMirrorUpdated, initMetrics as init, };