Apps Actually Speak
In this article, I’ll try to offer a perspective on how we, as developers, can interact with the applications we build.
If we think of the applications we build as living organisms and design them accordingly, our lives might become easier.
As a developer, I often think: wouldn’t it be wonderful if I could easily understand an app’s issues, receive immediate feedback when it has a problem, see exactly what’s going on inside it—essentially, take an X-ray of it? If that were possible, I could diagnose the problem and fix it right away. Being in constant communication with the app puts my mind at ease; I can say everything is running smoothly.
When developing software, we usually focus on coding. We can get caught up in the rush to analyze requirements, build the application, run our tests, and deliver it. However, our goal of making the application we’re developing a living, breathing system can sometimes take a back seat.
How Do Applications Communicate?
- Easy-to-understand logs
- Traceable metrics
Easy-to-understand logs
When developing, we usually focus on coding. We can get caught up in the rush to analyze requirements, build the application, run our tests, and deliver it. However, our goal of making the application we develop a living organism can sometimes take a back seat.
This is where we need to remember logs. Just as test code is a core element of test-driven development, writing code that generates logs should be equally essential. I chose testing as an example, but regardless of how we develop, logs should be one of the core elements of development. We should always be mindful of where and how to add clear, understandable logs.
When we talk about logs, log levels come to mind (ERROR, WARN, INFO, DEBUG, TRACE). Even by using just the ERROR and DEBUG levels, we can create quite useful solutions.
public class InvoiceDTO{
private invoiceNumber;
private String customerId;
private PaymentType paymentType;
...
public String toString(){...}
}
public Invoice createInvoice(InvoiceDTO invoiceDTO){
(1)log.debug("Fatura oluşturuluyor... Fatura bilgileri {}", invoiceDTO);
if(!hasCustomerPaymentDetail(invoiceDTO.customerId,invoiceDTO.paymentType)){
(2)throw new NoPaymentTypeDefinitionForCustomerException(String.format("%s numaralı müşteri için %s ödeme tipi tanımı yapılmamış!",invoiceDTO.customerId, invoiceDTO.paymentType));
}
if(InvoiceDTO.PaymentType.CREDIT_CARD.equals(paymentType)){
(3)log.debug("{} numaralı fatura için kredi kartı ödemesi {} bankası üzerinden yapılıyor.",invoiceNumber,bankService.getCode());
}
...
...
(4)log.debug("Fatura {} id'si ile kaydedildi.", invoice.getId());
}
I tried to demonstrate how to write a debug log above.
At the very beginning of the process (1), I added a debug log that includes the invoice information received from an external source. Here, `toString` is used directly, but in some cases this may not be necessary. You can choose to log only the data that is important.
Similarly, within the flow, if there’s a situation you consider important within code blocks that operate based on various (3)rules, you can add a log there. Here, there’s a critical situation involving a credit card payment, so I’m adding this information here because I believe it’s necessary.
Finally, a debug log can be placed at the very end of the process (4). It will be helpful to see certain details regarding the outcome of the process.
What I’m trying to say is: try to write information to the logs in a way that helps you understand what’s happening while the application is running. You can add logs wherever you think it’s necessary. I did this as an example.
ERROR-level logs are also important from a logging perspective. If we’re using frameworks that support them, they generally manage ERROR-level logs automatically. When an error occurs in the application, they add these to the log at the ERROR level appropriately. The key here is to handle exceptional situations using custom exception classes. For example, if a payment type definition hasn’t been set for a customer (4), we can throw the custom NoPaymentTypeDefinitionForCustomerException exception.
Logging means I/O, which incurs a computational cost for the application. On the other hand, striking a balance is crucial to avoid compromising the readability of the main code. For this reason, it is more effective to log only the most essential information that will help you understand the application.
If we follow these basic principles for logging, to use an analogy, the application will start to “speak” even from within itself. I say “from within” because the logs are still in log files. Think of it this way: since we can’t constantly monitor log files manually, we haven’t truly established interaction yet. We still can’t hear what’s going on inside the application.
Let’s move on to interaction
We’ve written our logs nicely; now let’s make the application “speak.” From here on, it’s more about infrastructure. Our application is working well and writing logs to the relevant outputs. Now, it’s necessary for these logs to be collected, easily accessible, monitorable, and readable. The ELK Stack usually comes to mind here. At least it does for me. I’ve written an article on this topic; those interested can find it at the link below.
https://medium.com/devopsturkiye/spring-boot-syslog-ng-elasticsearch-i%CC%87le-centralized-logging-51518ad21ec6
Collecting logs and monitoring them with Kibana isn’t exactly interactive. Interaction should be two-way. I shouldn’t have to log into Kibana and manually search for things.
For this, the Elasticsearch Alerting Plugin can be used. Of course, this plugin comes with X-Pack.
This plugin works by allowing you to set various conditions and notify users when those conditions are met at specific intervals. For example, you can choose to be notified if there have been more than 10 failed credit card transactions in the last 5 minutes. Or you can opt to be notified of failed login attempts. It enables you to stay informed about a wide range of situations.
X-Pack is a paid product. As an alternative, you can use the Elastalert product. You can create various types of rules and make your applications “speak.” I plan to write an article about Elastalert in the coming days.
Monitorable Metrics
Monitoring the runtime status of applications can be one of the ways to interact with them. For this purpose, the Prometheus and Grafana duo is widely used. As is well known, Prometheus is a time-series database, while Grafana is a tool that primarily visualizes time-series data.
Our applications have access to a wealth of information about their own runtime status. However, this information needs to be stored in a database like Prometheus. To achieve this, the Actuator module in Java/Spring provides APIs that expose application runtime metrics. By configuring these APIs as data sources in Prometheus, we can continuously store these metrics. So, what kinds of metrics might these be;
-
Memory
-
CPU
-
Error Log sayıları
-
Request count/ Response time
-
Thread pool
-
Connection pool
-
Garbage Collector
Many metrics like these can be tracked. Additionally, alerts can be set up in Grafana based on these metrics to enable real-time monitoring.
In conclusion:
When developing, writing descriptive debug logs and creating custom exceptions helps the application provide meaningful insights. Beyond that, we should identify and track various metrics to determine whether our application is healthy.
If we notice negative values and take corrective actions, this will help us build more sustainable applications.
On the other hand, ensuring that this information is communicated to us as soon as possible by setting up various alerts is an important step toward fostering mutual interaction.

Dilaver Demirel