Ilya Kaznacheev, yang telah mengembangkan selama delapan tahun dan bekerja sebagai pengembang backend di MTS, siap berbagi cara menggunakan OpenTelemetry di proyek Golang. Pada konferensi Golang Live 2020, dia berbicara tentang bagaimana mengatur penggunaan standar baru untuk penelusuran dan pemantauan dan membuatnya bersahabat dengan infrastruktur yang sudah ada dalam proyek tersebut.
OpenTelemetry adalah standar yang relatif baru, akhir tahun lalu. Pada saat yang sama, ia menerima distribusi dan dukungan yang luas dari banyak vendor perangkat lunak untuk penelusuran dan pemantauan.
Observabilitas, atau observabilitas, adalah istilah dari teori kontrol yang menentukan seberapa banyak seseorang dapat menilai keadaan internal suatu sistem dengan manifestasi eksternalnya. Dalam arsitektur sistem, ini berarti serangkaian pendekatan untuk memantau status sistem pada waktu proses. Pendekatan ini termasuk penebangan, penelusuran, dan pemantauan.
Ada banyak solusi vendor untuk penelusuran dan pemantauan. Hingga saat ini, terdapat dua standar terbuka: OpenTracing dari CNCF yang muncul pada 2016, dan Open Census, dari Google, yang muncul pada 2018.
Ini adalah dua standar yang cukup bagus yang saling bersaing beberapa lama, hingga pada tahun 2019 mereka memutuskan untuk menggabungkan diri menjadi satu standar baru yang disebut OpenTelemetry.
Standar ini mencakup pelacakan dan pemantauan terdistribusi. Ini kompatibel dengan dua yang pertama. Selain itu, OpenTracing dan Open Census telah menghentikan dukungan dalam dua tahun, yang pasti membawa kita lebih dekat ke transisi ke OpenTelemetry.
Kasus penggunaan
Standar mengasumsikan banyak peluang untuk menggabungkan segala sesuatu dengan segala sesuatu dan, pada kenyataannya, merupakan lapisan aktif antara sumber metrik dan jejak serta konsumen mereka.
Mari kita lihat skenario utama.
Untuk pelacakan terdistribusi, Anda dapat langsung mengatur koneksi ke Jaeger atau layanan apa pun yang Anda gunakan.
Jika pelacakan disiarkan secara langsung, Anda dapat menggunakan config dan hanya mengganti perpustakaan.
Jika aplikasi Anda sudah menggunakan OpenTracing, Anda bisa menggunakan OpenTracing Bridge, pembungkus yang akan mengonversi permintaan ke OpenTracing API menjadi OpenTelemetry API di tingkat atas.
Untuk mengumpulkan metrik, Anda juga dapat mengonfigurasi Prometheus untuk langsung mengakses port metrik untuk aplikasi Anda.
Ini berguna jika Anda memiliki infrastruktur sederhana dan Anda mengumpulkan metrik secara langsung. Tetapi standar juga memberikan lebih banyak fleksibilitas.
Skenario utama untuk menggunakan standar ini adalah mengumpulkan metrik dan pelacakan melalui kolektor, yang juga diluncurkan oleh aplikasi atau penampung terpisah ke dalam infrastruktur Anda. Selain itu, Anda dapat mengambil wadah yang sudah jadi dan memasangnya di rumah.
Untuk melakukan ini, cukup dengan mengkonfigurasi eksportir dalam format OTLP di aplikasi. Ini adalah skema grpc untuk transmisi data dalam format OpenTracing. Dari sisi kolektor, Anda dapat mengonfigurasi format dan parameter untuk mengekspor metrik dan jejak ke pengguna akhir, atau ke format lain. Misalnya, di OpenCensus.
Kolektor memungkinkan Anda menghubungkan sejumlah besar jenis sumber data dan banyak data sink pada keluarannya.
Jadi, standar OpenTelemetry menyediakan kompatibilitas dengan banyak standar open source dan vendor.
Manifold standar dapat diperluas. Oleh karena itu, sebagian besar vendor sudah memiliki eksportir yang siap untuk solusi mereka sendiri, jika ada. Anda dapat menggunakan OpenTelemetry meskipun Anda mengumpulkan metrik dan jejak dari beberapa vendor berpemilik. Ini memecahkan masalah dengan vendor lock-in. Meskipun sesuatu belum muncul secara langsung untuk OpenTelemetry, itu bisa diteruskan melalui OpenCensus.
Kolektor itu sendiri sangat mudah dikonfigurasi melalui konfigurasi YAML dangkal:
Penerima ditentukan di sini. Aplikasi Anda mungkin memiliki beberapa sumber lain (Kafka, dll.):
Eksportir - penerima data.
Prosesor - metode untuk memproses data di dalam kolektor:
Dan pipelines, yang secara langsung menentukan bagaimana setiap aliran data yang mengalir di dalam kolektor akan ditangani:
Mari kita lihat satu contoh ilustratif.
Katakanlah Anda memiliki layanan mikro yang telah Anda sekrup OpenTelemetry dan mengonfigurasinya. Dan satu layanan lagi dengan fragmentasi serupa.
Sejauh ini semuanya mudah. Tapi ada:
- layanan warisan yang dijalankan melalui OpenCensus;
- database yang mengirimkan data dalam formatnya sendiri (misalnya, langsung ke Prometheus, seperti yang dilakukan PostgreSQL);
- beberapa layanan lain yang berfungsi dalam penampung dan menyediakan metrik dalam formatnya sendiri. Anda tidak ingin membangun kembali penampung ini dan mengacaukan sidecars sehingga mereka memformat ulang metrik. Anda hanya ingin mengambil dan mengirimnya.
- perangkat keras tempat Anda juga mengumpulkan metrik dan ingin menggunakannya.
Semua metrik ini dapat digabungkan dalam satu kolektor.
Ini sudah mendukung banyak sumber metrik dan jejak yang digunakan dalam aplikasi yang ada. Dan jika Anda menggunakan sesuatu yang eksotis, Anda dapat menerapkan plugin Anda sendiri. Tetapi ini tidak mungkin diperlukan dalam praktiknya. Karena aplikasi yang mengekspor metrik atau jejak, dengan satu atau lain cara, menggunakan beberapa standar umum atau standar terbuka seperti OpenCensus.
Sekarang kami ingin menggunakan informasi ini. Anda dapat menentukan Jaeger sebagai eksportir jejak, dan mengirim metrik ke Prometheus, atau sesuatu yang kompatibel. Katakanlah VictoriaMetrics favorit semua orang.
Tetapi bagaimana jika kami tiba-tiba memutuskan untuk pindah ke AWS dan menggunakan pelacak X-Ray lokal? Tidak masalah. Ini dapat diteruskan melalui OpenCensus, yang memiliki eksportir X-Ray.
Dengan demikian, dari bagian ini Anda dapat mengumpulkan semua infrastruktur Anda untuk metrik dan pelacakan.
Teorinya sudah berakhir. Mari kita bahas tentang cara menggunakan penelusuran dalam praktik.
Instrumentasi aplikasi Golang: tracing
Pertama, Anda perlu membuat rentang akar, dari mana pohon panggilan akan tumbuh.
ctx := context.Background() tr := global.Tracer("github.com/me/otel-demo") ctx, span := tr.Start(ctx, "root") span.AddEvent(ctx, "I am a root span!") doSomeAction(ctx, "12345") span.End()
Ini adalah nama layanan atau perpustakaan Anda. Dengan cara ini, dalam pelacakan, Anda bisa menentukan span yang ada di dalam aplikasi Anda, dan span yang masuk ke library yang diimpor.
Selanjutnya, root span dibuat dengan nama:
ctx, span := tr.Start(ctx, "root")
Pilih nama yang akan menjelaskan tingkat jejak dengan jelas. Misalnya, dapat berupa nama metode (atau kelas dan metode) atau lapisan arsitektur. Misalnya, lapisan infrastruktur, lapisan logika, lapisan database, dll.
Data span juga dimasukkan ke dalam konteks:
ctx, span := tr.Start(ctx, "root") span.AddEvent(ctx, "I am a root span!") doSomeAction(ctx, "12345")
Oleh karena itu, Anda harus meneruskan metode yang ingin Anda lacak ke dalam konteks.
Span merepresentasikan proses pada level tertentu di pohon panggilan. Anda dapat meletakkan atribut, log, dan status kesalahan di dalamnya, jika itu terjadi. Span harus ditutup di bagian akhir. Saat ditutup, durasinya dihitung.
ctx, span := tr.Start(ctx, "root") span.AddEvent(ctx, "I am a root span!") doSomeAction(ctx, "12345") span.End()
Beginilah tampilan span kami di Jaeger: Anda
dapat mengembangkannya dan melihat log dan atributnya.
Kemudian Anda bisa mendapatkan rentang yang sama dari konteks jika Anda tidak ingin menyetel yang baru. Misalnya, Anda ingin menulis satu lapisan arsitektural dalam satu rentang, dan lapisan Anda tersebar di beberapa metode dan beberapa tingkat panggilan. Anda mendapatkannya, menulis padanya, dan kemudian ditutup.
func doSomeAction(ctx context.Context, requestID string) { span := trace.SpanFromContext(ctx) span.AddEvent(ctx, "I am the same span!") ... }
Perhatikan bahwa Anda tidak perlu menutupnya di sini, karena ini akan ditutup dengan metode yang sama di mana ia dibuat. Kami hanya mengeluarkannya dari konteks.
Menulis pesan ke root span:
Terkadang Anda perlu membuat span anak baru agar ada secara terpisah.
func doSomeAction(ctx context.Context, requestID string) { ctx, span := global.Tracer("github.com/me/otel-demo"). Start(ctx, "child") defer span.End() span.AddEvent(ctx, "I am a child span!") ... }
Di sini kita mendapatkan pustaka bernama pelacak global. Panggilan ini dapat digabungkan dalam beberapa metode, atau Anda dapat menggunakan variabel global, karena akan sama di seluruh layanan Anda.
Selanjutnya, rentang anak dibuat dari konteks, dan sebuah nama diberikan padanya, mirip dengan yang kita lakukan di awal:
Start(ctx, "child")
Ingatlah untuk menutup span di akhir metode pembuatannya.
ctx, span := global.Tracer("github.com/me/otel-demo"). Start(ctx, "child") defer span.End()
Kami menulis pesan ke dalamnya yang termasuk dalam rentang anak.
Di sini Anda dapat melihat bahwa pesan ditampilkan secara hierarki dan rentang anak berada di bawah induk. Ini diharapkan lebih pendek karena itu adalah panggilan sinkron.
Ini menunjukkan atribut yang dapat ditulis dalam span:
func doSomeAction(ctx context.Context, requestID string) { ... span.SetAttributes(label.String("request.id", requestID)) span.AddEvent(ctx, "request validation ok") span.AddEvent(ctx, "entities loaded", label.Int64("count", 123)) span.SetStatus(codes.Error, "insertion error") }
Misalnya, permintaan kami sampai di sini. id:
Anda dapat menambahkan acara:
span.AddEvent(ctx, "request validation ok")
Selain itu, Anda dapat menambahkan label di sini. Ini bekerja dengan cara yang sama seperti log terstruktur dalam bentuk logrus:
span.AddEvent(ctx, "entities loaded", label.Int64("count", 123))
Di sini kita melihat pesan kita di log span. Anda dapat mengembangkannya dan melihat label. Dalam kasus kami, jumlah label telah ditambahkan di sini:
Maka akan lebih mudah untuk menggunakannya saat memfilter dalam pencarian.
Jika terjadi kesalahan, Anda dapat menambahkan status ke span. Dalam kasus ini, itu akan ditandai sebagai tidak valid.
span.SetStatus(codes.Error, "insertion error")
Standar yang digunakan untuk menggunakan kode kesalahan dari OpenCensus dan berasal dari grpc. Sekarang hanya OK, ERROR dan UNSET yang tersisa. OK adalah defaultnya, ERROR ditambahkan jika terjadi kesalahan.
Di sini Anda dapat melihat bahwa jejak kesalahan ditandai dengan ikon merah. Ada kode kesalahan dan pesan tentangnya:
Kita tidak boleh lupa bahwa pelacakan bukanlah pengganti log. Poin utamanya adalah melacak aliran informasi melalui sistem terdistribusi, dan untuk ini Anda perlu membuat jejak dalam permintaan jaringan dan dapat membacanya dari sana.
Trace Microservices
OpenTelemetry sudah memiliki banyak implementasi pihak yang diatur dari interseptor dan middleware untuk berbagai kerangka kerja dan pustaka. Mereka dapat ditemukan di repositori: github.com/open-telemetry/opentelemetry-go-contrib
Daftar framework yang memiliki interseptor dan middleware:
- beego
- tenang
- gin
- gocql
- mux
- gema
- http
- grpc
- sarama
- memcache
- mongo
- macaron
Mari kita lihat bagaimana menggunakan ini menggunakan klien dan server http standar sebagai contoh.
middleware client
Di klien, kami hanya menambahkan interseptor sebagai transportasi, setelah itu permintaan kami diperkaya dengan trace.id dan informasi yang diperlukan untuk melanjutkan penelusuran.
client := http.Client{ Transport: otelhttp.NewTransport(http.DefaultTransport), } req, _ := http.NewRequestWithContext(ctx, "GET", url, nil) resp, err := client.Do(req)
middleware server
Sebuah middleware kecil dengan nama perpustakaan ditambahkan di server:
http.Handle("/", otelhttp.NewHandler( http.HandlerFunc(get), "root")) err := http.ListenAndServe(addr, nil)
Kemudian, seperti biasa: dapatkan span dari konteksnya, kerjakan dengannya, tulis sesuatu ke dalamnya, buat child span, tutup, dll.
Beginilah tampilan permintaan sederhana, melewati tiga layanan:
Tangkapan layar menunjukkan hierarki panggilan, pembagian ke dalam layanan, durasinya, urutannya. Anda dapat mengklik masing-masing dan melihat informasi yang lebih detail.
Dan seperti inilah tampilan kesalahannya:
Sangat mudah untuk melacak di mana terjadinya , kapan dan berapa lama waktu telah berlalu.
Dalam span, Anda dapat melihat informasi mendetail tentang konteks di mana kesalahan terjadi:
Selain itu, bidang yang merujuk ke seluruh rentang (berbagai id permintaan, bidang kunci dalam tabel dalam permintaan, beberapa data meta lain yang ingin Anda letakkan) dapat disarangkan dalam span saat dibuat. Secara kasar, Anda tidak perlu menyalin dan menempel semua bidang ini ke setiap tempat Anda menangani kesalahan. Anda dapat menulis data tentang itu untuk direntangkan.
fungsi middleware
Berikut adalah sedikit bonusnya: bagaimana membuat middleware sehingga Anda dapat menggunakannya sebagai middleware global untuk hal-hal seperti Gorilla dan Gin:
middleware := func(h http.Handler) http.Handler { return otelhttp.NewHandler(h, "root") }
Instrumentasi Aplikasi Golang: Monitoring
Saatnya berbicara tentang monitoring.
Koneksi ke sistem pemantauan dikonfigurasikan dengan cara yang sama seperti untuk penelusuran.
Pengukuran dibagi menjadi dua jenis:
1. Sinkron, ketika pengguna secara eksplisit mengirimkan nilai pada saat panggilan:
- Melawan
- UpDownCounter
- ValueRecorder
int64, float64
2. Asynchronous, yang dibaca SDK pada saat pengumpulan data dari aplikasi:
- SumObserver
- UpDownSumObserver
- ValueObserver
int64, float64
Metrik itu sendiri adalah:
- Aditif dan monoton (Counter, SumObserver) yang menjumlahkan bilangan positif dan tidak berkurang.
- Aditif tetapi tidak monoton (UpDownCounter, UpDownSumObserver), yang dapat menjumlahkan bilangan positif dan negatif.
- Non-aditif (ValueRecorder, ValueObserver) yang hanya merekam urutan nilai. Misalnya, semacam distribusi.
Di awal program, pengukur global dibuat, yang nama perpustakaan atau layanannya ditunjukkan.
meter := global.Meter("github.com/ilyakaznacheev/otel-demo") floatCounter := metric.Must(meter).NewFloat64Counter( "float_counter", metric.WithDescription("Cumulative float counter"), ).Bind(label.String("label_a", "some label")) defer floatCounter.Unbind()
Selanjutnya, metrik dibuat:
floatCounter := metric.Must(meter).NewFloat64Counter( "float_counter", metric.WithDescription("Cumulative float counter"), ).Bind(label.String("label_a", "some label"))
Dia diberi nama:
"float_counter",
Deskripsi:
… metric.WithDescription("Cumulative float counter"), …
Sekumpulan label yang selanjutnya Anda dapat memfilter permintaan. Misalnya, saat membuat dasbor di Grafana:
… ).Bind(label.String("label_a", "some label")) …
Di akhir program, Anda juga perlu memanggil Lepaskan untuk setiap metrik, yang akan mengosongkan sumber daya dan menutupnya dengan benar:
… defer floatCounter.Unbind() …
Mencatat perubahan itu sederhana:
var ( counter metric.BoundFloat64Counter udCounter metric.BoundFloat64UpDownCounter valueRecorder metric.BoundFloat64ValueRecorder ) ... counter.Add(ctx, 1.5) udCounter.Add(ctx, -2.5) valueRecorder.Record(ctx, 3.5)
Ini adalah angka positif untuk Counter, angka untuk UpDownCounter yang akan dijumlahkan, dan juga angka untuk ValueRecorder. Untuk semua jenis instrumen, Go mendukung int64 dan float64.
Inilah yang kami dapatkan pada output:
# HELP float_counter Cumulative float counter # TYPE float_counter counter float_counter{label_a="some label"} 20
Ini adalah metrik kami dengan komentar dan label tertentu. Kemudian Anda dapat mengambilnya langsung melalui Prometheus, atau mengekspornya melalui pengumpul OpenTelemetry, lalu menggunakannya di mana pun kami membutuhkannya.
Instrumentasi Aplikasi Golang: Perpustakaan
Hal terakhir yang ingin saya katakan adalah kemampuan yang disediakan standar untuk perpustakaan instrumen.
Sebelumnya, saat menggunakan OpenCensus dan OpenTracing, Anda tidak dapat melengkapi pustaka individual Anda, terutama pustaka sumber terbuka. Karena dalam kasus ini, Anda mendapatkan vendor lock-in. Siapa pun yang telah bekerja sama dengan pelacakan mungkin memperhatikan fakta bahwa pustaka klien yang besar, atau API besar untuk layanan cloud, dari waktu ke waktu mengalami crash dengan kesalahan yang sulit dijelaskan.
Menelusuri akan sangat berguna di sini. Terutama dalam produktivitas, ketika Anda memiliki semacam situasi yang tidak jelas, dan saya benar-benar ingin tahu mengapa itu terjadi. Tapi yang Anda miliki hanyalah pesan kesalahan dari perpustakaan yang Anda impor.
OpenTelemetry memecahkan masalah ini.
Karena SDK dan API dipisahkan dalam standar, API pelacakan metrik dapat digunakan terlepas dari SDK dan setelan ekspor data tertentu. Selain itu, pertama-tama Anda dapat melengkapi metode Anda, dan baru kemudian mengonfigurasi ekspor data ini ke luar.
Dengan cara ini, Anda dapat melengkapi pustaka yang diimpor tanpa mengkhawatirkan tentang bagaimana dan di mana data akan diekspor. Ini akan berfungsi untuk pustaka internal dan sumber terbuka.
Tidak perlu khawatir tentang penguncian vendor, tidak perlu khawatir tentang bagaimana informasi ini akan digunakan atau apakah akan digunakan sama sekali. Pustaka dan aplikasi diinstrumentasi terlebih dahulu, dan konfigurasi ekspor data ditentukan saat aplikasi diinisialisasi.
Dengan demikian, Anda dapat melihat bahwa pengaturan konfigurasi ditetapkan dalam aplikasi SDK. Selanjutnya, Anda perlu berurusan dengan eksportir pelacakan dan metrik. Ini bisa menjadi satu eksportir melalui OTLP jika Anda mengekspor ke kolektor OpenTelemetry. Kemudian semua jejak dan metrik yang diperlukan masuk ke dalam konteks, dan ini disebarkan ke pohon panggilan dengan metode lain.
Aplikasi mewarisi sisa span dari span root, cukup menggunakan OpenTelemetry API dan data yang ada dalam konteksnya. Dalam kasus ini, pustaka yang diimpor menerima metode konteks sebagai masukan, coba baca informasi tentang rentang akar dari metode ini. Jika tidak ada, mereka membuatnya sendiri, dan kemudian mereka menginstruksikan logika. Dengan cara ini Anda dapat melengkapi perpustakaan Anda terlebih dahulu.
Selain itu, Anda dapat melengkapi semuanya, tetapi tidak mengonfigurasi eksportir data, dan hanya menerapkannya.
Ini dapat bekerja untuk Anda dalam produksi, dan hingga infrastruktur diselesaikan, Anda tidak akan memiliki konfigurasi pelacakan dan pemantauan. Kemudian Anda mengonfigurasinya, menerapkan kolektor di sana, beberapa aplikasi untuk mengumpulkan data ini, dan semuanya akan bekerja untuk Anda. Anda tidak perlu mengubah apa pun secara langsung dalam metode itu sendiri.
Jadi, jika Anda memiliki pustaka sumber terbuka, Anda dapat melengkapi menggunakan OpenTelemetry. Kemudian orang yang menggunakannya akan mengkonfigurasi OpenTelemetry dan menggunakan data ini.
Sebagai kesimpulan, saya ingin mengatakan bahwa standar OpenTelemetry cukup menjanjikan. Mungkin, akhirnya, ini adalah standar universal yang sama yang ingin kita lihat.
Perusahaan kami secara aktif menggunakan standar OpenCensus untuk melacak dan memantau lanskap layanan mikro perusahaan. Direncanakan untuk mengimplementasikan OpenTelemetry setelah dirilis.