LoggingCwFactory.java

package com.cloudforgeci.api.observability;

import com.cloudforgeci.api.core.annotation.BaseFactory;
import com.cloudforge.core.annotation.DeploymentContext;
import com.cloudforge.core.annotation.SystemContext;
import com.cloudforgeci.api.core.util.RetentionDaysConverter;
import com.cloudforge.core.enums.RuntimeType;
import com.cloudforge.core.enums.SecurityProfile;
import software.amazon.awscdk.RemovalPolicy;
import software.amazon.awscdk.Stack;
import software.amazon.awscdk.services.iam.PolicyStatement;
import software.amazon.awscdk.services.iam.ServicePrincipal;
import software.amazon.awscdk.services.kms.Key;
import software.amazon.awscdk.services.logs.LogGroup;
import software.amazon.awscdk.services.logs.RetentionDays;
import software.constructs.Construct;

import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * CloudWatch Logging Factory using annotation-based context injection.
 * Configures CloudWatch log groups based on security profile settings.
 */
public class LoggingCwFactory extends BaseFactory {

    private static final Logger LOG = Logger.getLogger(LoggingCwFactory.class.getName());

    @SystemContext("security")
    private SecurityProfile security;

    @SystemContext("runtime")
    private RuntimeType runtime;

    @SystemContext("logs")
    private LogGroup logs;

    @SystemContext("stackName")
    private String stackName;

    @DeploymentContext("enableMonitoring")
    private Boolean enableMonitoring;

    @DeploymentContext("logRetentionDays")
    private Integer logRetentionDays;

    public LoggingCwFactory(Construct scope, String id) {
        super(scope, id);
    }

    @Override
    public void create() {
        try {
            // SecurityProfileConfiguration is now injected directly via annotation
            LOG.info("LoggingCwFactory: Starting create() method");
            LOG.info("LoggingCwFactory: ctx is null: " + (ctx == null));

            // Guard against null context - critical for operations
            if (ctx == null) {
                LOG.severe("LoggingCwFactory: SystemContext (ctx) is null!");
                throw new IllegalStateException("SystemContext is null - cannot configure logging");
            }

            if (security != null) {
                LOG.info("LoggingCwFactory: security = " + security);
            }
            LOG.info("LoggingCwFactory: config is null: " + (config == null));

            if (config == null) {
                LOG.severe("LoggingCwFactory: SecurityProfileConfiguration is null!");
                throw new IllegalStateException("SecurityProfileConfiguration is null");
            }

            LOG.info("LoggingCwFactory: About to check if logs are already configured");
            // Check if logs are already configured
            if (logs != null) {
                LOG.info("CloudWatch logs already configured, skipping");
                return;
            }

            LOG.info("LoggingCwFactory: About to create LogGroup");
            // Create log group with security profile-based settings
            String securityProfileName = (security != null) ? security.name().toLowerCase() : "unknown";
            String runtimeName = (runtime != null) ? runtime.name().toLowerCase() : "unknown";
            String logGroupName = "/aws/ecs/" + stackName + "/" + runtimeName + "/" + securityProfileName;
            LOG.info("LoggingCwFactory: Creating log group with name: " + logGroupName);

            // Use configurable log retention from DeploymentContext if monitoring is enabled
            RetentionDays retentionDays = config.getLogRetentionDays();
            if (Boolean.TRUE.equals(enableMonitoring) && logRetentionDays != null) {
                // Use RetentionDaysConverter for consistent retention mapping across all factories
                // This ensures compliance-aware thresholds (PCI-DSS, HIPAA, etc.) are properly handled
                retentionDays = RetentionDaysConverter.fromDays(logRetentionDays);
            }

            // Create log group with explicit name and removal policy
            // If RemovalPolicy is RETAIN, the log group will persist after stack deletion
            // To avoid conflicts on redeployment, we only set the logGroupName if removal policy is DESTROY
            software.amazon.awscdk.services.logs.LogGroup.Builder logGroupBuilder = LogGroup.Builder.create(this, "SecurityProfileLogs")
                    .retention(retentionDays)
                    .removalPolicy(config.getLogRemovalPolicy());

            // Only set explicit name if using DESTROY policy, otherwise let CDK generate unique name
            if (config.getLogRemovalPolicy() == software.amazon.awscdk.RemovalPolicy.DESTROY) {
                logGroupBuilder.logGroupName(logGroupName);
            }

            // Add KMS encryption when required by compliance frameworks (PCI-DSS, HIPAA, SOC2)
            if (config.isCloudWatchLogsKmsEncryptionEnabled()) {
                LOG.info("LoggingCwFactory: Enabling KMS encryption for CloudWatch Logs (compliance requirement)");
                Key logsKmsKey = Key.Builder.create(this, "LogsKmsKey")
                        .description("KMS key for CloudWatch Logs encryption (compliance requirement)")
                        .enableKeyRotation(true)
                        .removalPolicy(config.getLogRemovalPolicy() == RemovalPolicy.RETAIN
                                ? RemovalPolicy.RETAIN : RemovalPolicy.DESTROY)
                        .build();

                // Grant CloudWatch Logs service permission to use the KMS key
                logsKmsKey.addToResourcePolicy(PolicyStatement.Builder.create()
                        .sid("Allow CloudWatch Logs")
                        .principals(List.of(new ServicePrincipal("logs." + Stack.of(this).getRegion() + ".amazonaws.com")))
                        .actions(List.of("kms:Encrypt", "kms:Decrypt", "kms:ReEncrypt*", "kms:GenerateDataKey*", "kms:CreateGrant", "kms:DescribeKey"))
                        .resources(List.of("*"))
                        .build());

                logGroupBuilder.encryptionKey(logsKmsKey);
            }

            LogGroup logGroup = logGroupBuilder.build();

            LOG.info("LoggingCwFactory: Configured log group: " + logGroupName +
                     " with retention = " + retentionDays +
                     ", removal = " + config.getLogRemovalPolicy());

            LOG.info("LoggingCwFactory: About to set logs in context");
            ctx.logs.set(logGroup);

            LOG.info("CloudWatch logs configured for " + security + " profile: " +
                    "retention = " + config.getLogRetentionDays() +
                    ", removal = " + config.getLogRemovalPolicy());
        } catch (Exception e) {
            LOG.log(Level.SEVERE, "LoggingCwFactory: Exception in create() method", e);
            throw e;
        }
    }

}