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.