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

io.smartdatalake.util.azure.client.loganalytics.LogAnalyticsClient Maven / Gradle / Ivy

There is a newer version: 2.7.1
Show newest version
/*
 * Smart Data Lake - Build your data lake the smart way.
 *
 * Copyright © 2019-2021 ELCA Informatique SA ()
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see .
 */

package io.smartdatalake.util.azure.client.loganalytics;

import org.apache.commons.codec.binary.Base64;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.HttpClients;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.Closeable;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;

/**
 * This code originates from https://github.com/mspnp/spark-monitoring and is protected by its corresponding MIT license
 */
public class LogAnalyticsClient implements Closeable {
    private static final String HashAlgorithm = "HmacSHA256";
    private static final String HttpVerb = "POST";
    private static final String ContentType = "application/json";
    private static final String Resource = "/api/logs";
    private static final String AuthorizationFormat = "SharedKey %s:%s";

    private static final String DEFAULT_URL_SUFFIX = "ods.opinsights.azure.com";
    private static final String DEFAULT_API_VERSION = "2016-04-01";
    private static final String URL_FORMAT = "https://%s.%s/api/logs?api-version=%s";
    private static final String RESOURCE_ID = "/subscriptions/%s/resourceGroups/%s/providers/%s/%s/%s";

    @Override
    public void close() throws IOException {
        if (this.httpClient instanceof Closeable) {
            ((Closeable) this.httpClient).close();
        }
    }

    private static final class LogAnalyticsHttpHeaders {
        static final String LOG_TYPE = "Log-Type";
        static final String X_MS_DATE = "x-ms-date";
        static final String TIME_GENERATED_FIELD = "time-generated-field";
        static final String X_MS_AZURE_RESOURCE_ID = "x-ms-AzureResourceId";
    }

    private String workspaceId;
    private String workspaceKey;
    private String url;
    private HttpClient httpClient;

    public LogAnalyticsClient(String workspaceId, String workspaceKey) {
        this(workspaceId, workspaceKey, HttpClients.custom()
                .disableAuthCaching()
                .disableContentCompression()
                .disableCookieManagement()
                .build());
    }

    public LogAnalyticsClient(String workspaceId, String workspaceKey,
                              HttpClient httpClient) {
        this(workspaceId, workspaceKey, httpClient, DEFAULT_URL_SUFFIX);
    }

    public LogAnalyticsClient(String workspaceId, String workspaceKey,
                              HttpClient httpClient, String urlSuffix) {
        this(workspaceId, workspaceKey, httpClient, urlSuffix, DEFAULT_API_VERSION);
    }

    public LogAnalyticsClient(String workspaceId, String workspaceKey,
                              HttpClient httpClient, String urlSuffix, String apiVersion) {
        if (isNullOrWhitespace(workspaceId)) {
            throw new IllegalArgumentException("workspaceId cannot be null, empty, or only whitespace");
        }

        if (isNullOrWhitespace(workspaceKey)) {
            throw new IllegalArgumentException("workspaceKey cannot be null, empty, or only whitespace");
        }

        if (httpClient == null) {
            throw new IllegalArgumentException("httpClient cannot be null");
        }

        if (isNullOrWhitespace(urlSuffix)) {
            throw new IllegalArgumentException("urlSuffix cannot be null, empty, or only whitespace");
        }

        if (isNullOrWhitespace(apiVersion)) {
            throw new IllegalArgumentException("apiVersion cannot be null, empty, or only whitespace");
        }

        this.workspaceId = workspaceId;
        this.workspaceKey = workspaceKey;
        this.httpClient = httpClient;
        this.url = String.format(URL_FORMAT, this.workspaceId, urlSuffix, apiVersion);
    }

    public void send(String body, String logType) throws IOException {
        this.send(body, logType, null);
    }

    protected HttpPost getHttpPost() {
        return new HttpPost(this.url);
    }

    public void send(String body, String logType, String timestampFieldName) throws IOException {
        try {
            if (isNullOrWhitespace(body)) {
                throw new IllegalArgumentException("body cannot be null, empty, or only whitespace");
            }

            if (isNullOrWhitespace(logType)) {
                throw new IllegalArgumentException("logType cannot be null, empty, or only whitespace");
            }

            final Date xmsDate = Calendar.getInstance().getTime();

            SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
            dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
            String xmsDateString = dateFormat.format(xmsDate);

            HttpPost httpPost = getHttpPost();
            String signature = String.format(AuthorizationFormat, this.workspaceId,
                    buildSignature(this.workspaceKey,
                            xmsDateString,
                            body.length(),
                            HttpVerb,
                            ContentType,
                            Resource));

            httpPost.setEntity(new StringEntity(body));
            httpPost.setHeader(HttpHeaders.CONTENT_TYPE, ContentType);
            httpPost.setHeader(LogAnalyticsHttpHeaders.LOG_TYPE, logType);
            httpPost.setHeader(LogAnalyticsHttpHeaders.X_MS_DATE, xmsDateString);
            httpPost.setHeader(HttpHeaders.AUTHORIZATION, signature);
            if (timestampFieldName != null) {
                httpPost.setHeader(LogAnalyticsHttpHeaders.TIME_GENERATED_FIELD, timestampFieldName);
            }
            String resourceId = azResourceId();
            if (resourceId != null) {
                httpPost.setHeader(LogAnalyticsHttpHeaders.X_MS_AZURE_RESOURCE_ID, resourceId);
            }

            HttpResponse httpResponse = null;
            try {
                httpResponse = httpClient.execute(httpPost);
                if (httpResponse.getStatusLine().getStatusCode() != 200) {
                    throw new IOException(
                            String.format(
                                    "Error sending Log Analytics events: %s (%d)",
                                    httpResponse.getStatusLine().getReasonPhrase(),
                                    httpResponse.getStatusLine().getStatusCode()));
                }
            } finally {
                if (httpResponse instanceof Closeable) {
                    ((Closeable) httpResponse).close();
                }
            }
        } catch (Exception ex) {
            throw new IOException("Error sending to Log Analytics", ex);
        }
    }

    private String buildSignature(
            String primaryKey,
            String xmsDate,
            int contentLength,
            String method,
            String contentType,
            String resource
    ) throws UnsupportedEncodingException, GeneralSecurityException {
        String result = null;
        String xHeaders = String.format("%s:%s", LogAnalyticsHttpHeaders.X_MS_DATE, xmsDate);
        //xHeaders = "Fri, 20 Jul 2018 16:28:59 GMT";
        String stringToHash = String.format(
                "%s\n%d\n%s\n%s\n%s",
                method,
                contentLength,
                contentType,
                xHeaders,
                resource);
        byte[] decodedBytes = Base64.decodeBase64(primaryKey);
        Mac mac = Mac.getInstance(HashAlgorithm);
        mac.init(new SecretKeySpec(decodedBytes, HashAlgorithm));
        byte[] hash = mac.doFinal(stringToHash.getBytes(StandardCharsets.UTF_8));
        result = Base64.encodeBase64String(hash);
        return result;
    }

    private boolean isNullOrWhitespace(String str) {
        int strLen;
        if (str == null || (strLen = str.length()) == 0) {
            return true;
        }

        for (int i = 0; i < strLen; i++) {
            if (!Character.isWhitespace(str.charAt(i))) {
                return false;
            }
        }

        return true;
    }

    /**
     * Gets the value of a System.getenv call or null if it is not set or if the length is 0.
     *
     * @param key System environment variable.
     * @return value of System.getenv(key) or null.
     */
    private String sysEnvOrNull(final String key) {
        String val = System.getenv(key);
        if (val == null || val.length() == 0) {
            return null;
        }
        return val;
    }

    /**
     * Gets Azure Resource Id from System Environment variables.
     *
     * @return ResourceId in the form of:
     * /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{resourceProviderNamespace}/{resourceType}/{resourceName}
     */
    private String azResourceId() {
        final String AZ_SUBSCRIPTION_ID = "AZ_SUBSCRIPTION_ID";
        final String AZ_RSRC_GRP_NAME = "AZ_RSRC_GRP_NAME";
        final String AZ_RSRC_PROV_NAMESPACE = "AZ_RSRC_PROV_NAMESPACE";
        final String AZ_RSRC_TYPE = "AZ_RSRC_TYPE";
        final String AZ_RSRC_NAME = "AZ_RSRC_NAME";

        String id = sysEnvOrNull(AZ_SUBSCRIPTION_ID);
        String grpName = sysEnvOrNull(AZ_RSRC_GRP_NAME);
        String provName = sysEnvOrNull(AZ_RSRC_PROV_NAMESPACE);
        String type = sysEnvOrNull(AZ_RSRC_TYPE);
        String name = sysEnvOrNull(AZ_RSRC_NAME);
        if (id == null || grpName == null ||
                provName == null || type == provName ||
                name == null) {
            return null;
        }
        return String.format(RESOURCE_ID, id, grpName, provName, type, name);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy