Skip to content

Commit 88e175c

Browse files
Release (#1701)
2 parents eb4d3c4 + 132b733 commit 88e175c

5 files changed

Lines changed: 235 additions & 91 deletions

File tree

java/operating-applications/observability.md

Lines changed: 34 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -74,14 +74,14 @@ Similarly, no specific log output configuration is required for local developmen
7474
All logs are written, that have a log level greater or equal to the configured log level of the corresponding logger object.
7575
The following log levels are available:
7676

77-
| Level | Use case
78-
| :--------| :--------
79-
| `OFF` | Turns off the logger
80-
| `TRACE` | Tracks the application flow only
81-
| `DEBUG` | Shows diagnostic messages
82-
| `INFO` | Shows important flows of the application (default level)
83-
| `WARN` | Indicates potential error scenarios
84-
| `ERROR` | Shows errors and exceptions
77+
| Level | Use case |
78+
| :------ | :------------------------------------------------------- |
79+
| `OFF` | Turns off the logger |
80+
| `TRACE` | Tracks the application flow only |
81+
| `DEBUG` | Shows diagnostic messages |
82+
| `INFO` | Shows important flows of the application (default level) |
83+
| `WARN` | Indicates potential error scenarios |
84+
| `ERROR` | Shows errors and exceptions |
8585

8686
With Spring Boot, there are different convenient ways to configure log levels in a development scenario, which is explained in the following section.
8787

@@ -144,20 +144,20 @@ curl -X POST -H 'Content-Type: application/json' -d '{"configuredLevel": "DEBUG"
144144

145145
CAP Java SDK has useful built-in loggers that help to track runtime behavior:
146146

147-
| Logger | Use case
148-
| :------------------------------| :--------
149-
| `com.sap.cds.security.authentication` | Logs authentication and user information
150-
| `com.sap.cds.security.authorization` | Logs authorization decisions
151-
| `com.sap.cds.odata.v2` | Logs OData V2 request handling in the adapter
152-
| `com.sap.cds.odata.v4` | Logs OData V4 request handling in the adapter
153-
| `com.sap.cds.handlers` | Logs sequence of executed handlers as well as the lifecycle of RequestContexts and ChangeSetContexts
154-
| `com.sap.cds.persistence.sql` | Logs executed queries such as CQN and SQL statements (w/o parameters)
155-
| `com.sap.cds.persistence.sql-tx` | Logs transactions, ChangeSetContexts, and connection pool
156-
| `com.sap.cds.multitenancy` | Logs tenant-related events and sidecar communication
157-
| `com.sap.cds.messaging` | Logs messaging configuration and messaging events
158-
| `com.sap.cds.remote.odata` | Logs request handling for remote OData calls
159-
| `com.sap.cds.remote.wire` | Logs communication of remote OData calls
160-
| `com.sap.cds.auditlog` | Logs audit log events
147+
| Logger | Use case |
148+
| :------------------------------------ | :--------------------------------------------------------------------------------------------------- |
149+
| `com.sap.cds.security.authentication` | Logs authentication and user information |
150+
| `com.sap.cds.security.authorization` | Logs authorization decisions |
151+
| `com.sap.cds.odata.v2` | Logs OData V2 request handling in the adapter |
152+
| `com.sap.cds.odata.v4` | Logs OData V4 request handling in the adapter |
153+
| `com.sap.cds.handlers` | Logs sequence of executed handlers as well as the lifecycle of RequestContexts and ChangeSetContexts |
154+
| `com.sap.cds.persistence.sql` | Logs executed queries such as CQN and SQL statements (w/o parameters) |
155+
| `com.sap.cds.persistence.sql-tx` | Logs transactions, ChangeSetContexts, and connection pool |
156+
| `com.sap.cds.multitenancy` | Logs tenant-related events and sidecar communication |
157+
| `com.sap.cds.messaging` | Logs messaging configuration and messaging events |
158+
| `com.sap.cds.remote.odata` | Logs request handling for remote OData calls |
159+
| `com.sap.cds.remote.wire` | Logs communication of remote OData calls |
160+
| `com.sap.cds.auditlog` | Logs audit log events |
161161

162162
Most of the loggers are used on DEBUG level by default as they produce quite some log output. It's convenient to control loggers on package level, for example, `com.sap.cds.security` covers all loggers that belong to this package (namely `com.sap.cds.security.authentication` and `com.sap.cds.security.authorization`).
163163

@@ -289,7 +289,7 @@ Step-by-step description on how to access a bash session in the application's co
289289
1. Locate java executable and JDBC driver:
290290

291291
By default `JAVA_HOME` isn't set in the buildpack and contains minimal tooling, as it tries to minimize the container size. However, the default location of the `java` executable is `/layers/paketo-buildpacks_sap-machine/jre/bin`.
292-
292+
293293
For convenience, store the path into a variable, for example, `JAVA_HOME`:
294294
```sh
295295
export JAVA_HOME=/layers/paketo-buildpacks_sap-machine/jre/bin/
@@ -357,6 +357,10 @@ In addition, it's possible to add manual instrumentations using the [Open Teleme
357357
The configuration steps below assume that your application uses the [SAP Java Buildpack](https://help.sap.com/docs/btp/sap-business-technology-platform/sap-jakarta-buildpack).
358358
:::
359359

360+
:::tip
361+
You can conveniently enhance your application with Open Telemetry observability by running the command `cds add cloud-logging --with-telemetry` in your project directory.
362+
:::
363+
360364
Configure your application to enable the Open Telemetry Java Agent by adding or adapting the `JBP_CONFIG_JAVA_OPTS` parameter in your deployment descriptor:
361365

362366
::: code-group
@@ -461,7 +465,7 @@ The following steps describe the required configuration:
461465
By default, instrumentation for CAP-specific components is disabled, so that no traces and spans are created even if the Open Telemetry Java Agent has been configured. It's possible to selectively activate specific spans by changing the log level for a component.
462466

463467
| Logger | Required Level | Description |
464-
|------------------------------------------------|----------------|------------------------------------------------------------|
468+
| ---------------------------------------------- | -------------- | ---------------------------------------------------------- |
465469
| `com.sap.cds.otel.span.ODataBatch` | `INFO` | Spans for individual requests of a OData $batch request. |
466470
| `com.sap.cds.otel.span.CQN` | `INFO` | Spans for executed CQN statement. |
467471
| `com.sap.cds.otel.span.OutboxCollector` | `INFO` | Spans for execution of the transactional outbox collector. |
@@ -602,12 +606,12 @@ To add actuator support in your application, add the following dependency:
602606

603607
The following table lists some of the available actuators that might be helpful to understand the internal status of the application:
604608

605-
| Actuator | Description
606-
| :--------| :--------
607-
| `metrics` | Thread pools, connection pools, CPU, and memory usage of JVM and HTTP web server
608-
| `beans` | Information about Spring beans created in the application
609-
| `env` | Exposes the full Spring environment including application configuration
610-
| `loggers` | List and modify application loggers
609+
| Actuator | Description |
610+
| :-------- | :------------------------------------------------------------------------------- |
611+
| `metrics` | Thread pools, connection pools, CPU, and memory usage of JVM and HTTP web server |
612+
| `beans` | Information about Spring beans created in the application |
613+
| `env` | Exposes the full Spring environment including application configuration |
614+
| `loggers` | List and modify application loggers |
611615

612616
By default, nearly all actuators are active. You can switch off actuators individually in the configuration. The following configuration turns off `flyway` actuator:
613617

java/outbox.md

Lines changed: 140 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ Once the transaction succeeds, the messages are read from the database table and
3434

3535
To enable the persistence for the outbox, you need to add the service `outbox` of kind `persistent-outbox` to the `cds.requires` section in the _package.json_ or _cdsrc.json_, which will automatically enhance your CDS model in order to support the persistent outbox.
3636

37-
```jsonc
37+
```jsonc
3838
{
3939
// ...
4040
"cds": {
@@ -124,12 +124,12 @@ public class MySpringComponent {
124124
... it must be ensured that there are no unprocessed entries left.
125125

126126
Removing a custom outbox from the `cds.outbox.services` section doesn't remove the
127-
entries from the `cds.outbox.Messages` table. The entries remain in the `cds.outbox.Messages` table and isn't
127+
entries from the `cds.outbox.Messages` table. The entries remain in the `cds.outbox.Messages` table and aren't
128128
processed anymore.
129129

130130
:::
131131

132-
### Outbox Event Versions
132+
### Outbox Event Versions
133133

134134
In scenarios with multiple deployment versions (blue/green), situations may arise in which the outbox collectors of the older deployment cannot process the events generated by a newer deployment. In this case, the event can get stuck in the outbox, with all the resulting problems.
135135

@@ -182,7 +182,7 @@ CqnService remoteS4 = ...;
182182
CqnService outboxedS4 = myCustomOutbox.outboxed(remoteS4);
183183
```
184184

185-
If a method on the outboxed service has a return value, it will always return `null` since it is executed asynchronously. A common example for this are the `CqnService.run(...)` methods.
185+
If a method on the outboxed service has a return value, it will always return `null` since it's executed asynchronously. A common example for this are the `CqnService.run(...)` methods.
186186
To improve this the API `OutboxService.outboxed(Service, Class)` can be used, which wraps a service with an asynchronous suited API while outboxing it.
187187
This can be used together with the interface `AsyncCqnService` to outbox remote OData services:
188188

@@ -212,7 +212,7 @@ A service wrapped by an outbox can be unboxed by calling the API `OutboxService.
212212
service are executed synchronously without storing the event in an outbox.
213213

214214
::: warning Java Proxy
215-
A service wrapped by an outbox is a [Java Proxy](https://docs.oracle.com/javase/8/docs/technotes/guides/reflection/proxy.html). Such a proxy only implements the _interfaces_ of the object it is wrapping. This means an outboxed service proxy can't be casted to the class implementing the underlying service object.
215+
A service wrapped by an outbox is a [Java Proxy](https://docs.oracle.com/javase/8/docs/technotes/guides/reflection/proxy.html). Such a proxy only implements the _interfaces_ of the object that it's wrapping. This means an outboxed service proxy can't be casted to the class implementing the underlying service object.
216216
:::
217217

218218
::: tip Custom outbox for scaling
@@ -262,7 +262,7 @@ The outbox by default retries publishing a message, if an error occurs during pr
262262
This behavior makes applications resilient against unavailability of external systems, which is a typical use case for outbox message processing.
263263

264264
However, there might also be situations in which it is not reasonable to retry publishing a message.
265-
For example, when the processed message causes a semantic error - typically due to a 400 Bad request - on the external system.
265+
For example, when the processed message causes a semantic error - typically due to a `400 Bad request` - on the external system.
266266
Outbox messages causing such errors should be removed from the outbox message table before reaching the maximum number of retry attempts and instead application-specific
267267
counter-measures should be taken to correct the semantic error or ignore the message altogether.
268268

@@ -307,13 +307,142 @@ void handleAuditLogProcessingErrors(OutboxMessageEventContext context) {
307307

308308
[Learn more about `EventContext.proceed()`.](./event-handlers/#proceed-on){.learn-more}
309309

310-
## Troubleshooting
310+
## Outbox Dead Letter Queue
311311

312-
To manually delete entries in the `cds.outbox.Messages` table, you can either
313-
expose it in a service or programmatically modify it using the `cds.outbox.Messages`
314-
database entity.
312+
The transactional outbox tries to process each entry a specific number of times. The number of attempts is configurable per outbox by setting the configuration `cds.outbox.services.<key>.maxAttempts`.
313+
314+
[Learn more about CDS Properties.](./developing-applications/properties){.learn-more}
315+
316+
Once the maximum number of attempts is exceeded, the corresponding entry is not touched anymore and hence it can be regarded as dead. Dead outbox entries are not deleted automatically. They remain in the database and it's up to the application to take care of the entries. By defining a CDS service, the dead entries can be managed conveniently. Let's have a look, how you can develop a Dead Letter Queue for the transactional outbox.
317+
318+
::: warning Changing configuration between deployments
319+
320+
It's possible to increase the value of the configuration `cds.outbox.services.<key>.maxAttempts` in between of deployments. Older entries which have reached their max attempts in the past would be retried automatically after deployment of the new microservice version. If the dead letter queue has a large size, this leads to unintended load on the system.
321+
322+
:::
323+
324+
325+
### Define the Service
326+
327+
::: code-group
328+
329+
```cds [srv/outbox-dead-letter-queue-service.cds]
330+
using from '@sap/cds/srv/outbox';
331+
332+
@requires: 'internal-user'
333+
service OutboxDeadLetterQueueService {
334+
335+
@readonly
336+
entity DeadOutboxMessages as projection on cds.outbox.Messages
337+
actions {
338+
action revive();
339+
action delete();
340+
};
341+
342+
}
343+
```
344+
345+
:::
346+
347+
The `OutboxDeadLetterQueueService` provides an entity `DeadOutboxMessages` which is a projection on the outbox table `cds.outbox.Messages` that has two bound actions:
348+
349+
- `revive()` sets the number of attempts to `0` such that the outbox entry is going to be processed again.
350+
- `delete()` deletes the outbox entry from the database.
351+
352+
Filters can be applied as for any other CDS defined entity, for example, to filter for a specific outbox where the outbox name is stored in the field `target` of the entity `cds.outbox.Messages`.
353+
354+
::: warning `OutboxDeadLetterQueueService` for internal users only
355+
356+
It is crucial to make the service `OutboxDeadLetterQueueService` accessible for internal users only as it contains sensitive data that could be exploited for malicious purposes if unauthorized changes are performed.
357+
358+
[Learn more about pseudo roles](../guides/security/authorization#pseudo-roles){.learn-more}
359+
360+
:::
361+
362+
### Filter for Dead Entries
363+
364+
This filtering can't be done on the database since the maximum number of attempts is only available from the CDS properties.
365+
366+
To ensure that only dead outbox entries are returned when reading `DeadOutboxMessages`, the following code provides the handler for the `DeadLetterQueueService` and the `@After-READ` handler that filters for the dead outbox entries:
367+
368+
```java
369+
@Component
370+
@ServiceName(OutboxDeadLetterQueueService_.CDS_NAME)
371+
public class DeadOutboxMessagesHandler implements EventHandler {
372+
373+
@After(entity = DeadOutboxMessages_.CDS_NAME)
374+
public void filterDeadEntries(CdsReadEventContext context) {
375+
CdsProperties.Outbox outboxConfigs = context.getCdsRuntime().getEnvironment().getCdsProperties().getOutbox();
376+
List<DeadOutboxMessages> deadEntries = context
377+
.getResult()
378+
.listOf(DeadOutboxMessages.class)
379+
.stream()
380+
.filter(entry -> entry.getAttempts() >= outboxConfigs.getService(entry.getTarget()).getMaxAttempts())
381+
.toList();
382+
383+
context.setResult(deadEntries);
384+
}
385+
}
386+
```
387+
388+
[Learn more about event handlers.](./event-handlers/){.learn-more}
389+
390+
### Implement Bound Actions
391+
392+
```java
393+
@Autowired
394+
@Qualifier(PersistenceService.DEFAULT_NAME)
395+
private PersistenceService db;
396+
397+
@On
398+
public void reviveOutboxMessage(DeadOutboxMessagesReviveContext context) {
399+
CqnAnalyzer analyzer = CqnAnalyzer.create(context.getModel());
400+
AnalysisResult analysisResult = analyzer.analyze(context.getCqn());
401+
Map<String, Object> key = analysisResult.rootKeys();
402+
Messages deadOutboxMessage = Messages.create((String) key.get(Messages.ID));
403+
404+
deadOutboxMessage.setAttempts(0);
405+
406+
this.db.run(Update.entity(Messages_.class).entry(key).data(deadOutboxMessage));
407+
context.setCompleted();
408+
}
409+
410+
@On
411+
public void deleteOutboxEntry(DeadOutboxMessagesDeleteContext context) {
412+
CqnAnalyzer analyzer = CqnAnalyzer.create(context.getModel());
413+
AnalysisResult analysisResult = analyzer.analyze(context.getCqn());
414+
Map<String, Object> key = analysisResult.rootKeys();
415+
416+
this.db.run(Delete.from(Messages_.class).byId(key.get(Messages.ID)));
417+
context.setCompleted();
418+
}
419+
```
420+
421+
The injected `PersistenceService` instance is used to perform the operations on the `Messages` entity since the entity `DeadOutboxMessages` is read-only. Both handlers first retrieve the ID of the entry and then they perform the corresponding operation on the database.
422+
423+
[Learn more about CQL statement inspection.](./working-with-cql/query-introspection#cqnanalyzer){.learn-more}
315424

316425
::: tip Use paging logic
317-
Avoid to read all entries of the `cds.outbox.Messages` table at once, as the size of an entry is unpredictable
426+
Avoid to read all entries of the `cds.outbox.Messages` or `OutboxDeadLetterQueueService.DeadOutboxMessages` table at once, as the size of an entry is unpredictable
318427
and depends on the size of the payload. Prefer paging logic instead.
319428
:::
429+
430+
## Observability using Open Telemetry
431+
432+
The transactional outbox integrates Open Telemetry for logging telemetry data.
433+
434+
[Learn more about observability with Open Telemetry.](./operating-applications/observability#open-telemetry){.learn-more}
435+
436+
The following KPIs are logged in addition to the spans described in the [observability chapter](./operating-applications/observability):
437+
438+
| KPI Name | Description | KPI Type |
439+
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------ | -------- |
440+
| `com.sap.cds.outbox.coldEntries` | Number of entries that could not be delivered after repeated attempts and will not be retried anymore. | Gauge |
441+
| `com.sap.cds.outbox.remainingEntries` | Number of entries which are pending for delivery. | Gauge |
442+
| `com.sap.cds.outbox.maxStorageTimeSeconds` | Maximum time in seconds an entry was residing in the outbox. | Gauge |
443+
| `com.sap.cds.outbox.medStorageTimeSeconds` | Median time in seconds of an entry stored in the outbox." | Gauge |
444+
| `com.sap.cds.outbox.minStorageTimeSeconds` | Minimal time in seconds an entry was stored in the outbox. | Gauge |
445+
| `com.sap.cds.outbox.incomingMessages` | Number of incoming messages of the outbox. | Counter |
446+
| `com.sap.cds.outbox.outgoingMessages` | Number of outgoing messages of the outbox. | Counter |
447+
448+
The KPIs are logged per microservice instance (in case of horizontal scaling), outbox, and tenant.

0 commit comments

Comments
 (0)