Easily integrates with tomcat/spring.

Github repo with Java SDK samples

Simple Main class example:


<!-- https://mvnrepository.com/artifact/io.amberflo/metering-java-client -->

Batching Records

Amberflo.io libraries are built to support high throughput environments. That means you can safely send hundreds of meter records per second. For example, you can chose to deploy it on a web server that is serving hundreds of requests per second.
However, every call does not result in a HTTP request, but is queued in memory instead. Messages are batched and flushed in the background, allowing for much faster operation. The size of batch and rate of flush can be customized.
Defaults: The library will flush every 100 messages (configuration parameter: maxBatchSize) or if 0.5 seconds has passed since the last flush (configuration parameter: maxDelayInSec)

What happens if there are just too many messages?
If the module detects that it can’t flush faster than it’s receiving messages, it’ll simply stop accepting messages. This allows your program to continually run without ever crashing due to a backed up metering queue. The maximum meters kept in memory is 100,000 (configuration parameter: maxAsyncQueyeSize).

Flush on demand (Blocking call)

You can flush on demand. For example, at the end of your program, you’ll want to flush to make sure there’s nothing left in the queue. Just call the flush method:


Please note: Calling this method will block the calling thread until there are no messages left in the queue. You’ll want to use it as part of your cleanup scripts and avoid using it as part of the request lifecycle.

Using S3

You can use the java SDK to write to S3, and Amberflo will read it from there

Code examples

Simple ingest meter

final Double meterValue = 2D;
final LocalDateTime accurateMeterTime = LocalDateTime.now();
final Map<String, String> dimensions = new HashMap<>();
System.setProperty(MeteringContext.METERING_DOMAIN, Domain.Prod.name());
// Use the app key for authentication
//MeteringContext context = MeteringContext.createOrReplaceContext("appKey")
                "myApp", Domain.Prod,Region.US_West, 1, 10);
// Simple meter call. This call is non blocking and batched for performance
context.meteringInstance().meter("customerId", "meterName",meterValue,accurateMeterTime,dimensions);

Ingest examples

package demo;

import com.amberflo.metering.ingest.MeteringContext;
import com.amberflo.metering.ingest.extensions.ServiceMetering;
import com.amberflo.metering.ingest.meter_message.Domain;
import com.amberflo.metering.ingest.meter_message.MeterMessage;
import com.amberflo.metering.ingest.meter_message.MeterMessageBuilder;

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.HashMap;
import java.util.Map;

import com.amberflo.metering.ingest.meter_message.Region;

import static com.amberflo.metering.ingest.MeteringContext.metering;
import static com.amberflo.metering.ingest.extensions.ServiceMetering.serviceMetering;
import static com.amberflo.metering.ingest.extensions.CustomerMetering.customerMetering;

 * In this app we describe different ways to call the metering services:
 * 1. Builder.
 * 2. Factory.
 * 3. Templates.
 * The example itself used the {@link Domain#Prod} configuration to setup the metering client.
 * For more info about each of our api please see the java doc.
public class MeteringExamples {
    private final static String METER_NAME = "TrancsactionCount";
    private final static int METER_VALUE = 3;
    private final static String CUSTOMER_ID = "YWJjNDU2";
    private final static String SERVICE_CALL = "process-request";
    private final static String SERVICE_NAME = "magic-service";
    private final static LocalDateTime EVENT_TIME = LocalDateTime.now();
    private final static  Double MB_USED = 15.5;
    private final static double PROCESSING_TIME_MILLIS = 100;

    private final static boolean ERROR_FLAG = true;

    private final static Map<String, String> EXTRA_DIMENSIONS_MAP = null;

    public static void main(String[] args) throws Exception {
        // Step 1 - setting up the domain (prod dev)
        // You need to set the "metering domain" system env var to prod.
        System.setProperty(MeteringContext.METERING_DOMAIN, Domain.Prod.name());

        // Step 2 - send some meters to amberflo.

        // Step 3 - flush and close the metering client.
        // For efficiency, we send meters to amberflo in batches. You can configure the max batch size in
        // the prod/def metering.json file. Having said that, even if you use a max batch size of 1, you still want
        // to call this method, especially if use configured 'isAsync' to true.

        // Comment: 'Metering' is autoClosable - so instead of step 2 and 3, you can also send meters this way:
        try (final MeteringContext context = MeteringContext.getContext()) {
            // templates are just and abstraction layer on the top of the metering.
            customerMetering().signUp(CUSTOMER_ID, EVENT_TIME);

     * Here we describe the 3 methods to directly send meters:
     * 1. Builder.
     * 2. Factory.
     * 3. Templates.
    private static void sendSomeMeters() {
        // Option 1: Create a custom meter with a builder.
        // This option gives you more flexibility regarding the way you construct your meter event, but it
        // requires more effort on your side, and might be more errors prone than other options.
        // Use it only if you need a completely custom meter.
        final MeterMessage meter = MeterMessageBuilder
                .createInstance(METER_NAME, EVENT_TIME, CUSTOMER_ID)

        // Look at this method to see a more advanced example for how to set a meter this way.

        // Option 2: Create a custom message with a factory
        // A factory is a bit more structured way of creating a meter but less flexible. It lets you send
        // a meter which contains the:
        // a. The customer id - Mandatory (unless you invoke the method within a thread context with
        //    customer info). This is the id of your customer who made a call to your service (let's say).
        // b. The meter name and value - Required.
        // c. The start_time - Optional. this can be a start time of a call, or the event time which is relevant
        //    for the meter. If the start time is null then the event time will by the current time when sending
        //    the meter.
        // d. The extra dimensions map - Optional. this is just a map of string keys to string values containing
        //    important dimensions.
        metering().meter(CUSTOMER_ID, METER_NAME, METER_VALUE, EVENT_TIME, null);

        // Option 3: Templates
        // Templates are the most convenient but least flexible option of creating meters explicitly. Basically
        // templates are predefined meter types, with a predefined meter name, and predefined meter structure.
        // Why do we need templates ?
        // Let's assume we want to create meters for a service. What would we try to measure ? ... probably we would
        // want to record commons things such as 'service calls', 'data usage' or 'processing time'. Now, we don't
        // want you to reinvent the will for such common use cases, so we created a few domain-specific factories that
        // allows you to create meters more easily.

        // Option 3.1 - Service-Metering:
        // use the serviceMetering to create
        serviceMetering().call(CUSTOMER_ID, SERVICE_CALL, EVENT_TIME);
        serviceMetering().processingTime(CUSTOMER_ID, SERVICE_CALL, PROCESSING_TIME_MILLIS, EVENT_TIME);
        serviceMetering().dataUsage(CUSTOMER_ID, SERVICE_CALL, MB_USED, EVENT_TIME);
        // See the method below for more advanced\detailed examples of using the meters.

        // Option 3.2 - Customer-Metering.
        // This templates allow you to create customer (client) related meters.
        customerMetering().signUp(CUSTOMER_ID, EVENT_TIME);
        customerMetering().onboarded(CUSTOMER_ID, EVENT_TIME);
        customerMetering().offboarded(CUSTOMER_ID, EVENT_TIME);
        customerMetering().login(CUSTOMER_ID, EVENT_TIME);
        // This meter described a case a customer registered to your service, but you rejected the registration
        // (false-data or a malicious customer for example).
        customerMetering().onboardingRejected(CUSTOMER_ID, EVENT_TIME);

    private static void customBuilderAdvanced() {
        final Map<String, String> sessionInfo = new HashMap<>();
        sessionInfo.put("session", "789");

        final Map<String, String> countryAndStateInfo = new HashMap<>();
        sessionInfo.put("country", "US");
        sessionInfo.put("state", "WA");

        final MeterMessage meter = MeterMessageBuilder
                .createInstance(METER_NAME, EVENT_TIME, CUSTOMER_ID)
                // meter value is actually optional param. If not set the the meter value will be 1.
                // if you want to capture the end time of the meter event (now), and measure the duration of
                // it, you can use the this method which will set up the duration as the time diff in millis between
                // now and the START_TIME. Calling this method will set the meter-type to "Millis".
                .captureEndTimeAndDuration() // in our case the duration should be ~3 minutes (in millis).
                // meter type isn't currently in use by amberflo. Yet if you feel the need to have it
                // use it ... we will notice that and adapt our system accordingly.
                // Set up service related data.
                // Service name is a param to the 'DirectMeteringClient', nevertheless, you can override it here.
                // If you want to mark your meter as error related you can call.
                .asError() /* or */ .asError(IllegalArgumentException.class)
                // you can set up a region.
                // The dimensions Map gives you the option to add more properties to your meter, which don't
                // exist as part of the predefined methods of the builder.
                // For example, let's assume you want to add session info to your meter to track a process
                // or customer request e2e, you can add this dimensions this way:
                // Another example for custom dimensions:
                // Let's assume you want to measure customer related events. As part of that you want to partition
                // your customers by country and state. So you can add these two as custom attributes:
                // ^^ it's ok call setDimensionsMap multiple times as long as there is no intersection between
                // the keys of the maps.

     * The service-metering template contains the following:
     * {@link ServiceMetering#CALL}:
     * 1. CallCompleted (successfully).
     * 2. CallError.
     * 3. Call - this one is to try and measure a call regardless if it completed successfully or with an error.
     * All of these types of calls will produce a {@link ServiceMetering#CALL} meter as they indicates the event of finish
     * handling a call (with or without an error).
     * Other types of calls:
     * There are 3 other more specific type of call which will produce different meter
     * 1. CallStarted - Will produce a {@link ServiceMetering#CALL_STARTED} meter. Use this event if you want to
     * have different meters for the start and the end of a call.
     * 2. DataUsage - measure data used by the client (in Mb). Will produce a {@link ServiceMetering#CALL_DATA_USAGE} meter.
     * 3. ProcessingTime - Time it took to process a service call request (in millis). Will produce a
     * {@link ServiceMetering#CALL_PROCESSING_TIME} meter.
    private static void serviceMeteringAdvanced() {
        // 'call' will produce a meter called "Call" and it can be used in many ways:
        serviceMetering().call(CUSTOMER_ID, SERVICE_CALL, EVENT_TIME);
        serviceMetering().call(CUSTOMER_ID, SERVICE_CALL, ERROR_FLAG, EVENT_TIME); // if you want to mark it as an error
        serviceMetering().call(CUSTOMER_ID, SERVICE_CALL, ERROR_FLAG, IllegalAccessError.class, EVENT_TIME); // error + error type
        // The SERVICE_CALL in the example above isn't the meter name but a dimension.

        // If you find it more convenient you can also record a service call using
        serviceMetering().callCompleted(CUSTOMER_ID, SERVICE_CALL, EVENT_TIME); // To mark the end of a call that completed successfully.
        serviceMetering().callError(CUSTOMER_ID, SERVICE_CALL, EVENT_TIME); // To mark the end of a call that completed with an error.
        // We will see soon how these can be used.

        // 'callStarted' as the name suggests, measure an event of start handling a service call regardless of the
        // results (the call completed successfully or not).
        serviceMetering().callStarted(CUSTOMER_ID, SERVICE_CALL, EVENT_TIME);

        // 'processingTime' - measures the time it took to process the call in millis.
        serviceMetering().processingTime(CUSTOMER_ID, SERVICE_CALL, PROCESSING_TIME_MILLIS, EVENT_TIME);

        // 'dataUsage' - as the name suggests, measures the data returned to the client or used by the call in Mb.
        serviceMetering().dataUsage(CUSTOMER_ID, SERVICE_CALL, MB_USED, EVENT_TIME);

        // Examples of using multiple of the calls above:
        final Runnable methodToRun = () -> {
            try {
            } catch (InterruptedException e) {

     * You can have a similar decorator/interceptor method to this one in your code.
    private static void serviceMeteringMultiCalls(final Runnable runnable) {
        final LocalDateTime startTime = LocalDateTime.now();
        LocalDateTime endTime = startTime;
        try {
            serviceMetering().callStarted(CUSTOMER_ID, SERVICE_CALL, startTime);


            endTime = LocalDateTime.now();
            serviceMetering().callCompleted(CUSTOMER_ID, SERVICE_CALL, endTime);
        } catch (final Exception e) {
            endTime = LocalDateTime.now();
            serviceMetering().callError(CUSTOMER_ID, SERVICE_CALL, e.getClass(), endTime);
        } finally {
            final long durationInMillis  =
                    endTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli() -

            serviceMetering().processingTime(CUSTOMER_ID, SERVICE_CALL, durationInMillis, endTime);

Usage example


package demo.usage;

import com.amberflo.metering.usage.clients.UsageClient;
import com.amberflo.metering.usage.model.request.*;
import com.amberflo.metering.usage.model.response.DetailedMeterAggregation;

import java.time.OffsetDateTime;
import java.util.List;
import java.util.Map;

 * An example app to show the main use-cases and rules of the 'Usage-Api'.
public class UsageExample {
    private static final boolean ASCENDING = true;
    public static final String CUSTOMER_ID_FIELD = "customerId";

    public static void main(final String[] args) {
        // Step 1 - create the client
        final String appKey = System.getProperty("AMBERFLO_APP_KEY");
        final UsageClient usageClient = new UsageClient(appKey);

        // Step 2 - query the system

        // If you want simple statistics for all of the official meters in your account use the 'all' api.

        // If you want to create a custom-query for any specific meter use the "get" api.

        // For a batch of custom-queries use the "batch" api.

    private static void queryTheAllApi(final UsageClient usageClient) {
        // Creating a simple 'ALL' api request can be pretty simple.
        // 1. Use one of the TimeRangeFactory methods to easily create a time-range for your query.
        // 2. Use the AllMetersAggregationsRequestBuilder to create a request.
        // 3. Provide the all-request to the usage-client and get the results.
        final TimeRange timeRange1 = TimeRangeFactory.yearToDate();
        final AllMetersAggregationsRequest request1 = AllMetersAggregationsRequestBuilder.instance(timeRange1).build();
        // Each item in the list corresponds to an official 'meter' defined by you in advance.
        final List<DetailedMeterAggregation> meterAggregations1 = usageClient.getAll(request1);


        // You can also look at X days backwards (for example).
        // The start time of the result will be (now - X days) truncated to the beginning of the day.
        // For example if now is 3/27/2021-02:53 and X = 4 then the result of calling this method is 3/23/2021-00:00.
        final TimeRange timeRange2 = TimeRangeFactory.truncatedLastDays(4);
        final AllMetersAggregationsRequest request2 = AllMetersAggregationsRequestBuilder.instance(timeRange2).build();
        final List<DetailedMeterAggregation> meterAggregations2 = usageClient.getAll(request2);

        // You can also look at X month backwards (for example).
        // The start time of the result will be (now - X days) truncated to the beginning of the MONTH.
        // For example if now is 3/27/2021-02:53 and X = 2 then the result of calling this method is 1/1/2021-00:00.
        final TimeRange timeRange3 = TimeRangeFactory.truncatedLastMonths(2);
        final AllMetersAggregationsRequest request3 = AllMetersAggregationsRequestBuilder.instance(timeRange3).build();
        final List<DetailedMeterAggregation> meterAggregations3 = usageClient.getAll(request3);

        // truncatedLastWeeks and truncatedLastHours have similar truncation logic.
        // The reason we truncate is related to the way the usage-api works. Let's look at the example below and then
        // explain the reason.

        final TimeRange timeRange4 = TimeRangeFactory.truncatedLastDays(4);
        final AllMetersAggregationsRequest request4 =
        final List<DetailedMeterAggregation> meterAggregations4 = usageClient.getAll(request4);

        // As you can see in the example above we asked for the results with a TimeGroupingInterval of a day.
        // Asking this will cause the usage api to provide you with statistics for each day of the time range you
        // asked for. For example, if you asked for the time range {3/23/2021-05:00 - today}, then for each meter you
        // will get a statistic for the entire time range as well as a statistic for each day starting from hour 00:00.
        // So basically the usage-api will provide you with statistics for the time-range of {3/23/2021-05:00 - today}.
        // To make it less confusing, we provide you with "truncated" methods which already produce the truncated
        // hour/day/week/month on the client side.

        // The truncated methods can also help you getting the beginning of the week (let's say).
        final TimeRange timeRange5 = TimeRangeFactory.truncatedLastWeeks(0);
        final AllMetersAggregationsRequest request5 =
        final List<DetailedMeterAggregation> meterAggregations5 = usageClient.getAll(request5);

        // Using the all api you can ask to filter the meters for a certain customer id.
        final TimeRange timeRange6 = TimeRangeFactory.truncatedLastWeeks(0);
        final AllMetersAggregationsRequest request6 =
        final List<DetailedMeterAggregation> meterAggregations6 = usageClient.getAll(request6);

        // Last, regarding the time-range we will mention that you can define a custom time range for your query.
        // Just notice that the usage-api extends the time range you provide according to the provided
        // AggregationInterval (or to AggregationInterval.Hour if no AggregationInterval was provided).
        // So for example a query with a time of {3/23/2021-05:21 - 3/29/2021-08:21} and a AggregationInterval.DAY
        // will be treated as {3/23/2021-00:00 - 3/30/2021-00:00} and a AggregationInterval.DAY.
        final TimeRange timeRange = TimeRange.builder()
        final AllMetersAggregationsRequest request =
        final List<DetailedMeterAggregation> meterAggregations = usageClient.getAll(request);

    private static void queryTheGetApi(final UsageClient usageClient) {
        // Querying the get api is the custom and more advanced way of querying the usage-api.
        // At the very least you need to provide the: meter-name, aggregation type, and time range.
        final String meterApiName = "myMeter";
        final TimeRange timeRange = TimeRangeFactory.yearToDate();

        final MeterAggregationMetadata request1 =
                MeterAggregationMetadataBuilder.instance(meterApiName, AggregationType.SUM, timeRange).build();
        final DetailedMeterAggregation result1 = usageClient.get(request1);


        // You can use all available meter types, just be aware that (currently) Amberflo has indexes only
        // for the official 'AggregationType' of your meter. So querying for a different type might
        // cause the query to be slower.
        final MeterAggregationMetadata request2 =
                MeterAggregationMetadataBuilder.instance(meterApiName, AggregationType.MIN, timeRange).build();
        final DetailedMeterAggregation result2 = usageClient.get(request2);

        // You can ask to partition the results by a certain field(s). When doing this Amberflo will provide
        // you with statistics per group.
        final MeterAggregationMetadata request3 =
                MeterAggregationMetadataBuilder.instance(meterApiName, AggregationType.SUM, timeRange)
        final DetailedMeterAggregation result3 = usageClient.get(request3);

        // Partitioning can yield a lot of results, and there is a size limit of 6Mb for the entire serialized
        // DetailedMeterAggregation response. So you better add a 'take' clause when partitioning.
        // The query below will provide you with statistics for the top 5 customers with the highest meter sum for the
        // time range you provided.
        final MeterAggregationMetadata request4 =
                MeterAggregationMetadataBuilder.instance(meterApiName, AggregationType.SUM, timeRange)
                        .setTake(new Take(5, !ASCENDING))
        final DetailedMeterAggregation result4 = usageClient.get(request4);

        // As with the 'all' api you can also ask to partition each group of statistics by units of time.
        // In the example below we the usage-api will return you statistics for the top 5 customer, where for each
        // customer you will get statistics for the entire time range, as well as for each day.
        // Just notice that the usage-api extends the time range you provide according to the provided
        // AggregationInterval (or to AggregationInterval.Hour if no AggregationInterval was provided).
        // So for example a query with a time of {3/23/2021-05:21 - 3/29/2021-08:21} and a AggregationInterval.DAY
        // will be treated as {3/23/2021-00:00 - 3/30/2021-00:00} and a AggregationInterval.DAY.
        final MeterAggregationMetadata request5 =
                MeterAggregationMetadataBuilder.instance(meterApiName, AggregationType.SUM, timeRange)
                        .setTake(new Take(5, !ASCENDING))
        final DetailedMeterAggregation result5 = usageClient.get(request5);

        // Last we will mention that you can define filters for your query.
        // The example below yields statistics for the top 5 "premium" or "gold" customers in the US.
        // Assuming you have this fields - if you don't the usage api will throw an exception.
        try {
            final MeterAggregationMetadata request6 =
                    MeterAggregationMetadataBuilder.instance(meterApiName, AggregationType.SUM, timeRange)
                            .setFilter(Map.of("customerTier", List.of("Premium", "Gold"), "country", List.of("US")))
                            .setTake(new Take(5, !ASCENDING))
            final DetailedMeterAggregation result6 = usageClient.get(request6);
        } catch (final RuntimeException exception) {

    private static void queryTheGetBatchApi(final UsageClient usageClient) {
        // The "batch" api is exactly the same as the "get" api only the you provide it with a batch
        // of request and it return you a batch of results.
        final String meterApiName1 = "myMeter1";
        final String meterApiName2= "myMeter2";
        final TimeRange timeRange = TimeRangeFactory.yearToDate();

        final MeterAggregationMetadata request1 =
                MeterAggregationMetadataBuilder.instance(meterApiName1, AggregationType.SUM, timeRange).build();
        final MeterAggregationMetadata request2 =
                MeterAggregationMetadataBuilder.instance(meterApiName2, AggregationType.SUM, timeRange).build();
        final List<DetailedMeterAggregation> result1 = usageClient.getBatch(List.of(request1, request2));


Create a customer


package demo.customer_details;

import com.amberflo.metering.customer_details.clients.CustomerDetailsClient;
import com.amberflo.metering.customer_details.model.CustomerDetails;

import java.util.Map;
import java.util.UUID;

 * An example app which shows how to interact with the customer-details api.
public class CustomerDetailsExample {
    public static void main(final String[] args) {
        // create the client
        final String appKey = System.getProperty("AMBERFLO_APP_KEY");
        final CustomerDetailsClient client = new CustomerDetailsClient(appKey);

        // Create a customer is simple - just define a customer:
        // Id must be unique - and this will be the id that you use when ingesting a meter for the customer or
        // querying the usage api.
        final String customerId = UUID.randomUUID().toString();
        // Name is mandatory but not necessarily unique.
        final String customerName = "moishe oofnik"; // https://www.google.com/search?q=moishe+oofnik
        // Traits are optional and suppose to capture additional data about the user.
        final Map<String, String> traits = Map.of("tv-show", "Rechov Sumsum"); // https://www.google.com/search?q=rechov+sumsum

        final CustomerDetails customerDetails = new CustomerDetails(customerId, customerName, traits);

        // You shouldn't be able to add the same client again.
        try {
        } catch (final RuntimeException exception) {

        // But you can update it.
        final String customerName2 = "Kippi Ben Kippod"; // https://www.google.com/search?q=kippi+ben+kippod
        final CustomerDetails updatedCustomer = new CustomerDetails(customerId, customerName2, traits);

        // If you aren't sure if the customer exists you can query the system or just call

        // As mentioned you can always query the system for a given customer id.
        final CustomerDetails customerDetailsResult = client.get(customerId);
        System.out.println("retrieved customer:");

Configuration example

  "clientType": "DirectClient",
  "maxAsyncQueyeSize": 20000,
  "isAsync": true,
  "params": {
    "maxDelayInSec": 0.5,
    "apiKey": "Y2hhbmdlbWU=",
    "accountName": "myAccount",
    "maxBatchSize": 100,
    "serviceName": "myservicename"