Skip to Content

There are two kinds of event logs our SAP S/4HANA Cloud SDK supports you with: AuditLogger and CloudLogger.

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 S/4HANA 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 use our previously implemented example of CostCenters. For demo purposes we assume we are required to log any access or update operation to the “status” 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.odatav2.connectivity.ODataProperty;
import com.sap.cloud.sdk.odatav2.connectivity.ODataQueryBuilder;
import com.sap.cloud.sdk.odatav2.connectivity.ODataType;
import com.sap.cloud.sdk.s4hana.connectivity.ErpEndpoint;

@WebServlet("/costcenters")
public class CostCenterServlet extends HttpServlet {

    private static final long serialVersionUID = 1L;
    private static final Logger logger = CloudLoggerFactory.getLogger(CostCenterServlet.class);

    @Override
    protected void doGet(final HttpServletRequest request, final HttpServletResponse response)
            throws ServletException, IOException
    {
        String costCenterId = request.getParameter("costCenterId");

        AccessedAttribute attemptAttributes = new AccessedAttribute( // 1. Create AccessedAttribute Object for more insights on operation attempt of an attribute
            "CostCenter.status",
            AccessedAttribute.Operation.READ
        );
        AuditLogger.logDataReadAttempt( // 2. Log AccessedAttribute Object
            costCenterId,
            null,
            Collections.singleton(attemptAttributes),
            "Attempt to SELECT cost centers"
        );

        try {
            final ErpEndpoint endpoint = new ErpEndpoint();
            final CostCenterDetails costCenter = ODataQueryBuilder
                    .withEntity("/sap/opu/odata/sap/FCO_PI_COST_CENTER", "CostCenterCollection")
                    .select("CostCenterID", "Status", "CompanyCode", "Category", "CostCenterDescription")
                    .filter(ODataProperty.field("CostCenterID").eq(ODataType.of(costCenterId)))
                    .build()
                    .execute(endpoint)
                    .asList(CostCenterDetails.class).get(0);

            response.setContentType("application/json");
            response.getWriter().write(new Gson().toJson(costCenter));

            AccessedAttribute auditableValueAccessedAttribute = new AccessedAttribute( // 3. Create AccessedAttribute Object to log output of operation of an attribute
                    "CostCenter.status",
                    AccessedAttribute.Operation.READ,
                    "Status of Cost Center",
                    costCenter.getStatus(),
                    null,
                    true
                );

            AuditLogger.logDataRead( // 4a. Log AccessedAttribute Object
                    costCenter.getCostCenterID(),
                    costCenter.getCostCenterDescription(),
                    Collections.singleton(auditableValueAccessedAttribute),
                    "Succeeded to SELECT cost center",
                    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(
                "CostCenter.status",
                AccessedAttribute.Operation.READ,
                "Status of Cost Center",
                null,
                null,
                false
            );

            AuditLogger.logDataRead( // 4b. Log AccessedAttribute Object
                costCenterId,
                null,
                Collections.singleton(failedAttemptAttributes),
                "Failed to SELECT cost center",
                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 bind your application to the audit log service.  In order to access the audit logs, please request them using a BCP incident. The following information are required to handle your request as quickly as possible: Landscape (e.g. Factory EU, Account, Time frame, Tenant ID). In addition to the above information, please state that you want audit logs to be extracted. Below, you can find the information of the provided CSV log entry:

"URIEncoding","_raw","_time","account","accountExt","application","applicationName","binaries","category","clientTimestamp","component","compression","computeUnitSize","connectionTimeout","connection_timeout","correlation_id","crtAccount","crtApplication","crtComponent","crtHostname","crtPermissions","crtTenantId","custom","dataType","description","displayName","elasticity_data","eventtype","files","formatVersion","host","id","index","ip_address","jpaas_services","jvmVersion","linecount","loggedByClass","maxProcesses","maxThreads","max_threads","messageId","minProcesses","name","object","onBehalfOf","path","punct","runtimeArguments","runtimeId","runtimeName","runtimeVersion","serverTimestamp","source","sourcetype","splunk_server","splunk_server_group","tenantId","tenantName","timestamp","uriEncoding","username","value","verb","vmSize"

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 cost centre 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.odatav2.connectivity.ODataQueryBuilder;
import com.sap.cloud.sdk.s4hana.connectivity.ErpEndpoint;

@WebServlet("/costcenters")
public class CostCenterServlet extends HttpServlet {

    private static final long serialVersionUID = 1L;
    private static final Logger logger = CloudLoggerFactory.getLogger("/costcenters"); // 1. Get logger for our service

    @Override
    protected void doGet(final HttpServletRequest request, final HttpServletResponse response)
            throws ServletException, IOException
    {
        logger.info("Attempt to SELECT cost centers"); // 2. Log event providing INFO Level, info is moderate
        try {
            final ErpEndpoint endpoint = new ErpEndpoint();
            final List<CostCenterDetails> costCenters = ODataQueryBuilder
                    .withEntity("/sap/opu/odata/sap/FCO_PI_COST_CENTER", "CostCenterCollection")
                    .select("CostCenterID", "Status", "CompanyCode", "Category", "CostCenterDescription")
                    .build()
                    .execute(endpoint)
                    .asList(CostCenterDetails.class);

            response.setContentType("application/json");
            response.getWriter().write(new Gson().toJson(costCenters));

            logger.info("Succeeded to SELECT cost centers"); // 3a. Log event providing INFO Level, info is moderate

        } catch(final ODataException error) {
            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            response.getWriter().write(e.getMessage());

            logger.error("Failed to SELECT cost centers"); // 3b. Log event providing ERROR Level, we are using error here as an error is severe
        }
    }
}

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

To report this post you need to login first.

Be the first to leave a comment

You must be Logged on to comment or reply to a post.

Leave a Reply