Skip to main content

Logging

Fluent Bit DaemonSet is used to collect all container logs and send them to CloudWatch Logs. Structured JSON log format and trace ID integration enable efficient log analysis.

Architecture

Fluent Bit Configuration

DaemonSet Deployment

apiVersion: apps/v1
kind: DaemonSet
metadata:
name: fluent-bit
namespace: amazon-cloudwatch
spec:
template:
spec:
serviceAccountName: fluent-bit # IRSA
tolerations:
- key: node-role.kubernetes.io/control-plane
effect: NoSchedule
- key: node-pool
operator: Exists
effect: NoSchedule
containers:
- name: fluent-bit
image: public.ecr.aws/aws-observability/aws-for-fluent-bit:stable
env:
- name: AWS_REGION
valueFrom:
configMapKeyRef:
name: cluster-info
key: region
- name: CLUSTER_NAME
valueFrom:
configMapKeyRef:
name: cluster-info
key: cluster-name
- name: HOST_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 200m
memory: 256Mi
volumeMounts:
- name: varlog
mountPath: /var/log
readOnly: true
- name: varlibdockercontainers
mountPath: /var/lib/docker/containers
readOnly: true

Fluent Bit Configuration

[SERVICE]
Flush 5
Log_Level info
Daemon off
Parsers_File parsers.conf
HTTP_Server On
HTTP_Listen 0.0.0.0
HTTP_Port 2020

[INPUT]
Name tail
Tag kube.*
Path /var/log/containers/*.log
Parser docker
DB /var/log/flb_kube.db
Mem_Buf_Limit 50MB
Skip_Long_Lines On
Refresh_Interval 10

[FILTER]
Name kubernetes
Match kube.*
Kube_URL https://kubernetes.default.svc:443
Kube_CA_File /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
Kube_Token_File /var/run/secrets/kubernetes.io/serviceaccount/token
Merge_Log On
Merge_Log_Key log_processed
K8S-Logging.Parser On
K8S-Logging.Exclude Off
Labels On
Annotations Off

[FILTER]
Name modify
Match kube.*
Add cluster ${CLUSTER_NAME}
Add region ${AWS_REGION}

[OUTPUT]
Name cloudwatch_logs
Match kube.*
region ${AWS_REGION}
log_group_name /eks/${CLUSTER_NAME}/containers
log_stream_prefix ${HOST_NAME}-
auto_create_group true
log_format json/emf
retry_limit 2

Standard Log Format

All services should output logs in the following JSON format:

{
"timestamp": "2026-03-15T10:30:45.123Z",
"level": "INFO",
"service": "order-service",
"region": "us-east-1",
"trace_id": "0af7651916cd43dd8448eb211c80319c",
"span_id": "b7ad6b7169203331",
"message": "Order created successfully",
"order_id": "ORD-123456",
"user_id": "a0000001-0000-0000-0000-000000000001",
"amount": 159000,
"duration_ms": 45
}

Required Fields

FieldTypeDescription
timestampstring (ISO 8601)Log occurrence time
levelstringDEBUG, INFO, WARN, ERROR, FATAL
servicestringService name
regionstringAWS region
messagestringLog message
FieldTypeDescription
trace_idstringOpenTelemetry trace ID
span_idstringOpenTelemetry span ID
user_idstringUser ID
request_idstringRequest ID
duration_msnumberProcessing time (milliseconds)

Language-specific Logging Implementation

Go (zerolog)

import (
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"go.opentelemetry.io/otel/trace"
)

func init() {
zerolog.TimeFieldFormat = time.RFC3339Nano

log.Logger = zerolog.New(os.Stdout).With().
Str("service", "order-service").
Str("region", os.Getenv("AWS_REGION")).
Timestamp().
Logger()
}

// Logging with trace context
func LogWithTrace(ctx context.Context) zerolog.Logger {
span := trace.SpanFromContext(ctx)
if span.SpanContext().IsValid() {
return log.With().
Str("trace_id", span.SpanContext().TraceID().String()).
Str("span_id", span.SpanContext().SpanID().String()).
Logger()
}
return log.Logger
}

// Usage example
func CreateOrder(ctx context.Context, order *Order) error {
logger := LogWithTrace(ctx)

logger.Info().
Str("order_id", order.ID).
Str("user_id", order.UserID).
Int64("amount", order.Amount).
Msg("Order creation started")

// Processing logic...

logger.Info().
Str("order_id", order.ID).
Int64("duration_ms", elapsed.Milliseconds()).
Msg("Order created successfully")

return nil
}

Java (Logback + Logstash Encoder)

<!-- logback-spring.xml -->
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<customFields>
{"service":"payment-service","region":"${AWS_REGION:-unknown}"}
</customFields>
<provider class="net.logstash.logback.composite.loggingevent.LoggingEventPatternJsonProvider">
<pattern>
{
"trace_id": "%mdc{traceId}",
"span_id": "%mdc{spanId}"
}
</pattern>
</provider>
</encoder>
</appender>

<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
</configuration>
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;

@Service
public class PaymentService {
private static final Logger log = LoggerFactory.getLogger(PaymentService.class);

public PaymentResult processPayment(PaymentRequest request) {
// Set additional context in MDC
MDC.put("order_id", request.getOrderId());
MDC.put("user_id", request.getUserId());

try {
log.info("Payment processing started - Amount: {}", request.getAmount());

// Payment logic...

log.info("Payment processing completed - Transaction: {}", result.getTransactionId());
return result;

} catch (Exception e) {
log.error("Payment processing failed: {}", e.getMessage(), e);
throw e;
} finally {
MDC.clear();
}
}
}

Python (structlog)

import structlog
import os
from opentelemetry import trace

def configure_logging():
structlog.configure(
processors=[
structlog.stdlib.filter_by_level,
structlog.stdlib.add_logger_name,
structlog.stdlib.add_log_level,
structlog.stdlib.PositionalArgumentsFormatter(),
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.StackInfoRenderer(),
structlog.processors.format_exc_info,
structlog.processors.UnicodeDecoder(),
structlog.processors.JSONRenderer()
],
context_class=dict,
logger_factory=structlog.stdlib.LoggerFactory(),
wrapper_class=structlog.stdlib.BoundLogger,
cache_logger_on_first_use=True,
)

def get_logger(name: str):
logger = structlog.get_logger(name)
return logger.bind(
service="recommendation-service",
region=os.getenv("AWS_REGION", "unknown")
)

# Add trace context
def log_with_trace(logger, **kwargs):
span = trace.get_current_span()
if span.get_span_context().is_valid:
kwargs["trace_id"] = format(span.get_span_context().trace_id, "032x")
kwargs["span_id"] = format(span.get_span_context().span_id, "016x")
return logger.bind(**kwargs)

# Usage example
logger = get_logger(__name__)

async def get_recommendations(user_id: str):
ctx_logger = log_with_trace(logger, user_id=user_id)

ctx_logger.info("Recommendation generation started")

# Recommendation logic...

ctx_logger.info(
"Recommendation generation completed",
item_count=len(recommendations),
duration_ms=elapsed_ms
)

return recommendations

Log Level Guidelines

LevelPurposeExamples
DEBUGDetailed info for development/debuggingVariable values, SQL queries
INFONormal operation recordsRequest start/complete, state changes
WARNPotential issuesRetries, fallback usage
ERRORErrors occurred (recoverable)API call failures, validation failures
FATALSevere errors (unrecoverable)Service startup failure

CloudWatch Logs Structure

Log Groups

Log groups managed by Terraform:

# Log groups by namespace
/eks/multi-region-mall/core-services
/eks/multi-region-mall/user-services
/eks/multi-region-mall/fulfillment
/eks/multi-region-mall/business-services
/eks/multi-region-mall/platform

# Retention period: 90 days

Log Streams

{node-name}-{namespace}_{pod-name}_{container-name}-{container-id}

Example:

ip-10-0-1-123-core-services_order-service-abc123_order-service-def456

CloudWatch Logs Insights Queries

Search Error Logs

fields @timestamp, @message, service, trace_id, error
| filter level = "ERROR"
| sort @timestamp desc
| limit 100

Track Specific Order

fields @timestamp, @message, service, level
| filter order_id = "ORD-123456"
| sort @timestamp asc

Slow Request Analysis

fields @timestamp, service, message, duration_ms
| filter duration_ms > 1000
| stats avg(duration_ms) as avg_duration,
max(duration_ms) as max_duration,
count(*) as request_count
by service
| sort avg_duration desc

Connect Logs by Trace ID

fields @timestamp, @message, service, level, span_id
| filter trace_id = "0af7651916cd43dd8448eb211c80319c"
| sort @timestamp asc

Error Rate by Service

fields service, level
| stats count(*) as total,
sum(case when level = "ERROR" then 1 else 0 end) as errors
by service
| display service, errors, total, (errors * 100.0 / total) as error_rate
| sort error_rate desc

User Activity

fields @timestamp, service, message, user_id
| filter user_id = "a0000001-0000-0000-0000-000000000001"
| sort @timestamp desc
| limit 50

Connecting Logs and Traces

Logs and traces can be queried together in Grafana:

Grafana Data Source Connection Configuration

# Tempo data source
jsonData:
tracesToLogsV2:
datasourceUid: cloudwatch
filterByTraceID: true
filterBySpanID: true
customQuery: true
query: |
fields @timestamp, @message, service, level
| filter trace_id = "${__trace.traceId}"
| sort @timestamp asc

Sensitive Data Masking

Mask sensitive information to prevent it from appearing in logs:

Go

func maskSensitiveData(data string) string {
// Mask credit card numbers
cardRegex := regexp.MustCompile(`\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}`)
data = cardRegex.ReplaceAllString(data, "****-****-****-****")

// Mask email addresses
emailRegex := regexp.MustCompile(`[\w.-]+@[\w.-]+\.\w+`)
data = emailRegex.ReplaceAllStringFunc(data, func(email string) string {
parts := strings.Split(email, "@")
return parts[0][:2] + "***@" + parts[1]
})

return data
}

Python

import re

class SensitiveDataFilter:
PATTERNS = [
(re.compile(r'\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}'), '****-****-****-****'),
(re.compile(r'[\w.-]+@[\w.-]+\.\w+'), lambda m: m.group()[:2] + '***@***'),
(re.compile(r'password["\s:=]+["\']?\w+["\']?', re.I), 'password=***'),
]

@classmethod
def filter(cls, message: str) -> str:
for pattern, replacement in cls.PATTERNS:
if callable(replacement):
message = pattern.sub(replacement, message)
else:
message = pattern.sub(replacement, message)
return message

Troubleshooting

When Logs Are Not Collected

# 1. Check Fluent Bit status
kubectl get pods -n amazon-cloudwatch -l app=fluent-bit

# 2. Check Fluent Bit logs
kubectl logs -n amazon-cloudwatch -l app=fluent-bit --tail=100

# 3. Check CloudWatch log groups
aws logs describe-log-groups --log-group-name-prefix "/eks/multi-region-mall"

# 4. Check container logs directly
kubectl logs <pod-name> -n <namespace> --tail=50

Log Format Validation

# Verify Pod logs are in JSON format
kubectl logs <pod-name> | head -5 | jq .

# Verify required fields
kubectl logs <pod-name> | head -1 | jq 'has("timestamp", "level", "service", "message")'