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

com.newrelic.agent.HarvestServiceImpl Maven / Gradle / Ivy

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

package com.newrelic.agent;

import com.google.common.annotations.VisibleForTesting;
import com.newrelic.agent.config.AgentConfig;
import com.newrelic.agent.config.AgentConfigFactory;
import com.newrelic.agent.metric.MetricIdRegistry;
import com.newrelic.agent.service.AbstractService;
import com.newrelic.agent.service.ServiceFactory;
import com.newrelic.agent.stats.StatsEngine;
import com.newrelic.agent.stats.StatsEngineImpl;
import com.newrelic.agent.stats.StatsWorks;
import com.newrelic.agent.util.DefaultThreadFactory;
import com.newrelic.agent.util.SafeWrappers;

import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level;

import static com.newrelic.agent.config.SpanEventsConfig.SERVER_SPAN_HARVEST_CONFIG;
import static com.newrelic.agent.config.SpanEventsConfig.SERVER_SPAN_HARVEST_LIMIT;
import static com.newrelic.agent.transport.CollectorMethods.SPAN_EVENT_DATA;

/**
 * This class is responsible for running the harvest tasks. There is one harvest task per RPM service. A harvest task
 * reports metric data to the server.
 */
public class HarvestServiceImpl extends AbstractService implements HarvestService {

    public static final String HARVEST_THREAD_NAME = "New Relic Harvest Service";
    public static final String FASTER_HARVEST_THREAD_NAME = "New Relic Faster Harvest Service";
    private static final long INITIAL_DELAY_IN_MILLISECONDS = TimeUnit.MILLISECONDS.convert(30, TimeUnit.SECONDS);
    private static final long REPORTING_PERIOD_IN_MILLISECONDS = TimeUnit.MILLISECONDS.convert(60, TimeUnit.SECONDS);
    private static final long MIN_HARVEST_INTERVAL_IN_NANOSECONDS = TimeUnit.NANOSECONDS.convert(55, TimeUnit.SECONDS);
    public static final String HARVEST_LIMITS = "harvest_limits";
    private static final String REPORT_PERIOD_MS = "report_period_ms";

    /**
     * The main harvest task and all harvestables (faster event harvests) are scheduled on separate threads.
     */
    private final ScheduledExecutorService scheduledHarvestExecutor;
    private final ScheduledExecutorService scheduledFasterHarvestExecutor;
    private final List harvestListeners = new CopyOnWriteArrayList<>();
    private final Map harvestTasks = new HashMap<>();
    private final ConcurrentMap harvestables = new ConcurrentHashMap<>();
    private long overrideInitialDelay = -1;

    public HarvestServiceImpl() {
        super(HarvestService.class.getSimpleName());
        scheduledHarvestExecutor = Executors.newSingleThreadScheduledExecutor(new DefaultThreadFactory(HARVEST_THREAD_NAME, true));
        scheduledFasterHarvestExecutor = Executors.newSingleThreadScheduledExecutor(new DefaultThreadFactory(FASTER_HARVEST_THREAD_NAME, true));
        ServiceFactory.getRPMServiceManager().addConnectionListener(new ConnectionListenerImpl());
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

    @Override
    protected void doStart() {
    }

    /**
     * Start a harvest task for the RPM service (or restart it if the reporting period has changed).
     */
    @Override
    public void startHarvest(IRPMService rpmService) {
        HarvestTask harvestTask = getOrCreateHarvestTask(rpmService);
        harvestTask.start();
    }

    @VisibleForTesting
    public void startHarvestables(IRPMService rpmService, AgentConfig config) {
        Map eventHarvestConfig = config.getProperty(AgentConfigFactory.EVENT_HARVEST_CONFIG);
        Map spanHarvestConfig = config.getProperty(SERVER_SPAN_HARVEST_CONFIG);

        if (eventHarvestConfig == null) {
            ServiceFactory.getStatsService().doStatsWork(StatsWorks.getIncrementCounterWork(
                    MetricNames.SUPPORTABILITY_CONNECT_MISSING_EVENT_DATA, 1), MetricNames.SUPPORTABILITY_CONNECT_MISSING_EVENT_DATA);
        }

        for (HarvestableTracker tracker : harvestables.values()) {
            if (tracker.harvestable.getAppName().equals(rpmService.getApplicationName())) {
                int maxSamplesStored = tracker.harvestable.getMaxSamplesStored();
                long reportPeriodInMillis = HarvestServiceImpl.REPORTING_PERIOD_IN_MILLISECONDS;
                boolean isSpanEventEndpoint = tracker.harvestable.getEndpointMethodName().equals(SPAN_EVENT_DATA);

                // The event_harvest_config.harvest_limits received from server-side during the connect lifecycle
                // contains config for error_event_data, analytic_event_data, custom_event_data, and log_event_data
                if (eventHarvestConfig != null && !isSpanEventEndpoint) {
                    Agent.LOG.log(Level.FINE, "event_harvest_config from collector for {0} is: {1} max samples stored per minute",
                            tracker.harvestable.getEndpointMethodName(), maxSamplesStored);
                    Map harvestLimits = (Map) eventHarvestConfig.get(HARVEST_LIMITS);

                    Long harvestLimit = (Long) harvestLimits.get(tracker.harvestable.getEndpointMethodName());
                    if (harvestLimit != null) {
                        maxSamplesStored = harvestLimit.intValue();
                        reportPeriodInMillis = (long) eventHarvestConfig.get(REPORT_PERIOD_MS); // faster event harvest report period
                        float reportPeriodInSeconds = reportPeriodInMillis / 1000;
                        if (maxSamplesStored == 0) {
                            Agent.LOG.log(Level.INFO, "harvest limit has been disabled by the collector for {0}", tracker.harvestable.getEndpointMethodName());
                        }
                        Agent.LOG.log(Level.FINE, "harvest limit from collector for {0} is: {1} max samples stored per every {2} second harvest",
                                tracker.harvestable.getEndpointMethodName(), harvestLimit, reportPeriodInSeconds);
                        ServiceFactory.getStatsService().doStatsWork(
                                StatsWorks.getRecordMetricWork(MetricNames.SUPPORTABILITY_EVENT_HARVEST_REPORT_PERIOD_IN_SECONDS, reportPeriodInSeconds),
                                MetricNames.SUPPORTABILITY_EVENT_HARVEST_REPORT_PERIOD_IN_SECONDS);
                    }
                } else if (!isSpanEventEndpoint) {
                    Agent.LOG.log(Level.FINE, "event_harvest_config from collector for {0} was null. Using default value: {1} max samples stored per minute",
                            tracker.harvestable.getEndpointMethodName(), maxSamplesStored);
                }

                // The span_event_harvest_config received from server-side during the connect lifecycle contains config for span_event_data
                if (spanHarvestConfig != null && isSpanEventEndpoint) {
                    Agent.LOG.log(Level.FINE, "span_event_harvest_config from collector for {0} is: {1} max samples stored per minute",
                            tracker.harvestable.getEndpointMethodName(), maxSamplesStored);
                    Long harvestLimit = (Long) spanHarvestConfig.get(SERVER_SPAN_HARVEST_LIMIT);
                    if (harvestLimit != null) {
                        maxSamplesStored = harvestLimit.intValue();
                        reportPeriodInMillis = (long) spanHarvestConfig.get(REPORT_PERIOD_MS);
                        float reportPeriodInSeconds = reportPeriodInMillis / 1000;
                        Agent.LOG.log(Level.FINE, "harvest limit from collector for {0} is: {1} max samples stored per every {2} second harvest",
                                tracker.harvestable.getEndpointMethodName(), harvestLimit, reportPeriodInSeconds);
                    }
                } else if (isSpanEventEndpoint) {
                    Agent.LOG.log(Level.FINE,
                            "span_event_harvest_config from collector for {0} was null. Using default value: {1} max samples stored per minute",
                            tracker.harvestable.getEndpointMethodName(), maxSamplesStored);
                }

                tracker.start(reportPeriodInMillis, maxSamplesStored);
            }
        }
    }

    /**
     * Stop a harvest task for the RPM service
     */
    public void stopHarvest(IRPMService rpmService) {
        HarvestTask harvestTask = harvestTasks.remove(rpmService);
        if (harvestTask != null) {
            harvestTask.stop();
        }
    }

    private synchronized HarvestTask getOrCreateHarvestTask(IRPMService rpmService) {
        HarvestTask harvestTask = harvestTasks.get(rpmService);
        if (harvestTask == null) {
            harvestTask = new HarvestTask(rpmService);
            harvestTasks.put(rpmService, harvestTask);
        }
        return harvestTask;
    }

    private synchronized List getHarvestTasks() {
        return new ArrayList<>(harvestTasks.values());
    }

    @Override
    public void addHarvestable(final Harvestable harvestable) {
        HarvestableTracker existing = harvestables.putIfAbsent(harvestable, new HarvestableTracker(harvestable));
        if (existing != null) {
            Agent.LOG.log(Level.SEVERE, "Harvestable already added to the harvest service: {0}", harvestable);
            existing.stop();
        }
    }

    @Override
    public void removeHarvestable(Harvestable harvestable) {
        if (harvestable != null) {
            HarvestableTracker tracker = harvestables.remove(harvestable);
            if (tracker != null) {
                tracker.stop();
            }
        }
    }

    @Override
    public void removeHarvestablesByAppName(String appName) {
        for (HarvestableTracker tracker : harvestables.values()) {
            if (tracker.harvestable.getAppName().equals(appName)) {
                harvestables.remove(tracker.harvestable);
                if (tracker != null) {
                    tracker.stop();
                }
            }
        }
    }

    /**
     * Add a listener that will be called by the harvest tasks before and after metric data is sent.
     *
     * @param listener
     */
    @Override
    public void addHarvestListener(HarvestListener listener) {
        harvestListeners.add(listener);
    }

    /**
     * Remove a listener.
     *
     * @param listener
     */
    @Override
    public void removeHarvestListener(HarvestListener listener) {
        harvestListeners.remove(listener);
    }

    @Override
    protected void doStop() {
        List tasks = getHarvestTasks();
        for (HarvestTask task : tasks) {
            task.stop();
        }
        for (HarvestableTracker h : harvestables.values()) {
            h.stop();
        }
        scheduledHarvestExecutor.shutdown();
        scheduledFasterHarvestExecutor.shutdown();
    }

    /**
     * Schedule a harvest task.
     */
    private ScheduledFuture scheduleHarvestTask(HarvestTask harvestTask) {
        return scheduledHarvestExecutor.scheduleAtFixedRate(SafeWrappers.safeRunnable(harvestTask), getInitialDelay(),
                getReportingPeriod(), TimeUnit.MILLISECONDS);
    }

    /**
     * Get the initial delay in milliseconds.
     * 

* Tests can override. */ public long getInitialDelay() { return (overrideInitialDelay <= 0) ? INITIAL_DELAY_IN_MILLISECONDS : overrideInitialDelay; } /** * Override the initial delay. * * @param millis number of milliseconds to wait */ @VisibleForTesting public void setInitialDelayMillis(long millis) { this.overrideInitialDelay = millis; } /** * Get the reporting period in milliseconds. *

* Tests can override. */ public long getReportingPeriod() { return REPORTING_PERIOD_IN_MILLISECONDS; } /** * Get the minimum harvest interval in nanoseconds. *

* Tests can override. */ public long getMinHarvestInterval() { return MIN_HARVEST_INTERVAL_IN_NANOSECONDS; } /** * Run harvests now. */ @Override public void harvestNow() { List tasks = getHarvestTasks(); for (HarvestTask task : tasks) { // run the harvestables first - they often generate metric data for (HarvestableTracker h : harvestables.values()) { h.harvestable.harvest(); } // now run the normal harvest task.harvestNow(); } } public Map getEventDataHarvestLimits() { Map eventHarvest = new HashMap<>(); Map harvestLimits = new HashMap<>(); eventHarvest.put(HARVEST_LIMITS, harvestLimits); for (Harvestable harvestable : harvestables.keySet()) { harvestLimits.put(harvestable.getEndpointMethodName(), harvestable.getMaxSamplesStored()); } return eventHarvest; } /** * The harvest task is responsible for running the harvest for a RPM service. */ private final class HarvestTask implements Runnable { private final IRPMService rpmService; private ScheduledFuture task; private final Lock harvestLock = new ReentrantLock(); private StatsEngine lastStatsEngine = new StatsEngineImpl(); private long lastHarvestStartTime; private HarvestTask(IRPMService rpmService) { this.rpmService = rpmService; } @Override public void run() { try { if (shouldHarvest()) { harvest(); } } catch (Throwable t) { String msg = MessageFormat.format("Unexpected exception during harvest: {0}", t); if (getLogger().isLoggable(Level.FINER)) { getLogger().log(Level.WARNING, msg, t); } else { getLogger().warning(msg); } } } private boolean shouldHarvest() { return (System.nanoTime() - lastHarvestStartTime) >= getMinHarvestInterval(); } /** * Start the harvest task if it is not running. */ private synchronized void start() { if (!isRunning()) { stop(); String msg = MessageFormat.format("Scheduling harvest task for {0}", rpmService.getApplicationName()); getLogger().log(Level.FINE, msg); task = scheduleHarvestTask(this); } } /** * Stop the harvest task. */ private synchronized void stop() { if (task != null) { getLogger().fine(MessageFormat.format("Cancelling harvest task for {0}", rpmService.getApplicationName())); task.cancel(false); } } /** * Is the harvest task running. * * @return true if the harvest task is running, false otherwise */ private boolean isRunning() { if (task == null) { return false; } return !task.isCancelled() || task.isDone(); } /** * Do a harvest now. */ private void harvestNow() { if (rpmService.isConnected()) { String msg = MessageFormat.format("Sending metrics for {0} immediately", rpmService.getApplicationName()); getLogger().info(msg); harvest(); } } private void harvest() { harvestLock.lock(); try { doHarvest(); } catch (ServerCommandException | IgnoreSilentlyException e) { } catch (Throwable e) { getLogger().log(Level.INFO, "Error sending metric data for {0}: {1}", rpmService.getApplicationName(), e.toString()); } finally { harvestLock.unlock(); } } private void doHarvest() throws Exception { lastHarvestStartTime = System.nanoTime(); String appName = rpmService.getApplicationName(); if (getLogger().isLoggable(Level.FINE)) { String msg = MessageFormat.format("Starting harvest for {0}", appName); getLogger().fine(msg); // Returns the application link and version at every harvest cycle. Helps verify customer agent // config when finest logs is sent to Support/Dev without a restart. String linkText = rpmService.getApplicationLink(); String version = Agent.getVersion(); String reportingToAndVersion = MessageFormat.format("Application link: {0}, Agent version: {1}", linkText, version); getLogger().fine(reportingToAndVersion); } StatsEngine harvestStatsEngine = ServiceFactory.getStatsService().getStatsEngineForHarvest(appName); harvestStatsEngine.mergeStats(lastStatsEngine); try { for (HarvestListener listener : harvestListeners) { notifyListenerBeforeHarvest(appName, harvestStatsEngine, listener); } // RPMService metric harvest only, other harvests are triggered in the before/after harvest listener loops reportHarvest(appName, harvestStatsEngine, rpmService); for (HarvestListener listener : harvestListeners) { notifyListenerAfterHarvest(appName, listener); } } finally { if (harvestStatsEngine.getSize() > MetricIdRegistry.METRIC_LIMIT) { harvestStatsEngine.clear(); } lastStatsEngine = harvestStatsEngine; long duration = TimeUnit.MILLISECONDS.convert(System.nanoTime() - lastHarvestStartTime, TimeUnit.NANOSECONDS); harvestStatsEngine.getResponseTimeStats(MetricNames.SUPPORTABILITY_HARVEST_SERVICE_RESPONSE_TIME) .recordResponseTime(duration, TimeUnit.MILLISECONDS); if (getLogger().isLoggable(Level.FINE)) { String msg = MessageFormat.format("Harvest for {0} took {1} milliseconds", appName, duration); getLogger().fine(msg); } } } } private void reportHarvest(String appName, StatsEngine statsEngine, IRPMService rpmService) { try { rpmService.harvest(statsEngine); } catch (Exception e) { String msg = MessageFormat.format("Error reporting harvest data for {0}: {1}", appName, e); if (getLogger().isLoggable(Level.FINER)) { getLogger().log(Level.FINER, msg, e); } else { getLogger().finer(msg); } } } private void notifyListenerBeforeHarvest(String appName, StatsEngine statsEngine, HarvestListener listener) { try { listener.beforeHarvest(appName, statsEngine); } catch (Throwable e) { String msg = MessageFormat.format("Error harvesting data for {0}: {1}", appName, e); if (getLogger().isLoggable(Level.FINER)) { getLogger().log(Level.FINER, msg, e); } else { getLogger().finer(msg); } } } private void notifyListenerAfterHarvest(String appName, HarvestListener listener) { try { listener.afterHarvest(appName); } catch (Throwable e) { String msg = MessageFormat.format("Error harvesting data for {0}: {1}", appName, e); if (getLogger().isLoggable(Level.FINER)) { getLogger().log(Level.FINER, msg, e); } else { getLogger().finer(msg); } } } private class HarvestableTracker { private final Harvestable harvestable; private final List> tasks = new ArrayList<>(); public HarvestableTracker(Harvestable harvestable) { this.harvestable = harvestable; } public synchronized void start(long reportPeriodInMillis, int maxSamplesStored) { stop(); harvestable.configure(reportPeriodInMillis, maxSamplesStored); Runnable harvestTask = new Runnable() { @Override public void run() { getLogger().log(Level.FINER, "Harvestable: {0}/{1} running", harvestable.getAppName(), harvestable.getEndpointMethodName()); harvestable.harvest(); } }; tasks.add( scheduledFasterHarvestExecutor.scheduleAtFixedRate(SafeWrappers.safeRunnable(harvestTask), 0, reportPeriodInMillis, TimeUnit.MILLISECONDS)); } public synchronized void stop() { for (ScheduledFuture task : tasks) { task.cancel(false); } tasks.clear(); } } private class ConnectionListenerImpl implements ConnectionListener { @Override public void connected(IRPMService rpmService, AgentConfig agentConfig) { startHarvest(rpmService); startHarvestables(rpmService, agentConfig); } @Override public void disconnected(IRPMService rpmService) { for (HarvestableTracker h : harvestables.values()) { h.stop(); } } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy