Article
While AppDynamics provides a robust Machine Agent out-of-the-box, there are scenarios where you need custom monitoring, integration with proprietary systems, or specialized metrics collection. This guide covers how to implement Machine Agent-like functionality in Java, create custom monitors, and extend AppDynamics' capabilities.
Understanding AppDynamics Machine Agent
The Machine Agent provides:
- System Metrics: CPU, memory, disk I/O, network
- Process Monitoring: Application process health
- Custom Metrics: Extension points for business metrics
- Network Monitoring: Connectivity and performance
- Container Monitoring: Docker and Kubernetes support
Project Setup and Dependencies
Maven Dependencies:
<properties> <appdynamics.version>23.6.0</appdynamics.version> <micrometer.version>1.11.5</micrometer.version> <jackson.version>2.15.2</jackson.version> </properties> <dependencies> <!-- AppDynamics Java Agent API --> <dependency> <groupId>com.appdynamics</groupId> <artifactId>appdynamics-agent</artifactId> <version>${appdynamics.version}</version> <scope>provided</scope> </dependency> <!-- AppDynamics Machine Agent API --> <dependency> <groupId>com.appdynamics</groupId> <artifactId>machineagent</artifactId> <version>${appdynamics.version}</version> <scope>provided</scope> </dependency> <!-- Micrometer for metrics --> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-core</artifactId> <version>${micrometer.version}</version> </dependency> <!-- HTTP Client --> <dependency> <groupId>org.apache.httpcomponents.client5</groupId> <artifactId>httpclient5</artifactId> <version>5.2.1</version> </dependency> <!-- JSON Processing --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>${jackson.version}</version> </dependency> <!-- JMX for system monitoring --> <dependency> <groupId>com.sun.jmx</groupId> <artifactId>jmxri</artifactId> <version>1.2.1</version> </dependency> </dependencies> 1. Custom Machine Agent Implementation
Base Machine Agent Service:
package com.example.appd.machineagent; import com.appdynamics.agent.api.AppdynamicsAgent; import com.appdynamics.agent.api.MetricPublisher; import com.appdynamics.machineagent.api.MachineAgentContext; import com.appdynamics.machineagent.api.Monitor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.lang.management.ManagementFactory; import java.lang.management.OperatingSystemMXBean; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; public class CustomMachineAgent implements Monitor { private static final Logger logger = LoggerFactory.getLogger(CustomMachineAgent.class); private final AtomicBoolean running = new AtomicBoolean(false); private final ScheduledExecutorService scheduler; private final MachineAgentContext context; private final MetricPublisher metricPublisher; private static final String METRIC_PREFIX = "Custom Metrics|"; public CustomMachineAgent(MachineAgentContext context) { this.context = context; this.metricPublisher = AppdynamicsAgent.getMetricPublisher(); this.scheduler = new ScheduledThreadPoolExecutor(2, r -> { Thread t = new Thread(r, "custom-machine-agent"); t.setDaemon(true); return t; }); } @Override public void configure() { logger.info("Configuring Custom Machine Agent"); // Configuration from machine-agent.xml or other sources } @Override public void start() { if (running.compareAndSet(false, true)) { logger.info("Starting Custom Machine Agent"); // Schedule system metrics collection scheduler.scheduleAtFixedRate(this::collectSystemMetrics, 0, 60, TimeUnit.SECONDS); // Schedule custom business metrics scheduler.scheduleAtFixedRate(this::collectBusinessMetrics, 0, 30, TimeUnit.SECONDS); // Schedule process monitoring scheduler.scheduleAtFixedRate(this::monitorProcesses, 0, 120, TimeUnit.SECONDS); logger.info("Custom Machine Agent started successfully"); } } @Override public void stop() { if (running.compareAndSet(true, false)) { logger.info("Stopping Custom Machine Agent"); scheduler.shutdown(); try { if (!scheduler.awaitTermination(30, TimeUnit.SECONDS)) { scheduler.shutdownNow(); } } catch (InterruptedException e) { scheduler.shutdownNow(); Thread.currentThread().interrupt(); } } } @Override public boolean isRunning() { return running.get(); } private void collectSystemMetrics() { try { OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean(); Runtime runtime = Runtime.getRuntime(); // CPU Metrics double systemLoad = osBean.getSystemLoadAverage(); int availableProcessors = osBean.getAvailableProcessors(); // Memory Metrics long maxMemory = runtime.maxMemory(); long totalMemory = runtime.totalMemory(); long freeMemory = runtime.freeMemory(); long usedMemory = totalMemory - freeMemory; double memoryUsagePercent = (double) usedMemory / maxMemory * 100; // Publish metrics to AppDynamics metricPublisher.reportMetric(METRIC_PREFIX + "System|CPU|System Load", systemLoad); metricPublisher.reportMetric(METRIC_PREFIX + "System|CPU|Available Processors", availableProcessors); metricPublisher.reportMetric(METRIC_PREFIX + "System|Memory|Used MB", bytesToMB(usedMemory)); metricPublisher.reportMetric(METRIC_PREFIX + "System|Memory|Usage Percent", memoryUsagePercent); metricPublisher.reportMetric(METRIC_PREFIX + "System|Memory|Max MB", bytesToMB(maxMemory)); logger.debug("Collected system metrics: CPU Load={}, Memory Usage={}%", systemLoad, String.format("%.2f", memoryUsagePercent)); } catch (Exception e) { logger.error("Error collecting system metrics", e); } } private void collectBusinessMetrics() { try { // Custom business metrics int activeUsers = getActiveUsersCount(); int pendingOrders = getPendingOrdersCount(); double revenueToday = getRevenueToday(); int databaseConnections = getDatabaseConnectionCount(); // Publish business metrics metricPublisher.reportMetric(METRIC_PREFIX + "Business|Active Users", activeUsers); metricPublisher.reportMetric(METRIC_PREFIX + "Business|Pending Orders", pendingOrders); metricPublisher.reportMetric(METRIC_PREFIX + "Business|Revenue Today", revenueToday); metricPublisher.reportMetric(METRIC_PREFIX + "Business|Database Connections", databaseConnections); // Custom health scores double applicationHealth = calculateApplicationHealth(); metricPublisher.reportMetric(METRIC_PREFIX + "Business|Health Score", applicationHealth); } catch (Exception e) { logger.error("Error collecting business metrics", e); } } private void monitorProcesses() { try { // Monitor critical processes monitorJavaProcesses(); monitorDatabaseProcesses(); monitorExternalServiceHealth(); } catch (Exception e) { logger.error("Error monitoring processes", e); } } private void monitorJavaProcesses() { try { // Get all Java processes Process process = Runtime.getRuntime().exec("jcmd -l"); java.util.Scanner scanner = new java.util.Scanner(process.getInputStream()); int javaProcessCount = 0; while (scanner.hasNextLine()) { String line = scanner.nextLine(); if (line.contains("sun.tools.jcmd.JCmd") || line.trim().isEmpty()) { continue; } javaProcessCount++; } metricPublisher.reportMetric(METRIC_PREFIX + "Processes|Java Process Count", javaProcessCount); scanner.close(); } catch (Exception e) { logger.error("Error monitoring Java processes", e); } } private void monitorDatabaseProcesses() { // Implement database process monitoring boolean databaseHealthy = checkDatabaseHealth(); metricPublisher.reportMetric(METRIC_PREFIX + "Processes|Database Health", databaseHealthy ? 1 : 0); if (!databaseHealthy) { logger.warn("Database health check failed"); } } private void monitorExternalServiceHealth() { // Monitor external dependencies String[] services = {"auth-service", "payment-gateway", "notification-service"}; for (String service : services) { boolean healthy = checkServiceHealth(service); metricPublisher.reportMetric( METRIC_PREFIX + "Services|" + service + "|Health", healthy ? 1 : 0 ); if (!healthy) { logger.warn("Service {} is unhealthy", service); } } } // Helper methods private long bytesToMB(long bytes) { return bytes / (1024 * 1024); } // Mock implementations - replace with actual logic private int getActiveUsersCount() { // Implement actual logic return (int) (Math.random() * 1000); } private int getPendingOrdersCount() { // Implement actual logic return (int) (Math.random() * 100); } private double getRevenueToday() { // Implement actual logic return Math.random() * 10000; } private int getDatabaseConnectionCount() { // Implement actual logic return (int) (Math.random() * 50); } private double calculateApplicationHealth() { // Implement health calculation logic return Math.random() * 100; } private boolean checkDatabaseHealth() { // Implement actual database health check return Math.random() > 0.1; // 90% healthy } private boolean checkServiceHealth(String service) { // Implement actual service health check return Math.random() > 0.05; // 95% healthy } } 2. Custom JMX Monitor
JMX-based System Monitor:
package com.example.appd.monitors; import com.appdynamics.agent.api.AppdynamicsAgent; import com.appdynamics.agent.api.MetricPublisher; import com.appdynamics.machineagent.api.Monitor; import javax.management.*; import javax.management.remote.JMXConnector; import javax.management.remote.JMXConnectorFactory; import javax.management.remote.JMXServiceURL; import java.lang.management.*; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; public class JmxSystemMonitor implements Monitor { private final MetricPublisher metricPublisher; private final ScheduledExecutorService scheduler; private final AtomicBoolean running = new AtomicBoolean(false); private static final String METRIC_PREFIX = "JMX Metrics|"; public JmxSystemMonitor() { this.metricPublisher = AppdynamicsAgent.getMetricPublisher(); this.scheduler = new ScheduledThreadPoolExecutor(1); } @Override public void start() { if (running.compareAndSet(false, true)) { scheduler.scheduleAtFixedRate(this::collectJmxMetrics, 0, 30, TimeUnit.SECONDS); } } private void collectJmxMetrics() { collectLocalJmxMetrics(); collectRemoteJmxMetrics(); } private void collectLocalJmxMetrics() { try { MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer(); // Memory metrics MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean(); MemoryUsage heapUsage = memoryBean.getHeapMemoryUsage(); MemoryUsage nonHeapUsage = memoryBean.getNonHeapMemoryUsage(); metricPublisher.reportMetric(METRIC_PREFIX + "Memory|Heap|Used", heapUsage.getUsed()); metricPublisher.reportMetric(METRIC_PREFIX + "Memory|Heap|Max", heapUsage.getMax()); metricPublisher.reportMetric(METRIC_PREFIX + "Memory|NonHeap|Used", nonHeapUsage.getUsed()); // Thread metrics ThreadMXBean threadBean = ManagementFactory.getThreadMXBean(); metricPublisher.reportMetric(METRIC_PREFIX + "Threads|Count", threadBean.getThreadCount()); metricPublisher.reportMetric(METRIC_PREFIX + "Threads|Daemon Count", threadBean.getDaemonThreadCount()); metricPublisher.reportMetric(METRIC_PREFIX + "Threads|Peak Count", threadBean.getPeakThreadCount()); // Garbage collection metrics for (GarbageCollectorMXBean gcBean : ManagementFactory.getGarbageCollectorMXBeans()) { String gcName = sanitizeMetricName(gcBean.getName()); metricPublisher.reportMetric(METRIC_PREFIX + "GC|" + gcName + "|Collection Count", gcBean.getCollectionCount()); metricPublisher.reportMetric(METRIC_PREFIX + "GC|" + gcName + "|Collection Time", gcBean.getCollectionTime()); } // Operating system metrics OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean(); if (osBean instanceof com.sun.management.OperatingSystemMXBean) { com.sun.management.OperatingSystemMXBean sunOsBean = (com.sun.management.OperatingSystemMXBean) osBean; metricPublisher.reportMetric(METRIC_PREFIX + "OS|Process CPU Load", sunOsBean.getProcessCpuLoad() * 100); metricPublisher.reportMetric(METRIC_PREFIX + "OS|System CPU Load", sunOsBean.getSystemCpuLoad() * 100); metricPublisher.reportMetric(METRIC_PREFIX + "OS|Physical Memory Free", sunOsBean.getFreePhysicalMemorySize()); metricPublisher.reportMetric(METRIC_PREFIX + "OS|Physical Memory Total", sunOsBean.getTotalPhysicalMemorySize()); } } catch (Exception e) { logger.error("Error collecting local JMX metrics", e); } } private void collectRemoteJmxMetrics() { // Monitor remote Java applications via JMX Map<String, String> remoteApps = getRemoteJmxConfigurations(); for (Map.Entry<String, String> entry : remoteApps.entrySet()) { String appName = entry.getKey(); String jmxUrl = entry.getValue(); try { collectMetricsFromRemoteJmx(appName, jmxUrl); } catch (Exception e) { logger.error("Error collecting JMX metrics from {}: {}", appName, jmxUrl, e); metricPublisher.reportMetric(METRIC_PREFIX + "Remote|" + appName + "|Health", 0); } } } private void collectMetricsFromRemoteJmx(String appName, String jmxUrl) throws Exception { JMXServiceURL url = new JMXServiceURL(jmxUrl); Map<String, Object> env = new HashMap<>(); try (JMXConnector jmxc = JMXConnectorFactory.connect(url, env)) { MBeanServerConnection mbsc = jmxc.getMBeanServerConnection(); // Query for specific MBeans Set<ObjectName> memoryBeans = mbsc.queryNames( new ObjectName("java.lang:type=Memory,*"), null); for (ObjectName memoryBean : memoryBeans) { AttributeList attributes = mbsc.getAttributes(memoryBean, new String[]{"HeapMemoryUsage", "NonHeapMemoryUsage"}); for (Attribute attribute : attributes.asList()) { if (attribute.getValue() instanceof CompositeData) { CompositeData data = (CompositeData) attribute.getValue(); long used = (Long) data.get("used"); String metricName = METRIC_PREFIX + "Remote|" + appName + "|Memory|" + attribute.getName() + "|Used"; metricPublisher.reportMetric(metricName, used); } } } // Report health metricPublisher.reportMetric(METRIC_PREFIX + "Remote|" + appName + "|Health", 1); } } private Map<String, String> getRemoteJmxConfigurations() { // Load from configuration file or environment Map<String, String> configs = new HashMap<>(); configs.put("Database-App", "service:jmx:rmi:///jndi/rmi://db-host:9090/jmxrmi"); configs.put("Cache-App", "service:jmx:rmi:///jndi/rmi://cache-host:9091/jmxrmi"); return configs; } private String sanitizeMetricName(String name) { return name.replaceAll("[^a-zA-Z0-9]", "_"); } @Override public void stop() { running.set(false); scheduler.shutdown(); } @Override public boolean isRunning() { return running.get(); } } 3. Custom Business Transaction Monitor
Business-specific Metrics Monitor:
package com.example.appd.monitors; import com.appdynamics.agent.api.AppdynamicsAgent; import com.appdynamics.agent.api.MetricPublisher; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.hc.client5.http.classic.methods.HttpGet; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; import org.apache.hc.client5.http.impl.classic.HttpClients; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; public class BusinessTransactionMonitor implements Monitor { private final MetricPublisher metricPublisher; private final ScheduledExecutorService scheduler; private final AtomicBoolean running = new AtomicBoolean(false); private final ObjectMapper objectMapper; private final CloseableHttpClient httpClient; private final AtomicLong totalOrders = new AtomicLong(0); private final AtomicLong failedOrders = new AtomicLong(0); private final AtomicLong totalRevenue = new AtomicLong(0); private static final String METRIC_PREFIX = "Business Transactions|"; public BusinessTransactionMonitor() { this.metricPublisher = AppdynamicsAgent.getMetricPublisher(); this.scheduler = new ScheduledThreadPoolExecutor(2); this.objectMapper = new ObjectMapper(); this.httpClient = HttpClients.createDefault(); } @Override public void start() { if (running.compareAndSet(false, true)) { // Collect metrics every 30 seconds scheduler.scheduleAtFixedRate(this::collectBusinessMetrics, 0, 30, TimeUnit.SECONDS); // Collect external API metrics every minute scheduler.scheduleAtFixedRate(this::collectExternalApiMetrics, 0, 60, TimeUnit.SECONDS); // Calculate and report KPIs every 5 minutes scheduler.scheduleAtFixedRate(this::calculateKPIs, 0, 5, TimeUnit.MINUTES); } } private void collectBusinessMetrics() { try { // Simulate collecting from database or message queue long newOrders = getNewOrderCount(); long newFailedOrders = getFailedOrderCount(); long newRevenue = getRevenueSinceLastCheck(); totalOrders.addAndGet(newOrders); failedOrders.addAndGet(newFailedOrders); totalRevenue.addAndGet(newRevenue); // Report real-time metrics metricPublisher.reportMetric(METRIC_PREFIX + "Orders|New", newOrders); metricPublisher.reportMetric(METRIC_PREFIX + "Orders|Failed", newFailedOrders); metricPublisher.reportMetric(METRIC_PREFIX + "Revenue|New", newRevenue); // Calculate success rate if (newOrders > 0) { double successRate = ((double) (newOrders - newFailedOrders) / newOrders) * 100; metricPublisher.reportMetric(METRIC_PREFIX + "Orders|Success Rate", successRate); } } catch (Exception e) { logger.error("Error collecting business metrics", e); } } private void collectExternalApiMetrics() { try { // Monitor external API health and performance monitorPaymentGateway(); monitorShippingService(); monitorInventoryService(); } catch (Exception e) { logger.error("Error collecting external API metrics", e); } } private void monitorPaymentGateway() { try { long startTime = System.currentTimeMillis(); HttpGet request = new HttpGet("https://api.payment-gateway.com/health"); try (CloseableHttpResponse response = httpClient.execute(request)) { int statusCode = response.getCode(); long responseTime = System.currentTimeMillis() - startTime; boolean isHealthy = statusCode == 200; metricPublisher.reportMetric(METRIC_PREFIX + "APIs|Payment Gateway|Health", isHealthy ? 1 : 0); metricPublisher.reportMetric(METRIC_PREFIX + "APIs|Payment Gateway|Response Time", responseTime); if (!isHealthy) { logger.warn("Payment gateway health check failed with status: {}", statusCode); } } } catch (Exception e) { logger.error("Error monitoring payment gateway", e); metricPublisher.reportMetric(METRIC_PREFIX + "APIs|Payment Gateway|Health", 0); } } private void monitorShippingService() { try { long startTime = System.currentTimeMillis(); HttpGet request = new HttpGet("https://api.shipping.com/v1/status"); request.setHeader("Authorization", "Bearer " + getShippingApiKey()); try (CloseableHttpResponse response = httpClient.execute(request)) { int statusCode = response.getCode(); long responseTime = System.currentTimeMillis() - startTime; boolean isHealthy = statusCode == 200; metricPublisher.reportMetric(METRIC_PREFIX + "APIs|Shipping Service|Health", isHealthy ? 1 : 0); metricPublisher.reportMetric(METRIC_PREFIX + "APIs|Shipping Service|Response Time", responseTime); if (isHealthy) { // Parse response for additional metrics String responseBody = EntityUtils.toString(response.getEntity()); JsonNode jsonResponse = objectMapper.readTree(responseBody); boolean operational = jsonResponse.path("operational").asBoolean(); int queueSize = jsonResponse.path("queue_size").asInt(); metricPublisher.reportMetric(METRIC_PREFIX + "APIs|Shipping Service|Operational", operational ? 1 : 0); metricPublisher.reportMetric(METRIC_PREFIX + "APIs|Shipping Service|Queue Size", queueSize); } } } catch (Exception e) { logger.error("Error monitoring shipping service", e); metricPublisher.reportMetric(METRIC_PREFIX + "APIs|Shipping Service|Health", 0); } } private void monitorInventoryService() { // Similar implementation for inventory service } private void calculateKPIs() { try { // Calculate Key Performance Indicators double conversionRate = calculateConversionRate(); double averageOrderValue = calculateAverageOrderValue(); double customerSatisfaction = calculateCustomerSatisfaction(); metricPublisher.reportMetric(METRIC_PREFIX + "KPIs|Conversion Rate", conversionRate); metricPublisher.reportMetric(METRIC_PREFIX + "KPIs|Average Order Value", averageOrderValue); metricPublisher.reportMetric(METRIC_PREFIX + "KPIs|Customer Satisfaction", customerSatisfaction); // Reset counters for next period totalOrders.set(0); failedOrders.set(0); totalRevenue.set(0); } catch (Exception e) { logger.error("Error calculating KPIs", e); } } private double calculateConversionRate() { // Implement actual conversion rate logic long sessions = getSessionCount(); long orders = totalOrders.get(); return sessions > 0 ? (double) orders / sessions * 100 : 0; } private double calculateAverageOrderValue() { long orders = totalOrders.get(); return orders > 0 ? (double) totalRevenue.get() / orders : 0; } private double calculateCustomerSatisfaction() { // Implement CSAT calculation logic return Math.random() * 100; } // Mock methods - replace with actual implementations private long getNewOrderCount() { return (long) (Math.random() * 100); } private long getFailedOrderCount() { return (long) (Math.random() * 5); } private long getRevenueSinceLastCheck() { return (long) (Math.random() * 10000); } private long getSessionCount() { return (long) (Math.random() * 500); } private String getShippingApiKey() { return "mock-api-key"; } @Override public void stop() { running.set(false); scheduler.shutdown(); try { httpClient.close(); } catch (Exception e) { logger.error("Error closing HTTP client", e); } } @Override public boolean isRunning() { return running.get(); } } 4. Machine Agent Configuration
machine-agent.xml Configuration:
<?xml version="1.0" encoding="UTF-8"?> <machine-agent> <controller-info> <controller-host>yourappdynamics.controller.com</controller-host> <controller-port>8090</controller-port> <controller-ssl-enabled>false</controller-ssl-enabled> <account-name>customer1</account-name> <account-access-key>your-access-key</account-access-key> <application-name>Custom-Monitoring</application-name> <tier-name>Machine-Monitoring</tier-name> <node-name>Machine-Node-1</node-name> </controller-info> <monitors> <!-- Custom Machine Agent --> <monitor> <name>CustomMachineAgent</name> <monitor-class>com.example.appd.machineagent.CustomMachineAgent</monitor-class> <enabled>true</enabled> <properties> <property name="collection-interval" value="60"/> <property name="business-metrics-enabled" value="true"/> </properties> </monitor> <!-- JMX System Monitor --> <monitor> <name>JmxSystemMonitor</name> <monitor-class>com.example.appd.monitors.JmxSystemMonitor</monitor-class> <enabled>true</enabled> </monitor> <!-- Business Transaction Monitor --> <monitor> <name>BusinessTransactionMonitor</name> <monitor-class>com.example.appd.monitors.BusinessTransactionMonitor</monitor-class> <enabled>true</enabled> <properties> <property name="api-timeout-ms" value="5000"/> <property name="kpi-interval-minutes" value="5"/> </properties> </monitor> </monitors> <metric-prefix>Custom Machine Agent</metric-prefix> </machine-agent>
5. Spring Boot Integration
AppDynamics Configuration Bean:
package com.example.appd.config; import com.appdynamics.agent.api.AppdynamicsAgent; import com.example.appd.monitors.BusinessTransactionMonitor; import com.example.appd.monitors.JmxSystemMonitor; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; @Configuration @EnableConfigurationProperties(AppDynamicsProperties.class) public class AppDynamicsConfig { private final AppDynamicsProperties properties; private BusinessTransactionMonitor businessMonitor; private JmxSystemMonitor jmxMonitor; public AppDynamicsConfig(AppDynamicsProperties properties) { this.properties = properties; } @PostConstruct public void initializeMonitors() { if (properties.isEnabled()) { logger.info("Initializing AppDynamics custom monitors"); // Initialize and start monitors this.businessMonitor = new BusinessTransactionMonitor(); this.jmxMonitor = new JmxSystemMonitor(); businessMonitor.start(); jmxMonitor.start(); logger.info("AppDynamics custom monitors started"); } } @PreDestroy public void shutdownMonitors() { if (businessMonitor != null) { businessMonitor.stop(); } if (jmxMonitor != null) { jmxMonitor.stop(); } logger.info("AppDynamics custom monitors stopped"); } @Bean public AppDynamicsMetricsService appDynamicsMetricsService() { return new AppDynamicsMetricsService(); } } @ConfigurationProperties(prefix = "appdynamics.custom") class AppDynamicsProperties { private boolean enabled = true; private long collectionInterval = 30000; private String metricPrefix = "Custom Metrics"; private Map<String, String> jmxConnections = new HashMap<>(); // Getters and setters } Metrics Service:
package com.example.appd.service; import com.appdynamics.agent.api.AppdynamicsAgent; import com.appdynamics.agent.api.MetricPublisher; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; @Service public class AppDynamicsMetricsService { private final MetricPublisher metricPublisher; private final AtomicInteger activeSessions = new AtomicInteger(0); private final AtomicLong totalRequests = new AtomicLong(0); private final AtomicLong errorCount = new AtomicLong(0); public AppDynamicsMetricsService() { this.metricPublisher = AppdynamicsAgent.getMetricPublisher(); } public void recordRequest(boolean success, long duration) { totalRequests.incrementAndGet(); if (!success) { errorCount.incrementAndGet(); } metricPublisher.reportMetric("Custom Metrics|Application|Request Count", 1); metricPublisher.reportMetric("Custom Metrics|Application|Request Duration", duration); if (!success) { metricPublisher.reportMetric("Custom Metrics|Application|Error Count", 1); } } public void recordUserSession(boolean login) { if (login) { activeSessions.incrementAndGet(); } else { activeSessions.decrementAndGet(); } metricPublisher.reportMetric("Custom Metrics|Application|Active Sessions", activeSessions.get()); } @Scheduled(fixedRate = 30000) public void reportCustomMetrics() { // Calculate error rate long total = totalRequests.get(); long errors = errorCount.get(); double errorRate = total > 0 ? (double) errors / total * 100 : 0; metricPublisher.reportMetric("Custom Metrics|Application|Error Rate", errorRate); metricPublisher.reportMetric("Custom Metrics|Application|Total Requests", total); // Reset counters for next period totalRequests.set(0); errorCount.set(0); } } 6. Docker Deployment
Dockerfile for Custom Machine Agent:
FROM openjdk:17-jre-slim # Install required tools RUN apt-get update && apt-get install -y \ procps \ net-tools \ && rm -rf /var/lib/apt/lists/* # Copy AppDynamics Machine Agent COPY appdynamics/machine-agent /opt/appdynamics/machine-agent # Copy custom monitors COPY target/custom-monitors.jar /opt/appdynamics/plugins/ # Copy configuration COPY config/machine-agent.xml /opt/appdynamics/machine-agent/conf/ WORKDIR /opt/appdynamics/machine-agent # Start Machine Agent with custom monitors CMD ["java", "-jar", "machineagent.jar"]
docker-compose.yml:
version: '3.8' services: appd-machine-agent: build: . environment: - APPDYNAMICS_CONTROLLER_HOST_NAME=controller.appdynamics.com - APPDYNAMICS_CONTROLLER_PORT=8090 - APPDYNAMICS_AGENT_ACCOUNT_NAME=customer1 - APPDYNAMICS_AGENT_ACCOUNT_ACCESS_KEY=your-access-key - APPDYNAMICS_AGENT_APPLICATION_NAME=Custom-Monitoring - APPDYNAMICS_AGENT_TIER_NAME=Machine-Monitoring - APPDYNAMICS_AGENT_NODE_NAME=${HOSTNAME} volumes: - /var/run/docker.sock:/var/run/docker.sock - /proc:/host/proc:ro restart: unless-stopped 7. Kubernetes Deployment
ConfigMap for Configuration:
apiVersion: v1 kind: ConfigMap metadata: name: appd-machine-agent-config data: machine-agent.xml: | <?xml version="1.0" encoding="UTF-8"?> <machine-agent> <controller-info> <controller-host>${APPDYNAMICS_CONTROLLER_HOST}</controller-host> <controller-port>${APPDYNAMICS_CONTROLLER_PORT}</controller-port> <account-name>${APPDYNAMICS_ACCOUNT_NAME}</account-name> <account-access-key>${APPDYNAMICS_ACCESS_KEY}</account-access-key> </controller-info> <monitors> <monitor> <name>CustomMachineAgent</name> <monitor-class>com.example.appd.machineagent.CustomMachineAgent</monitor-class> <enabled>true</enabled> </monitor> </monitors> </machine-agent> Deployment:
apiVersion: apps/v1 kind: Deployment metadata: name: appd-machine-agent spec: replicas: 1 selector: matchLabels: app: appd-machine-agent template: metadata: labels: app: appd-machine-agent spec: containers: - name: machine-agent image: your-registry/appd-machine-agent:latest env: - name: APPDYNAMICS_CONTROLLER_HOST value: "controller.appdynamics.com" - name: APPDYNAMICS_CONTROLLER_PORT value: "8090" - name: APPDYNAMICS_ACCOUNT_NAME value: "customer1" - name: APPDYNAMICS_ACCESS_KEY valueFrom: secretKeyRef: name: appd-secrets key: access-key volumeMounts: - name: appd-config mountPath: /opt/appdynamics/machine-agent/conf - name: proc mountPath: /host/proc readOnly: true resources: requests: memory: "512Mi" cpu: "250m" limits: memory: "1Gi" cpu: "500m" volumes: - name: appd-config configMap: name: appd-machine-agent-config - name: proc hostPath: path: /proc
8. Best Practices
Performance Considerations:
- Use async operations for external service checks
- Implement proper connection pooling
- Use batching for high-frequency metrics
- Set appropriate collection intervals
Error Handling:
public class MonitorErrorHandler { private final MetricPublisher metricPublisher; private final AtomicLong errorCount = new AtomicLong(0); public void handleError(String monitorName, Exception e) { errorCount.incrementAndGet(); metricPublisher.reportMetric("Custom Metrics|Errors|" + monitorName, 1); logger.error("Error in monitor: {}", monitorName, e); // Alert on consecutive errors if (errorCount.get() > 5) { triggerAlert(monitorName, "High error rate detected"); } } public void resetErrorCount() { errorCount.set(0); } } Security:
- Secure credentials using Kubernetes Secrets
- Use HTTPS for external API calls
- Implement proper authentication for JMX connections
- Regularly rotate API keys and credentials
Conclusion
Building custom AppDynamics Machine Agent functionality in Java provides:
- Extended Monitoring: Beyond standard system metrics
- Business Context: Custom KPIs and business metrics
- Integration: Connect with proprietary systems and APIs
- Flexibility: Tailor monitoring to specific needs
- Cost Optimization: Reduce reliance on commercial plugins
This approach allows you to create a comprehensive monitoring solution that combines AppDynamics' powerful platform with your specific business requirements and infrastructure needs.