Distributed Tracing dengan Jaeger di Go

Distributed tracing digunakan oleh software engineer untuk memonitor atau men-debug aplikasi. Ini sangat berguna untuk menemukan proses mana yang memakan waktu paling banyak, atau fungsi apa yang paling banyak terjadi error. Salah satu system untuk melakukan distributed tracing adalah Jaeger. Artikel ini akan menunjukkan bagaimana menjalankan Jaeger di local environment dan melakukan tracing aplikasi Go.

Apa itu distributed tracing?

Berdasarkan opentracing.io, distributed tracing adalah metode untuk mem-profile dan memonitor aplikasi, terutama aplikasi-aplikasi yang dibuat menggunakan architecture microservice. Distributed tracing dapat sangat berguna ketika terjadi performance issue pada aplikasi kita, atau ketika kita ingin mengimprove performa aplikasi kita. Ini juga dapat berguna untuk melakukan root cause analysis dari suatu masalah. Trace direpresentasikan sebagai rangkaian dari fungsi-fungsi yang dipanggil. Sehingga kita dapat men-debug aplikasi kita lebih mudah, atau mungkin dapat menemukan flow yang tidak seharusnya terjadi pada aplikasi kita.

Jaeger adalah salah satu system yang paling populer untuk distributed tracing. Jaeger adalah open-source, end-to-end distributed tracing system. Jaeger di-release oleh Uber Technology. Jaeger menggunakan data model dan instrumentation libaries yang compatible dengan OpenTracing, sehingga API dan penggunaan distributed tracing terstandarisasi.

Jalankan jaeger

Untuk menjalankan Jaeger di local environment, kita dapat menggunakan all in one Jaeger docker image. Untuk metode deployment lainnya, bisa lihat di https://www.jaegertracing.io/docs/1.22/deployment/.

docker run -d -p6831:6831/udp -p16686:16686 jaegertracing/all-in-one:latest

Image ini sudah berisi Jaeger UI, collector, query, dan agent, yang mana cukup untuk melakukan tracing aplikasi lokal kita. Pergi ke http://localhost:16686 untuk membuka Jaeger UI.

Jaeger UI

Setelah Jaeger berhasil kita jalankan, kita bisa mulai trace aplikasi kita. Pada contoh ini, kita akan tracing webserver dari Go.

Distributed tracing aplikasi Go

Initialization

Untuk melakukan tracing suatu aplikasi Go, kita perlu menginisialisasi tracer nya terlebih dahulu. Code dibawah ini untuk menginisialisasi Jaeger tracer:

1
2
3
4
5
6
7
8
cfg := config.Configuration{
    Sampler: &config.SamplerConfig{
        Type:  jaeger.SamplerTypeRateLimiting,
        Param: 100,
    },
}

tracer, closer, err := cfg.New("myservice")

Pengaturan sampler yang digunakan disini adalah rate-limiting dengan param 100. Ini berarti Jaeger akan mengumpulkan maksimum 100 trace per detik. Kita bisa mengubah tipe dan param yang cocok untuk keperluan kita.
Kemudian set global tracer dengan Jaeger tracer.

1
opentracing.SetGlobalTracer(tracer)
Kita bisa menginisialisasi Jaeger tracer di function main dari aplikasi kita.

Trace the processes

Untuk men-trace function-function di aplikasi, kita perlu membuat sebuah opentracing span dari suatu context pada awal fungsi dan panggil method Finish dari span tersebut sebelum keluar dari fungsi. Yang perlu diperhatikan adalah kita harus melempar context yang dibuat ke fungsi yang dipanggil selanjutnya, supaya Jaeger tau kalau fungsi-fungsi tersebut merupakan satu rangkaian. Di bawah ini adalah contoh tracing sebuah HTTP handler.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func getCityHandler(w http.ResponseWriter, r *http.Request) {
    span, ctx := opentracing.StartSpanFromContext(r.Context(), "Handle /get_cities")
    defer span.Finish()

    if !isLoggedIn(ctx, r) {
        w.Write([]byte("you need to login first"))
        return
    }

    countryName := r.FormValue("country_name")
    cities, err := getCityByCountryName(ctx, countryName)
    ...
Pada line 2-3 kita buat opentracing span dari context dari http.Request, lalu menaruh span.Finish() dengan defer. Bisa dilihat kita menggunakan ctx yang dibuat di line 2 untuk memanggil fungsi isLoggedIn di line 5 dan getCityByCountryName di line 11. Fungsi-fungsi tersebut juga membuat opentracing dan memanggil finish sebelum keluar fungsi.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func isLoggedIn(ctx context.Context, r *http.Request) bool {
    span, ctx := opentracing.StartSpanFromContext(ctx, "isLoggedIn")
    defer span.Finish()
    ...


func getCityByCountryName(ctx context.Context, countryName string) ([]City, error) {
    span, ctx := opentracing.StartSpanFromContext(ctx, "getCityByCountryName")
    defer span.Finish()
    ...

Selanjutnya, kita akan coba memanggil fungsi tersebut beberapa kali dan melihat trace nya di Jaeger UI. Buka Jaeger UI di browser dan click Find Traces.

Jaeger UI Traces

Click salah satu trace untuk melihat detail nya.

Jaeger UI Trace detail

Kita dapat melihat trace tersebut dimulai dari HTTP handler, kemudian ke fungsi-fungsi yang dia panggil. Panjang span merepresentasikan lama waktu yang dibutuhkan fungsi tersebut. Coba explore Jaeger UI dan kau akan merasakan betapa berguna dan sangat membantunya jaeger untuk kita.

Kesimpulan

Distributed tracing sangat berguna untuk memonitor aplikasimu. Dia dapat menampilkan trace sebagai suatu rangkaian fungsi-fungsi yang dipanggil sehingga dapat membantumu men-debug aplikasi lebih mudah. Jaeger adalah salah satu system yang paling populer untuk distributed tracing. Mudah dijalankan dan digunakan, dan mengimplementasi opentracing. Kita dapat melihat hasil tracing di Jaeger UI dan menemukan berapa lama waktu yang diperlukan setiap fungsi. Ini mempermudah kita menemukan fungsi yang membuat bottleneck.
Sample code lengkap yang digunakan di sini dapat dilihat di bawah.

The complete code

The complete code is here (click to expand)
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
package main

import (
	"context"
	"database/sql"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"sync"
	"time"

	_ "github.com/go-sql-driver/mysql"
	"github.com/opentracing/opentracing-go"
	"github.com/uber/jaeger-client-go"
	"github.com/uber/jaeger-client-go/config"
)

var db *sql.DB

func main() {
	dtbs, err := sql.Open("mysql", "myuser:password@/mydb")
	if err != nil {
		panic(err.Error())
	}
	db = dtbs

	tracer, trCloser, err := InitJaeger()
	if err != nil {
		fmt.Printf("error init jaeger %v", err)
	} else {
		opentracing.SetGlobalTracer(tracer)
		defer trCloser.Close()
	}

	http.HandleFunc("/get_cities", getCityHandler)

	fmt.Println("listening..")
	http.ListenAndServe(":4560", nil)
}

func InitJaeger() (opentracing.Tracer, io.Closer, error) {
	cfg := config.Configuration{
		Sampler: &config.SamplerConfig{
			Type:  jaeger.SamplerTypeRateLimiting,
			Param: 100,
		},
	}

	tracer, closer, err := cfg.New("myservice")
	return tracer, closer, err
}

func getCityHandler(w http.ResponseWriter, r *http.Request) {
	span, ctx := opentracing.StartSpanFromContext(r.Context(), "Handle /get_cities")
	defer span.Finish()

	if !isLoggedIn(ctx, r) {
		w.Write([]byte("you need to login first"))
		return
	}

	countryName := r.FormValue("country_name")
	cities, err := getCityByCountryName(ctx, countryName)
	if err != nil {
		w.Write([]byte("got error: " + err.Error()))
		return
	}

	for k, v := range cities {
		lat, long, err := getGeoposition(ctx, v.Name)
		if err != nil {
			w.Write([]byte("got error: " + err.Error()))
			return
		}
		cities[k].Lat = lat
		cities[k].Long = long
	}

	b, err := json.Marshal(cities)
	if err != nil {
		w.Write([]byte("got error: " + err.Error()))
		return
	}

	w.Write(b)
}

type geopos struct {
	Standard struct {
		City string `json:"city"`
	} `json:"standard"`
	Longt string `json:"longt"`
	Latt  string `json:"latt"`
}

func getGeoposition(ctx context.Context, cityName string) (lat, long string, err error) {
	span, ctx := opentracing.StartSpanFromContext(ctx, "getGeoposition")
	defer span.Finish()

	url := fmt.Sprintf("https://geocode.xyz/%s?json=1", cityName)
	resp, err := http.Get(url)
	if err != nil {
		return "", "", err
	}
	defer resp.Body.Close()

	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return "", "", err
	}

	g := geopos{}
	err = json.Unmarshal(body, &g)
	if err != nil {
		return "", "", err
	}

	return g.Latt, g.Longt, nil
}

type City struct {
	Name        string `json:"name"`
	CountryName string `json:"country_name"`
	Lat         string `json:"lat"`
	Long        string `json:"long"`
}

func getCityByCountryName(ctx context.Context, countryName string) ([]City, error) {
	span, ctx := opentracing.StartSpanFromContext(ctx, "getCityByCountryName")
	defer span.Finish()

	cached := getCityByCountryNameFromCache(ctx, countryName)
	if cached != nil {
		return *cached, nil
	}

	cities, err := getCityByCountryNameFromDB(ctx, countryName)
	if err != nil {
		return nil, err
	}

	setCityByCountryNameFromCache(ctx, countryName, cities)
	return cities, nil
}

func getCityByCountryNameFromDB(ctx context.Context, countryName string) ([]City, error) {
	span, ctx := opentracing.StartSpanFromContext(ctx, "getCityByCountryNameFromDB")
	defer span.Finish()

	rows, err := db.QueryContext(ctx, "SELECT name, country_name FROM city WHERE country_name = ?", countryName)
	if err != nil {
		return nil, err
	}

	var cities []City
	for rows.Next() {
		var city City
		err := rows.Scan(&city.Name, &city.CountryName)
		if err != nil {
			return nil, err
		}
		cities = append(cities, city)
	}
	return cities, nil
}

func isLoggedIn(ctx context.Context, r *http.Request) bool {
	span, ctx := opentracing.StartSpanFromContext(ctx, "isLoggedIn")
	defer span.Finish()

	time.Sleep(5 * time.Millisecond)
	return true
}

var cachedCityByCountry = map[string]*[]City{}
var cachedCityByCountryLock sync.RWMutex

func getCityByCountryNameFromCache(ctx context.Context, countryName string) *[]City {
	span, ctx := opentracing.StartSpanFromContext(ctx, "getCityByCountryNameFromCache")
	defer span.Finish()

	cachedCityByCountryLock.RLock()
	defer cachedCityByCountryLock.RUnlock()

	return cachedCityByCountry[countryName]
}

func setCityByCountryNameFromCache(ctx context.Context, countryName string, cities []City) {
	span, ctx := opentracing.StartSpanFromContext(ctx, "setCityByCountryNameFromCache")
	defer span.Finish()

	cachedCityByCountryLock.Lock()
	defer cachedCityByCountryLock.Unlock()

	cachedCityByCountry[countryName] = &cities
}

See also