2026-07-14 · 8 min
Distributed Tracing OTEL di Microservices BCA-like: Lessons
Setahun lalu saya jadi consultant arsitektur untuk implementasi distributed tracing di bank besar Indonesia (tidak BCA exact, tapi kelasnya mirip — top 10 bank umum, 84 microservice di domain core banking + digital channel, ~28k span/detik aggregate). Saya tidak akan share nama, tapi share angka, pattern, dan lessons yang transferable.
Konteks (digeneralisasi untuk privacy)
- Industri: bank umum top-10 Indonesia.
- Skala: 84 microservice, tim 240 engineer (15 squad), 4 datacenter Jabodetabek.
- Workload: core banking (account, transaction, loan, deposit) + digital channel (mobile banking, internet banking, API for partners).
- Compliance: BI (Bank Indonesia) regulation, ISO 27001, PCI-DSS Level 1, SOC 2 Type 2.
- Throughput: peak 4.2k TPS aggregate di payment flow, 28k span/detik trace volume.
- Existing observability: AppDynamics (kontrak akan habis), Splunk untuk log, custom dashboard untuk metric.
Pain point sebelum project:
- Vendor lock-in AppDynamics: instrumentation specific, sulit migrate.
- Trace incomplete cross-service: payment flow hit 12+ service, trace pecah di service boundary lama (mostly Java legacy + beberapa Go newer).
- MTTR tinggi: incident “transaksi gagal di tahap X” butuh manual correlation 30-90 menit.
- PII di trace: ada NIK, account number, NPWP keluar di trace attribute (audit flag).
Strategi: OTel sebagai standard, hybrid backend
Saya tidak rekomendasi langsung kill AppDynamics. Strategi:
- OTel SDK di semua service (standardize instrumentation).
- Dual-export ke AppDynamics (legacy backend, masih ada license) + Tempo self-host (target backend).
- Validate parity 6 bulan, lalu sunset AppDynamics.
- Compliance review PII scrubbing sebelum production.
Total estimated timeline: 18 bulan.
Architecture OTel di bank ini
[84 microservices]
↓ OTLP/gRPC
[OTel Collector Agent (DaemonSet per node, 240 node)]
↓
[OTel Collector Gateway (HPA, 12 instance avg)]
├─ PII scrubbing (regex + ML-based)
├─ Tail sampling (12% hit ratio)
├─ Export to Tempo (S3 backend)
└─ Dual-export to AppDynamics (during migration)
[Tempo distributor → ingester → compactor]
↓
[S3-compatible (MinIO on-prem)]
↓ query
[Grafana Tempo UI]
Why on-prem MinIO bukan AWS S3?
Bank Indonesia regulation: data residency. Trace data berisi info transaksi (meskipun PII di-scrub, masih sensitive). Harus on-prem DC Indonesia.
MinIO cluster: 6 node, 4 disk × 16TB = 384TB raw, ~256TB usable dengan erasure coding EC:4+2.
PII scrubbing critical
Multi-layer:
Layer 1: SDK level (per-service)
@Configuration
public class OtelConfig {
@Bean
public OpenTelemetry openTelemetry() {
SpanProcessor scrubProcessor = new PiiScrubSpanProcessor();
SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
.addSpanProcessor(scrubProcessor)
.addSpanProcessor(BatchSpanProcessor.builder(otlpExporter).build())
.build();
return OpenTelemetrySdk.builder()
.setTracerProvider(tracerProvider)
.setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance()))
.build();
}
}
public class PiiScrubSpanProcessor implements SpanProcessor {
// NIK: 16 digit, NPWP: 15-16 digit dengan format, account: 10-13 digit
private static final Pattern NIK = Pattern.compile("\\b\\d{16}\\b");
private static final Pattern NPWP = Pattern.compile("\\b\\d{2}\\.\\d{3}\\.\\d{3}\\.\\d-\\d{3}\\.\\d{3}\\b");
private static final Pattern ACCOUNT = Pattern.compile("\\b\\d{10,13}\\b");
@Override
public void onEnd(ReadableSpan span) {
// Mutate before export
span.getAttributes().forEach((key, value) -> {
if (value instanceof String s) {
String scrubbed = scrub(s);
if (!scrubbed.equals(s)) {
((ReadWriteSpan) span).setAttribute(key, scrubbed);
}
}
});
}
private String scrub(String input) {
input = NIK.matcher(input).replaceAll("[NIK_REDACTED]");
input = NPWP.matcher(input).replaceAll("[NPWP_REDACTED]");
input = ACCOUNT.matcher(input).replaceAll(m -> {
String s = m.group();
return s.substring(0, 4) + "***" + s.substring(s.length() - 3);
});
return input;
}
}
Layer 2: OTel Collector (defense in depth)
processors:
redaction:
allow_all_keys: true
blocked_values:
- "[1-9]\\d{15}" # NIK pattern
- "\\d{2}\\.\\d{3}\\.\\d{3}\\.\\d-\\d{3}\\.\\d{3}" # NPWP
summary: silent
Layer 3: audit script harian
Cron daily yang query Tempo dengan regex match PII pattern. Kalau ditemukan: incident report ke compliance team.
#!/bin/bash
# audit-pii-traces.sh
# Run daily 03:00
COUNT=$(tempo-cli query --search 'attribute=~"\\b\\d{16}\\b"' --since=24h | wc -l)
if [ "$COUNT" -gt 0 ]; then
curl -X POST $COMPLIANCE_WEBHOOK -d "{\"alert\":\"PII detected in traces\",\"count\":$COUNT}"
fi
Tail-based sampling tuning
Default 100% sampling = ~28k span/detik × storage cost prohibitive. Tail sampling:
processors:
tail_sampling:
decision_wait: 60s
num_traces: 200000
expected_new_traces_per_sec: 5000
policies:
- name: error-policy
type: status_code
status_code: {status_codes: [ERROR]}
- name: slow-policy
type: latency
latency: {threshold_ms: 800}
- name: high-value-flow
type: string_attribute
string_attribute:
key: business.flow
values: ["payment", "transfer", "loan-disbursement"]
- name: rare-endpoint
type: probabilistic
probabilistic: {sampling_percentage: 25}
and_sub_policy:
name: rare-tag
type: string_attribute
string_attribute: {key: endpoint.popularity, values: ["rare"]}
- name: baseline-probabilistic
type: probabilistic
probabilistic: {sampling_percentage: 5}
Effective sampling rate:
- Error trace: 100%
- Slow trace (>800ms): 100%
- High-value business flow (payment, transfer, loan): 100%
- Rare endpoint: 25%
- Everything else: 5%
Aggregate observed: 12% trace persisted to storage. ~3.36k span/detik sustained = ~290M span/day = ~110GB compressed/day = ~3.3TB/month.
30-day retention: ~10TB storage. Fit di MinIO cluster comfortably.
Cross-service context propagation
W3C Trace Context standard. Tapi banyak service Java legacy pakai traceparent header lama atau custom format.
Solusi: OTel Collector punya Receivers yang bisa parse multiple format dan normalize ke W3C.
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
zipkin:
endpoint: 0.0.0.0:9411 # untuk service yang masih Zipkin format
jaeger:
protocols:
thrift_http:
endpoint: 0.0.0.0:14268 # legacy Jaeger
Plus per-service: SDK pakai composite propagator yang try multiple:
.setPropagators(ContextPropagators.create(
TextMapPropagator.composite(
W3CTraceContextPropagator.getInstance(),
B3Propagator.injectingMultiHeaders()
)
))
Hasil 12 bulan
| Metric | Sebelum (AppDynamics) | Sesudah (OTel + Tempo) |
|---|---|---|
| Trace coverage (cross-service complete) | 62% | 96% |
| MTTR (incident detection-to-resolution) | 42 menit avg | 12 menit avg |
| Cost per month | $14k (AppDynamics) | $1.2k (Tempo + MinIO) |
| PII leak incident | 3 (in last 12 mo before) | 0 |
| Vendor lock-in | high | low |
| Tim onboard time (new service to traced) | 2 minggu | 1 hari |
MTTR turun 71% karena: lebih banyak trace context (sampling lebih agresif untuk error), Grafana correlate trace ↔ log ↔ metric unified UI mengurangi context switch.
Use case real: payment flow incident
Insiden bulan ke-7: customer report transfer “Lambat” untuk amount > Rp 100 juta selama 2 jam.
Workflow investigation:
- Alert fire:
payment.latency.p99 > 5s for amount > 100M. - Click trace_id sample yang trigger alert → buka di Tempo.
- Trace span hierarchy:
payment-bff(320ms) →account-service(180ms) →ledger-service(140ms) →aml-screening-service(4.2 detik ← bottleneck!)
- Drill ke
aml-screening-servicespan detail:- Child span
external-call.dukcapil-api: 4.1 detik.
- Child span
- Issue: integrasi Dukcapil API untuk enhanced screening high-amount lambat. Vendor partner kena DDoS pagi tadi.
- Mitigation: temporary fallback ke screening internal saja untuk amount < Rp 500 juta.
Total time-to-root-cause: 14 menit. Sebelum OTel: estimasi 90 menit (manual log correlation).
Yang break
1. AppDynamics dual-export overhead
Initial setup dual-export ke AppDynamics + Tempo bikin SDK overhead 2x. CPU service-service naik ~3-5%.
Fix: dual-export di OTel Collector level, bukan SDK level. SDK push ke Collector once, Collector fan-out ke 2 destination.
2. Sampling decision lag spike
decision_wait: 60s window. Saat traffic spike, Collector buffer overflow, drop trace.
Fix:
- Scale up Collector HPA berdasarkan queue depth metric (bukan cuma CPU).
- Increase
num_traces: 500000(lebih besar in-memory buffer, lebih banyak memory).
Collector resource per pod jadi 4 CPU + 16GB. 12 pod = 48 CPU, 192GB. Acceptable.
3. JVM agent + Spring Boot 2.x compatibility
Beberapa service legacy Spring Boot 2.7 dengan Java 11. OTel agent latest support Java 11 tapi ada quirk di reflection di Spring Boot 2.7 (security manager).
Fix: upgrade service-service Java 17 dulu sebelum enable OTel, atau pakai OTel agent versi LTS (1.32.x) yang compatible dengan Java 11.
4. Tempo compactor lag
Bulan ke-9, compactor mulai lag. Trace search untuk time range > 2 hari slow karena banyak block yang belum compacted.
Fix:
- Scale compactor dari 2 ke 4 replica.
- Tune
compaction.compacted_block_retentiondancompaction.block_retention.
5. Cardinality lagi (selalu cardinality)
Salah satu service set span attribute request.body_hash. Unique per request. Cardinality di Tempo index explode, query slow.
Fix: review semua high-cardinality attribute, mark sebagai “trace_only” (tidak di-index). Tempo support trace_qualifier_attribute config untuk separate indexed vs unindexed attribute.
Pelajaran untuk implementasi bank-scale
1. Compliance team adalah engineering team paling penting di project ini
PII scrubbing bukan afterthought. Saya spend 6 minggu diskusi dengan compliance team untuk define what’s PII, what’s quasi-identifier (cuma sebagian PII tapi sensitive saat combine), how to audit.
2. Dual-stack period tidak negotiable
Tidak ada “big bang switch” dari AppDynamics ke OTel. 6 bulan minimum dual-stack untuk parity validation. Cost extra periode itu adalah insurance.
3. SDK overhead matter di hot path
Service core banking (account, ledger) sensitif terhadap latency. SDK overhead 1ms per span = significant di flow dengan 12 hop. Saya tune sampling decision lebih agresif di SDK level untuk service ini (5% baseline, 100% error).
4. Operational ownership clarity
Trace data baru = pertanyaan baru. Siapa investigate kalau trace incomplete? Siapa tune sampling? Siapa audit PII? Saya define RACI matrix sebelum production. Tanpa itu, trace data jadi “punya everyone, dimiliki no one”.
Verdict
OpenTelemetry di bank Indonesia: feasible, dengan disiplin PII scrubbing + compliance integration dari awal. Self-host backend cost 10-12x lebih murah dari vendor APM komersial dengan compliance posture lebih baik (data residency on-prem).
Tapi: butuh tim observability dedicated (3-4 FTE), bukan side-project. Untuk bank kecil (BPR) atau institusi non-tier-1: vendor managed mungkin lebih cocok.
Bukan magic. Implementasi 18 bulan, 240 engineer affected, 4 compliance review formal. Worth, tapi mata harus terbuka.
Lihat juga OpenTelemetry di SaaS Jakarta untuk konteks SMB-scale OTel deployment.
Ditulis oleh Reza Pradipta