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

io.monalabs.client.MonaClient Maven / Gradle / Ivy

There is a newer version: 1.3.3
Show newest version
package io.monalabs.client;

/*-
 * #%L
 * mona-java-client
 * %%
 * Copyright (C) 2019 Mona Labs
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */

import com.fasterxml.jackson.databind.ObjectMapper;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.komamitsu.fluency.Fluency;
import org.komamitsu.fluency.fluentd.FluencyBuilderForFluentd;

import java.io.Closeable;
import java.time.Instant;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.logging.Logger;



/**
 * The default implementation for a Mona java client.
 */
public class MonaClient implements Closeable {

    private final static String MONA_ARC_CLASS_FIELD_NAME = "MONA_ARC_CLASS";

    private final static Logger LOGGER = Logger.getLogger(MonaClient.class.getName());


    /**
     * The client to communicate with Mona's fluentd on Mona's servers.
     */
    private final Fluency fluency;

    /**
     * A user id to be added to any message sent to Mona, for identification purposes.
     */
    private final String monaUserId;

    protected MonaClient(Fluency fluency, String monaUserId) {
        this.fluency = fluency;
        this.monaUserId = monaUserId;
    }

    private MonaClient(Builder builder) {
        FluencyBuilderForFluentd fluencyBuilder = new FluencyBuilderForFluentd();
        fluencyBuilder.setBufferChunkInitialSize(builder.bufferChunkInitialSize);
        fluencyBuilder.setBufferChunkRetentionSize(builder.bufferChunkRetentionSize);
        fluencyBuilder.setMaxBufferSize(builder.maxBufferSize);
        fluencyBuilder.setFlushIntervalMillis(builder.flushIntervalMillis);
        fluencyBuilder.setSslEnabled(true);
        fluencyBuilder.setErrorHandler(e -> {
            LOGGER.warning(String.format("Error sending data to Mona: %s", e.getMessage()));
        });
        fluencyBuilder.setAckResponseMode(builder.ackResponseMode);
        fluencyBuilder.setConnectionTimeoutMilli(builder.connectionTimeoutMilli);
        fluencyBuilder.setReadTimeoutMilli(builder.readTimeoutMilli);
        fluencyBuilder.setSenderMaxRetryCount(builder.senderMaxRetryCount);

        this.fluency = fluencyBuilder.build(builder.host, builder.port);
        this.monaUserId = builder.monaUserId;
    }

    /**
     * Closes the inner connection socket to Mona's fluentd.
     */
    @Override
    public void close() {
        try {
            fluency.close();
        } catch (Exception e) {
            LOGGER.warning(String.format("IO Exception when closing link to fluentd: %s", e.getMessage()));
        }
    }

    public long getCurrentUnsentDataSize() {
        return this.fluency.getBufferedDataSize();
    }

    private int amountOfDotsInString(String checkedString) {
        return checkedString.length() - checkedString.replace(".", "").length();
    }

    /**
     * The main exporting mechanism. Use this method, or any of its variants, to send context-free messages to Mona's
     * system.
     *
     * @param arcClass  A tag for the ARC class that will be sent using this exportStandalone method. Mona uses ARC
     *                  classes to know which arcs belong together and should be compared. See Readme for explanation
     *                  about ARCs. This string can have dots, which allows the use of sub-ARC-classes.
     * @param contextId The ID to use for this context. Can be empty or null, in which case the client will create a
     *                  random UUID as the context id.
     *                  When using sub-ARCs (i.e., when the given arcClass param has dots in it), the context id may
     *                  also contain dots (at most the amount of dots in the arcClass param), to infer the context
     *                  ids for each sub-ARC.
     * @param message   A json-like object consisting of the actual message to send to Mona.
     */
    public void exportStandalone(String arcClass, String contextId, Map message) {
        if (message == null) {
            LOGGER.warning("Tried to export null message");
            return;
        }
        if (message.isEmpty()) {
            // Never send empty messages.
            LOGGER.warning("Tried to export empty message");
            return;
        }
        if (arcClass == null || arcClass.isEmpty()) {
            // Don't allow empty arc classes
            LOGGER.warning("Tried to export with empty ARC class");
            return;
        }
        if (arcClass.endsWith(".")) {
            LOGGER.warning("ARC classes cannot end with dots");
            return;
        }

        // Context specific checks.
        if (contextId == null) {
            // Avoid handling null values.
            contextId = "";
        }
        if (contextId.endsWith(".")) {
            LOGGER.warning("Context ids cannot end with dots");
            return;
        }
        int missingContextIdsCount = amountOfDotsInString(arcClass) - amountOfDotsInString(contextId);
        if (missingContextIdsCount < 0) {
            LOGGER.warning("Tried to export a context ID with more sub-contexts than in the given ARC class.");
            return;
        }
        // We allow the user to only supply context ids for a prefix of the arc class, and add context ids to the
        // suffix as required. This allows the user to know of a specific top context, but not to care about the ids
        // of sub contexts.
        StringBuilder contextBuilder = new StringBuilder().append(contextId);
        for (int i = 0; i < missingContextIdsCount; ++i) {
            contextBuilder.append(".");
            contextBuilder.append(UUID.randomUUID().toString());
        }
        contextId = contextBuilder.toString();

        Map output_inner_message = new HashMap<>(message);
        output_inner_message.put(MONA_ARC_CLASS_FIELD_NAME, arcClass);
        Map outerMessage = new HashMap<>();
        outerMessage.put("user_id", monaUserId);
        outerMessage.put("context", contextId);
        outerMessage.put("message", output_inner_message);
        outerMessage.put("export_timestamp", Instant.now().getEpochSecond());
        try {
            fluency.emit("mona.client.message", outerMessage);
        } catch (Exception e) {
            LOGGER.warning(String.format("IO Exception when emitting to fluentd: %s", e.getMessage()));
        }
    }

    /**
     * Shorthand for the above exportStandalone variant, leaving the contextId empty.
     */
    public void exportStandalone(String arcClass, Map message) {
        exportStandalone(arcClass, "", message);
    }

    /**
     * Use this exportStandalone variant to send a batch (list) of unrelated messages to Mona. Each message in the
     * list will receive its own random context.
     *
     * @param messages a list of json-like messages to send to Mona
     */
    public void exportStandalone(String arcClass, List> messages) {
        if (messages == null) {
            LOGGER.warning("Tried to export null messages");
            return;
        }
        for (Map innerMessage : messages) {
            // TODO(itai): Make sure this is reasonable using integration tests, also for list size ~100,000 with
            // total size of ~20MB.
            exportStandalone(arcClass, innerMessage);
        }
    }

    // JSON based API.

    /**
     * Use this exportStandalone variant if the input is an actual json string.
     * If the input string is a json object, this object will be considered as one ARC. If it's a json array, each
     * object in the array will be considered one arc (unless one of the objects is an array itself, in that case,
     * the logic is recursive).
     *
     * @param jsonString A valid json string (either object or array)
     */
    public void exportStandalone(String arcClass, String jsonString) {
        if (jsonString == null) {
            LOGGER.warning("Tried to export null json String");
            return;
        }
        try {
            exportStandalone(arcClass, new JSONObject(jsonString));
        } catch (JSONException ex) {
            // This can still be a json array.
            try {
                exportStandalone(arcClass, new JSONArray(jsonString));
            } catch (JSONException ex1) {
                LOGGER.warning("Bad Json String.");
            }
        }
    }

    /**
     * Export variant for using with a {@link JSONObject}.
     *
     * @param jsonObject
     */
    public void exportStandalone(String arcClass, JSONObject jsonObject) {
        if (jsonObject == null) {
            LOGGER.warning("Tried to export null JSONObject");
            return;
        }
        try {
            exportStandalone(arcClass, new ObjectMapper().readValue(jsonObject.toString(), HashMap.class));
        } catch (Exception e) {
            LOGGER.warning("Exception when trying to exportStandalone JSON object");
        }
    }

    /**
     * Export variant for using with a {@link JSONArray}.
     *
     * @param jsonArray
     */
    public void exportStandalone(String arcClass, JSONArray jsonArray) {
        if (jsonArray == null) {
            LOGGER.warning("Tried to export null JSONArray");
            return;
        }
        for (int i = 0; i < jsonArray.length(); i++) {
            exportStandalone(arcClass, jsonArray.getJSONObject(i));
        }
    }

    public static class Builder {

        private static final int DEFAULT_BUFFER_CHUNK_INITIAL_SIZE = 100 * 1024;  // 100 KB buffer size to start
        private static final int DEFAULT_BUFFER_CHUNK_RETENTION_SIZE = 200 * 1024;  // 200 KB threshold to flush
        private static final long DEFAULT_MAX_BUFFER_SIZE = 2 * 1024 * 1024L;  // 2 MB max buffer size
        private static final int DEFAULT_INTERVAL_MILLIS = 100;
        private static final int DEFAULT_CONNECTION_TIMEOUT_MILLI = 10000;
        private static final int DEFAULT_READ_TIMEOUT_MILLI = 10000;
        private static final int DEFAULT_SENDER_MAX_RETRY_COUNT = 10;

        private final String host;
        private final int port;
        private final String monaUserId;

        private int bufferChunkInitialSize;
        private int bufferChunkRetentionSize;
        private long maxBufferSize;
        private int flushIntervalMillis;
        private boolean ackResponseMode;
        private int connectionTimeoutMilli;
        private int readTimeoutMilli;
        private int senderMaxRetryCount;


        public Builder(String host, int port, String monaUserId) {
            this.host = host;
            this.port = port;
            this.monaUserId = monaUserId;
            this.bufferChunkInitialSize = DEFAULT_BUFFER_CHUNK_INITIAL_SIZE;
            this.bufferChunkRetentionSize = DEFAULT_BUFFER_CHUNK_RETENTION_SIZE;
            this.maxBufferSize = DEFAULT_MAX_BUFFER_SIZE;
            this.flushIntervalMillis = DEFAULT_INTERVAL_MILLIS;
            this.ackResponseMode = false;
            this.connectionTimeoutMilli = DEFAULT_CONNECTION_TIMEOUT_MILLI;
            this.readTimeoutMilli = DEFAULT_READ_TIMEOUT_MILLI;
            this.senderMaxRetryCount = DEFAULT_SENDER_MAX_RETRY_COUNT;
        }

        public Builder setSenderMaxRetryCount(int senderMaxRetryCount) {
            this.senderMaxRetryCount = senderMaxRetryCount;
            return this;
        }

        public Builder setReadTimeoutMilli(int readTimeoutMilli) {
            this.readTimeoutMilli = readTimeoutMilli;
            return this;
        }

        public Builder setConnectionTimeoutMilli(int connectionTimeoutMilli) {
            this.connectionTimeoutMilli = connectionTimeoutMilli;
            return this;
        }

        public Builder setAckResponseMode(boolean ackResponseMode) {
            this.ackResponseMode = ackResponseMode;
            return this;
        }

        public Builder setBufferChunkInitialSize(int bufferChunkInitialSize) {
            this.bufferChunkInitialSize = bufferChunkInitialSize;
            return this;
        }

        public Builder setBufferChunkRetentionSize(int bufferChunkRetentionSize) {
            this.bufferChunkRetentionSize = bufferChunkRetentionSize;
            return this;
        }

        public Builder setMaxBufferSize(long maxBufferSize) {
            this.maxBufferSize = maxBufferSize;
            return this;
        }

        public Builder setFlushIntervalMillis(int flushIntervalMillis) {
            this.flushIntervalMillis = flushIntervalMillis;
            return this;
        }

        public MonaClient build() {
            return new MonaClient(this);
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy