All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sap.cloud.sdk.cloudplatform.metering.MeteringStats Maven / Gradle / Ivy

Go to download

Implementation of the Cloud platform abstraction for API metering functionality on the SAP Cloud Platform (Neo).

There is a newer version: 2.28.0
Show newest version
/*
 * Copyright (c) 2018 SAP SE or an SAP affiliate company. All rights reserved.
 */

package com.sap.cloud.sdk.cloudplatform.metering;

import java.io.IOException;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
import java.util.Map;

import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.slf4j.Logger;

import com.google.common.util.concurrent.AtomicLongMap;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.sap.cloud.account.TenantContext;
import com.sap.cloud.sdk.cloudplatform.CloudPlatform;
import com.sap.cloud.sdk.cloudplatform.CloudPlatformAccessor;
import com.sap.cloud.sdk.cloudplatform.ScpNeoCloudPlatform;
import com.sap.cloud.sdk.cloudplatform.connectivity.HttpClientAccessor;
import com.sap.cloud.sdk.cloudplatform.connectivity.HttpEntityUtil;
import com.sap.cloud.sdk.cloudplatform.connectivity.exception.DestinationAccessException;
import com.sap.cloud.sdk.cloudplatform.connectivity.exception.DestinationNotFoundException;
import com.sap.cloud.sdk.cloudplatform.exception.ShouldNotHappenException;
import com.sap.cloud.sdk.cloudplatform.logging.CloudLoggerFactory;
import com.sap.cloud.sdk.cloudplatform.security.Role;
import com.sap.cloud.sdk.cloudplatform.tenant.ScpNeoTenant;
import com.sap.cloud.sdk.cloudplatform.tenant.TenantAccessor;
import com.sap.cloud.sdk.cloudplatform.tenant.exception.TenantAccessException;
import com.sap.cloud.sdk.cloudplatform.tenant.exception.TenantNotAvailableException;

/**
 * Stores access numbers to the different APIs.
 */
public class MeteringStats
{
    private static final Logger logger = CloudLoggerFactory.getSanitizedLogger(MeteringStats.class);

    private static final DateTimeFormatter DATE_TIME_FORMATTER =
        DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss 'GMT'").withZone(ZoneId.of("UTC")).withLocale(Locale.US);

    private final AtomicLongMap apiCounter = AtomicLongMap.create();

    private final String serviceId;
    private final Role meteringRole;
    private final String meteringServiceDestinationName;

    /**
     * Initializes a new {@code MeteringStats} with the given service id, role and metering service destination name.
     * 
     * @param serviceId
     *            The id needed for the metering service.
     * @param meteringRole
     *            The role needed to obtain tenant information.
     * @param meteringServiceDestinationName
     *            The name of the metering destination.
     */
    public MeteringStats( final String serviceId, final Role meteringRole, final String meteringServiceDestinationName )
    {
        this.serviceId = serviceId;
        this.meteringRole = meteringRole;
        this.meteringServiceDestinationName = meteringServiceDestinationName;
    }

    /**
     * Record a single access by the tenant to the API specified by the key.
     * 
     * @param key
     *            The metric key.
     * @return The updated number of accesses.
     */
    public long record( final TenantMetricKeyPair key )
    {
        return apiCounter.addAndGet(key, 1);
    }

    /**
     * Record a specified number accesses by a the tenant to the API specified by the key.
     * 
     * @param key
     *            The metric key.
     * @param increment
     *            The number of API accesses to record.
     * @return The updated number of accesses.
     */
    public long record( final TenantMetricKeyPair key, final long increment )
    {
        return apiCounter.addAndGet(key, increment);
    }

    /**
     * Getter for the current number of API accesses.
     * 
     * @param key
     *            The metric key to get the number of accesses for.
     * @return The number of accesses of the tenant to the API specified by the key.
     */
    public long getApiCountsFor( final TenantMetricKeyPair key )
    {
        return apiCounter.get(key);
    }

    /**
     * Aggregates the number of API accesses over all entries.
     * 
     * @return The number of overall API accesses.
     */
    public long getSumOfApiCounts()
    {
        return apiCounter.sum();
    }

    /**
     * Number of metric key stored.
     * 
     * @return The number of stored metric keys.
     */
    public long getDistinctApiCounts()
    {
        return apiCounter.size();
    }

    private TenantContext getCurrentTenantContext()
        throws TenantNotAvailableException,
            TenantAccessException
    {
        return ((ScpNeoTenant) TenantAccessor.getCurrentTenant()).getTenantContext();
    }

    private TenantUserPair getNumberOfTenantUsers( final String tenantId )
    {
        try {
            return getCurrentTenantContext().execute(tenantId, new AuthorizationServiceCallable(meteringRole));
        }
        catch( final Exception e ) {
            logger.error("Failed to execute call to authorization API on behalf of tenant " + tenantId + ".", e);
        }

        return null;
    }

    /**
     * Sends the stored data to the metering service.
     *
     * @throws TenantNotAvailableException
     *             If there is currently no Tenant available. This typically occurs when trying to access the tenant
     *             within code that is running outside any tenant context, e.g., within a background task.
     *
     * @throws TenantAccessException
     *             If there is an issue while accessing the Tenant.
     */
    public void sendToMeteringService()
        throws TenantNotAvailableException,
            TenantAccessException
    {
        if( logger.isInfoEnabled() ) {
            logger.info("Sending collected results to metering service.");
        }

        final HttpClient httpClient;
        try {
            httpClient = HttpClientAccessor.getHttpClient(meteringServiceDestinationName);
        }
        catch( final DestinationNotFoundException | DestinationAccessException e ) {
            logger.error("Failed to send metering results to metering service.", e);
            return;
        }

        final String sampleTimestamp = ZonedDateTime.now().format(DATE_TIME_FORMATTER);
        final JsonArray payload = new JsonArray();

        final CloudPlatform cloudPlatform = CloudPlatformAccessor.getCloudPlatform();

        if( !(cloudPlatform instanceof ScpNeoCloudPlatform) ) {
            throw new ShouldNotHappenException(
                "The current Cloud platform is not an instance of "
                    + ScpNeoCloudPlatform.class.getSimpleName()
                    + ". Please make sure to specify a dependency to com.sap.cloud.s4hana.cloudplatform:core-scp-neo.");
        }

        final ScpNeoCloudPlatform scpNeoCloudPlatform = (ScpNeoCloudPlatform) cloudPlatform;

        for( final com.sap.cloud.account.Tenant tenant : getCurrentTenantContext().getSubscribedTenants() ) {
            final String tenantId = tenant.getId();

            if( tenantId.equals(scpNeoCloudPlatform.getProviderAccountId()) ) {
                continue;
            }

            if( logger.isDebugEnabled() ) {
                logger.debug(
                    "Processing tenantId: "
                        + tenantId
                        + ", accountId: "
                        + tenant.getAccount().getId()
                        + ", accountName: "
                        + tenant.getAccount().getName()
                        + ".");
            }

            final TenantUserPair tenantUserPair = getNumberOfTenantUsers(tenantId);

            if( tenantUserPair != null && tenantUserPair.getNumberOfUsers() > 0 ) {
                if( logger.isDebugEnabled() ) {
                    logger.debug("Processing tenant and user pair: " + tenantUserPair + ".");
                }

                final JsonObject metric = new JsonObject();

                metric.addProperty("metricKey", serviceId + ".AssignedUsers");
                metric.addProperty("account", scpNeoCloudPlatform.getProviderAccountId());
                metric.addProperty("application", cloudPlatform.getApplicationName());
                metric.addProperty("tenantUuid", tenantUserPair.getTenantId());
                metric.addProperty("tenantAccount", tenantUserPair.getAccountId());
                metric.addProperty("instant", sampleTimestamp);
                metric.addProperty("value", tenantUserPair.getNumberOfUsers());

                payload.add(metric);
            }
        }

        final Map bufferMap = apiCounter.asMap();

        for( final Map.Entry entry : bufferMap.entrySet() ) {
            if( entry.getValue() > 0 ) {
                final JsonObject metric = new JsonObject();

                metric.addProperty("metricKey", serviceId + "." + entry.getKey().getMeteringCategory());
                metric.addProperty("account", scpNeoCloudPlatform.getProviderAccountId());
                metric.addProperty("application", cloudPlatform.getApplicationName());
                metric.addProperty("tenantUuid", entry.getKey().getTenantId());
                metric.addProperty("tenantAccount", entry.getKey().getAccountId());
                metric.addProperty("instant", sampleTimestamp);
                metric.addProperty("value", entry.getValue());

                payload.add(metric);
            }
        }

        // FIXME wrap remote call in Hystrix command and handle Hystrix exceptions (HystrixRuntimeException etc.)
        try {
            final HttpPost post = new HttpPost("/");

            post.setEntity(new StringEntity(payload.toString()));
            post.setHeader("Content-Type", "application/json");

            final HttpResponse response = httpClient.execute(post);
            final int statusCode = response.getStatusLine().getStatusCode();

            if( statusCode == HttpStatus.SC_NO_CONTENT ) {
                for( final Map.Entry entry : bufferMap.entrySet() ) {
                    apiCounter.put(entry.getKey(), apiCounter.get(entry.getKey()) - entry.getValue());
                }
                apiCounter.removeAllZeros();
            } else {
                logger.error(
                    "Metering service responded with error "
                        + statusCode
                        + ". Payload: "
                        + HttpEntityUtil.getResponseBody(response)
                        + ".");
            }
        }
        catch( final IOException e ) {
            logger.error("Failed to send results to metering service.", e);
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy