{"id":21048,"date":"2024-03-15T14:23:23","date_gmt":"2024-03-15T13:23:23","guid":{"rendered":"https:\/\/trifork.nl\/blog\/?p=21048"},"modified":"2024-03-15T15:29:10","modified_gmt":"2024-03-15T14:29:10","slug":"spring-boot-observability-spring-batch-jobs","status":"publish","type":"post","link":"https:\/\/trifork.nl\/blog\/spring-boot-observability-spring-batch-jobs\/","title":{"rendered":"Spring Boot Observability: Spring Batch Jobs"},"content":{"rendered":"\n<h1 class=\"wp-block-heading\">Watching paint dry<\/h1>\n\n\n\n<p>In my <a href=\"https:\/\/trifork.nl\/blog\/spring-boot-observability-database\/\" target=\"_blank\" rel=\"noreferrer noopener\">previous blog post<\/a> I described how we provide insight into our database interactions inside Datadog for the subscription system that we develop for Nederlandse Loterij.<br>Processes executed by this system are often batch-oriented, which is why the system contains a dedicated service that uses the Spring Batch framework to execute these long-running jobs.<\/p>\n\n\n\n<p>Of course it then becomes important to be able to observe these executions: how long do the jobs take to run, are there failures, what logging is associated with these jobs, etc.<br>Spring Boot and Batch provide some metrics out of the box, but to really make the most effective use of the frameworks you need some tuning and some understanding of the components involved. In this post I want to showcase some things that we\u2019ve done to optimize the observability of our Spring Batch jobs.<\/p>\n\n\n\n<p>We are using Datadog, but these tips apply to other observability tools as well, esp. those that combine metrics with logging.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">First things first: ensuring consistent tag names<\/h1>\n\n\n\n<p>Spring Batch provides metrics for components at different levels: jobs, steps, chunks and items. With version 4 of the frameworks, the tags used in these different metrics to identify things like job names are unfortunately not consistent. Metrics for jobs have a \u201c<code>name<\/code>\u201d tag, but the step- and chunk-related metrics refer to a \u201c<code>job.name<\/code>\u201d.<br>This makes it harder to build a dashboard where you can easily filter by job name using a template variable.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Batch 4 vs Batch 5<\/h2>\n\n\n\n<p>Our team is still busy to complete the migration to Boot 3 and Batch 5. One of the reasons that this is far from trivial for us is that we\u2019re using a 3rd-party add-on called <a href=\"http:\/\/htmlpreview.github.io\/?https:\/\/github.com\/tuxdevelop\/spring-batch-lightmin\/blob\/2.2.x\/spring-batch-lightmin-documentation\/src\/main\/doc\/spring_batch_lightmin.html\" target=\"_blank\" rel=\"noreferrer noopener\">Lightmin<\/a>, which provides a nice Web UI for inspecting jobs and executions and also allows you to schedule these executions while storing the schedule in your database: functionality that\u2019s lacking in Spring Batch itself. <br>However, Lightmin isn\u2019t actively maintained anymore, and Batch 5 contains a ton of breaking changes. That means we have to effectively create a private fork and update everything ourselves.<\/p>\n\n\n\n<p>In the process of doing this I discovered that Batch 5 also changed its metric tag names: for jobs it now uses \u201c<code>spring.batch.job.name<\/code>\u201d, while in steps it uses \u201c<code>spring.batch.step.job.name<\/code>\u201d to identify the job. The tag name for names of steps changed as well. So these tags are still inconsistent, but now in a different way \ud83e\udd79<br>It also seems that the <a href=\"https:\/\/docs.spring.io\/spring-batch\/reference\/monitoring-and-metrics.html#built-in-metrics\" target=\"_blank\" rel=\"noreferrer noopener\">documentation hasn&#8217;t been updated<\/a> to reflect these changes yet.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Taking Control<\/h2>\n\n\n\n<p>To ensure that our metrics tags are consistent today with Batch 4 and will stay consistent when we update to Batch 5, I wrote some Micrometer meter filters:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: java; title: ; notranslate\" title=\"\">\n\/**\n* @see org.springframework.batch.core.observability.BatchJobObservation.JobLowCardinalityTags#JOB_NAME\n*\/\n@Bean\nMeterFilter jobJobNameTagRenamingMeterFilter() {\n   String prefix = &quot;spring.batch.job&quot;;\n   boolean batchV5 = ClassUtils.isPresent(&quot;org.springframework.batch.core.observability.BatchJobObservation&quot;, null);\n   String from = batchV5 ? prefix + &quot;.name&quot; : &quot;name&quot;;\n   return MeterFilter.renameTag(prefix, from, &quot;job.name&quot;);\n}\n\n\n\/**\n* @see org.springframework.batch.core.observability.BatchStepObservation.StepLowCardinalityTags#JOB_NAME\n*\/\n@Bean @ConditionalOnClass(name = &quot;org.springframework.batch.core.observability.BatchStepObservation&quot;)\nMeterFilter stepJobNameTagRenamingMeterFilter() {\n   String prefix = &quot;spring.batch.step&quot;;\n   return MeterFilter.renameTag(prefix, prefix + &quot;.job.name&quot;, &quot;job.name&quot;);\n}\n\n\n\/**\n* @see org.springframework.batch.core.observability.BatchStepObservation.StepLowCardinalityTags#STEP_NAME\n*\/\n@Bean\nMeterFilter stepStepNameTagRenamingMeterFilter() {\n   String prefix = &quot;spring.batch.step&quot;;\n   boolean batchV5 = ClassUtils.isPresent(&quot;org.springframework.batch.core.observability.BatchStepObservation&quot;, null);\n   String from = batchV5 ? prefix + &quot;.name&quot; : &quot;name&quot;;\n   return MeterFilter.renameTag(prefix, from, &quot;step.name&quot;);\n}\n<\/pre><\/div>\n\n\n<p>This will cause some sane and consistent naming across the various batch metrics that we care about.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Correlating Metrics and Logging<\/h1>\n\n\n\n<p>The real power of observability comes from being able to combine various types of information: metrics, logging and traces. With the above changes, it becomes very easy to create a dashboard that shows various graphs based on batch metrics which can all be filtered by job. It would be really nice if we could include log messages in that same dashboard that would <em>also<\/em> respect the overall filtering on a job name.<\/p>\n\n\n\n<p>To make that work, we need two things.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Include the job name in log message attributes<\/h2>\n\n\n\n<p>First of all, we obviously need to add the job name to all log messages that are produced in the context of a job execution. This is easiest to do by adding an MDC attribute using your own JobLauncher. You can see how to do just that in <a href=\"https:\/\/trifork.nl\/blog\/how-to-send-your-spring-batch-job-log-messages-to-a-separate-file\/\" target=\"_blank\" rel=\"noreferrer noopener\">one of our old blogs<\/a>.<\/p>\n\n\n\n<p>While working on this system, I basically copied the code from my own blog and patted myself on the back. This is what it looks like:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: java; title: ; notranslate\" title=\"\">\n\/**\n * Ensures that logging performed in the context of a batch job includes a {@code job.name} MDC entry.\n *\/\npublic class SubscriptionJobLauncher extends SimpleJobLauncher {\n\n    private static final String MDC_KEY = &quot;job.name&quot;;\n\n    @Override\n    public JobExecution run(Job job, JobParameters jobParameters) throws JobExecutionAlreadyRunningException, JobRestartException, JobInstanceAlreadyCompleteException, JobParametersInvalidException {\n        try (var c = MDC.putCloseable(MDC_KEY, job.getName())) {\n            return super.run(job, jobParameters);\n        }\n    }\n\n    @Override\n    public void setTaskExecutor(TaskExecutor taskExecutor) {\n        super.setTaskExecutor(new MDCPopulatingTaskExecutor(taskExecutor));\n    }\n\n    private static class MDCPopulatingTaskExecutor implements TaskExecutor {\n        private final TaskExecutor targetExecutor;\n\n        MDCPopulatingTaskExecutor(TaskExecutor targetExecutor) {\n            this.targetExecutor = targetExecutor;\n        }\n\n        @Override\n        public void execute(Runnable task) {\n            String mdcValue = MDC.get(MDC_KEY);\n            if (mdcValue == null || targetExecutor instanceof SyncTaskExecutor) {\n                targetExecutor.execute(task);\n            } else {\n                targetExecutor.execute(() -&gt; {\n                    try (var c = MDC.putCloseable(MDC_KEY, mdcValue)) {\n                        task.run();\n                    }\n                });\n            }\n        }\n    }\n}\n<\/pre><\/div>\n\n\n<p>However, when I tested it I didn\u2019t see anything\u2026&nbsp;<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Ensure Lightmin uses our own JobLauncher<\/h3>\n\n\n\n<p>As it turns out, our system simply ignores the job launcher that you can configure if you override the <a href=\"https:\/\/docs.spring.io\/spring-boot\/docs\/2.0.6.RELEASE\/api\/org\/springframework\/boot\/autoconfigure\/batch\/BasicBatchConfigurer.html#createJobLauncher--\" target=\"_blank\" rel=\"noreferrer noopener\">createJobLauncher from a batch configurer parent class<\/a>&nbsp;because Lightmin has its <em>own<\/em> configuration for creating launchers.<\/p>\n\n\n\n<p>You can override that by defining two beans yourself, a scheduler configuration and a listener configuration. Here\u2019s the code for the scheduler, the listener version is basically the same:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: java; title: ; notranslate\" title=\"\">\n@Component \npublic class SubscriptionSchedulerService extends DefaultSchedulerService {\n    public SubscriptionSchedulerService(BeanRegistrar beanRegistrar, JobRepository jobRepository, JobRegistry jobRegistry) {\n        super(beanRegistrar, jobRepository, jobRegistry);\n    }\n\n    @Override\n    public JobLauncher createLobLauncher(TaskExecutorType taskExecutorType, JobRepository jobRepository) {\n        return SubscriptionJobLauncher.forLightmin(taskExecutorType, jobRepository);\n    }\n}\n<\/pre><\/div>\n\n\n<p>I added a static method to our custom job launcher that we\u2019re calling in the above code:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: java; title: ; notranslate\" title=\"\">\n\/**\n  * @see org.tuxdevelop.spring.batch.lightmin.service.ServiceUtil#createJobLauncher(TaskExecutorType, JobRepository)\n*\/\nstatic SubscriptionJobLauncher forLightmin(TaskExecutorType taskExecutorType, JobRepository jobRepository) {\n    var jobLauncher = new SubscriptionJobLauncher();\n    jobLauncher.setJobRepository(jobRepository);\n    if (taskExecutorType == TaskExecutorType.ASYNCHRONOUS) {\n        jobLauncher.setTaskExecutor(new SimpleAsyncTaskExecutor());\n    } else if (taskExecutorType == TaskExecutorType.SYNCHRONOUS) {\n        jobLauncher.setTaskExecutor(new SyncTaskExecutor());\n    }\n    return jobLauncher;\n}\n<\/pre><\/div>\n\n\n<h2 class=\"wp-block-heading\">Ensuring consistent tag name values: are we there yet?<\/h2>\n\n\n\n<p>As I already mentioned, we actually need to do two things to be able to allow our dashboard filtering to apply to logging as well.<br>We just covered the first: job names will now end up in our MDC using a \u201cjob.name\u201d key. We use <a href=\"https:\/\/youtu.be\/PUqalWaZaXY?si=oewlfKg6eotFpohM&amp;t=1574\" target=\"_blank\" rel=\"noreferrer noopener\">structured JSON logging<\/a> that adds all MDC entries to our log message JSON document, and the Datadog agent will forward that to the server where these entries will show up as attributes.<\/p>\n\n\n\n<p><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/lh7-us.googleusercontent.com\/J0dyAegRAFMkroAoqVmNN5FVQeaksxGmtUoBOpDWMiSPLzs0NAF1iLLnXdhN8LpEqZHfnKOjSoQT5TOQDCOcqwZIe6_aDCIMdb15J36N4LhqhYIy61dDhymKQdAg57R9I19S9fqAVkcS2bcVBYT8SFo\" width=\"780\" height=\"465\"><\/p>\n\n\n\n<p>However, the value that\u2019s shown is different from the values that are used for our metrics. This is what shows up there:<\/p>\n\n\n\n<p><img loading=\"lazy\" decoding=\"async\" width=\"780\" height=\"329\" src=\"https:\/\/lh7-us.googleusercontent.com\/wSuwiqoOBkHU56eaZJzq0vDa8_LIYBlR9OWBFPOvIL_0kH5cb0bnRwINu9UifRhip5FDsl_UbYeYLw76mOFDYZeTvg4i4vOt670FlY8d5oe5iWjknOiirCM8SomvWOPWT4d8_2CleiF0VyzLbKsIo3A\"><\/p>\n\n\n\n<p>As it turns out, Datadog only supports certain characters to be used in a tag value and replaces the rest with underscores. It also lowercases everything. <br>Since we want consistency, I wrote a function that will rewrite the job names we put in the MDC to match Datadog\u2019s behavior:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: java; title: ; notranslate\" title=\"\">\nstatic final Pattern ILLEGAL_CHARS = Pattern.compile(&quot;&#x5B;^A-Za-z0-9_:.\/-]&quot;);\n\n\/**\n* Rewrites the given value in the same way that the value would end up as a Datadog metric tag value,\n* meaning it's lower-cased and non-allowed characters are converted to underscores.\n*\n* @param value to convert, may be {@code null}\n* @return converted value\n* @see &lt;a href=&quot;https:\/\/docs.datadoghq.com\/getting_started\/tagging\/#define-tags&quot;&gt;Datadog tag docs&lt;\/a&gt;\n*\/\npublic static String asDatadogTag(String value) {\n   if (value == null) return null;\n   return ILLEGAL_CHARS.matcher(value.toLowerCase()).replaceAll(&quot;_&quot;);\n}\n<\/pre><\/div>\n\n\n<p>Now we simply call that when adding the job name to the MDC and everything starts working exactly like we want it to.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Building the Datadog dashboard<\/h1>\n\n\n\n<p>We\u2019re ready to create our dashboard! <br>Right now we show what jobs failed and some info about their execution times. We also do that for steps and then include some info on chunks, although I\u2019m not sure yet how useful the latter will turn out to be.<\/p>\n\n\n\n<figure class=\"wp-block-video alignfull\"><video controls src=\"https:\/\/trifork.nl\/blog\/wp-content\/uploads\/sites\/3\/2024\/03\/Untitled-video-Made-with-Clipchamp.mp4\"><\/video><\/figure>\n\n\n\n<p>As shown, we can see metrics for all jobs, and filter by job name consistently. In Datadog you do this by adding a template variable, which automatically get populated with possible values if it matches an existing metrics tag. In our case that\u2019s our <code>job.name<\/code>, of course. In the widget\u2019s definition you then filter by that variable:<\/p>\n\n\n\n<p><img loading=\"lazy\" decoding=\"async\" width=\"780\" height=\"109\" src=\"https:\/\/lh7-us.googleusercontent.com\/6CI2PT50Ae78PmEBwOEe8t60no-bUjoaJwaZhoWql1VFEm-Ukw-fewErAwT8K7Cu--eOHNw1u09fpjP-y9Cwi3doqVnqwualgboWB8XnyRyedrNZPOKYHKGcNlzwok3k9gbSpPSrEytrAIakiTSdj7k\"><\/p>\n\n\n\n<p>For our logging this needs to be a bit different, though: when you simply add <code>$job.name<\/code>, it inserts a criteria like \u201c<code>job.name:my_awesome_job<\/code>\u201d. However, for the logging we need to filter on an attribute, which requires the criteria to look like \u201c<code>@job.name:my_awesome_job<\/code>\u201d, so with a leading at-sign.<\/p>\n\n\n\n<p>Fortunately this is easy to achieve: we simply prefix the criteria. The logging will then only show matching messages. However, not all log messages from our batch service have a <code>job.name<\/code> attribute, simply because not all logging is performed in the context of a job execution. I want to make sure that we see those messages as well.<br>This is the solution I\u2019m using to show the correct logs:<\/p>\n\n\n\n<p><img loading=\"lazy\" decoding=\"async\" width=\"780\" height=\"150\" src=\"https:\/\/lh7-us.googleusercontent.com\/-TK9tZdXFxc-GLnxdEQv9pXdzyuq_It0EWpjFFzf6_-G9glALZuaJQTGjl-MIDIqtKxcXi0kOyCvThreg-vr9rko1pwGqF_5pYgX14OA-krcQo2nQPSq4vKpju1m9F6lJSW3ISdEx5Qk5i2AQUqcNTY\"><\/p>\n\n\n\n<p>This selects log messages from the relevant service that have no job.name attribute OR have a matching one.<\/p>\n\n\n\n<p>As you can see, I\u2019ve also introduced an additional template variable that selects messages based on log level, where I\u2019m only referencing the variable\u2019s value (instead of a key:value pair). That one is not populated based on existing values, but uses a hardcoded set of options instead:<\/p>\n\n\n\n<p><img loading=\"lazy\" decoding=\"async\" width=\"286\" height=\"331\" src=\"https:\/\/lh7-us.googleusercontent.com\/-2IyaaW1D2BELtgz9JL0yUJnrAu4l584MCt9JsMnHiOM51sKKSsDHU-_enxyQ1rWTQDTX5U2JlWzt4BYV4fTsO_mGfBeQ3OJCPyUEO3PJabGsSdGjwEpyO-3IDSzr5IcgtkKKgr9_4TNlbjapKYuLNM\"><\/p>\n\n\n\n<p>With this we\u2019re pretty much done. There\u2019s just one catch: everything I\u2019ve shown work perfectly if you select a job name to filter on. However, when you use the default selection of \u201c*\u201d for the <code>job.name<\/code> variable, you\u2019ll see that only log messages without a <code>job.name<\/code> attribute are now shown. Why?<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Teaching an old Datadog new tricks<\/h2>\n\n\n\n<p>I investigated, and it turns out that when Datadog sees that your variable uses * as its value it simply drops the \u201c<code> OR @$job.name<\/code>\u201d from our logging query. That results in the query using <code>(-@job.name)<\/code> instead of <code>(-@job.name OR @job.name:*)<\/code> like we want to. <br>I\u2019ve opened up a case for this with Datadog, but for now a simple workaround is to use a default value that has an actual character in there that will still match all possible values. In our case <code>*_*<\/code> work well for this:<\/p>\n\n\n\n<p><img loading=\"lazy\" decoding=\"async\" width=\"318\" height=\"365\" src=\"https:\/\/lh7-us.googleusercontent.com\/J0HbFfSRlyDTi3Hq4ovR1vcBIJHW5vKKo1ckc5GvIXe7KJbXVOkc0ZRiJiP8CvauoZ6DNB15feKBBuWUyLsaY3XOexTf01rOcyY2dKrhBig_oyws_4vfuK2FHmxnyEXdiHs5UcGjeTcTUvvXVP4m5tE\"><\/p>\n\n\n\n<p>With that final hack in place, by default all metrics and logging are shown, and are consistently filtered by job name when a selection is made in the dashboard.<\/p>\n\n\n\n<p>A typical workflow would now be to look at the dashboard, notice some failures or unexpected slowness of a job, filter on it, look at the warnings and errors in the log and if those aren\u2019t sufficient, change the log level to see the INFO messages as well to understand what\u2019s happening exactly. <\/p>\n\n\n\n<p>Batch Observability Nirvana!<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Conclusion<\/h1>\n\n\n\n<p>Observability for batch applications is as critically important as it is for other types of applications. Fortunately a framework like Spring Batch provides a lot of metrics out of the box already. For use in efficient dashboarding it requires some tweaking to make things more consistent, though.<\/p>\n\n\n\n<p>If you then ensure that your logging follows the same conventions, it becomes easy to create very powerful dashboard that allow you to quickly zoom in on specific jobs and see relevant error rates, timings, and log messages all together for quick analysis and trouble shooting.<\/p>\n\n\n\n<p>I hope this blog post helps you to come up with ways to improve the observability of your own batch applications! If you\u2019d like Trifork to help you with that, don\u2019t hesitate to <a href=\"https:\/\/trifork.nl\/contact\/\">contact us<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Watching paint dry In my previous blog post I described how we provide insight into our database interactions inside Datadog for the subscription system that we develop for Nederlandse Loterij.Processes executed by this system are often batch-oriented, which is why the system contains a dedicated service that uses the Spring Batch framework to execute these [&hellip;]<\/p>\n","protected":false},"author":62,"featured_media":21060,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"content-type":"","footnotes":""},"categories":[88,337,94],"tags":[],"class_list":["post-21048","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-devops","category-from-the-trenches","category-spring"],"acf":[],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v24.4 - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>Spring Boot Observability: Spring Batch Jobs - Trifork Blog<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/trifork.nl\/blog\/spring-boot-observability-spring-batch-jobs\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Spring Boot Observability: Spring Batch Jobs - Trifork Blog\" \/>\n<meta property=\"og:description\" content=\"Watching paint dry In my previous blog post I described how we provide insight into our database interactions inside Datadog for the subscription system that we develop for Nederlandse Loterij.Processes executed by this system are often batch-oriented, which is why the system contains a dedicated service that uses the Spring Batch framework to execute these [&hellip;]\" \/>\n<meta property=\"og:url\" content=\"https:\/\/trifork.nl\/blog\/spring-boot-observability-spring-batch-jobs\/\" \/>\n<meta property=\"og:site_name\" content=\"Trifork Blog\" \/>\n<meta property=\"article:published_time\" content=\"2024-03-15T13:23:23+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2024-03-15T14:29:10+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/trifork.nl\/blog\/wp-content\/uploads\/sites\/3\/2024\/03\/1-SjJ12zamKV3ERm6LMatoA.png\" \/>\n\t<meta property=\"og:image:width\" content=\"1400\" \/>\n\t<meta property=\"og:image:height\" content=\"849\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"author\" content=\"Joris Kuipers\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Joris Kuipers\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"8 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\/\/trifork.nl\/blog\/spring-boot-observability-spring-batch-jobs\/\",\"url\":\"https:\/\/trifork.nl\/blog\/spring-boot-observability-spring-batch-jobs\/\",\"name\":\"Spring Boot Observability: Spring Batch Jobs - Trifork Blog\",\"isPartOf\":{\"@id\":\"https:\/\/trifork.nl\/blog\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/trifork.nl\/blog\/spring-boot-observability-spring-batch-jobs\/#primaryimage\"},\"image\":{\"@id\":\"https:\/\/trifork.nl\/blog\/spring-boot-observability-spring-batch-jobs\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/trifork.nl\/blog\/wp-content\/uploads\/sites\/3\/2024\/03\/1-SjJ12zamKV3ERm6LMatoA.png\",\"datePublished\":\"2024-03-15T13:23:23+00:00\",\"dateModified\":\"2024-03-15T14:29:10+00:00\",\"author\":{\"@id\":\"https:\/\/trifork.nl\/blog\/#\/schema\/person\/265bd41e503f7176742258a927de598b\"},\"breadcrumb\":{\"@id\":\"https:\/\/trifork.nl\/blog\/spring-boot-observability-spring-batch-jobs\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/trifork.nl\/blog\/spring-boot-observability-spring-batch-jobs\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/trifork.nl\/blog\/spring-boot-observability-spring-batch-jobs\/#primaryimage\",\"url\":\"https:\/\/trifork.nl\/blog\/wp-content\/uploads\/sites\/3\/2024\/03\/1-SjJ12zamKV3ERm6LMatoA.png\",\"contentUrl\":\"https:\/\/trifork.nl\/blog\/wp-content\/uploads\/sites\/3\/2024\/03\/1-SjJ12zamKV3ERm6LMatoA.png\",\"width\":1400,\"height\":849},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/trifork.nl\/blog\/spring-boot-observability-spring-batch-jobs\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/trifork.nl\/blog\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Spring Boot Observability: Spring Batch Jobs\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/trifork.nl\/blog\/#website\",\"url\":\"https:\/\/trifork.nl\/blog\/\",\"name\":\"Trifork Blog\",\"description\":\"Keep updated on the technical solutions Trifork is working on!\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/trifork.nl\/blog\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Person\",\"@id\":\"https:\/\/trifork.nl\/blog\/#\/schema\/person\/265bd41e503f7176742258a927de598b\",\"name\":\"Joris Kuipers\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/trifork.nl\/blog\/#\/schema\/person\/image\/\",\"url\":\"https:\/\/secure.gravatar.com\/avatar\/9ab8da0d60582bad84342d4602d23dbd?s=96&d=mm&r=g\",\"contentUrl\":\"https:\/\/secure.gravatar.com\/avatar\/9ab8da0d60582bad84342d4602d23dbd?s=96&d=mm&r=g\",\"caption\":\"Joris Kuipers\"},\"sameAs\":[\"http:\/\/www.trifork.nl\"],\"url\":\"https:\/\/trifork.nl\/blog\/author\/jorisk\/\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Spring Boot Observability: Spring Batch Jobs - Trifork Blog","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/trifork.nl\/blog\/spring-boot-observability-spring-batch-jobs\/","og_locale":"en_US","og_type":"article","og_title":"Spring Boot Observability: Spring Batch Jobs - Trifork Blog","og_description":"Watching paint dry In my previous blog post I described how we provide insight into our database interactions inside Datadog for the subscription system that we develop for Nederlandse Loterij.Processes executed by this system are often batch-oriented, which is why the system contains a dedicated service that uses the Spring Batch framework to execute these [&hellip;]","og_url":"https:\/\/trifork.nl\/blog\/spring-boot-observability-spring-batch-jobs\/","og_site_name":"Trifork Blog","article_published_time":"2024-03-15T13:23:23+00:00","article_modified_time":"2024-03-15T14:29:10+00:00","og_image":[{"width":1400,"height":849,"url":"https:\/\/trifork.nl\/blog\/wp-content\/uploads\/sites\/3\/2024\/03\/1-SjJ12zamKV3ERm6LMatoA.png","type":"image\/png"}],"author":"Joris Kuipers","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Joris Kuipers","Est. reading time":"8 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/trifork.nl\/blog\/spring-boot-observability-spring-batch-jobs\/","url":"https:\/\/trifork.nl\/blog\/spring-boot-observability-spring-batch-jobs\/","name":"Spring Boot Observability: Spring Batch Jobs - Trifork Blog","isPartOf":{"@id":"https:\/\/trifork.nl\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/trifork.nl\/blog\/spring-boot-observability-spring-batch-jobs\/#primaryimage"},"image":{"@id":"https:\/\/trifork.nl\/blog\/spring-boot-observability-spring-batch-jobs\/#primaryimage"},"thumbnailUrl":"https:\/\/trifork.nl\/blog\/wp-content\/uploads\/sites\/3\/2024\/03\/1-SjJ12zamKV3ERm6LMatoA.png","datePublished":"2024-03-15T13:23:23+00:00","dateModified":"2024-03-15T14:29:10+00:00","author":{"@id":"https:\/\/trifork.nl\/blog\/#\/schema\/person\/265bd41e503f7176742258a927de598b"},"breadcrumb":{"@id":"https:\/\/trifork.nl\/blog\/spring-boot-observability-spring-batch-jobs\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/trifork.nl\/blog\/spring-boot-observability-spring-batch-jobs\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/trifork.nl\/blog\/spring-boot-observability-spring-batch-jobs\/#primaryimage","url":"https:\/\/trifork.nl\/blog\/wp-content\/uploads\/sites\/3\/2024\/03\/1-SjJ12zamKV3ERm6LMatoA.png","contentUrl":"https:\/\/trifork.nl\/blog\/wp-content\/uploads\/sites\/3\/2024\/03\/1-SjJ12zamKV3ERm6LMatoA.png","width":1400,"height":849},{"@type":"BreadcrumbList","@id":"https:\/\/trifork.nl\/blog\/spring-boot-observability-spring-batch-jobs\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/trifork.nl\/blog\/"},{"@type":"ListItem","position":2,"name":"Spring Boot Observability: Spring Batch Jobs"}]},{"@type":"WebSite","@id":"https:\/\/trifork.nl\/blog\/#website","url":"https:\/\/trifork.nl\/blog\/","name":"Trifork Blog","description":"Keep updated on the technical solutions Trifork is working on!","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/trifork.nl\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Person","@id":"https:\/\/trifork.nl\/blog\/#\/schema\/person\/265bd41e503f7176742258a927de598b","name":"Joris Kuipers","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/trifork.nl\/blog\/#\/schema\/person\/image\/","url":"https:\/\/secure.gravatar.com\/avatar\/9ab8da0d60582bad84342d4602d23dbd?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/9ab8da0d60582bad84342d4602d23dbd?s=96&d=mm&r=g","caption":"Joris Kuipers"},"sameAs":["http:\/\/www.trifork.nl"],"url":"https:\/\/trifork.nl\/blog\/author\/jorisk\/"}]}},"_links":{"self":[{"href":"https:\/\/trifork.nl\/blog\/wp-json\/wp\/v2\/posts\/21048","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/trifork.nl\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/trifork.nl\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/trifork.nl\/blog\/wp-json\/wp\/v2\/users\/62"}],"replies":[{"embeddable":true,"href":"https:\/\/trifork.nl\/blog\/wp-json\/wp\/v2\/comments?post=21048"}],"version-history":[{"count":10,"href":"https:\/\/trifork.nl\/blog\/wp-json\/wp\/v2\/posts\/21048\/revisions"}],"predecessor-version":[{"id":21063,"href":"https:\/\/trifork.nl\/blog\/wp-json\/wp\/v2\/posts\/21048\/revisions\/21063"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/trifork.nl\/blog\/wp-json\/wp\/v2\/media\/21060"}],"wp:attachment":[{"href":"https:\/\/trifork.nl\/blog\/wp-json\/wp\/v2\/media?parent=21048"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/trifork.nl\/blog\/wp-json\/wp\/v2\/categories?post=21048"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/trifork.nl\/blog\/wp-json\/wp\/v2\/tags?post=21048"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}