Pelacakan End-To-End dengan OpenTelemetry

Nicolas Fränkel

Nicolas Fränkel

August 31, 2022

Ecosystem

Apakah Anda menerapkan atau tidak microservices (dan mungkin sebaiknya tidak), sistem Anda kemungkinan besar terdiri dari beberapa komponen. Sistem yang paling sederhana mungkin terdiri dari reverse proxy, aplikasi, dan database. Dalam kasus ini, monitoring bukan hanya ide yang baik; itu adalah suatu keharusan. Semakin banyak komponen yang dilalui oleh sebuah permintaan, semakin kuat pula keharusan tersebut.

Namun, monitoring hanyalah awal dari perjalanan. Ketika permintaan mulai gagal secara massal, Anda memerlukan pandangan agregat di semua komponen. Ini disebut tracing, dan ini adalah salah satu pilar dari observability; dua lainnya adalah metrik dan log.

Dalam posting ini, saya akan fokus hanya pada traces dan menjelaskan bagaimana Anda dapat memulai perjalanan Anda ke dalam observability.

Spesifikasi W3C Trace Context

Solusi tracing harus menyediakan format standar untuk bekerja di berbagai tumpukan teknologi. Format seperti itu perlu mengikuti spesifikasi, baik yang formal maupun yang de facto.

Seseorang perlu memahami bahwa spesifikasi jarang muncul begitu saja. Secara umum, pasar sudah memiliki beberapa implementasi yang berbeda. Sebagian besar waktu, spesifikasi baru mengarah pada implementasi tambahan, seperti yang dijelaskan oleh komik XKCD yang terkenal:

komik XKCD yang terkenal

Namun, terkadang keajaiban terjadi: pasar mengikuti spesifikasi baru. Di sini, Trace Context adalah spesifikasi W3C, dan tampaknya telah berhasil:

Spesifikasi ini mendefinisikan header HTTP standar dan format nilai untuk menyebarkan informasi konteks yang memungkinkan skenario tracing terdistribusi. Spesifikasi ini menstandarkan bagaimana informasi konteks dikirim dan dimodifikasi antara layanan. Informasi konteks secara unik mengidentifikasi permintaan individu dalam sistem terdistribusi dan juga mendefinisikan cara untuk menambahkan dan menyebarkan informasi konteks khusus penyedia. -- https://www.w3.org/TR/trace-context/

Dua konsep kritis muncul dari dokumen tersebut:

  • Sebuah trace mengikuti jalur permintaan yang melintasi beberapa komponen
  • Sebuah span terikat pada satu komponen dan terhubung ke span lain melalui hubungan anak-orang tua

![Traces dan spans](https://static-site.apiseven.com/wp-content/uploads/2022/08/trace-spans-1024x393.png "Traces dan spans" "Traces dan span")

Pada saat penulisan ini, spesifikasi ini adalah rekomendasi W3C, yang merupakan tahap final.

Trace Context sudah memiliki banyak implementasi. Salah satunya adalah OpenTelemetry.

OpenTelemetry sebagai standar emas

Semakin dekat Anda dengan bagian operasional IT, semakin besar kemungkinan Anda pernah mendengar tentang OpenTelemetry:

OpenTelemetry adalah kumpulan alat, API, dan SDK. Gunakan untuk menginstrumentasi, menghasilkan, mengumpulkan, dan mengekspor data telemetri (metrik, log, dan traces) untuk membantu Anda menganalisis kinerja dan perilaku perangkat lunak Anda. OpenTelemetry umumnya tersedia di beberapa bahasa dan cocok untuk digunakan. -- https://opentelemetry.io/

OpenTelemetry adalah proyek yang dikelola oleh CNCF. Sebelum OpenTelemetry, ada dua proyek:

  • OpenTracing, yang fokus pada traces seperti namanya
  • OpenCensus, yang tujuannya adalah mengelola metrik dan traces

Kedua proyek tersebut bergabung dan menambahkan log di atasnya. OpenTelemetry sekarang menawarkan serangkaian "lapisan" yang fokus pada observability:

  • API instrumentasi dalam berbagai bahasa
  • Implementasi kanonik, lagi-lagi dalam berbagai bahasa
  • Komponen infrastruktur seperti kolektor
  • Format interoperabilitas, seperti Trace Context W3C

Perhatikan bahwa meskipun OpenTelemetry adalah implementasi Trace Context, ia melakukan lebih banyak hal. Trace Context membatasi diri pada HTTP, sementara OpenTelemetry memungkinkan spans melintasi komponen non-web, seperti Kafka. Ini di luar cakupan posting blog ini.

Kasus penggunaan

Kasus penggunaan favorit saya adalah toko e-commerce, jadi mari kita tidak mengubahnya. Dalam kasus ini, toko dirancang di sekitar microservices, masing-masing dapat diakses melalui REST API dan dilindungi di belakang API Gateway. Untuk menyederhanakan arsitektur untuk posting blog, saya hanya akan menggunakan dua microservices: catalog mengelola produk, dan pricing menangani harga produk.

Ketika pengguna tiba di aplikasi, halaman utama mengambil semua produk, mendapatkan harga masing-masing, dan menampilkannya.

Contoh aliran permintaan di beberapa komponen

Untuk membuatnya lebih menarik, catalog adalah aplikasi Spring Boot yang dikodekan dalam Kotlin, sementara pricing adalah aplikasi Python Flask.

Tracing seharusnya memungkinkan kita untuk mengikuti jalur permintaan melintasi gateway, kedua microservices, dan jika memungkinkan, database.

Traces di gateway

Titik masuk adalah bagian paling menarik dari tracing, karena seharusnya menghasilkan trace ID: dalam kasus ini, titik masuk adalah gateway. Saya akan menggunakan Apache APISIX untuk mengimplementasikan demo:

Apache APISIX menyediakan fitur manajemen lalu lintas yang kaya seperti Load Balancing, Dynamic Upstream, Canary Release, Circuit Breaking, Authentication, Observability, dll.-- https://apisix.apache.org/

Apache APISIX berbasis pada arsitektur plugin dan menawarkan plugin OpenTelemetry:

Plugin opentelemetry dapat digunakan untuk melaporkan data tracing sesuai dengan spesifikasi OpenTelemetry. Plugin ini hanya mendukung OLTP yang dikodekan biner melalui HTTP. -- https://apisix.apache.org/docs/apisix/plugins/opentelemetry/

Mari kita konfigurasi plugin opentelemetry:

apisix: enable_admin: false #1 config_center: yaml #1 plugins: - opentelemetry #2 plugin_attr: opentelemetry: resource: service.name: APISIX #3 collector: address: jaeger:4318 #4
  1. Jalankan Apache APISIX dalam mode standalone untuk membuat demo lebih mudah diikuti. Ini adalah praktik yang baik dalam produksi
  2. Konfigurasi opentelemetry sebagai plugin global
  3. Tetapkan nama layanan. Ini adalah nama yang akan muncul di komponen tampilan trace
  4. Kirim traces ke layanan jaeger. Bagian berikutnya akan menjelaskannya.

Kami ingin melacak setiap rute, jadi alih-alih menambahkan plugin ke setiap rute, kami harus mengatur plugin sebagai global:

global_rules: - id: 1 plugins: opentelemetry: sampler: name: always_on #1
  1. Tracing memiliki dampak pada kinerja. Semakin banyak kita melacak, semakin besar dampaknya. Oleh karena itu, kita harus dengan hati-hati menyeimbangkan dampak kinerja vs. manfaat observability. Namun, untuk demo, kami ingin melacak setiap permintaan.

Mengumpulkan, menyimpan, dan menampilkan traces

Meskipun Trace Context adalah spesifikasi W3C dan OpenTelemetry adalah standar de facto, banyak solusi yang ada di pasar untuk mengumpulkan, menyimpan, dan menampilkan traces. Setiap solusi mungkin menyediakan semua tiga kemampuan atau hanya sebagian. Misalnya, stack Elastic menangani penyimpanan dan tampilan, tetapi Anda harus mengandalkan sesuatu yang lain untuk pengumpulan. Di sisi lain, Jaeger dan Zipkin menyediakan suite lengkap untuk memenuhi semua tiga kemampuan.

Jaeger dan Zipkin mendahului OpenTelemetry, jadi masing-masing memiliki format transport trace sendiri. Namun, mereka menyediakan integrasi dengan format OpenTelemetry.

Dalam cakupan posting blog ini, solusi yang tepat tidak relevan, karena kita hanya membutuhkan kemampuan. Saya memilih Jaeger karena menyediakan gambar Docker all-in-one: setiap kemampuan memiliki komponennya sendiri, tetapi semuanya tertanam dalam gambar yang sama, yang membuat konfigurasi jauh lebih mudah.

Port yang relevan dari gambar tersebut adalah sebagai berikut:

PortProtokolKomponenFungsi
16686HTTPquerymelayani frontend
4317HTTPcollectormenerima OpenTelemetry Protocol (OTLP) melalui gRPC, jika diaktifkan
4318HTTPcollectormenerima OpenTelemetry Protocol (OTLP) melalui HTTP, jika diaktifkan

Bagian Docker Compose terlihat seperti ini:

services: jaeger: image: jaegertracing/all-in-one:1.37 #1 environment: - COLLECTOR_OTLP_ENABLED=true #2 ports: - "16686:16686" #3
  1. Gunakan gambar all-in-one
  2. Sangat penting: aktifkan kolektor dalam format OpenTelemetry
  3. Ekspos port UI

Sekarang setelah kita mengatur infrastruktur, kita dapat fokus pada mengaktifkan traces di aplikasi kita.

Traces di aplikasi Flask

Layanan pricing adalah aplikasi Flask sederhana. Ini menawarkan satu endpoint untuk mengambil harga satu produk dari database.

@app.route('/price/<product_str>') #1-2 def price(product_str: str) -> Dict[str, object]: product_id = int(product_str) price: Price = Price.query.get(product_id) #3 if price is None: return jsonify({'error': 'Product not found'}), 404 else: low: float = price.value - price.jitter #4 high: float = price.value + price.jitter #4 return { 'product_id': product_id, 'price': round(uniform(low, high), 2) #4 }
  1. Endpoint
  2. Rute memerlukan id produk
  3. Ambil data dari database menggunakan SQLAlchemy
  4. Mesin harga nyata tidak pernah mengembalikan harga yang sama dari waktu ke waktu; mari kita acak harga sedikit untuk bersenang-senang

Peringatan: Mengambil satu harga per panggilan sangat tidak efisien. Ini memerlukan sebanyak panggilan seperti produk, tetapi ini membuat trace lebih menarik. Dalam kehidupan nyata, rute harus dapat menerima beberapa id produk dan mengambil semua harga terkait dalam satu permintaan-respons.

Sekarang saatnya untuk menginstrumentasi aplikasi. Dua opsi tersedia: instrumentasi otomatis dan manual. Instrumentasi otomatis adalah upaya rendah dan kemenangan cepat; manual memerlukan waktu pengembangan yang fokus. Saya menyarankan untuk memulai dengan otomatis dan hanya menambahkan manual jika diperlukan.

Kita perlu menambahkan beberapa paket Python:

opentelemetry-distro[otlp]==0.33b0 opentelemetry-instrumentation opentelemetry-instrumentation-flask

Kita perlu mengkonfigurasi beberapa parameter:

pricing: build: ./pricing environment: OTEL_EXPORTER_OTLP_ENDPOINT: http://jaeger:4317 #1 OTEL_RESOURCE_ATTRIBUTES: service.name=pricing #2 OTEL_METRICS_EXPORTER: none #3 OTEL_LOGS_EXPORTER: none #3
  1. Kirim traces ke Jaeger
  2. Tetapkan nama layanan. Ini adalah nama yang akan muncul di komponen tampilan trace
  3. Kami tidak tertarik pada log maupun metrik

Sekarang, alih-alih menggunakan perintah flask run standar, kita membungkusnya:

opentelemetry-instrument flask run

Hanya dengan ini, kita sudah mengumpulkan spans dari panggilan metode dan rute Flask.

Kita dapat secara manual menambahkan spans tambahan jika diperlukan, misalnya:

from opentelemetry import trace @app.route('/price/<product_str>') def price(product_str: str) -> Dict[str, object]: product_id = int(product_str) with tracer.start_as_current_span("SELECT * FROM PRICE WHERE ID=:id", attributes={":id": product_id}) as span: #1 price: Price = Price.query.get(product_id) # ...
  1. Tambahkan span tambahan dengan label dan atribut yang dikonfigurasi

Traces di aplikasi Spring Boot

Layanan catalog adalah aplikasi Spring Boot Reactive yang dikembangkan dalam Kotlin. Ini menawarkan dua endpoint:

  • Satu untuk mengambil satu produk
  • Yang lainnya untuk mengambil semua produk

Keduanya pertama-tama mencari di database produk, kemudian meminta layanan pricing di atas untuk harga.

Seperti pada Python, kita dapat memanfaatkan instrumentasi otomatis dan manual. Mari kita mulai dengan yang mudah, instrumentasi otomatis. Pada JVM, kita mencapainya melalui agen:

java -javaagent:opentelemetry-javaagent.jar -jar catalog.jar

Seperti pada Python, ini membuat spans untuk setiap panggilan metode dan titik masuk HTTP. Ini juga menginstrumentasi panggilan JDBC, tetapi kita memiliki stack Reactive dan dengan demikian menggunakan R2DBC. Sebagai catatan, masalah GitHub terbuka untuk menambahkan dukungan.

Kita perlu mengkonfigurasi perilaku default:

catalog: build: ./catalog environment: APP_PRICING_ENDPOINT: http://pricing:5000/price OTEL_EXPORTER_OTLP_ENDPOINT: http://jaeger:4317 #1 OTEL_RESOURCE_ATTRIBUTES: service.name=orders #2 OTEL_METRICS_EXPORTER: none #3 OTEL_LOGS_EXPORTER: none #3
  1. Kirim traces ke Jaeger
  2. Tetapkan nama layanan. Ini adalah nama yang akan muncul di komponen tampilan trace
  3. Kami tidak tertarik pada log maupun metrik

Seperti pada Python, kita dapat meningkatkan permainan dengan menambahkan instrumentasi manual. Dua opsi tersedia, berbasis program dan berbasis anotasi. Yang pertama sedikit rumit kecuali kita memperkenalkan Spring Cloud Sleuth. Mari kita tambahkan anotasi.

Kita memerlukan dependensi tambahan:

<dependency> <groupId>io.opentelemetry.instrumentation</groupId> <artifactId>opentelemetry-instrumentation-annotations</artifactId> <version>1.17.0-alpha</version> </dependency>

Hati-hati, artefak ini baru saja dipindahkan dari io.opentelemetry:opentelemetry-extension-annotations.

Kita sekarang dapat menganotasi kode kita:

@WithSpan("ProductHandler.fetch") //1 suspend fun fetch(@SpanAttribute("id") id: Long): Result<Product> { //2 val product = repository.findById(id) return if (product == null) Result.failure(IllegalArgumentException("Product $id not found")) else Result.success(product) }
  1. Tambahkan span tambahan dengan label yang dikonfigurasi
  2. Gunakan parameter sebagai atribut, dengan kunci diatur ke id dan nilai adalah nilai runtime parameter

Hasilnya

Kita sekarang dapat bermain dengan demo sederhana kita untuk melihat hasilnya:

curl localhost:9080/products curl localhost:9080/products/1

Responsnya tidak menarik, tetapi mari kita lihat UI Jaeger. Kita menemukan kedua traces, satu per panggilan:

Daftar traces di UI Jaeger

Kita dapat menyelami spans dari satu trace:

Semua spans yang membentuk satu trace

Perhatikan bahwa kita dapat menyimpulkan aliran urutan tanpa diagram UML di atas. Bahkan lebih baik, urutan menampilkan panggilan internal ke komponen.

Setiap span berisi atribut yang ditambahkan oleh instrumentasi otomatis dan yang kita tambahkan secara manual:

Atribut dari sebuah span

Kesimpulan

Dalam posting ini, saya telah menunjukkan tracing dengan mengikuti permintaan melintasi API gateway, dua aplikasi berdasarkan tumpukan teknologi yang berbeda, dan database masing-masing. Saya hanya menyentuh permukaan tracing: di dunia nyata, tracing mungkin melibatkan komponen yang tidak terkait dengan HTTP, seperti Kafka dan antrian pesan.

Namun, sebagian besar sistem bergantung pada HTTP dalam satu atau lain cara. Meskipun tidak sepele untuk diatur, itu juga tidak terlalu sulit. Tracing permintaan HTTP melintasi komponen adalah awal yang baik dalam perjalanan Anda menuju observability sistem Anda.

Kode sumber lengkap untuk posting ini dapat ditemukan di GitHub.

Untuk melanjutkan:

Tags: