Membangun Sistem Observability Terdistribusi dengan OpenTelemetry & Prometheus
1. Konteks Industri & Analisis Akar Masalah
Dalam lanskap arsitektur sistem terdistribusi modern, transisi dari monolitik ke mikroservis telah memecah aplikasi monolitik menjadi ratusan komponen independen yang berkomunikasi melalui jaringan. Meskipun pendekatan ini menawarkan skalabilitas horizontal, ketahanan, dan kebebasan teknologi yang lebih besar, ia memunculkan tantangan baru yang kompleks dalam hal visibilitas operasional. Ketika sebuah permintaan HTTP masuk ke sistem, permintaannya dapat melewati Nginx reverse proxy, diteruskan ke API Gateway, divalidasi oleh layanan autentikasi berbasis Go, memanggil layanan pengguna berbasis Node.js, melakukan query ke cluster Redis, dan akhirnya mengambil data dari database MySQL yang dipartisi.
Di dunia nyata, ketika terjadi latensi tinggi atau kegagalan sistem (seperti HTTP 500 Internal Server Error yang sporadis), proses debugging menjadi mimpi buruk bagi SRE dan Platform Engineer. Tanpa sistem observability yang terpadu, tim sering kali bergantung pada metode konvensional: menebak-nebak, melakukan tailing log secara manual pada puluhan file log yang terfragmentasi, dan menggunakan metrik infrastruktur yang terisolasi tanpa konteks bisnis. Bottleneck performa sering kali tersembunyi di balik komunikasi jaringan antar-layanan, antrean pesan yang menumpuk di RabbitMQ atau Redis, atau query database yang tidak terindeks yang lolos dari tahap pengujian lokal.
Sebelum penerapan observability terdistribusi berbasis OpenTelemetry dan Prometheus, kondisi operasional ditandai oleh 'Three Pillars of Observability' yang terkotak-kotak (siloed metrics, logs, dan traces). Engineer harus melompat dari satu sistem monitoring ke sistem lain untuk menghubungkan titik-titik anomali. Setelah penerapan sistem ini, alur data distandarisasi menggunakan spesifikasi OpenTelemetry, di mana setiap request diberikan trace ID dan span ID yang merambat secara otomatis melintasi batas bahasa pemrograman (Node.js ke Go) dan lapisan infrastruktur (Nginx ke S3, MySQL, Redis). Prometheus bertindak sebagai pengumpul metrik deret waktu (time-series) yang sangat efisien untuk memantau Golden Signals (Latensi, Traffic, Errors, dan Saturation), sementara Grafana menyajikan panel visualisasi terpadu yang memungkinkan identifikasi akar masalah (root cause analysis) dalam hitungan detik, bukan jam.
2. Konsep Inti & Deep Dive Arsitektur
OpenTelemetry (OTel) adalah standar industri open-source yang dikelola oleh Cloud Native Computing Foundation (CNCF), yang menggabungkan proyek OpenTracing dan OpenCensus. OTel menyediakan spesifikasi dan perangkat SDK yang seragam untuk mengumpulkan Traces (jejak eksekusi terdistribusi), Metrics (pengukuran numerik agregat), dan Logs (catatan peristiwa terstruktur).
Arsitektur pengumpulan data dalam sistem observability terdistribusi umumnya melibatkan komponen-komponen berikut:
- Instrumented Application: Aplikasi Backend (Go, Node.js) yang menggunakan OTel SDK untuk menghasilkan telemetry data.
- OpenTelemetry Collector: Agen atau layanan kolektor yang menerima, memproses, melakukan batching, dan mengekspor data telemetri ke backend penyimpanan yang sesuai (Prometheus, Jaeger, Zipkin, atau OTLP compatible backends).
- Prometheus Server: Sistem monitoring berbasis pull yang mengambil (scrape) metrik dari aplikasi atau OTel Collector secara berkala.
- Grafana: Platform visualisasi data yang menghubungkan sumber data Prometheus dan Jaeger/Tempo untuk menampilkan metrik dan traces secara berdampingan (side-by-side).
Berikut adalah representasi ASCII dari alur arsitektur pengumpulan metrik, logs, dan traces:
+-------------------------------------------------------------------------+
| Distributed Clients |
+-------------------------------------------------------------------------+
|
v
+-------------------------------------------------------------------------+
| Nginx / Caddy Edge Proxy & WAF |
+-------------------------------------------------------------------------+
|
+------------------+------------------+
| HTTP Request w/ TraceContext |
v v
+-----------------------------------+ +---------------------------------+
| Go Microservice (Auth/API) | | Node.js Microservice (Core API) |
| - OTel Go SDK | | - OTel JS SDK |
+-----------------------------------+ +---------------------------------+
| |
+------------------+------------------+
|
v
+-------------------------------------------------------------------------+
| OpenTelemetry Collector |
| - Receivers (OTLP, Prometheus) |
| - Processors (Batch, Memory Limiter, Attributes) |
| - Exporters (Prometheus remote_write, OTLP Trace) |
+-------------------------------------------------------------------------+
| |
v (Scrape / Metrics) v (Export / Traces)
+-----------------------+ +---------------------+
| Prometheus Server | | Jaeger / Tempo |
| - Time-Series DB | | - Distributed Trace |
+-----------------------+ +---------------------+
| |
+------------------+ +------------------+
| |
v v
+-------------------------------------------+
| Grafana Dashboard |
| - Golden Signals (Latency, Traffic, etc) |
| - Trace-to-Metrics / Metrics-to-Trace |
+-------------------------------------------+
Alur eksekusi dimulai ketika pengguna mengirimkan permintaan HTTP ke Nginx. Nginx menyuntikkan header W3C Trace Context (traceparent dan tracestate) jika belum ada, atau meneruskannya ke layanan backend Go. Layanan Go membaca konteks tersebut, membuat span baru untuk operasinya sendiri, memanggil layanan Node.js via HTTP/gRPC dengan meneruskan trace context, melakukan query ke Redis, dan mencatat metrik durasi eksekusi. Seluruh data ini dikirimkan ke OTel Collector, yang kemudian meneruskan metrik ke Prometheus dan traces ke sistem penyimpanan trace.
3. Perbandingan Komparatif / Analisis Pendekatan
Untuk merancang sistem observability yang efisien, kita harus memahami trade-off antara berbagai pendekatan pengumpulan dan penyimpanan data telemetri. Tabel berikut membandingkan pendekatan tradisional dengan pendekatan berbasis OpenTelemetry dan Prometheus.
| Parameter Komparasi | Logging Tradisional & Custom Script | OpenTelemetry + Prometheus + Grafana | Keunggulan & Trade-off Utama |
|---|---|---|---|
| Standarisasi | Proprietary, tidak ada format baku | Standar CNCF (OTel W3C Trace Context) | OTel memberikan interoperabilitas lintas bahasa dan vendor tanpa vendor lock-in. |
| Overhead CPU & RAM | Rendah secara lokal, tinggi saat agregasi manual | Sangat rendah hingga sedang (dapat diatur via OTel Collector) | OTel SDK dioptimalkan untuk meminimalkan alokasi memori garbage collection. |
| Granularitas Tracing | Terbatas pada log teks per aplikasi | End-to-end distributed tracing lintas jaringan & servis | Memungkinkan pelacakan latency bottleneck hingga ke baris kode spesifik. |
| Biaya Infrastruktur | Murah di awal, mahal dalam pemeliharaan manual | Memerlukan resource tambahan untuk Collector & TSDB | Prometheus efisien dalam penyimpanan metrik (time-series kompresi tinggi). |
| Waktu Deteksi Insiden | Lambat (harus grep log manual) | Cepat (Alertmanager + Dashboard Grafana real-time) | Mengurangi MTTR (Mean Time to Resolution) secara drastis. |
Pertimbangan sumber daya (Resource Footprint):
- Prometheus: Menggunakan memori RAM secara signifikan karena menyimpan seluruh metrik aktif di in-memory chunks sebelum di-flush ke disk. Penggunaan disk I/O bergantung pada tingkat kardinalitas metrik (metric cardinality).
- OpenTelemetry Collector: Bertindak sebagai buffer yang sangat penting. Dengan menggunakan
memory_limiterprocessor, collector mencegah lonjakan traffic telemetri membebani backend monitoring.
4. Prasyarat Sistem & Persiapan Lingkungan
Sebelum melakukan implementasi, pastikan infrastruktur dan lingkungan pengembangan memenuhi spesifikasi berikut:
- Sistem Operasi: Ubuntu 22.04 LTS / Debian 12 / RHEL 9 (Minimum 2 vCPU, 4GB RAM untuk server observability).
- Software Dependencies:
- Go 1.21 atau versi lebih baru
- Node.js 20.x LTS
- Docker & Docker Compose v2.x
- Prometheus v2.45+
- OpenTelemetry Collector Contrib v0.90+
- Grafana 10.x+
Lakukan verifikasi awal pada sistem dengan menjalankan perintah berikut untuk memastikan port yang dibutuhkan (9090 untuk Prometheus, 3000 untuk Grafana, 4317 & 4318 untuk OTel Collector) tidak digunakan oleh proses lain:
sudo ss -tulpn | grep -E '9090|3000|4317|4318'
Buat struktur direktori proyek untuk deployment lokal menggunakan Docker Compose:
mkdir -p /opt/observability-stack/{prometheus,otel-collector,grafana/provisioning}
cd /opt/observability-stack
5. Panduan Implementasi Langkah demi Langkah
Langkah 1: Konfigurasi OpenTelemetry Collector
Buat file konfigurasi untuk OpenTelemetry Collector di /opt/observability-stack/otel-collector/otel-collector-config.yaml:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
memory_limiter:
check_interval: 1s
limit_percentage: 75
spike_limit_percentage: 15
batch:
send_batch_size: 8192
timeout: 1s
send_batch_max_size: 10240
exporters:
prometheus:
endpoint: "0.0.0.0:8889"
namespace: "otel"
logging:
verbosity: detailed
service:
pipelines:
metrics:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [prometheus, logging]
traces:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [logging]
telemetry:
logs:
level: "info"
Langkah 2: Konfigurasi Prometheus
Buat file konfigurasi Prometheus di /opt/observability-stack/prometheus/prometheus.yml:
global:
scrape_interval: 15s
evaluation_interval: 15s
alerting:
alertmanagers:
- static_configs:
- targets: []
rule_files:
- "alert.rules.yml"
scrape_configs:
- job_name: 'otel-collector'
static_configs:
- targets: ['otel-collector:8889']
- job_name: 'node-backend'
static_configs:
- targets: ['node-app:4000']
Buat file aturan alerting di /opt/observability-stack/prometheus/alert.rules.yml:
groups:
- name: HighLatencyAlerts
rules:
- alert: HighHTTPResponseTime
expr: histogram_quantile(0.95, sum(rate(http_server_duration_bucket[5m])) by (le)) > 0.5
for: 2m
labels:
severity: critical
annotations:
summary: "High HTTP Latency detected"
description: "P95 latency is above 500ms for more than 2 minutes."
Langkah 3: Orchestration dengan Docker Compose
Buat file /opt/observability-stack/docker-compose.yml:
version: '3.8'
services:
otel-collector:
image: otel/opentelemetry-collector-contrib:0.90.0
container_name: otel-collector
command: ["--config=/etc/otel-collector-config.yaml"]
volumes:
- ./otel-collector/otel-collector-config.yaml:/etc/otel-collector-config.yaml
ports:
- "4317:4317"
- "4318:4318"
- "8889:8889"
restart: unless-stopped
prometheus:
image: prom/prometheus:v2.45.0
container_name: prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
- ./prometheus/alert.rules.yml:/etc/prometheus/alert.rules.yml
- prometheus_data:/prometheus
ports:
- "9090:9090"
restart: unless-stopped
grafana:
image: grafana/grafana:10.2.0
container_name: grafana
environment:
- GF_SECURITY_ADMIN_USER=admin
- GF_SECURITY_ADMIN_PASSWORD=securepassword123
ports:
- "3000:3000"
volumes:
- grafana_data:/var/lib/grafana
restart: unless-stopped
volumes:
prometheus_data:
grafana_data:
Jalankan stack menggunakan Docker Compose:
docker compose up -d
Langkah 4: Instrumentasi Backend Go
Buat direktori baru untuk aplikasi Go, inisialisasi modul, dan buat file main.go:
package main
import (
"context"
"log"
"net/http"
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.21.0"
)
func initTracer(ctx context.Context) (*sdktrace.TracerProvider, error) {
exporter, err := otlptracegrpc.New(ctx,
otlptracegrpc.WithEndpoint("localhost:4317"),
otlptracegrpc.WithInsecure(),
)
if err != nil {
return nil, err
}
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter),
sdktrace.WithResource(resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String("go-auth-service"),
)),
)
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{},
propagation.Baggage{},
))
return tp, nil
}
func main() {
ctx := context.Background()
tp, err := initTracer(ctx)
if err != nil {
log.Fatalf("Failed to initialize tracer: %v", err)
}
defer func() {
if err := tp.Shutdown(ctx); err != nil {
log.Printf("Error terminating TracerProvider: %v", err)
}
}()
http.HandleFunc("/api/auth", func(w http.ResponseWriter, r *http.Request) {
tr := otel.Tracer("go-auth-service")
_, span := tr.Start(r.Context(), "AuthenticateUser")
defer span.End()
// Simulasi pemrosesan autentikasi
time.Sleep(50 * time.Millisecond)
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status": "authenticated"}`))
})
log.Println("Go Auth Service running on port 8080...")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}
Langkah 5: Instrumentasi Backend Node.js
Inisialisasi proyek Node.js dan install dependensi OpenTelemetry:
npm init -y
npm install @opentelemetry/api @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node @opentelemetry/exporter-metrics-otlp-grpc @opentelemetry/exporter-trace-otlp-grpc
Buat file tracing.js untuk menginisialisasi SDK OpenTelemetry sebelum aplikasi dijalankan:
'use strict';
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-grpc');
const { OTLPMetricExporter } = require('@opentelemetry/exporter-metrics-otlp-grpc');
const { PeriodicExportingMetricReader } = require('@opentelemetry/sdk-metrics');
const traceExporter = new OTLPTraceExporter({
url: 'http://localhost:4317',
});
const metricExporter = new OTLPMetricExporter({
url: 'http://localhost:4317',
});
const sdk = new NodeSDK({
traceExporter,
metricReader: new PeriodicExportingMetricReader({
exporter: metricExporter,
exportIntervalMillis: 5000,
}),
instrumentations: [getNodeAutoInstrumentations()],
serviceName: 'node-core-api',
});
sdk.start();
process.on('SIGTERM', () => {
sdk.shutdown()
.then(() => console.log('Tracing terminated'))
.catch((error) => console.log('Error terminating tracing', error))
.finally(() => process.exit(0));
});
Buat file server.js:
require('./tracing');
const http = require('http');
const PORT = 4000;
const server = http.createServer((req, res) => {
if (req.url === '/api/core' && req.method === 'GET') {
setTimeout(() => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ message: 'Core API operational', timestamp: Date.now() }));
}, 100);
} else {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Not Found' }));
}
});
server.listen(PORT, () => {
console.log(`Node Core API listening on port ${PORT}`);
});
Jalankan aplikasi Node.js dengan perintah:
node server.js
6. Bedah Kode & Penjelasan Detail Baris Kunci
Mari kita bedah beberapa baris kode paling krusial dari implementasi di atas untuk memahami bagaimana mekanisme tracing dan pengumpulan metrik bekerja di tingkat rendah:
-
propagation.NewCompositeTextMapPropagator(Go):otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator( propagation.TraceContext{}, propagation.Baggage{}, ))Baris ini mengonfigurasi bagaimana konteks trace (W3C
traceparentheader) disuntikkan ke dalam panggilan HTTP ke layanan downstream dan bagaimana konteks tersebut diekstrak dari header permintaan masuk. Jika konfigurasi propagator tidak sinkron antar layanan mikroservis, trace tree akan terpotong dan menghasilkan orphan spans. -
memory_limiterProcessor (OTel Collector):memory_limiter: check_interval: 1s limit_percentage: 75 spike_limit_percentage: 15Konfigurasi ini memantau penggunaan RAM OTel Collector secara real-time. Jika penggunaan memori mencapai 75% dari batas sistem, kolektor akan secara otomatis menolak atau melakukan drop pada batch telemetri berprioritas rendah guna mencegah Out-Of-Memory (OOM) Killer menghentikan proses collector secara paksa di lingkungan produksi.
-
getNodeAutoInstrumentations()(Node.js):
instrumentations: [getNodeAutoInstrumentations()]
Fungsi ini secara otomatis melakukan instrumentasi pada modul inti Node.js seperti `http`, `https`, `express`, `pg`, `redis`, dan `mongodb` tanpa mengharuskan developer mengubah baris kode bisnis apa pun. Ini menghemat ratusan jam waktu pengembangan dalam penerapan enterprise.
> [!WARNING]
> Jangan pernah mengaktifkan *debug logging* pada OTel Collector atau SDK di lingkungan produksi dengan traffic tinggi. Hal ini dapat menyebabkan lonjakan penggunaan disk I/O, saturasi CPU, dan berpotensi menghabiskan ruang penyimpanan server (*disk full*).
## 7. Skenario Produksi & Studi Kasus Dunia Nyata
Dalam sebuah perusahaan *e-commerce* dengan *traffic* puncak mencapai 50.000 *request per second* (RPS) saat kampanye flash sale, sistem mengalami lonjakan latensi yang tidak terduga pada layanan keranjang belanja (*cart-service*).
**Investigasi Menggunakan Grafana & OpenTelemetry:**
1. **Golden Signals Dashboard**: SRE melihat grafik latensi P99 pada endpoint `/api/cart` melonjak dari 45ms menjadi 2.400ms, sementara penggunaan CPU pod Kubernetes berada di bawah 40% (menunjukkan tidak ada *resource starvation*).
2. **Trace-to-Metrics Correlation**: SRE mengeklik tombol *Trace* langsung dari lonjakan latensi di dashboard Prometheus/Grafana. Trace memperlihatkan bahwa *request* masuk ke `cart-service`, yang kemudian memanggil `pricing-service`, yang pada gilirannya melakukan *query* ke cluster Redis.
3. **Identifikasi Bottleneck**: *Span* pada panggilan Redis menunjukkan durasi eksekusi yang sangat lama. Berkat atribut OTel yang merekam argumen query, ditemukan bahwa layanan melakukan operasi `KEYS *` (full key space scan) di dalam loop Redis untuk menghitung diskon dinamis.
4. **Resolusi**: Tim backend segera mengganti operasi `KEYS *` dengan struktur data `HSCAN` yang terindeks dengan benar. Latensi kembali normal dalam waktu kurang dari 5 menit setelah deployment *hotfix*.
## 8. Common Pitfalls, Anti-Patterns & Solusi
Dalam membangun sistem *observability* skala besar, beberapa kesalahan umum sering dilakukan oleh insinyur pemula hingga menengah:
- **Anti-Pattern 1: Metric High Cardinality (Kardinalitas Metrik Tinggi)**
* *Kasus*: Menyertakan *User ID*, *UUID pesanan*, atau *IP Address* sebagai *label* atau *dimension* pada metrik Prometheus.
* *Dampak*: Menyebabkan ledakan kombinasi deret waktu (*time-series explosion*), menghabiskan RAM Prometheus, dan mengakibatkan kegagalan *scraping*.
* *Solusi*: Batasi label metrik hanya pada atribut dengan kardinalitas rendah seperti `status_code`, `http_method`, dan `service_name`. Simpan data unik di dalam *Traces* atau *Structured Logs*, bukan di *Metrics*.
- **Anti-Pattern 2: Tidak Mengonfigurasi Sampling pada Traces**
* *Kasus*: Mengirimkan 100% dari seluruh *trace* yang dihasilkan oleh aplikasi ke backend penyimpanan tanpa *sampling*.
* *Dampak*: Membebani bandwidth jaringan, memakan penyimpanan disk yang masif, dan menaikkan biaya infrastruktur penyimpanan log/trace.
* *Solusi*: Terapkan *Head-based sampling* atau *Tail-based sampling* di OTel Collector. Untuk lingkungan produksi skala besar, tingkat *sampling* 5% hingga 10% sudah lebih dari cukup untuk menangkap anomali statistik.
## 9. Benchmark, Metrik Kinerja & Tuning Performa
Untuk memastikan sistem *observability* itu sendiri tidak menjadi beban performa bagi aplikasi utama, pengujian beban (*load testing*) harus dilakukan menggunakan tool seperti `k6` atau `wrk`.
Berikut adalah skrip pengujian beban menggunakan `k6` untuk menguji latensi aplikasi Node.js yang telah diinstrumentasi:
```javascript
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '30s', target: 100 },
{ duration: '1m', target: 500 },
{ duration: '30s', target: 0 },
],
};
export default function () {
const res = http.get('http://localhost:4000/api/core');
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 200ms': (r) => r.timings.duration < 200,
});
sleep(0.1);
}
Jalankan benchmark dengan perintah:
k6 run load-test.js
Hasil Tuning Parameter Lanjutan:
- Batching Processor: Pastikan
send_batch_sizepada OTel Collector diatur ke nilai yang lebih besar (misal:8192) untuk mengurangi overhead panggilan jaringan TCP/gRPC. - Prometheus Retention: Atur retensi penyimpanan tsdb sesuai kebutuhan kepatuhan regulasi menggunakan bendera
--storage.tsdb.retention.time=15duntuk mencegah kehabisan kapasitas disk.
10. Checklist Penerapan Produksi & Kesimpulan
Sebelum meluncurkan sistem observability terdistribusi ke lingkungan production, pastikan seluruh poin dalam checklist berikut telah terpenuhi:
- OTel Collector dikonfigurasi dengan
memory_limiteruntuk mencegah OOM. - Kardinalitas label metrik Prometheus telah diaudit (tidak ada User ID atau UUID pada label metrik).
- Kebijakan Sampling (Head-based / Tail-based) telah diaktifkan untuk menekan biaya penyimpanan traces.
- Dashboard Grafana telah diuji terhadap skenario lonjakan traffic menggunakan alat beban seperti
k6. - Aturan Alerting (Alertmanager) telah diintegrasikan dengan kanal eskalasi nyata (PagerDuty, Slack, atau Webhook).
- Enkripsi TLS diaktifkan pada komunikasi OTLP gRPC antar aplikasi mikroservis dan collector di lingkungan cross-host.
Kesimpulan: Membangun sistem observability terdistribusi dengan OpenTelemetry dan Prometheus bukan sekadar tren teknologi, melainkan fondasi esensial untuk menjaga ketahanan operasional infrastruktur web modern. Dengan pendekatan yang terstruktur, standardisasi telemetri yang konsisten, dan manajemen sumber daya yang cermat, tim SRE dapat mengubah kompleksitas mikroservis menjadi wawasan operasional yang transparan dan dapat ditindaklanjuti.