diff --git a/src/v2/components/web-server/builder.ts b/src/v2/components/web-server/builder.ts index 1fc5a08..d7673e1 100644 --- a/src/v2/components/web-server/builder.ts +++ b/src/v2/components/web-server/builder.ts @@ -2,6 +2,7 @@ import * as pulumi from '@pulumi/pulumi'; import * as awsx from '@pulumi/awsx'; import { EcsService } from '../ecs-service'; import { WebServer } from '.'; +import { OtelCollector } from '../../otel'; export namespace WebServerBuilder { export type EcsConfig = Omit; @@ -26,7 +27,7 @@ export class WebServerBuilder { private _domain?: pulumi.Input; private _hostedZoneId?: pulumi.Input; private _healthCheckPath?: pulumi.Input; - private _otelCollectorConfig?: pulumi.Input; + private _otelCollectorConfig?: pulumi.Input; private _initContainers: pulumi.Input[] = []; private _sidecarContainers: pulumi.Input[] = []; private _volumes: EcsService.PersistentStorageVolume[] = []; @@ -97,7 +98,7 @@ export class WebServerBuilder { return this; } - public withOtelCollector(config: pulumi.Input): this { + public withOtelCollector(config: pulumi.Input): this { this._otelCollectorConfig = config; return this; diff --git a/src/v2/components/web-server/index.ts b/src/v2/components/web-server/index.ts index af88936..735e590 100644 --- a/src/v2/components/web-server/index.ts +++ b/src/v2/components/web-server/index.ts @@ -5,7 +5,7 @@ import { commonTags } from '../../../constants'; import { AcmCertificate } from '../../../components/acm-certificate'; import { EcsService } from '../ecs-service'; import { WebServerLoadBalancer } from './load-balancer'; -import { OtelCollector } from '../../otel/container'; +import { OtelCollector } from '../../otel'; const stack = pulumi.getStack(); const otelConfigVolume = { name: 'otel-config-efs-volume' }; @@ -56,7 +56,7 @@ export namespace WebServer { healthCheckPath?: pulumi.Input; initContainers?: pulumi.Input[]>; sidecarContainers?: pulumi.Input[]>; - otelCollectorConfig?: pulumi.Input; + otelCollectorConfig?: pulumi.Input; }; } @@ -278,7 +278,7 @@ export class WebServer extends pulumi.ComponentResource { }, { parent: this }); } - private createOtelCollector(config: string) { + private createOtelCollector(config: OtelCollector.Config) { return new OtelCollector({ containerName: `${this.name}-otel-collector`, serviceName: this.name, diff --git a/src/v2/index.ts b/src/v2/index.ts index 710acbf..8cf221c 100644 --- a/src/v2/index.ts +++ b/src/v2/index.ts @@ -4,5 +4,5 @@ export { WebServerBuilder } from './components/web-server/builder'; export { WebServerLoadBalancer } from './components/web-server/load-balancer'; import { OtelCollectorConfigBuilder } from './otel/config'; -import { OtelCollector } from './otel/container'; +import { OtelCollector } from './otel'; export const openTelemetry = { OtelCollector, OtelCollectorConfigBuilder }; diff --git a/src/v2/otel/config.ts b/src/v2/otel/config.ts index 89e02fe..ccaa1d0 100644 --- a/src/v2/otel/config.ts +++ b/src/v2/otel/config.ts @@ -1,105 +1,7 @@ -import * as yaml from 'yaml'; - -export namespace OtelCollectorConfigBuilder { - export type ReceiverType = 'otlp'; - export type ReceiverProtocol = 'http' | 'rpc'; - export type ReceiverConfig = { - protocols: { - [K in ReceiverProtocol]?: { - endpoint: string; - }; - }; - }; - export type Receiver = { - [K in ReceiverType]?: ReceiverConfig; - }; - - export type ProcessorType = 'batch' | 'memory_limiter'; - export type BatchProcessorConfig = { - send_batch_size: number; - send_batch_max_size: number; - timeout: string; - }; - export type MemoryLimiterProcessorConfig = { - check_interval: string; - limit_percentage: number; - spike_limit_percentage: number; - }; - export type Processor = { - batch?: BatchProcessorConfig; - memory_limiter?: MemoryLimiterProcessorConfig; - }; - - export type ExporterType = 'prometheusremotewrite' | 'awsxray' | 'debug'; - export type PrometheusRemoteWriteExporterConfig = { - namespace: string; - endpoint: string; - auth?: { - authenticator: string; - }; - }; - - export type AwsXRayExporterConfig = { - region: string; - }; - - export type DebugExportedConfig = { - verbosity: string; - }; - - export type Exporter = { - prometheusremotewrite?: PrometheusRemoteWriteExporterConfig; - awsxray?: AwsXRayExporterConfig; - debug?: DebugExportedConfig; - }; - - export type ExtensionType = 'sigv4auth' | 'health_check' | 'pprof'; - export type SigV4AuthExtensionConfig = { - region: string; - service: string; - }; - - export type HealthCheckExtensionConfig = { - endpoint: string; - }; - - export type PprofExtensionConfig = { - endpoint: string; - }; - - export type Extension = { - sigv4auth?: SigV4AuthExtensionConfig; - health_check?: HealthCheckExtensionConfig; - pprof?: PprofExtensionConfig; - }; - - export type PipelineConfig = { - receivers: ReceiverType[]; - processors: ProcessorType[]; - exporters: ExporterType[]; - }; - - export type TelemetryConfig = { - logs?: { - level: string; - }; - metrics?: { - level: string; - }; - }; - - export type Service = { - pipelines: { - metrics?: PipelineConfig; - traces?: PipelineConfig; - }; - extensions?: ExtensionType[]; - telemetry?: TelemetryConfig; - }; -} +import type { OtelCollector } from './index'; const OTLPReceiverProtocols = { - rpc: { + grpc: { endpoint: '0.0.0.0:4317' }, http: { @@ -108,16 +10,16 @@ const OTLPReceiverProtocols = { }; export class OtelCollectorConfigBuilder { - private readonly _receivers: OtelCollectorConfigBuilder.Receiver = {}; - private readonly _processors: OtelCollectorConfigBuilder.Processor = {}; - private readonly _exporters: OtelCollectorConfigBuilder.Exporter = {}; - private readonly _extensions: OtelCollectorConfigBuilder.Extension = {}; - private readonly _service: OtelCollectorConfigBuilder.Service = { + private readonly _receivers: OtelCollector.Receiver = {}; + private readonly _processors: OtelCollector.Processor = {}; + private readonly _exporters: OtelCollector.Exporter = {}; + private readonly _extensions: OtelCollector.Extension = {}; + private readonly _service: OtelCollector.Service = { pipelines: {} }; withOTLPReceiver( - protocols: OtelCollectorConfigBuilder.ReceiverProtocol[] = ['http'] + protocols: OtelCollector.ReceiverProtocol[] = ['http'] ): this { if (!protocols.length) { throw new Error('At least one OTLP receiver protocol should be provided'); @@ -152,9 +54,9 @@ export class OtelCollectorConfigBuilder { } withMemoryLimiterProcessor( - checkInterval = '5s', + checkInterval = '1s', limitPercentage = 80, - spikeLimitPercentage = 25 + spikeLimitPercentage = 15 ): this { this._processors.memory_limiter = { check_interval: checkInterval, @@ -241,9 +143,9 @@ export class OtelCollectorConfigBuilder { } withMetricsPipeline( - receivers: OtelCollectorConfigBuilder.ReceiverType[], - processors: OtelCollectorConfigBuilder.ProcessorType[], - exporters: OtelCollectorConfigBuilder.ExporterType[], + receivers: OtelCollector.ReceiverType[], + processors: OtelCollector.ProcessorType[], + exporters: OtelCollector.ExporterType[], ): this { this._service.pipelines.metrics = { receivers, @@ -255,9 +157,9 @@ export class OtelCollectorConfigBuilder { } withTracesPipeline( - receivers: OtelCollectorConfigBuilder.ReceiverType[], - processors: OtelCollectorConfigBuilder.ProcessorType[], - exporters: OtelCollectorConfigBuilder.ExporterType[], + receivers: OtelCollector.ReceiverType[], + processors: OtelCollector.ProcessorType[], + exporters: OtelCollector.ExporterType[], ): this { this._service.pipelines.traces = { receivers, @@ -274,8 +176,8 @@ export class OtelCollectorConfigBuilder { awsRegion: string ): this { return this.withOTLPReceiver(['http']) - .withBatchProcessor() .withMemoryLimiterProcessor() + .withBatchProcessor() .withAPS(prometheusNamespace, prometheusWriteEndpoint, awsRegion) .withAWSXRayExporter(awsRegion) .withHealthCheckExtension() @@ -292,26 +194,40 @@ export class OtelCollectorConfigBuilder { .withTelemetry(); } - build(): string { + build(): OtelCollector.Config { this.validatePipelineComponents('metrics'); this.validatePipelineComponents('traces'); + this.validatePipelineProcessorOrder('metrics'); + this.validatePipelineProcessorOrder('traces'); // FIX: Fix type inference const extensions = Object.keys( this._extensions - ) as OtelCollectorConfigBuilder.ExtensionType[]; + ) as OtelCollector.ExtensionType[]; if (extensions.length) { this._service.extensions = extensions; } // TODO: Add schema validation (non-empty receivers, non-empty receiver protocols) - return yaml.stringify({ + return { receivers: this._receivers, processors: this._processors, exporters: this._exporters, extensions: this._extensions, service: this._service - }); + }; + } + + private validatePipelineProcessorOrder(pipelineType: 'metrics' | 'traces'): void { + const pipeline = this._service.pipelines[pipelineType]; + if (!pipeline) return; + + const { processors } = pipeline; + const memoryLimiterIndex = processors + .findIndex(processor => processor === 'memory_limiter'); + if (memoryLimiterIndex > 0) { + throw new Error(`memory_limiter processor is not the first processor in the ${pipelineType} pipeline.`); + } } private validatePipelineComponents(pipelineType: 'metrics' | 'traces'): void { diff --git a/src/v2/otel/container.ts b/src/v2/otel/container.ts deleted file mode 100644 index 9afcc57..0000000 --- a/src/v2/otel/container.ts +++ /dev/null @@ -1,91 +0,0 @@ -import * as pulumi from '@pulumi/pulumi'; -import { EcsService } from '../components/ecs-service'; - -export namespace OtelCollector { - export type Args = { - containerName: pulumi.Input; - serviceName: pulumi.Input; - env: pulumi.Input; - config: pulumi.Input; - configVolumeName: pulumi.Input; - }; -} - -export class OtelCollector { - container: EcsService.Container; - configContainer: EcsService.Container; - - constructor({ - containerName, - serviceName, - env, - config, - configVolumeName, - }: OtelCollector.Args) { - this.configContainer = this.createConfigContainer( - config, - configVolumeName - ); - - this.container = this.createContainer( - containerName, - configVolumeName, - serviceName, - env - ); - } - - get containers(): EcsService.Container[] { - return [this.configContainer, this.container]; - } - - private createContainer( - containerName: pulumi.Input, - configVolumeName: pulumi.Input, - serviceName: pulumi.Input, - env: pulumi.Input - ): EcsService.Container { - return { - name: containerName, - image: 'otel/opentelemetry-collector-contrib:latest', - portMappings: [ - { containerPort: 4317, hostPort: 4317, protocol: 'tcp' }, - { containerPort: 4318, hostPort: 4318, protocol: 'tcp' }, - // TODO: Expose 8888 for collector telemetry - { containerPort: 13133, hostPort: 13133, protocol: 'tcp' }, - ], - mountPoints: [{ - sourceVolume: configVolumeName, - containerPath: '/etc/otelcol-contrib', - readOnly: true - }], - dependsOn: [{ - containerName: this.configContainer.name, - condition: 'COMPLETE' - }], - environment: [ - { name: 'OTEL_RESOURCE_ATTRIBUTES', value: `service.name=${serviceName},env=${env}` }, - { name: 'OTEL_LOG_LEVEL', value: 'debug' }, - { name: 'OTELCOL_CONTRIB_DEBUG', value: '1' } - ] - }; - } - - private createConfigContainer( - config: pulumi.Input, - volume: pulumi.Input - ): EcsService.Container { - return { - name: 'otel-config-writer', - image: 'amazonlinux:latest', - essential: false, - command: [ - 'sh', '-c', - pulumi.interpolate`echo '${config}' > /etc/otelcol-contrib/config.yaml`], - mountPoints: [{ - sourceVolume: volume, - containerPath: '/etc/otelcol-contrib', - }] - } - } -} diff --git a/src/v2/otel/index.ts b/src/v2/otel/index.ts new file mode 100644 index 0000000..61e2128 --- /dev/null +++ b/src/v2/otel/index.ts @@ -0,0 +1,224 @@ +import * as pulumi from '@pulumi/pulumi'; +import * as aws from '@pulumi/aws'; +import * as yaml from 'yaml'; +import { EcsService } from '../components/ecs-service'; + +export namespace OtelCollector { + export type ReceiverType = 'otlp'; + export type ReceiverProtocol = 'http' | 'grpc'; + export type ReceiverConfig = { + protocols: { + [K in ReceiverProtocol]?: { + endpoint: string; + }; + }; + }; + export type Receiver = { + [K in ReceiverType]?: ReceiverConfig; + }; + + export type ProcessorType = 'batch' | 'memory_limiter'; + export type BatchProcessorConfig = { + send_batch_size: number; + send_batch_max_size: number; + timeout: string; + }; + export type MemoryLimiterProcessorConfig = { + check_interval: string; + limit_percentage: number; + spike_limit_percentage: number; + }; + export type Processor = { + batch?: BatchProcessorConfig; + memory_limiter?: MemoryLimiterProcessorConfig; + }; + + export type ExporterType = 'prometheusremotewrite' | 'awsxray' | 'debug'; + export type PrometheusRemoteWriteExporterConfig = { + namespace: string; + endpoint: string; + auth?: { + authenticator: string; + }; + }; + + export type AwsXRayExporterConfig = { + region: string; + }; + + export type DebugExportedConfig = { + verbosity: string; + }; + + export type Exporter = { + prometheusremotewrite?: PrometheusRemoteWriteExporterConfig; + awsxray?: AwsXRayExporterConfig; + debug?: DebugExportedConfig; + }; + + export type ExtensionType = 'sigv4auth' | 'health_check' | 'pprof'; + export type SigV4AuthExtensionConfig = { + region: string; + service: string; + }; + + export type HealthCheckExtensionConfig = { + endpoint: string; + }; + + export type PprofExtensionConfig = { + endpoint: string; + }; + + export type Extension = { + sigv4auth?: SigV4AuthExtensionConfig; + health_check?: HealthCheckExtensionConfig; + pprof?: PprofExtensionConfig; + }; + + export type PipelineConfig = { + receivers: ReceiverType[]; + processors: ProcessorType[]; + exporters: ExporterType[]; + }; + + export type TelemetryConfig = { + logs?: { + level: string; + }; + metrics?: { + level: string; + }; + }; + + export type Service = { + pipelines: { + metrics?: PipelineConfig; + traces?: PipelineConfig; + }; + extensions?: ExtensionType[]; + telemetry?: TelemetryConfig; + }; + + export type Config = { + receivers: Receiver; + processors: Processor; + exporters: Exporter; + extensions: Extension; + service: Service; + } + + export type Args = { + containerName: pulumi.Input; + serviceName: pulumi.Input; + env: pulumi.Input; + config: pulumi.Input; + configVolumeName: pulumi.Input; + }; +} + +export class OtelCollector { + container: pulumi.Output; + configContainer: EcsService.Container; + + constructor({ + containerName, + serviceName, + env, + config, + configVolumeName, + }: OtelCollector.Args) { + this.configContainer = this.createConfigContainer( + pulumi.output(config), + configVolumeName + ); + + this.container = this.createContainer( + containerName, + pulumi.output(config), + configVolumeName, + serviceName, + env + ); + } + + private createContainer( + containerName: pulumi.Input, + config: pulumi.Input, + configVolumeName: pulumi.Input, + serviceName: pulumi.Input, + env: pulumi.Input + ): pulumi.Output { + return pulumi.all([ + containerName, + config, + configVolumeName, + serviceName, + env + ]).apply(([ + containerName, + config, + configVolumeName, + serviceName, + env + ]) => ({ + name: containerName, + image: 'otel/opentelemetry-collector-contrib:latest', + portMappings: this.getCollectorPortMappings(config), + mountPoints: [{ + sourceVolume: configVolumeName, + containerPath: '/etc/otelcol-contrib', + readOnly: true + }], + dependsOn: [{ + containerName: this.configContainer.name, + condition: 'COMPLETE' + }], + environment: this.getCollectorEnvironment(serviceName, env) + })); + } + + private getCollectorEnvironment( + serviceName: string, + env: string, + ): { name: string; value: string; }[] { + return [{ + name: 'OTEL_RESOURCE_ATTRIBUTES', + value: `service.name=${serviceName},env=${env}` + },]; + } + + private getCollectorPortMappings( + config: OtelCollector.Config + ): EcsService.Container['portMappings'] { + const hasOTLPGRpcReceiver = !!config.receivers.otlp?.protocols.grpc; + const hasOTLPHttpReceiver = !!config.receivers.otlp?.protocols.http; + const protocol: aws.ecs.Protocol = 'tcp'; + + return [ + ...(hasOTLPGRpcReceiver ? [{ containerPort: 4317, hostPort: 4317, protocol }] : []), + ...(hasOTLPHttpReceiver ? [{ containerPort: 4318, hostPort: 4318, protocol }] : []), + // TODO: Expose 8888 for collector telemetry + { containerPort: 13133, hostPort: 13133, protocol }, + ]; + } + + private createConfigContainer( + config: pulumi.Output, + volume: pulumi.Input + ): EcsService.Container { + return { + name: 'otel-config-writer', + image: 'amazonlinux:latest', + essential: false, + command: config.apply(config => [ + 'sh', '-c', + `echo '${yaml.stringify(config)}' > /etc/otelcol-contrib/config.yaml` + ]), + mountPoints: [{ + sourceVolume: volume, + containerPath: '/etc/otelcol-contrib', + }] + } + } +} diff --git a/tests/otel/index.test.ts b/tests/otel/index.test.ts index fc5e5aa..907add3 100644 --- a/tests/otel/index.test.ts +++ b/tests/otel/index.test.ts @@ -1,6 +1,5 @@ import { describe, it } from 'node:test'; import * as assert from 'node:assert'; -import * as yaml from 'yaml'; import { OtelCollectorConfigBuilder } from '../../src/v2/otel/config'; import { testOtelCollectorConfigBuilderValidation } from './validation.test'; @@ -8,17 +7,26 @@ const awsRegion = 'us-west-2'; const prometheusNamespace = 'test-namespace'; const prometheusWriteEndpoint = 'https://aps-workspaces.us-west-2.amazonaws.com/workspaces/ws-12345/api/v1/remote_write'; +const defaultMemoryLimiterConfig = { + check_interval: '1s', + limit_percentage: 80, + spike_limit_percentage: 15 +}; +const defaultBatchConfig = { + send_batch_size: 8192, + send_batch_max_size: 10000, + timeout: '5s' +}; + describe('OtelCollectorConfigBuilder', () => { it( 'should generate minimal configuration with OTLP receiver and debug exporter', () => { - const yamlOutput = new OtelCollectorConfigBuilder() + const result = new OtelCollectorConfigBuilder() .withOTLPReceiver(['http']) .withDebug() .build(); - const result = yaml.parse(yamlOutput); - const expected = { receivers: { otlp: { @@ -40,20 +48,15 @@ describe('OtelCollectorConfigBuilder', () => { ); it('should configure batch processor', () => { - const yamlOutput = new OtelCollectorConfigBuilder() + const result = new OtelCollectorConfigBuilder() .withBatchProcessor() .build(); - const result = yaml.parse(yamlOutput); const expected = { receivers: {}, processors: { - batch: { - send_batch_size: 8192, - send_batch_max_size: 10000, - timeout: '5s' - } + batch: defaultBatchConfig }, exporters: {}, extensions: {}, @@ -64,20 +67,15 @@ describe('OtelCollectorConfigBuilder', () => { }); it('should configure memory limiter processor', () => { - const yamlOutput = new OtelCollectorConfigBuilder() + const result = new OtelCollectorConfigBuilder() .withMemoryLimiterProcessor() .build(); - const result = yaml.parse(yamlOutput); const expected = { receivers: {}, processors: { - memory_limiter: { - check_interval: '5s', - limit_percentage: 80, - spike_limit_percentage: 25 - } + memory_limiter: defaultMemoryLimiterConfig }, exporters: {}, extensions: {}, @@ -88,11 +86,10 @@ describe('OtelCollectorConfigBuilder', () => { }); it('withBatchProcessor should use provided parameters', () => { - const yamlOutput = new OtelCollectorConfigBuilder() + const result = new OtelCollectorConfigBuilder() .withBatchProcessor(5000, 8000, '10s') .build(); - const result = yaml.parse(yamlOutput); assert.deepStrictEqual(result.processors.batch, { send_batch_size: 5000, send_batch_max_size: 8000, @@ -101,11 +98,10 @@ describe('OtelCollectorConfigBuilder', () => { }); it('withMemoryLimiterProcessor should use provided parameters', () => { - const yamlOutput = new OtelCollectorConfigBuilder() + const result = new OtelCollectorConfigBuilder() .withMemoryLimiterProcessor('3s', 70, 15) .build(); - const result = yaml.parse(yamlOutput); assert.deepStrictEqual(result.processors.memory_limiter, { check_interval: '3s', limit_percentage: 70, @@ -114,11 +110,10 @@ describe('OtelCollectorConfigBuilder', () => { }); it('should configure Amazon Prometheus Service (APS) exporter', () => { - const yamlOutput = new OtelCollectorConfigBuilder() + const result = new OtelCollectorConfigBuilder() .withAPS(prometheusNamespace, prometheusWriteEndpoint, awsRegion) .build(); - const result = yaml.parse(yamlOutput); const expected = { receivers: {}, processors: {}, @@ -145,11 +140,10 @@ describe('OtelCollectorConfigBuilder', () => { }); it('should configure AWS X-Ray exporter', () => { - const yamlOutput = new OtelCollectorConfigBuilder() + const result = new OtelCollectorConfigBuilder() .withAWSXRayExporter(awsRegion) .build(); - const result = yaml.parse(yamlOutput); const expected = { receivers: {}, processors: {}, @@ -164,11 +158,10 @@ describe('OtelCollectorConfigBuilder', () => { }); it('should configure SigV4Auth extension', () => { - const yamlOutput = new OtelCollectorConfigBuilder() + const result = new OtelCollectorConfigBuilder() .withSigV4AuthExtension(awsRegion) .build(); - const result = yaml.parse(yamlOutput); const expected = { receivers: {}, processors: {}, @@ -189,11 +182,10 @@ describe('OtelCollectorConfigBuilder', () => { }); it('should configure health check extension', () => { - const yamlOutput = new OtelCollectorConfigBuilder() + const result = new OtelCollectorConfigBuilder() .withHealthCheckExtension() .build(); - const result = yaml.parse(yamlOutput); const expected = { receivers: {}, processors: {}, @@ -211,11 +203,10 @@ describe('OtelCollectorConfigBuilder', () => { }); it('should configure pprof extension', () => { - const yamlOutput = new OtelCollectorConfigBuilder() + const result = new OtelCollectorConfigBuilder() .withPprofExtension() .build(); - const result = yaml.parse(yamlOutput); const expected = { receivers: {}, processors: {}, @@ -233,7 +224,7 @@ describe('OtelCollectorConfigBuilder', () => { }); it('should configure pipelines', () => { - const yamlOutput = new OtelCollectorConfigBuilder() + const result = new OtelCollectorConfigBuilder() .withOTLPReceiver(['http']) .withBatchProcessor() .withMemoryLimiterProcessor() @@ -251,7 +242,6 @@ describe('OtelCollectorConfigBuilder', () => { ) .build(); - const result = yaml.parse(yamlOutput); const expected = { receivers: { otlp: { @@ -261,16 +251,8 @@ describe('OtelCollectorConfigBuilder', () => { } }, processors: { - batch: { - send_batch_size: 8192, - send_batch_max_size: 10000, - timeout: '5s' - }, - memory_limiter: { - check_interval: '5s', - limit_percentage: 80, - spike_limit_percentage: 25 - } + batch: defaultBatchConfig, + memory_limiter: defaultMemoryLimiterConfig }, exporters: { awsxray: { region: awsRegion }, @@ -299,20 +281,18 @@ describe('OtelCollectorConfigBuilder', () => { it('withDebug should use provided verbosity', () => { const verbosity = 'basic'; - const yamlOutput = new OtelCollectorConfigBuilder() + const result = new OtelCollectorConfigBuilder() .withDebug(verbosity) .build(); - const result = yaml.parse(yamlOutput); - assert.strictEqual(result.exporters.debug.verbosity, verbosity); + assert.strictEqual(result.exporters.debug?.verbosity, verbosity); }); it('should generate default configuration', () => { - const yamlOutput = new OtelCollectorConfigBuilder() + const result = new OtelCollectorConfigBuilder() .withDefault(prometheusNamespace, prometheusWriteEndpoint, awsRegion) .build(); - const result = yaml.parse(yamlOutput); const expected = { receivers: { @@ -323,16 +303,8 @@ describe('OtelCollectorConfigBuilder', () => { } }, processors: { - batch: { - send_batch_size: 8192, - send_batch_max_size: 10000, - timeout: '5s' - }, - memory_limiter: { - check_interval: '5s', - limit_percentage: 80, - spike_limit_percentage: 25 - } + batch: defaultBatchConfig, + memory_limiter: defaultMemoryLimiterConfig }, exporters: { prometheusremotewrite: { @@ -374,7 +346,7 @@ describe('OtelCollectorConfigBuilder', () => { }); it('should generate complete configuration', () => { - const yamlOutput = new OtelCollectorConfigBuilder() + const result = new OtelCollectorConfigBuilder() .withOTLPReceiver(['http']) .withBatchProcessor() .withMemoryLimiterProcessor() @@ -396,7 +368,6 @@ describe('OtelCollectorConfigBuilder', () => { ) .build(); - const result = yaml.parse(yamlOutput); const expected = { receivers: { @@ -407,16 +378,8 @@ describe('OtelCollectorConfigBuilder', () => { } }, processors: { - batch: { - send_batch_size: 8192, - send_batch_max_size: 10000, - timeout: '5s' - }, - memory_limiter: { - check_interval: '5s', - limit_percentage: 80, - spike_limit_percentage: 25 - } + batch: defaultBatchConfig, + memory_limiter: defaultMemoryLimiterConfig }, exporters: { prometheusremotewrite: { diff --git a/tests/otel/validation.test.ts b/tests/otel/validation.test.ts index 64e2b55..1b1450b 100644 --- a/tests/otel/validation.test.ts +++ b/tests/otel/validation.test.ts @@ -97,4 +97,34 @@ export function testOtelCollectorConfigBuilderValidation() { message: "Exporter 'awsxray' is used in traces pipeline but not defined" }); }); + + it('should throw error when memory_limiter is not the first processor in traces pipeline ', () => { + const createInvalidConfig = () => new OtelCollectorConfigBuilder() + .withOTLPReceiver(['http']) + .withMemoryLimiterProcessor() + .withBatchProcessor() + .withDebug() + .withTracesPipeline(['otlp'], ['batch', 'memory_limiter'], ['debug']) + .build(); + + assert.throws(createInvalidConfig, { + name: 'Error', + message: 'memory_limiter processor is not the first processor in the traces pipeline.' + }); + }); + + it('should throw error when memory_limiter is not the first processor in metrics pipeline ', () => { + const createInvalidConfig = () => new OtelCollectorConfigBuilder() + .withOTLPReceiver(['http']) + .withMemoryLimiterProcessor() + .withBatchProcessor() + .withDebug() + .withMetricsPipeline(['otlp'], ['batch', 'memory_limiter'], ['debug']) + .build(); + + assert.throws(createInvalidConfig, { + name: 'Error', + message: 'memory_limiter processor is not the first processor in the metrics pipeline.' + }); + }); }