Sets up SAP Cloud Logging and OpenTelemetry for Java Cloud Foundry apps, replacing Neo monitoring with centralized logging, custom metrics, and health checks.
npx claudepluginhub sap-samples/btp-neo-java-app-migration --plugin sap-btp-neo-migrationThis skill is limited to using the following tools:
Set up SAP Cloud Logging for application monitoring in Cloud Foundry.
Guides Next.js Cache Components and Partial Prerendering (PPR): 'use cache' directives, cacheLife(), cacheTag(), revalidateTag() for caching, invalidation, static/dynamic optimization. Auto-activates on cacheComponents: true.
Processes PDFs: extracts text/tables/images, merges/splits/rotates pages, adds watermarks, creates/fills forms, encrypts/decrypts, OCRs scans. Activates on PDF mentions or output requests.
Share bugs, ideas, or general feedback.
Set up SAP Cloud Logging for application monitoring in Cloud Foundry.
Replace Neo's built-in monitoring and availability checks with SAP Cloud Logging service for centralized logging, custom metrics, and application observability.
This skill applies if any of these patterns are found:
Working directory: This skill must run inside the
-cf-migrationcopy of your app, created byjakarta-java25-migrationorneo-to-cf-migration-orchestrator. If your current directory does not end in-cf-migration, switch to it before proceeding.
Before invoking this skill, ensure you have invoked:
Use the sdk-replacement skill
Also required:
Add Cloud Logging service and OpenTelemetry configuration:
_schema-version: "3.2"
ID: ${app-name}
version: 0.0.1
modules:
- name: ${app-name}
type: java.tomcat
path: target/${app-name}.war
parameters:
buildpack: sap_java_buildpack_jakarta
disk-quota: 512MB
memory: 512MB
properties:
ENABLE_SECURITY_JAVA_API_V2: true
SET_LOGGING_LEVEL: 'ROOT: INFO, com.example: DEBUG'
# OpenTelemetry configuration
OTEL_JAVAAGENT_ENABLED: true
OTEL_SERVICE_NAME: ${app-name}
OTEL_TRACES_EXPORTER: otlp
OTEL_METRICS_EXPORTER: otlp
OTEL_LOGS_EXPORTER: otlp
requires:
- name: ${app-name}-cls
resources:
- name: ${app-name}-cls
type: org.cloudfoundry.managed-service
parameters:
service: cloud-logging
service-plan: standard
Create or update src/main/resources/logback.xml:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- Console appender for Cloud Foundry log collection -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- JSON format for structured logging (recommended) -->
<appender name="JSON" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<includeMdcKeyName>correlationId</includeMdcKeyName>
<includeMdcKeyName>userId</includeMdcKeyName>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="JSON" />
</root>
<!-- Application-specific logging -->
<logger name="com.example" level="DEBUG" />
<!-- SAP libraries -->
<logger name="com.sap.cloud.sdk" level="WARN" />
</configuration>
Add logstash encoder dependency to pom.xml:
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>7.4</version>
</dependency>
Create a logging utility:
package com.example.logging;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
public class AppLogger {
private final Logger logger;
public AppLogger(Class<?> clazz) {
this.logger = LoggerFactory.getLogger(clazz);
}
/**
* Log with correlation ID for request tracing
*/
public void logWithCorrelation(String correlationId, String message, Object... args) {
try {
MDC.put("correlationId", correlationId);
logger.info(message, args);
} finally {
MDC.remove("correlationId");
}
}
/**
* Log with user context
*/
public void logWithUser(String userId, String message, Object... args) {
try {
MDC.put("userId", userId);
logger.info(message, args);
} finally {
MDC.remove("userId");
}
}
/**
* Log business event with structured data
*/
public void logBusinessEvent(String eventType, String entityId, String action) {
try {
MDC.put("eventType", eventType);
MDC.put("entityId", entityId);
MDC.put("action", action);
logger.info("Business event: {} on {} - {}", eventType, entityId, action);
} finally {
MDC.clear();
}
}
// Standard logging methods
public void info(String message, Object... args) {
logger.info(message, args);
}
public void error(String message, Throwable t) {
logger.error(message, t);
}
public void debug(String message, Object... args) {
logger.debug(message, args);
}
}
Replace Neo availability checks with a health endpoint:
package com.example.health;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import javax.sql.DataSource;
import java.io.IOException;
import java.sql.Connection;
@WebServlet("/health")
public class HealthCheckServlet extends HttpServlet {
private static final ObjectMapper objectMapper = new ObjectMapper();
// Inject or lookup DataSource if database is used
private DataSource dataSource;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
ObjectNode health = objectMapper.createObjectNode();
health.put("status", "UP");
health.put("timestamp", System.currentTimeMillis());
// Check database connectivity
ObjectNode database = objectMapper.createObjectNode();
try {
if (dataSource != null) {
try (Connection conn = dataSource.getConnection()) {
database.put("status", "UP");
}
} else {
database.put("status", "N/A");
}
} catch (Exception e) {
database.put("status", "DOWN");
database.put("error", e.getMessage());
health.put("status", "DOWN");
}
health.set("database", database);
// Check memory
ObjectNode memory = objectMapper.createObjectNode();
Runtime runtime = Runtime.getRuntime();
memory.put("total", runtime.totalMemory());
memory.put("free", runtime.freeMemory());
memory.put("used", runtime.totalMemory() - runtime.freeMemory());
memory.put("max", runtime.maxMemory());
health.set("memory", memory);
resp.setContentType("application/json");
resp.setStatus(health.get("status").asText().equals("UP") ? 200 : 503);
objectMapper.writeValue(resp.getOutputStream(), health);
}
}
logs-cfsyslog-*cf.app_name: ${app-name}metrics-otel-v1-*app.requests.totalcf.app_name: "my-app" AND level: "ERROR"
cf.app_name: "my-app" AND http.response_time > 1000
cf.app_name: "my-app" AND userId: *
| File | Location | Purpose |
|---|---|---|
logback.xml | src/main/resources/ | Logging configuration |
| Service | Plan | Purpose |
|---|---|---|
cloud-logging | standard | Centralized logging and monitoring |
cf services
# Should show cloud-logging service bound
curl "https://${app-url}/test"
# Should generate log entries
cf logs ${app-name} --recent
curl "https://${app-url}/health"
# Should return JSON health status
Cause: Service not bound or wrong index pattern. Solution:
Cause: OTEL agent not enabled.
Solution: Set OTEL_JAVAAGENT_ENABLED: true in mtad.yaml.
Solution: Adjust log levels:
properties:
SET_LOGGING_LEVEL: 'ROOT: WARN, com.example: INFO'
| Neo Feature | CF Equivalent |
|---|---|
| Application logs | Cloud Logging / cf logs |
| Availability checks | Health endpoint + external monitoring |
| JMX metrics | OpenTelemetry + Cloud Logging metrics |
| Performance tracing | Distributed tracing with OTEL |
| Alerting | Cloud Logging alerting rules |
After completing this skill, your monitoring setup is complete. Consider: