Technical Articles
Step 12 with SAP Cloud SDK: Logging with SAP Cloud SDK
There are two kinds of event logs our SAP Cloud SDK supports you with: AuditLogger and CloudLogger.
Caution: the service for writing logs to the AuditLogger
is not available publicly. The audit logging functionality is only documented below for transparency, but most likely not usable in your situation as of today.
Disclaimer: This blog post is only applicable for the SAP Cloud SDK version of at most 2.19.2. It has not been updated to version 3.0.0 or later. Specifically, CloudLoggerFactory
has been removed – we recommend to use the LoggerFactory
from plain SLF4J instead.
Goal of this blog post
Our tutorial will guide you through both options explaining which mechanism to use in what context and how to use them properly. First, we will use AuditLogger to fulfil auditing requirements. Next, we will walk you through the steps required to log any other information using CloudLogger and support you in better understanding and monitoring your application. In both scenarios, we will outline where you can find your application logs.
As with all powerful tools, they can cause undesired side-effects if not used properly. Do not miss our “When and What to Log” and “Behind the Scene” sections at the end of this tutorial.
Note: This post is part of a series. For a complete overview visit the SAP Cloud SDK Overview.
Creating Your First Audit Log
Systems handling confidential data such as personal or financial information are often faced with additional audit requirements. In these cases, your logging needs should be reflected in a corresponding security concept. In some business areas, it is even required by law to log who accessed or modified what data and when. To make the application development experience delightful, the SDK provides AuditLogger which acts as an abstraction layer from the underlying cloud platform implementation (SCP Neo or Cloud Foundry).
The AuditLogger is intended to be used to document access and modifications to critical data, e.g. when retrieving data from a connected ERP system. Imagine your application uses the SAP S/4 HANA Cloud SDK to integrate closely with an ERP system and the therein stored data.
To integrate AuditLogger we will slightly adapt our previously implemented example of business partners, now selecting a single business partner by key. For demo purposes we assume we are required to log any access or update operation to the “CreationDate” attribute. To continue, we will slightly adopt this example as follows:
package com.sap.cloud.sdk.tutorial;
import com.google.gson.Gson;
import org.slf4j.Logger;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collections;
import com.sap.cloud.sdk.cloudplatform.auditlog.AccessedAttribute;
import com.sap.cloud.sdk.cloudplatform.auditlog.AuditLogger;
import com.sap.cloud.sdk.cloudplatform.logging.CloudLoggerFactory;
import com.sap.cloud.sdk.odatav2.connectivity.ODataException;
import com.sap.cloud.sdk.s4hana.datamodel.odata.namespaces.businesspartner.BusinessPartner;
import com.sap.cloud.sdk.s4hana.datamodel.odata.services.DefaultBusinessPartnerService;
@WebServlet("/businesspartners")
public class BusinessPartnerServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private static final Logger logger = CloudLoggerFactory.getLogger(BusinessPartnerServlet.class);
@Override
protected void doGet(final HttpServletRequest request, final HttpServletResponse response)
throws ServletException, IOException {
String businessPartnerId = request.getParameter("id");
// 1. Create AccessedAttribute Object for more insights on operation attempt of an attribute
AccessedAttribute attemptAttributes = new AccessedAttribute(
"BusinessPartner.CreationDate",
AccessedAttribute.Operation.READ
);
// 2. Log AccessedAttribute Object
AuditLogger.logDataReadAttempt(
businessPartnerId,
null,
Collections.singleton(attemptAttributes),
"Attempt to SELECT business partner"
);
try {
final BusinessPartner businessPartner =
new DefaultBusinessPartnerService()
.getBusinessPartnerByKey(businessPartnerId)
.select(BusinessPartner.BUSINESS_PARTNER,
BusinessPartner.LAST_NAME,
BusinessPartner.FIRST_NAME,
BusinessPartner.IS_MALE,
BusinessPartner.IS_FEMALE,
BusinessPartner.CREATION_DATE)
.execute();
response.setContentType("application/json");
response.getWriter().write(new Gson().toJson(businessPartner));
// 3. Create AccessedAttribute Object to log output of operation of an attribute
AccessedAttribute auditableValueAccessedAttribute = new AccessedAttribute(
"BusinessPartner.CreationDate",
AccessedAttribute.Operation.READ,
"Creation date of business partner",
businessPartner.getCreationDate(),
null,
true
);
// 4a. Log AccessedAttribute Object
AuditLogger.logDataRead(
businessPartner.getBusinessPartner(),
businessPartner.getLastName(),
Collections.singleton(auditableValueAccessedAttribute),
"Succeeded to SELECT business partner",
null
);
} catch (final ODataException e) {
logger.error(e.getMessage(), e);
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
response.getWriter().write(e.getMessage());
AccessedAttribute failedAttemptAttributes = new AccessedAttribute(
"BusinessPartner.CreationDate",
AccessedAttribute.Operation.READ,
"Creation date of business partner",
null,
null,
false
);
// 4b. Log AccessedAttribute Object
AuditLogger.logDataRead(
businessPartnerId,
null,
Collections.singleton(failedAttemptAttributes),
"Failed to SELECT business partner",
e
);
}
}
}
In this example, we use a method of the static class AuditLogger to log a data read and its attempt. Possible parameters are:
- objectId (String): Provide a unique identifier, for instance a UUID or a primary key of a database entry.
- objectName (String, optional): Human readable identifier, typically the class or file that initiated the event.
- attributesAffected (Iterable<AccessedAttribute>, optional): a list of attributes that are used for operation. Therein you can specify the title of the attribute and the performed operation, e.g. READ or WRITE.
- message (String, optional): provide expressive message string to make the log more descriptive.
- accessRequester (AccessRequester, optional): An object containing http request information, typically used to log web-service activities
Please note that the operation specified in attributesAffected should match the used logging method. Also, you are encouraged to create designated Instances of AccessedAttribute for each property you access or change that requires auditing.
Depending on different type of data access or configuration you will want to use different logging methods:
- logSecurityEventBeginning
- logSecurityEvent
- logConfigChangeBeginning
- logConfigChange
- logDataReadAttempt
- logDataRead
- logDataWriteAttempt
- logDataWrite
As the names suggest those should typically be used to log security relevant events, changes to configuration data, read or change access to sensitive personal data or any other project-specific event requirements. You will find method pairs consisting of two flavours: plain and ending with “Attempt” or “Beginning”. With the plain method, you document the actual outcome of an operation, whether successful or not. The latter can be used to document the operations’ starting point as suggested in our code example; its usage, however, is optional.
Viewing Your Audit Logs
Once your app is deployed to SCP Neo, audit logs are automatically created, whenever invoked. On Cloud Foundry you need to additionally create a service instance (cf create-service auditlog standard my-auditlog
) and bind your application to the audit log service.
In order to access the audit logs, please study the corresponding help documentation for Cloud Foundry and Neo. On Cloud Foundry, there is also an audit log viewer application available.
Note: When using AuditLogger in a locally deployed development environment logs are forwarded to SCP using CloudLogger. Due to the simpler accessibility of those logs, this will greatly enhance your development and testing capabilities. For more information on how to read CloudLogger events continue reading our next section.
Creating Your First Cloud Log
The previously described AuditLogger focuses on documenting access and modifications to critical data to comply with security and auditing requirements. It thus does not support different logging levels, which makes it an impractical choice for development and support centered logging needs.
Let’s have a look at our business partners example again. Assuming we are not required to document audit relevant events anymore, we are now going to use CloudLogger to log application events of any kind. Afterwards we will read out our generated logs and change the logging level to something higher (e.g. DEBUG) to avoid cluttering our log files with too much information.
package com.sap.cloud.sdk.tutorial;
import com.google.gson.Gson;
import org.slf4j.Logger;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import com.sap.cloud.sdk.cloudplatform.logging.CloudLoggerFactory;
import com.sap.cloud.sdk.odatav2.connectivity.ODataException;
import com.sap.cloud.sdk.s4hana.datamodel.odata.helper.Order;
import com.sap.cloud.sdk.s4hana.datamodel.odata.namespaces.businesspartner.BusinessPartner;
import com.sap.cloud.sdk.s4hana.datamodel.odata.services.DefaultBusinessPartnerService;
@WebServlet("/businesspartners")
public class BusinessPartnerServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
// 1. Get logger for our servlet
private static final Logger logger = CloudLoggerFactory.getLogger(BusinessPartnerServlet.class);
private static final String CATEGORY_PERSON = "1";
@Override
protected void doGet(final HttpServletRequest request, final HttpServletResponse response)
throws ServletException, IOException {
//2. Log event providing INFO level, info is moderate
logger.info("Attempt to SELECT business partners");
try {
final List<BusinessPartner> businessPartners =
new DefaultBusinessPartnerService()
.getAllBusinessPartner()
.select(BusinessPartner.BUSINESS_PARTNER,
BusinessPartner.LAST_NAME,
BusinessPartner.FIRST_NAME,
BusinessPartner.IS_MALE,
BusinessPartner.IS_FEMALE,
BusinessPartner.CREATION_DATE)
.filter(BusinessPartner.BUSINESS_PARTNER_CATEGORY.eq(CATEGORY_PERSON))
.orderBy(BusinessPartner.LAST_NAME, Order.ASC)
.execute();
response.setContentType("application/json");
response.getWriter().write(new Gson().toJson(businessPartners));
// 3a. Log event including basic information providing INFO level
logger.info("Succeeded to SELECT {} business partners", businessPartners.size());
} catch (final ODataException e) {
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
response.getWriter().write(e.getMessage());
// 3b. Log event including exception providing ERROR level
logger.error("Failed to SELECT business partners", e);
}
}
}
Before creating a new log entry, the SAP S/4 HANA Cloud SDK checks the configured logging level and only persist the log if it is above our specified logging level. Possible logging levels are:
ERROR > WARN > INFO > DEBUG > TRACE
Thus, the log above will only be saved if the configured logging level is TRACE, DEBUG or INFO, but not if it is WARN or ERROR.
There are cases, in which you want to clean your logs from certain data or adjust the format of it to better fit your logging needs. Sometimes, this step can be computationally intensive and is best omitted if logging is not enabled in the first place. To check for this, we encourage you to use CloudLogger’s isDebugEnabled method and skip any log relevant tasks if debug is disabled as their outcome will not be used anyway.
Adjust Cloud Log Level
Applications running on SCP Neo and SCP CloudFoundry use the same interface to generate application logs. Adjusting the debug level, however, differs slightly between the two platforms. For SCP CloudFoundry changing the debug level during runtime is not supported yet. In order to adjust the debug level, you need to redeploy your application. For latest information on debug levels in CloudFoundry please check out the official SCP Documentation linked in ‘Additional Reading’
In SCP Neo all events come with a default debug level. Adjustments can be configured at any time using the SCP Cockpit. Follow the steps in the next section below to view the logs and possibly adjust their debug levels as required. Please note, that loggers are not listed – and thus can’t be configured – until the relevant application code has been executed at least once.
Viewing Your Cloud Logs
The logs can then be viewed via the log viewer, which is part of the SCP Cockpit. The following screen shows you an exemplary view:
Similar to the AuditLogger, the SAP S/4 HANA Cloud SDK adds additional attributes to the log message to facilitate debugging. This includes for example a timestamp, the application name and the executing thread.
When and What to Log
There are many different logging strategies. In the end, when and what to log is highly dependent on the use case and the expected debugging and support requirements. Typically logs should be written to cater for the reader’s requirements: Ensure that your logs are in English, meaningful, precise, and contain enough context to be understandable. Events worth logging are mostly exceptions, software life cycle events and database requests. But again: Logging every validation error or every small database request might not be the way to go. Using logs too excessively may entail cluttered log files and contradict their initial purpose. As a best practice, any semantic event should be logged only once – if you encounter an exception that is e.g. propagated up the exception handler chain, not every exception handler should print information about the error. The SDK comes with a handful of PMD rules that support you to use our logging mechanism everywhere and also draws your attention to missing exceptions you might want to log. Check our blogpost on static code analysis. For extended readings suggestions on the topic check out our “Additional Reading” section below.
Behind the Scenes
Behind the scene the SAP S/4 HANA Cloud SDK uses state of the art logging libraries such as SLF4J (Simple Logging Facade for Java). These are wrapped to provide the necessary abstraction to the underlying cloud platform implementation. That said, we recommend using the logging facilities of the SDK. This also ensures that you can leverage future SDK improvements and are more independent of changes to SCP Neo and Cloud Foundry. That being said, Happy logging!
Of course, this post does not include a full overview over all the different aspects of the logging functionality of the SAP S/4 HANA Cloud SDK. For further details please take a look at our API documentation.
Troubleshooting
- SDK error message: “Unable to instantiate ScpCfAuditLog. Falling back to DefaultLoggerAuditLog. This issue may be caused by a missing dependency. No audit log entries will be written, log output will be redirected to the default log instead. This issue should never occur in productive environments.” -> This message is expected to occur while debugging or using an unsupported deployment platform. It can be safely ignored during development.
- SDK warning message: “Unable to get audit logger object from JNDI. Will instantiate directly” -> This message is expected to occur while debugging or using an unsupported deployment platform. It can be safely ignored during development
Additional Reading
- SAP Cloud SDK API: https://sap.github.io/cloud-s4-sdk-examples/docs/latest/javadoc-api/index.html
- Logging best-practices: https://dzone.com/articles/application-logging-what-when
- SLF4J.Logger API: https://www.slf4j.org/api/org/slf4j/Logger.html
- SCP NEO Logging Doc: https://help.sap.com/viewer/65de2977205c403bbc107264b8eccf4b/Cloud/en-US/e6e8ccd3bb571014b6afdc54744eef4d.html
- SCP CF Logging Doc: https://help.sap.com/viewer/65de2977205c403bbc107264b8eccf4b/Cloud/en-US/68454d44ad41458788959485a24305e2.html
- Java Logging Support for Cloud Foundry: https://github.com/SAP/cf-java-logging-support
Change log (January 04, 2018):
Change log (June 14, 2018):
Hello Henning,
Can you share link to: "The SDK comes with a handful of PMD rules that support you to use our logging mechanism everywhere and also draws your attention to missing exceptions you might want to log. Check our blogpost on static code analysis."
Thanks
Hi Sergey,
here is the link to the blog post:
https://blogs.sap.com/2017/09/20/static-code-checks/
The first paragraph links also the overview page with all blog posts.
Best regards,
Daniel
Hi,
Need one info regarding application logger service. As per the docs, the retention period for the logs is 7 days. For extended period, how do we save the logs?
Thanks,
Arun
Hello Arun,
I assume you could save relevant logs yourself, while considering data privacy and protection requirements. As far as I know, the application logger does not allow to extend the retention period. It is not meant for logging audit-relevant facts, for which you should use the audit logger.
Best regards,
Henning
Change log May 2019: update name to SAP Cloud SDK (background)
Hello Henning,
I have some questions regarding the tenant which is used for writing the audit log:
Thanks
André
Hi Andre,
The audit logger uses the TenantAccessor and its tryGetCurrentTenant() method to get the tenant. This first tries to get the tenant from the incoming HTTP request and the JWT token within (specifically, the 'zid' and 'iss' fields). If there is no suitable token available, then it will fall back to asking the XSUAA instance bound to your application for a service JWT. Finally if none of those are available, then the tenant ID of AccessRequester would not be populated.
To answer your questions specifically:
Regards,
Pablo
Hi Pablo,
thanks for the answers. One additional questions:
Where the XSUAA token is retrieved from? SecurityContext or somewhere else?
Thanks
Hi Andre,
Currently the audit logging and the rest of the SAP Cloud SDK makes XSUAA token requests directly to the bound XSUAA service, and caches to our own ThreadContext.
Regards,
Pablo