How to configure AWS CDK ApplicationLoadBalancedFargateService to log parsed JSON lines with Firelens and Firebit

2.7k views Asked by At

When I create an ApplicationLoadBalancedFargateService with a Firelens logdriver, and the application writes JSON lines as the log message, such as when using net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder with Logback, the log messages are displayed in my logging repository (ex. Sumo Logic), as an escaped string, like:

enter image description here

How can I get the log messages to save as parsed JSON?

1

There are 1 answers

3
Richard Collette On BEST ANSWER

After scanning CDK source code, browsing several related references (which I will provide links for to help direct appropriate traffic here), and using cdk diff until the only change was to enable json parsing, I was able to make is work as shown in the following code. The key here is the use of the addFirelensLogRouter method and the Firelens config contained therein.

TaskDefinition code does not automatically create a LogRouter container, if the task definition already contains one, which is what allows us to override the default behavior.

protected _createFargateService() {
    const logDriver = LogDrivers.firelens({
        options: {
            Name: 'http',
            Host: this._props.containerLogging.endpoint,
            URI: this._props.containerLogging.uri,
            Port: '443',
            tls: 'on',
            'tls.verify': 'off',
            Format: 'json_lines'
        }
    });
    const fargateService = new ApplicationLoadBalancedFargateService(this, this._props.serviceName, {
        cluster: this._accountEnvironmentLookups.getComputeCluster(),
        cpu: this._props.cpu, // Default is 256
        desiredCount: this._props.desiredCount, // Default is 1
        taskImageOptions: {
            image: ContainerImage.fromEcrRepository(this._props.serviceRepository, this._props.imageVersion),
            environment: this._props.environment,
            containerPort: this._props.containerPort,
            logDriver
        },
        memoryLimitMiB: this._props.memoryLimitMiB, // Default is 512
        publicLoadBalancer: this._props.publicLoadBalancer, // Default is false
        domainName: this._props.domainName,
        domainZone: !!this._props.hostedZoneDomain ? HostedZone.fromLookup(this, 'ZoneFromLookup', {
            domainName: this._props.hostedZoneDomain
        }) : undefined,
        certificate: !!this._props.certificateArn ? Certificate.fromCertificateArn(this, 'CertificateFromArn', this._props.certificateArn) : undefined,
        serviceName: `${this._props.accountShortName}-${this._props.deploymentEnvironment}-${this._props.serviceName}`,
        // The new ARN and resource ID format must be enabled to work with ECS managed tags.
        //enableECSManagedTags: true,
        //propagateTags: PropagatedTagSource.SERVICE,
        // CloudMap properties cannot be set from a stack separate from the stack where the cluster is created.
        // see https://github.com/aws/aws-cdk/issues/7825
    });
    if (this._props.logMessagesAreJsonLines) {
        // The default log driver setup doesn't enable json line parsing.
        const firelensLogRouter = fargateService.service.taskDefinition.addFirelensLogRouter('log-router', {
            // Figured out how get the default fluent bit ECR image from here https://github.com/aws/aws-cdk/blob/60c782fe173449ebf912f509de7db6df89985915/packages/%40aws-cdk/aws-ecs/lib/base/task-definition.ts#L509
            image: obtainDefaultFluentBitECRImage(fargateService.service.taskDefinition, fargateService.service.taskDefinition.defaultContainer?.logDriverConfig),
            essential: true,
            firelensConfig: {
                type: FirelensLogRouterType.FLUENTBIT,
                options: {
                    enableECSLogMetadata: true,
                    configFileType: FirelensConfigFileType.FILE,
                    // This enables parsing of log messages that are json lines
                    configFileValue: '/fluent-bit/configs/parse-json.conf'
                }
            },
            memoryReservationMiB: 50,
            logging: new AwsLogDriver({streamPrefix: 'firelens'})
        });
        firelensLogRouter.logDriverConfig;
    }
    fargateService.targetGroup.configureHealthCheck({
        path: this._props.healthUrlPath,
        port: this._props.containerPort.toString(),
        interval: Duration.seconds(120),
        unhealthyThresholdCount: 5
    });
    const scalableTaskCount = fargateService.service.autoScaleTaskCount({
        minCapacity: this._props.desiredCount,
        maxCapacity: this._props.maxCapacity
    });
    scalableTaskCount.scaleOnCpuUtilization(`ScaleOnCpuUtilization${this._props.cpuTargetUtilization}`, {
        targetUtilizationPercent: this._props.cpuTargetUtilization
    });
    scalableTaskCount.scaleOnMemoryUtilization(`ScaleOnMemoryUtilization${this._props.memoryTargetUtilization}`, {
        targetUtilizationPercent: this._props.memoryTargetUtilization
    });
    this.fargateService = fargateService;
}

Resources: