Spring Cloud Sleuth dalam Aplikasi Monolith

1. Ikhtisar

Dalam artikel ini, kami memperkenalkan Spring Cloud Sleuth - alat yang ampuh untuk meningkatkan log di aplikasi apa pun, tetapi terutama dalam sistem yang dibangun dari beberapa layanan.

Dan untuk artikel ini, kami akan fokus menggunakan Sleuth dalam aplikasi monolit, bukan di seluruh layanan mikro .

Kita semua pernah mengalami pengalaman yang tidak menguntungkan saat mencoba mendiagnosis masalah dengan tugas terjadwal, operasi multi-thread, atau permintaan web yang kompleks. Seringkali, meskipun ada logging, sulit untuk mengatakan tindakan apa yang perlu dikorelasikan bersama untuk membuat satu permintaan.

Hal ini dapat membuat diagnosis tindakan kompleks menjadi sangat sulit atau bahkan tidak mungkin. Seringkali menghasilkan solusi seperti meneruskan id unik ke setiap metode dalam permintaan untuk mengidentifikasi log.

Datang Sleuth . Pustaka ini memungkinkan untuk mengidentifikasi log yang berkaitan dengan pekerjaan, utas, atau permintaan tertentu. Sleuth terintegrasi dengan mudah dengan kerangka kerja logging seperti Logback dan SLF4J untuk menambahkan pengenal unik yang membantu melacak dan mendiagnosis masalah menggunakan log.

Mari kita lihat cara kerjanya.

2. Penyiapan

Kami akan mulai dengan membuat proyek web Spring Boot di IDE favorit kami dan menambahkan ketergantungan ini ke file pom.xml kami :

 org.springframework.cloud spring-cloud-starter-sleuth 

Aplikasi kami berjalan dengan Spring Boot dan pom induk menyediakan versi untuk setiap entri. Versi terbaru dari ketergantungan ini dapat ditemukan di sini: spring-cloud-starter-sleuth. Untuk melihat seluruh POM, lihat proyek di Github.

Selain itu, mari tambahkan nama aplikasi untuk menginstruksikan Sleuth mengidentifikasi log aplikasi ini.

Dalam file application.properties kami, tambahkan baris ini:

spring.application.name=Baeldung Sleuth Tutorial

3. Konfigurasi Sleuth

Sleuth mampu meningkatkan log dalam banyak situasi. Dimulai dengan versi 2.0.0, Spring Cloud Sleuth menggunakan Brave sebagai pustaka pelacakan yang menambahkan id unik ke setiap permintaan web yang masuk ke aplikasi kita. Selain itu, tim Spring telah menambahkan dukungan untuk membagikan ID ini melintasi batas utas.

Jejak dapat dianggap seperti satu permintaan atau pekerjaan yang dipicu dalam aplikasi. Semua berbagai langkah dalam permintaan itu, bahkan di seluruh aplikasi dan batasan utas, akan memiliki traceId yang sama.

Span, di sisi lain, dapat dianggap sebagai bagian dari pekerjaan atau permintaan. Jejak tunggal dapat terdiri dari beberapa span yang masing-masing berkorelasi dengan langkah atau bagian tertentu dari permintaan. Dengan menggunakan trace dan id span, kita dapat menunjukkan dengan tepat kapan dan di mana aplikasi kita berada saat memproses permintaan. Membuat pembacaan log kami jauh lebih mudah.

Dalam contoh kami, kami akan mengeksplorasi kemampuan ini dalam satu aplikasi.

3.1. Permintaan Web Sederhana

Pertama, mari buat kelas pengontrol untuk menjadi titik masuk untuk bekerja dengan:

@RestController public class SleuthController { @GetMapping("/") public String helloSleuth() { logger.info("Hello Sleuth"); return "success"; } }

Jalankan aplikasi kita dan arahkan ke "// localhost: 8080". Perhatikan log untuk keluaran yang terlihat seperti:

2017-01-10 22:36:38.254 INFO [Baeldung Sleuth Tutorial,4e30f7340b3fb631,4e30f7340b3fb631,false] 12516 --- [nio-8080-exec-1] c.b.spring.session.SleuthController : Hello Sleuth

Ini terlihat seperti log normal, kecuali untuk bagian di awal antara tanda kurung. Ini adalah informasi inti yang ditambahkan Spring Sleuth . Data ini mengikuti format:

[nama aplikasi, traceId, spanId, ekspor]

  • Nama aplikasi - Ini adalah nama yang kami tetapkan di file properti dan dapat digunakan untuk menggabungkan log dari beberapa contoh aplikasi yang sama.
  • TraceId - Ini adalah id yang ditetapkan ke satu permintaan, pekerjaan, atau tindakan. Sesuatu seperti setiap permintaan web yang dimulai oleh pengguna unik akan memiliki traceId -nya sendiri .
  • SpanId - Melacak unit kerja. Pikirkan permintaan yang terdiri dari beberapa langkah. Setiap langkah dapat memiliki spanId sendiri dan dilacak secara individual. Secara default, aliran aplikasi apa pun akan dimulai dengan TraceId dan SpanId yang sama.
  • Ekspor - Properti ini berupa boolean yang menunjukkan apakah log ini diekspor ke aggregator seperti Zipkin atau tidak . Zipkin berada di luar cakupan artikel ini, tetapi memainkan peran penting dalam menganalisis log yang dibuat oleh Sleuth .

Sekarang, Anda seharusnya sudah memiliki gambaran tentang kekuatan perpustakaan ini. Mari kita lihat contoh lain untuk lebih mendemonstrasikan bagaimana integral perpustakaan ini untuk logging.

3.2. Permintaan Web Sederhana Dengan Akses Layanan

Mari kita mulai dengan membuat layanan dengan satu metode:

@Service public class SleuthService { public void doSomeWorkSameSpan() { Thread.sleep(1000L); logger.info("Doing some work"); } }

Sekarang mari menyuntikkan layanan kita ke pengontrol kita dan menambahkan metode pemetaan permintaan yang mengaksesnya:

@Autowired private SleuthService sleuthService; @GetMapping("/same-span") public String helloSleuthSameSpan() throws InterruptedException { logger.info("Same Span"); sleuthService.doSomeWorkSameSpan(); return "success"; }

Terakhir, restart aplikasi dan arahkan ke "// localhost: 8080 / same-span". Perhatikan keluaran log yang terlihat seperti:

2017-01-10 22:51:47.664 INFO [Baeldung Sleuth Tutorial,b77a5ea79036d5b9,b77a5ea79036d5b9,false] 12516 --- [nio-8080-exec-3] c.b.spring.session.SleuthController : Same Span 2017-01-10 22:51:48.664 INFO [Baeldung Sleuth Tutorial,b77a5ea79036d5b9,b77a5ea79036d5b9,false] 12516 --- [nio-8080-exec-3] c.baeldung.spring.session.SleuthService : Doing some work

Perhatikan bahwa ID pelacakan dan span sama di antara kedua log meskipun pesan berasal dari dua kelas yang berbeda. Ini membuatnya mudah untuk mengidentifikasi setiap log selama permintaan dengan mencari traceId dari permintaan tersebut.

Ini adalah perilaku default, satu permintaan mendapat satu traceId dan spanId . Tapi kita bisa menambahkan span secara manual sesuai keinginan kita. Mari kita lihat contoh yang menggunakan fitur ini.

3.3. Menambahkan Span Secara Manual

Untuk memulai, mari tambahkan pengontrol baru:

@GetMapping("/new-span") public String helloSleuthNewSpan() { logger.info("New Span"); sleuthService.doSomeWorkNewSpan(); return "success"; }

Dan sekarang mari tambahkan metode baru di dalam layanan kita:

@Autowired private Tracer tracer; // ... public void doSomeWorkNewSpan() throws InterruptedException { logger.info("I'm in the original span"); Span newSpan = tracer.nextSpan().name("newSpan").start(); try (SpanInScope ws = tracer.withSpanInScope(newSpan.start())) { Thread.sleep(1000L); logger.info("I'm in the new span doing some cool work that needs its own span"); } finally { newSpan.finish(); } logger.info("I'm in the original span"); }

Note that we also added a new object, Tracer. The tracer instance is created by Spring Sleuth during startup and is made available to our class through dependency injection.

Traces must be manually started and stopped. To accomplish this, code that runs in a manually created span is placed inside a try-finally block to ensure the span is closed regardless of the operation's success. Also, notice that new span has to be placed in scope.

Restart the application and navigate to “//localhost:8080/new-span”. Watch for the log output that looks like:

2017-01-11 21:07:54.924 INFO [Baeldung Sleuth Tutorial,9cdebbffe8bbbade,9cdebbffe8bbbade,false] 12516 --- [nio-8080-exec-6] c.b.spring.session.SleuthController : New Span 2017-01-11 21:07:54.924 INFO [Baeldung Sleuth Tutorial,9cdebbffe8bbbade,9cdebbffe8bbbade,false] 12516 --- [nio-8080-exec-6] c.baeldung.spring.session.SleuthService : I'm in the original span 2017-01-11 21:07:55.924 INFO [Baeldung Sleuth Tutorial,9cdebbffe8bbbade,1e706f252a0ee9c2,false] 12516 --- [nio-8080-exec-6] c.baeldung.spring.session.SleuthService : I'm in the new span doing some cool work that needs its own span 2017-01-11 21:07:55.924 INFO [Baeldung Sleuth Tutorial,9cdebbffe8bbbade,9cdebbffe8bbbade,false] 12516 --- [nio-8080-exec-6] c.baeldung.spring.session.SleuthService : I'm in the original span

We can see that the third log shares the traceId with the others, but it has a unique spanId. This can be used to locate different sections in a single request for more fine-grained tracing.

Now let's take a look at Sleuth's support for threads.

3.4. Spanning Runnables

To demonstrate the threading capabilities of Sleuth let's first add a configuration class to set up a thread pool:

@Configuration public class ThreadConfig { @Autowired private BeanFactory beanFactory; @Bean public Executor executor() { ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor(); threadPoolTaskExecutor.setCorePoolSize(1); threadPoolTaskExecutor.setMaxPoolSize(1); threadPoolTaskExecutor.initialize(); return new LazyTraceExecutor(beanFactory, threadPoolTaskExecutor); } }

It is important to note here the use of LazyTraceExecutor. This class comes from the Sleuth library and is a special kind of executor that will propagate our traceIds to new threads and create new spanIds in the process.

Now let's wire this executor into our controller and use it in a new request mapping method:

@Autowired private Executor executor; @GetMapping("/new-thread") public String helloSleuthNewThread() { logger.info("New Thread"); Runnable runnable = () -> { try { Thread.sleep(1000L); } catch (InterruptedException e) { e.printStackTrace(); } logger.info("I'm inside the new thread - with a new span"); }; executor.execute(runnable); logger.info("I'm done - with the original span"); return "success"; }

With our runnable in place, let's restart our application and navigate to “//localhost:8080/new-thread”. Watch for log output that looks like:

2017-01-11 21:18:15.949 INFO [Baeldung Sleuth Tutorial,96076a78343c364d,96076a78343c364d,false] 12516 --- [nio-8080-exec-9] c.b.spring.session.SleuthController : New Thread 2017-01-11 21:18:15.950 INFO [Baeldung Sleuth Tutorial,96076a78343c364d,96076a78343c364d,false] 12516 --- [nio-8080-exec-9] c.b.spring.session.SleuthController : I'm done - with the original span 2017-01-11 21:18:16.953 INFO [Baeldung Sleuth Tutorial,96076a78343c364d,e3b6a68013ddfeea,false] 12516 --- [lTaskExecutor-1] c.b.spring.session.SleuthController : I'm inside the new thread - with a new span

Much like the previous example we can see that all the logs share the same traceId. But the log coming from the runnable has a unique span that will track the work done in that thread. Remember that this happens because of the LazyTraceExecutor, if we were to use a normal executor we would continue to see the same spanId used in the new thread.

Now let's look into Sleuth's support for @Async methods.

3.5. @Async Support

To add async support let's first modify our ThreadConfig class to enable this feature:

@Configuration @EnableAsync public class ThreadConfig extends AsyncConfigurerSupport { //... @Override public Executor getAsyncExecutor() { ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor(); threadPoolTaskExecutor.setCorePoolSize(1); threadPoolTaskExecutor.setMaxPoolSize(1); threadPoolTaskExecutor.initialize(); return new LazyTraceExecutor(beanFactory, threadPoolTaskExecutor); } }

Note that we extend AsyncConfigurerSupport to specify our async executor and use LazyTraceExecutor to ensure traceIds and spanIds are propagated correctly. We have also added @EnableAsync to the top of our class.

Let's now add an async method to our service:

@Async public void asyncMethod() { logger.info("Start Async Method"); Thread.sleep(1000L); logger.info("End Async Method"); }

Now let's call into this method from our controller:

@GetMapping("/async") public String helloSleuthAsync() { logger.info("Before Async Method Call"); sleuthService.asyncMethod(); logger.info("After Async Method Call"); return "success"; }

Finally, let's restart our service and navigate to “//localhost:8080/async”. Watch for the log output that looks like:

2017-01-11 21:30:40.621 INFO [Baeldung Sleuth Tutorial,c187f81915377fff,c187f81915377fff,false] 10072 --- [nio-8080-exec-2] c.b.spring.session.SleuthController : Before Async Method Call 2017-01-11 21:30:40.622 INFO [Baeldung Sleuth Tutorial,c187f81915377fff,c187f81915377fff,false] 10072 --- [nio-8080-exec-2] c.b.spring.session.SleuthController : After Async Method Call 2017-01-11 21:30:40.622 INFO [Baeldung Sleuth Tutorial,c187f81915377fff,8a9f3f097dca6a9e,false] 10072 --- [lTaskExecutor-1] c.baeldung.spring.session.SleuthService : Start Async Method 2017-01-11 21:30:41.622 INFO [Baeldung Sleuth Tutorial,c187f81915377fff,8a9f3f097dca6a9e,false] 10072 --- [lTaskExecutor-1] c.baeldung.spring.session.SleuthService : End Async Method

We can see here that much like our runnable example, Sleuth propagates the traceId into the async method and adds a unique spanId.

Let's now work through an example using spring support for scheduled tasks.

3.6. @Scheduled Support

Finally, let's look at how Sleuth works with @Scheduled methods. To do this let's update our ThreadConfig class to enable scheduling:

@Configuration @EnableAsync @EnableScheduling public class ThreadConfig extends AsyncConfigurerSupport implements SchedulingConfigurer { //... @Override public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) { scheduledTaskRegistrar.setScheduler(schedulingExecutor()); } @Bean(destroyMethod = "shutdown") public Executor schedulingExecutor() { return Executors.newScheduledThreadPool(1); } }

Note that we have implemented the SchedulingConfigurer interface and overridden its configureTasks method. We have also added @EnableScheduling to the top of our class.

Next, let's add a service for our scheduled tasks:

@Service public class SchedulingService { private Logger logger = LoggerFactory.getLogger(this.getClass()); @Autowired private SleuthService sleuthService; @Scheduled(fixedDelay = 30000) public void scheduledWork() throws InterruptedException { logger.info("Start some work from the scheduled task"); sleuthService.asyncMethod(); logger.info("End work from scheduled task"); } }

In this class, we have created a single scheduled task with a fixed delay of 30 seconds.

Let's now restart our application and wait for our task to be executed. Watch the console for output like this:

2017-01-11 21:30:58.866 INFO [Baeldung Sleuth Tutorial,3605f5deaea28df2,3605f5deaea28df2,false] 10072 --- [pool-1-thread-1] c.b.spring.session.SchedulingService : Start some work from the scheduled task 2017-01-11 21:30:58.866 INFO [Baeldung Sleuth Tutorial,3605f5deaea28df2,3605f5deaea28df2,false] 10072 --- [pool-1-thread-1] c.b.spring.session.SchedulingService : End work from scheduled task

We can see here that Sleuth has created new trace and span ids for our task. Each instance of a task will get it's own trace and span by default.

4. Conclusion

In conclusion, we have seen how Spring Sleuth can be used in a variety of situations inside a single web application. We can use this technology to easily correlate logs from a single request, even when that request spans multiple threads.

By now we can see how Spring Cloud Sleuth can help us keep our sanity when debugging a multi-threaded environment. By identifying each operation in a traceId and each step in a spanId we can really begin to break down our analysis of complex jobs in our logs.

Bahkan jika kita tidak menggunakan cloud, Spring Sleuth kemungkinan merupakan ketergantungan kritis di hampir semua proyek; itu mulus untuk diintegrasikan dan merupakan tambahan nilai yang sangat besar .

Dari sini Anda mungkin ingin menyelidiki fitur lain dari Sleuth . Ini dapat mendukung pelacakan dalam sistem terdistribusi menggunakan RestTemplate , di seluruh protokol perpesanan yang digunakan oleh RabbitMQ dan Redis , dan melalui gateway seperti Zuul.

Seperti biasa, Anda dapat menemukan kode sumber di Github.