Skip to Content
Technical Articles
Author's profile photo Henning Heitkoetter

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

Assigned Tags

      11 Comments
      You must be Logged on to comment or reply to a post.
      Author's profile photo Henning Heitkoetter
      Henning Heitkoetter
      Blog Post Author

      Change log (January 04, 2018):

      • Adapted to new business partners example.
      Author's profile photo Henning Heitkoetter
      Henning Heitkoetter
      Blog Post Author

      Change log (June 14, 2018):

      • Author changed to Henning Heitkoetter
      Author's profile photo Sergey Dobrynin
      Sergey Dobrynin

      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

      Author's profile photo Daniel Kurzynski
      Daniel Kurzynski

      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

       

      Author's profile photo arun kumar pai
      arun kumar pai

      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

      Author's profile photo Henning Heitkoetter
      Henning Heitkoetter
      Blog Post Author

      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

      Author's profile photo Henning Heitkoetter
      Henning Heitkoetter
      Blog Post Author

      Change log May 2019: update name to SAP Cloud SDK (background)

      Author's profile photo Andre Adam
      Andre Adam

      Hello Henning,

      I have some questions regarding the tenant which is used for writing the audit log:

      1. We have to log positive security events like someone calls us and logged on successfully using a JWT. From where the tenant is retrieved in general when you write the audit log?
      2. If we are in an async task, where the tenant is coming from there?
      3. We also have to write an audit log in case of negative security events like someone calls us without JWT or with a JWT which is not valid. In this case which tenant is used for the audit log?

      Thanks

      André

      Author's profile photo Pablo Caceres
      Pablo Caceres

      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:

      1. From JWT of incoming HTTP request, then falls back to XSUAA token.
      2. Since there's no incoming request, then by default try to get the XSUAA token. So the provider tenant hosting the application.
      3. It would behave like #2.

       

      Regards,
      Pablo

      Author's profile photo Andre Adam
      Andre Adam

      Hi Pablo,

       

      thanks for the answers. One additional questions:

      Where the XSUAA token is retrieved from? SecurityContext or somewhere else?

       

      Thanks

      Author's profile photo Pablo Caceres
      Pablo Caceres

      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