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

com.newrelic.agent.transport.DataSenderImpl Maven / Gradle / Ivy

The newest version!
/*
 *
 *  * Copyright 2020 New Relic Corporation. All rights reserved.
 *  * SPDX-License-Identifier: Apache-2.0
 *
 */

package com.newrelic.agent.transport;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableSet;
import com.newrelic.agent.ForceDisconnectException;
import com.newrelic.agent.ForceRestartException;
import com.newrelic.agent.LicenseException;
import com.newrelic.agent.MaxPayloadException;
import com.newrelic.agent.MetricData;
import com.newrelic.agent.MetricNames;
import com.newrelic.agent.config.AgentConfig;
import com.newrelic.agent.config.ConfigService;
import com.newrelic.agent.config.DataSenderConfig;
import com.newrelic.agent.config.LaspPolicies;
import com.newrelic.agent.errors.TracedError;
import com.newrelic.agent.logging.IAgentLogger;
import com.newrelic.agent.model.AnalyticsEvent;
import com.newrelic.agent.model.CustomInsightsEvent;
import com.newrelic.agent.model.ErrorEvent;
import com.newrelic.agent.model.LogEvent;
import com.newrelic.agent.model.SpanEvent;
import com.newrelic.agent.profile.ProfileData;
import com.newrelic.agent.service.ServiceFactory;
import com.newrelic.agent.sql.SqlTrace;
import com.newrelic.agent.stats.StatsService;
import com.newrelic.agent.stats.StatsWorks;
import com.newrelic.agent.superagent.AgentHealth;
import com.newrelic.agent.superagent.HealthDataChangeListener;
import com.newrelic.agent.superagent.HealthDataProducer;
import com.newrelic.agent.superagent.SuperAgentIntegrationUtils;
import com.newrelic.agent.trace.TransactionTrace;
import org.json.simple.JSONObject;
import org.json.simple.JSONStreamAware;
import org.json.simple.JSONValue;
import org.json.simple.parser.JSONParser;

import javax.net.ssl.SSLHandshakeException;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.MalformedURLException;
import java.net.SocketException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.rmi.UnexpectedException;
import java.security.NoSuchAlgorithmException;
import java.text.MessageFormat;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Level;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.GZIPOutputStream;

import static com.newrelic.agent.util.LicenseKeyUtil.obfuscateLicenseKey;

/**
 * A class for sending and receiving New Relic data.
 *
 * This class is thread-safe.
 */
public class DataSenderImpl implements DataSender, HealthDataProducer {

    private static final String MODULE_TYPE = "Jars";
    private static final int PROTOCOL_VERSION = 17;
    private static final String PROTOCOL = "https";
    private static final String BEFORE_LICENSE_KEY_URI_PATTERN = "/agent_listener/invoke_raw_method?method={0}";
    private static final String AFTER_LICENSE_KEY_URI_PATTERN = "&marshal_format=json&protocol_version=";
    private static final String LICENSE_KEY_URI_PATTERN = "&license_key={0}";
    private static final String RUN_ID_PATTERN = "&run_id={1}";

    public static final String DEFLATE_ENCODING = "deflate";
    public static final String GZIP_ENCODING = "gzip";
    private static final String IDENTITY_ENCODING = "identity";
    private static final String EXCEPTION_MAP_RETURN_VALUE_KEY = "return_value";
    private static final Object NO_AGENT_RUN_ID = null;
    private static final String NULL_RESPONSE = "null";
    private static final int COMPRESSION_LEVEL = Deflater.DEFAULT_COMPRESSION;
    private static final String REDIRECT_HOST = "redirect_host";
    private static final String SECURITY_POLICIES = "security_policies";
    private static final String MAX_PAYLOAD_SIZE_IN_BYTES = "max_payload_size_in_bytes";
    // to query the environment variables
    private static final String METADATA_PREFIX = "NEW_RELIC_METADATA_";
    // the block of env vars we send up to rpm
    private static final String ENV_METADATA = "metadata";
    private static final int DEFAULT_MAX_PAYLOAD_SIZE_IN_BYTES = 1_000_000;

    // Destinations for agent data
    private static final String COLLECTOR = "Collector";

    // As of P17 these are the only agent endpoints that actually contain data in the response payload for a successful request
    private static final Set METHODS_WITH_RESPONSE_BODY = ImmutableSet.of(
            CollectorMethods.PRECONNECT,
            CollectorMethods.CONNECT,
            CollectorMethods.GET_AGENT_COMMANDS,
            CollectorMethods.PROFILE_DATA);

    private final HttpClientWrapper httpClientWrapper;

    private final String originalHost;
    private volatile String redirectHost;
    private final int port;

    private volatile boolean auditMode;
    private Set auditModeEndpoints;
    private final IAgentLogger logger;
    private final ConfigService configService;
    private volatile Object agentRunId = NO_AGENT_RUN_ID;
    private final String agentRunIdUriPattern;
    private final String noAgentRunIdUriPattern;
    private final DataSenderListener dataSenderListener;
    private final String compressedEncoding;
    private final boolean putForDataSend;
    private Map policiesJson;
    private volatile int maxPayloadSizeInBytes = DEFAULT_MAX_PAYLOAD_SIZE_IN_BYTES;
    private volatile Map requestMetadata;
    private volatile Map metadata;
    private final List healthDataChangeListeners = new CopyOnWriteArrayList<>();
    private final boolean isSuperAgentEnabled;

    public DataSenderImpl(
            DataSenderConfig config,
            HttpClientWrapper httpClientWrapper,
            DataSenderListener dataSenderListener,
            IAgentLogger logger,
            ConfigService configService) {
        auditMode = config.isAuditMode();
        auditModeEndpoints = config.getAuditModeConfig().getEndpoints();
        this.logger = logger;
        this.configService = configService;
        logger.info(MessageFormat.format("Setting audit_mode to {0}", auditMode));
        originalHost = config.getHost();
        redirectHost = config.getHost();
        port = config.getPort();

        String licenseKeyUri = MessageFormat.format(LICENSE_KEY_URI_PATTERN, config.getLicenseKey());
        noAgentRunIdUriPattern = BEFORE_LICENSE_KEY_URI_PATTERN + licenseKeyUri + AFTER_LICENSE_KEY_URI_PATTERN + PROTOCOL_VERSION;
        agentRunIdUriPattern = noAgentRunIdUriPattern + RUN_ID_PATTERN;
        this.dataSenderListener = dataSenderListener;
        this.compressedEncoding = config.getCompressedContentEncoding();
        this.putForDataSend = config.isPutForDataSend();

        this.metadata = new HashMap<>();
        Map env = System.getenv();
        for (Map.Entry entry : env.entrySet()) {
            if (entry.getKey().startsWith(METADATA_PREFIX)) {
                this.metadata.put(entry.getKey(), entry.getValue());
            }
        }

        this.httpClientWrapper = httpClientWrapper;
        this.isSuperAgentEnabled = configService.getDefaultAgentConfig().getSuperAgentIntegrationConfig().isEnabled();
    }

    private void checkAuditMode() {
        boolean auditMode2 = configService.getLocalAgentConfig().isAuditMode();
        if (auditMode != auditMode2) {
            auditMode = auditMode2;
            logger.info(MessageFormat.format("Setting audit_mode to {0}", auditMode));
        }

        Set auditModeEndpoints2 = configService
                .getLocalAgentConfig()
                .getAuditModeConfig()
                .getEndpoints();
        if (auditModeEndpoints != auditModeEndpoints2) {
            auditModeEndpoints = auditModeEndpoints2;
            logger.info(MessageFormat.format("Setting audit_mode.endpoints to {0}", auditModeEndpoints));
        }
    }

    @VisibleForTesting
    void setAgentRunId(Object runId) {
        agentRunId = runId;
        if (runId != NO_AGENT_RUN_ID) {
            logger.info("Agent run id: " + runId);
        }
    }

    @VisibleForTesting
    Object getAgentRunId() {
        return agentRunId;
    }

    @Override
    public Map connect(Map startupOptions) throws Exception {
        String redirectHost = parsePreconnectAndReturnHost();
        if (redirectHost != null) {
            this.redirectHost = redirectHost;
            logger.info(MessageFormat.format("Collector redirection to {0}:{1}", this.redirectHost, Integer.toString(port)));
        } else if (configService.getDefaultAgentConfig().laspEnabled()) {
            throw new ForceDisconnectException("The agent did not receive one or more security policies that it expected and will shut down."
                    + " Please contact support.");
        }
        return doConnect(startupOptions);
    }

    private String parsePreconnectAndReturnHost() throws Exception {
        AgentConfig agentConfig = configService.getDefaultAgentConfig();

        InitialSizedJsonArray params = new InitialSizedJsonArray(1);
        JSONObject token = new JSONObject();

        if (agentConfig.laspEnabled()) {
            token.put("security_policies_token", agentConfig.securityPoliciesToken());
        }
        token.put("high_security", agentConfig.isHighSecurity());
        params.add(token);
        Object response = invokeNoRunId(originalHost, CollectorMethods.PRECONNECT, compressedEncoding, params);

        if (response != null) {
            Map returnValue = (Map) response;
            String host = returnValue.get(REDIRECT_HOST).toString();

            JSONObject policies = (JSONObject) returnValue.get(SECURITY_POLICIES);
            this.policiesJson = LaspPolicies.validatePolicies(policies);

            return host;
        }

        return null;
    }

    @SuppressWarnings("unchecked")
    private Map doConnect(Map startupOptions) throws Exception {
        InitialSizedJsonArray params = new InitialSizedJsonArray(1);
        if (policiesJson != null && !policiesJson.isEmpty()) {
            startupOptions.put("security_policies", LaspPolicies.convertToConnectPayload(policiesJson));
        }

        startupOptions.put(ENV_METADATA, metadata);

        params.add(startupOptions);
        Object response = invokeNoRunId(redirectHost, CollectorMethods.CONNECT, compressedEncoding, params);
        if (!(response instanceof Map)) {
            throw new UnexpectedException(MessageFormat.format("Expected a map of connection data, got {0}", response));
        }
        Map data = (Map) response;
        if (data.containsKey(MAX_PAYLOAD_SIZE_IN_BYTES)) {
            Object maxPayloadSize = data.get(MAX_PAYLOAD_SIZE_IN_BYTES);
            if (maxPayloadSize instanceof Number) {
                maxPayloadSizeInBytes = ((Number) maxPayloadSize).intValue();
                logger.log(Level.INFO, "Max payload size is {0} bytes", maxPayloadSizeInBytes);
            }
        }

        if (data.containsKey(ConnectionResponse.REQUEST_HEADERS)) {
            final Object requestMetadata = data.get(ConnectionResponse.REQUEST_HEADERS);
            if (requestMetadata instanceof Map) {
                this.requestMetadata = (Map) requestMetadata;
            } else {
                logger.log(Level.WARNING, "Expected a map but got {0}. Not setting requestMetadata", requestMetadata);
            }
        } else {
            logger.log(Level.WARNING, "Did not receive requestMetadata on connect");
        }

        if (data.containsKey(ConnectionResponse.AGENT_RUN_ID_KEY)) {
            Object runId = data.get(ConnectionResponse.AGENT_RUN_ID_KEY);
            setAgentRunId(runId);
        } else {
            throw new UnexpectedException(MessageFormat.format("Missing {0} connection parameter", ConnectionResponse.AGENT_RUN_ID_KEY));
        }
        configService.setLaspPolicies(policiesJson);

        return data;
    }

    @Override
    @SuppressWarnings("unchecked")
    public List> getAgentCommands() throws Exception {
        checkAuditMode();
        Object runId = agentRunId;
        if (runId == NO_AGENT_RUN_ID) {
            return Collections.emptyList();
        }
        InitialSizedJsonArray params = new InitialSizedJsonArray(1);
        params.add(runId);

        Object response = invokeRunId(CollectorMethods.GET_AGENT_COMMANDS, compressedEncoding, runId, params);
        if (response == null || NULL_RESPONSE.equals(response)) {
            return Collections.emptyList();
        }
        try {
            return (List>) response;
        } catch (ClassCastException e) {
            logger.warning(MessageFormat.format("Invalid response from New Relic when getting agent commands: {0}", e));
            throw e;
        }
    }

    @Override
    public void sendCommandResults(Map commandResults) throws Exception {
        Object runId = agentRunId;
        if (runId == NO_AGENT_RUN_ID || commandResults.isEmpty()) {
            return;
        }

        InitialSizedJsonArray params = new InitialSizedJsonArray(2);
        params.add(runId);
        params.add(commandResults);

        invokeRunId(CollectorMethods.AGENT_COMMAND_RESULTS, compressedEncoding, runId, params);
    }

    @Override
    public void sendErrorData(List errors) throws Exception {
        Object runId = agentRunId;
        if (runId == NO_AGENT_RUN_ID || errors.isEmpty()) {
            return;
        }

        InitialSizedJsonArray params = new InitialSizedJsonArray(2);
        params.add(runId);
        params.add(errors);

        invokeRunId(CollectorMethods.ERROR_DATA, compressedEncoding, runId, params);
    }

    @Override
    public void sendErrorEvents(int reservoirSize, int eventsSeen, Collection errorEvents) throws Exception {
        sendAnalyticEventsForReservoir(CollectorMethods.ERROR_EVENT_DATA, compressedEncoding, reservoirSize, eventsSeen, errorEvents);
    }

    @Override
    public  void sendAnalyticsEvents(int reservoirSize, int eventsSeen, Collection events) throws Exception {
        sendAnalyticEventsForReservoir(CollectorMethods.ANALYTIC_EVENT_DATA, compressedEncoding, reservoirSize, eventsSeen, events);
    }

    @Override
    public void sendCustomAnalyticsEvents(int reservoirSize, int eventsSeen, Collection events) throws Exception {
        sendAnalyticEventsForReservoir(CollectorMethods.CUSTOM_EVENT_DATA, compressedEncoding, reservoirSize, eventsSeen, events);
    }

    @Override
    public void sendLogEvents(Collection events) throws Exception {
        sendLogEventsForReservoir(CollectorMethods.LOG_EVENT_DATA, compressedEncoding, events);
    }

    @Override
    public void sendSpanEvents(int reservoirSize, int eventsSeen, Collection events) throws Exception {
        sendAnalyticEventsForReservoir(CollectorMethods.SPAN_EVENT_DATA, compressedEncoding, reservoirSize, eventsSeen, events);
    }

    private  void sendAnalyticEventsForReservoir(String method, String encoding, int reservoirSize, int eventsSeen,
            Collection events) throws Exception {
        Object runId = agentRunId;
        if (runId == NO_AGENT_RUN_ID || events.isEmpty()) {
            return;
        }
        InitialSizedJsonArray params = new InitialSizedJsonArray(3);
        params.add(runId);

        JSONObject metadata = new JSONObject();
        metadata.put("reservoir_size", reservoirSize);
        metadata.put("events_seen", eventsSeen);
        params.add(metadata);

        params.add(events);
        invokeRunId(method, encoding, runId, params);
    }

    // Sends LogEvent data in the MELT format for logs
    // https://docs.newrelic.com/docs/logs/log-api/introduction-log-api/#log-attribute-example
    private  void sendLogEventsForReservoir(String method, String encoding, Collection events) throws Exception {
        Object runId = agentRunId;
        if (runId == NO_AGENT_RUN_ID || events.isEmpty()) {
            return;
        }

        JSONObject commonAttributes = new JSONObject();

        // build attributes object
        JSONObject attributes = new JSONObject();
        attributes.put("attributes", commonAttributes);

        // build common object
        JSONObject common = new JSONObject();
        common.put("common", attributes);

        // build logs object
        JSONObject logs = new JSONObject();
        logs.put("logs", events);

        // params is top level
        InitialSizedJsonArray params = new InitialSizedJsonArray(3);
        params.add(common);
        params.add(logs);
        invokeRunId(method, encoding, runId, params);
    }

    @Override
    public void sendMetricData(long beginTimeMillis, long endTimeMillis, List metricData) throws Exception {
        Object runId = agentRunId;
        if (runId == NO_AGENT_RUN_ID || metricData.isEmpty()) {
            return;
        }

        InitialSizedJsonArray params = new InitialSizedJsonArray(4);
        params.add(runId);
        params.add(beginTimeMillis / 1000);
        params.add(endTimeMillis / 1000);
        params.add(metricData);

        invokeRunId(CollectorMethods.METRIC_DATA, compressedEncoding, runId, params);
    }

    @Override
    @SuppressWarnings("unchecked")
    public List sendProfileData(List profiles) throws Exception {
        Object runId = agentRunId;
        if (runId == NO_AGENT_RUN_ID || profiles.isEmpty()) {
            return Collections.emptyList();
        }

        InitialSizedJsonArray params = new InitialSizedJsonArray(2);
        params.add(runId);
        params.add(profiles);

        Object response = invokeRunId(CollectorMethods.PROFILE_DATA, getEncodingForComplexCompression(), runId, params);
        if (response == null || NULL_RESPONSE.equals(response)) {
            return Collections.emptyList();
        }
        try {
            return (List) response;
        } catch (ClassCastException e) {
            logger.warning(MessageFormat.format("Invalid response from New Relic sending profiles: {0}", e));
            throw e;
        }
    }

    /**
     * Sends the jars with versions to the collector.
     *
     * @param jarDataList The new jars which need to be sent to the collector.
     */
    @Override
    public void sendModules(List jarDataList) throws Exception {
        Object runId = agentRunId;
        if (runId == NO_AGENT_RUN_ID || jarDataList == null || jarDataList.isEmpty()) {
            return;
        }
        InitialSizedJsonArray params = new InitialSizedJsonArray(2);

        // Module type must always be first - it should always be jars
        params.add(MODULE_TYPE);
        params.add(jarDataList);

        invokeRunId(CollectorMethods.UPDATE_LOADED_MODULES, compressedEncoding, runId, params);
    }

    /**
     * Some of our data calls are json documents of base 64 encoded strings with gzipped json docs inside of them.
     * We normally send these requests with IDENTITY encoding because a large portion of the payload is already compressed.
     * When the "simple_compression" flag is on, we directly include the json docs instead of compressing them, and we
     * DEFLATE the entire json document instead.
     *
     * @return the type of encoding to use based on the simple_compression configuration value
     */
    private String getEncodingForComplexCompression() {
        return configService.getDefaultAgentConfig().isSimpleCompression() ? compressedEncoding : IDENTITY_ENCODING;
    }

    @Override
    public void sendSqlTraceData(List sqlTraces) throws Exception {
        Object runId = agentRunId;
        if (runId == NO_AGENT_RUN_ID || sqlTraces.isEmpty()) {
            return;
        }

        InitialSizedJsonArray params = new InitialSizedJsonArray(1);
        params.add(sqlTraces);

        invokeRunId(CollectorMethods.SQL_TRACE_DATA, getEncodingForComplexCompression(), runId, params);
    }

    @Override
    public void sendTransactionTraceData(List traces) throws Exception {
        Object runId = agentRunId;
        if (runId == NO_AGENT_RUN_ID || traces.isEmpty()) {
            return;
        }

        InitialSizedJsonArray params = new InitialSizedJsonArray(2);
        params.add(runId);
        params.add(traces);

        invokeRunId(CollectorMethods.TRANSACTION_SAMPLE_DATA, getEncodingForComplexCompression(), runId, params);
    }

    // The fix for JAVA-2965 assumes RPMService.shutdown() is the only caller of this method.
    // There's no way to avoid this bogus assumption short of a major rewrite of this layer.
    @Override
    public void shutdown(long timeMillis) throws Exception {
        Object runId = agentRunId;
        if (runId == NO_AGENT_RUN_ID) {
            return;
        }

        InitialSizedJsonArray params = new InitialSizedJsonArray(2);
        params.add(runId);
        params.add(timeMillis);
        try {
            invokeRunId(CollectorMethods.SHUTDOWN, compressedEncoding, runId, params);
        } finally {
            setAgentRunId(NO_AGENT_RUN_ID);
            this.httpClientWrapper.shutdown();
        }
    }

    @VisibleForTesting
    void setMaxPayloadSizeInBytes(int payloadSizeInBytes) {
        maxPayloadSizeInBytes = payloadSizeInBytes;
    }

    private Object invokeRunId(String method, String encoding, Object runId, JSONStreamAware params) throws Exception {
        String uri = MessageFormat.format(agentRunIdUriPattern, method, runId.toString());
        return invoke(redirectHost, method, encoding, uri, params);
    }

    private Object invokeNoRunId(String host, String method, String encoding, JSONStreamAware params) throws Exception {
        String uri = MessageFormat.format(noAgentRunIdUriPattern, method);
        return invoke(host, method, encoding, uri, params);
    }

    private Object invoke(String host, String method, String encoding, String uri, JSONStreamAware params) throws Exception {
        // ReadResult should be from a valid 2xx response at this point otherwise send method throws an exception here
        ReadResult readResult = send(host, method, encoding, uri, params);
        Map responseMap = null;
        String responseBody = readResult.getResponseBody();

        if (responseBody != null && !responseBody.isEmpty()) {
            try {
                responseMap = getResponseMap(responseBody);
            } catch (Exception e) {
                logger.log(Level.WARNING, "Error parsing response JSON({0}) from NewRelic: {1}", method, e.toString());
                logger.log(Level.FINEST, "Invalid response JSON({0}): {1}", method, responseBody);
                throw e;
            }
        } else if (METHODS_WITH_RESPONSE_BODY.contains(method)) {
            // Only log this if it's a method that we would expect to have data in the response payload
            logger.log(Level.FINER, "Response was null ({0})", method);
        }

        if (responseMap != null) {
            if (dataSenderListener != null) {
                dataSenderListener.dataReceived(method, encoding, uri, responseMap);
            }

            try {
                return responseMap.get(EXCEPTION_MAP_RETURN_VALUE_KEY);
            } catch (ClassCastException ex) {
                logger.log(Level.WARNING, "Error parsing response JSON({0}) from NewRelic: {1}", method, ex.toString());
                return null;
            }
        } else {
            return null;
        }
    }

    /*
     * As of Protocol 17 agents MUST NOT depend on the content of the response body for any behavior; just the integer
     * response code value. The previous behavior of a 200 ("OK") with an exact string in the body that should be
     * matched/parsed has been deprecated.
     */
    private ReadResult connectAndSend(String host, String method, String encoding, String uri, JSONStreamAware params) throws Exception {
        byte[] data = writeData(encoding, params);

        /*
         * We don't enforce max_payload_size_in_bytes for error_data (aka error traces). Instead, we halve the
         * payload and try again. See RPMService sendErrorData
         */
        if (data.length > maxPayloadSizeInBytes && !method.equals(CollectorMethods.ERROR_DATA)) {
            ServiceFactory.getStatsService().doStatsWork(StatsWorks.getIncrementCounterWork(
                    MessageFormat.format(MetricNames.SUPPORTABILITY_PAYLOAD_SIZE_EXCEEDS_MAX, method), 1), MetricNames.SUPPORTABILITY_PAYLOAD_SIZE_EXCEEDS_MAX);
            String msg = MessageFormat.format("Payload of size {0} exceeded maximum size {1} for {2} method ",
                    data.length, maxPayloadSizeInBytes, method);
            logger.log(Level.WARNING, msg);
            throw new MaxPayloadException(msg);
        }

        final URL url = new URL(PROTOCOL, host, port, uri);
        HttpClientWrapper.Request request = createRequest(method, encoding, url, data);

        httpClientWrapper.captureSupportabilityMetrics(ServiceFactory.getStatsService(), host);

        ReadResult result = httpClientWrapper.execute(request, new TimingEventHandler(method, ServiceFactory.getStatsService()));

        String payloadJsonSent = DataSenderWriter.toJSONString(params);

        if (auditMode && methodShouldBeAudited(method)) {

            String msg = MessageFormat.format("Sent JSON({0}) to: {1}, with payload: {2}", method, obfuscateLicenseKey(url.toString()), obfuscateLicenseKey(payloadJsonSent));
            logger.info(msg);
        }

        // Create supportability metric for all response codes
        ServiceFactory.getStatsService().doStatsWork(StatsWorks.getIncrementCounterWork(
                MessageFormat.format(MetricNames.SUPPORTABILITY_HTTP_CODE, result.getStatusCode()), 1), MetricNames.SUPPORTABILITY_HTTP_CODE);

        if (result.getStatusCode() != HttpResponseCode.OK && result.getStatusCode() != HttpResponseCode.ACCEPTED) {
            throwExceptionFromStatusCode(method, result, data, request);
        }

        String payloadJsonReceived = result.getResponseBody();

        // received successful 2xx response
        if (auditMode && methodShouldBeAudited(method)) {
            logger.info(MessageFormat.format("Received JSON({0}): {1}", method, payloadJsonReceived));
        }

        recordDataUsageMetrics(method, payloadJsonSent, payloadJsonReceived);

        SuperAgentIntegrationUtils.reportHealthyStatus(healthDataChangeListeners, AgentHealth.Category.HARVEST, AgentHealth.Category.CONFIG);

        if (dataSenderListener != null) {
            dataSenderListener.dataSent(method, encoding, uri, data);
        }

        return result;
    }

    /**
     * Record metrics tracking amount of bytes sent and received for each agent endpoint payload
     *
     * @param method method for the agent endpoint
     * @param payloadJsonSent JSON String of the payload that was sent
     * @param payloadJsonReceived JSON String of the payload that was received
     */
    private void recordDataUsageMetrics(String method, String payloadJsonSent, String payloadJsonReceived) {
        int payloadBytesSent = payloadJsonSent.getBytes().length;
        int payloadBytesReceived = payloadJsonReceived.getBytes().length;

        // COLLECTOR is always the destination for data reported via DataSenderImpl.
        // OTLP as a destination is not currently supported by the Java agent.
        // INFINITE_TRACING destined usage data is sent via SpanEventSender.
        ServiceFactory.getStatsService().doStatsWork(
                StatsWorks.getRecordDataUsageMetricWork(
                        MessageFormat.format(MetricNames.SUPPORTABILITY_DATA_USAGE_DESTINATION_OUTPUT_BYTES, COLLECTOR),
                        payloadBytesSent, payloadBytesReceived), MetricNames.SUPPORTABILITY_DATA_USAGE_DESTINATION_OUTPUT_BYTES + " " + COLLECTOR);

        ServiceFactory.getStatsService().doStatsWork(
                StatsWorks.getRecordDataUsageMetricWork(
                        MessageFormat.format(MetricNames.SUPPORTABILITY_DATA_USAGE_DESTINATION_ENDPOINT_OUTPUT_BYTES, COLLECTOR, method),
                        payloadBytesSent, payloadBytesReceived),
                        MetricNames.SUPPORTABILITY_DATA_USAGE_DESTINATION_ENDPOINT_OUTPUT_BYTES + " " + COLLECTOR);
    }

    private void throwExceptionFromStatusCode(String method, ReadResult result, byte[] data, HttpClientWrapper.Request request)
            throws HttpError, LicenseException, ForceRestartException, ForceDisconnectException {
        // Comply with spec and send supportability metric only for error responses
        ServiceFactory.getStatsService().doStatsWork(StatsWorks.getIncrementCounterWork(
                MessageFormat.format(MetricNames.SUPPORTABILITY_AGENT_ENDPOINT_HTTP_ERROR, result.getStatusCode()), 1), MetricNames.SUPPORTABILITY_AGENT_ENDPOINT_HTTP_ERROR);
        ServiceFactory.getStatsService().doStatsWork(StatsWorks.getIncrementCounterWork(
                MessageFormat.format(MetricNames.SUPPORTABILITY_AGENT_ENDPOINT_ATTEMPTS, method), 1), MetricNames.SUPPORTABILITY_AGENT_ENDPOINT_ATTEMPTS);

        // HttpError exceptions are typically handled in RPMService or the harvestable service for the requested endpoint
        switch (result.getStatusCode()) {
            case HttpResponseCode.PROXY_AUTHENTICATION_REQUIRED:
                // agent receives a 407 response due to a misconfigured proxy (not from NR backend), throw exception
                SuperAgentIntegrationUtils.reportUnhealthyStatus(healthDataChangeListeners, AgentHealth.Status.PROXY_ERROR,
                        Integer.toString(result.getStatusCode()), method);
                final String authField = result.getProxyAuthenticateHeader();
                if (authField != null) {
                    throw new HttpError("Proxy Authentication Mechanism Failed: " + authField, result.getStatusCode(), data.length);
                } else {
                    throw new HttpError("Proxy Authentication Mechanism Failed: " + "null Proxy-Authenticate header", result.getStatusCode(), data.length);
                }
            case HttpResponseCode.UNAUTHORIZED:
                // received 401 Unauthorized, throw exception instead of parsing LicenseException from 200 response body
                SuperAgentIntegrationUtils.reportUnhealthyStatus(healthDataChangeListeners, AgentHealth.Status.INVALID_LICENSE);
                throw new LicenseException(parseExceptionMessage(result.getResponseBody()));
            case HttpResponseCode.CONFLICT:
                // received 409 Conflict, throw exception instead of parsing ForceRestartException from 200 response body
                throw new ForceRestartException(parseExceptionMessage(result.getResponseBody()));
            case HttpResponseCode.GONE:
                // received 410 Gone, throw exception instead of parsing ForceDisconnectException from 200 response body
                SuperAgentIntegrationUtils.reportUnhealthyStatus(healthDataChangeListeners, AgentHealth.Status.FORCED_DISCONNECT);
                throw new ForceDisconnectException(parseExceptionMessage(result.getResponseBody()));
            default:
                // response is bad (neither 200 nor 202), throw generic HttpError exception
                SuperAgentIntegrationUtils.reportUnhealthyStatus(healthDataChangeListeners, AgentHealth.Status.HTTP_ERROR,
                        Integer.toString(result.getStatusCode()), method);
                logger.log(Level.FINER, "Connection http status code: {0}", result.getStatusCode());
                throw HttpError.create(result.getStatusCode(), request.getURL().getHost(), data.length);
        }
    }

    private void reportUnhealthyStatusToSuperAgent(AgentHealth.Status status, String ... additionalInfo) {
        if (isSuperAgentEnabled) {
            SuperAgentIntegrationUtils.reportUnhealthyStatus(healthDataChangeListeners, status, additionalInfo);
        }
    }

    private String parseExceptionMessage(String responseBody) {
        try {
            JSONParser parser = new JSONParser();
            JSONObject responseMessageObject = (JSONObject) parser.parse(responseBody);
            JSONObject exception = (JSONObject) responseMessageObject.get("exception");
            return exception.get("message").toString();
        } catch (Exception ignored) {
            return responseBody;
        }
    }

    private boolean methodShouldBeAudited(String method) {
        if (auditModeEndpoints != null && auditModeEndpoints.size() > 0) {
            return auditModeEndpoints.contains(method);
        }
        return true;
    }

    private ReadResult send(String host, String method, String encoding, String uri, JSONStreamAware params) throws Exception {
        try {
            return connectAndSend(host, method, encoding, uri, params);
        } catch (MalformedURLException e) {
            logger.log(Level.SEVERE, "You have requested a connection to New Relic via a protocol which is unavailable in your runtime: {0}", e.toString());
            throw new ForceDisconnectException(e.toString());
        } catch (SocketException e) {
            if (e.getCause() instanceof NoSuchAlgorithmException) {
                String msg = MessageFormat.format("You have requested a connection to New Relic via an algorithm which is unavailable in your runtime: {0}."
                        + " This may also be indicative of a corrupted keystore or trust store on your server.", e.getCause().toString());
                logger.error(msg);
                // this is a recoverable error. Try again later
            } else {
                logger.log(Level.INFO, "A socket exception was encountered while sending data to New Relic ({0})."
                        + " Please check your network / proxy settings.", e.toString());
                if (logger.isLoggable(Level.FINE)) {
                    logger.log(Level.FINE, "Error sending JSON({0}): {1}", method, DataSenderWriter.toJSONString(params));
                }
                logger.log(Level.FINEST, e, e.toString());
            }
            throw e;
        } catch (HttpError e) {
            // These errors are logged upstream of this call.
            throw e;
        } catch (Exception e) {
            if (e instanceof SSLHandshakeException) {
                logger.log(Level.INFO, "Unable to connect to New Relic due to an SSL error."
                        + " Consider enabling -Djavax.net.debug=all to debug your SSL configuration such as your trust store.", e);
            }
            logger.log(Level.INFO, "Remote {0} call failed : {1}.", method, e.toString());
            if (logger.isLoggable(Level.FINE)) {
                logger.log(Level.FINE, "Error sending JSON({0}): {1}", method, DataSenderWriter.toJSONString(params));
            }
            logger.log(Level.FINEST, e, e.toString());
            throw e;
        }
    }

    private HttpClientWrapper.Request createRequest(String method, String encoding, URL url, byte[] data) {
        final boolean isConnectOrPreconnect = method.equals(CollectorMethods.CONNECT) || method.equals(CollectorMethods.PRECONNECT);
        final Map requestMetadata = (this.requestMetadata != null && !isConnectOrPreconnect)
                ? this.requestMetadata
                : Collections.emptyMap();

        return new HttpClientWrapper.Request()
                .setURL(url)
                .setVerb(putForDataSend ? HttpClientWrapper.Verb.PUT : HttpClientWrapper.Verb.POST)
                .setEncoding(encoding)
                .setData(data)
                .setRequestMetadata(requestMetadata);
    }

    private byte[] writeData(String encoding, JSONStreamAware params) throws IOException {
        ByteArrayOutputStream outStream = new ByteArrayOutputStream();
        try (
                OutputStream os = getOutputStream(outStream, encoding);
                Writer out = new OutputStreamWriter(os, StandardCharsets.UTF_8);
        ) {
            JSONValue.writeJSONString(params, out);
            out.flush();
        }
        return outStream.toByteArray();
    }

    private OutputStream getOutputStream(OutputStream out, String encoding) throws IOException {
        if (DEFLATE_ENCODING.equals(encoding)) {
            return new DeflaterOutputStream(out, new Deflater(COMPRESSION_LEVEL));
        } else if (GZIP_ENCODING.equals(encoding)) {
            return new GZIPOutputStream(out);
        } else {
            return out;
        }
    }

    private Map getResponseMap(String responseBody) throws Exception {
        JSONParser parser = new JSONParser();
        Object response = parser.parse(responseBody);
        return (Map) response;
    }

    private static class TimingEventHandler implements HttpClientWrapper.ExecuteEventHandler {
        private final String method;
        private final StatsService statsService;
        private long requestSent;

        TimingEventHandler(String method, StatsService statsService) {
            this.method = method;
            this.statsService = statsService;
        }

        @Override
        public void requestStarted() {
            requestSent = System.currentTimeMillis();
        }

        @Override
        public void requestEnded() {
            long requestDuration = System.currentTimeMillis() - requestSent;

            statsService.doStatsWork(StatsWorks.getRecordResponseTimeWork(
                    MessageFormat.format(MetricNames.SUPPORTABILITY_AGENT_ENDPOINT_DURATION, method), requestDuration),
                    MetricNames.SUPPORTABILITY_AGENT_ENDPOINT_DURATION + " " + method);
        }
    }

    @Override
    public void registerHealthDataChangeListener(HealthDataChangeListener listener) {
        healthDataChangeListeners.add(listener);
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy