How to Add Telemetry to an Etendo Module
Overview
The Etendo Telemetry Module adds a thread-safe telemetry layer that lets you audit how end users interact with Etendo. By collecting metrics from servlets, processes, and REST endpoints you can build historical usage dashboards, detect regressions, and better understand feature adoption. This guide explains how to wire telemetry events into any custom module using the TelemetryUsageInfo helper and the same approach followed by the com.etendoerp.telemetry reference module.
Example Module
All of the snippets referenced here come from the com.etendoerp.telemetry module, which ships with reusable helpers and sample services. Clone that repository and review the TrackingUtil class if you want to see a complete implementation that you can extend in your own module.
Telemetry building blocks
TelemetryUsageInfo is the core component. It manages telemetry entries through a ThreadLocal instance to avoid data races when multiple requests are processed at the same time.
- Thread-local lifecycle: call
TelemetryUsageInfo.getInstance()to obtain the current request scope, and always invokeTelemetryUsageInfo.clear()in afinallyblock when you are done. - Persistence target: data is inserted into
ad_session_usage_audit, which stores session, command, module, and JSON payload information for later analysis. - Auto population: if you omit
userId,moduleId,objectId, orobjecttype, the helper pulls defaults fromSessionInfoso you can focus on the data that is unique to your module.
Required fields before saving
Use the following checklist before calling saveUsageAudit():
| Field | Description | How to set it |
|---|---|---|
sessionId |
Etendo database session identifier (#AD_Session_ID) |
Extract from the HTTP request or SessionInfo |
command |
Logical action being tracked | Choose descriptive constants such as AGENT_EXECUTION |
userId |
Current user | Omit only if SessionInfo already holds the user |
moduleId |
Module sending the telemetry event | You can hardcode your module ID in a wrapper |
objectId |
Domain object or entity | Use process ID, REST endpoint, or feature identifier |
objecttype |
Process type, P (process) by default |
Optional unless you need custom types |
jsonObject |
Optional metadata payload | Use JSON to describe request, response, or timings |
If any required field is empty the helper logs an error and no row is inserted, so validate your payload when debugging.
Capturing the session ID
Telemetry events must be tied to an Etendo session. Pick whichever approach fits your entry point:
VariablesSecureApp(servlets and controllers)
VariablesSecureApp vars = new VariablesSecureApp(request);
String sessionId = vars.getSessionValue("#AD_Session_ID");
TelemetryUsageInfo telemetry = TelemetryUsageInfo.getInstance();
telemetry.setSessionId(sessionId);
SessionInfo(secure contexts)
TelemetryUsageInfo telemetry = TelemetryUsageInfo.getInstance();
telemetry.setSessionId(SessionInfo.getSessionId());
HttpSessionfallback
HttpSession session = request.getSession(false);
if (session != null) {
telemetry.setSessionId((String) session.getAttribute("#AD_Session_ID"));
}
- Auto population
Skipping setSessionId forces TelemetryUsageInfo to read from SessionInfo, which works for simple synchronous processes that already initialize the context.
Recording usage data
Once you have the session ID, populate the remaining fields and call saveUsageAudit():
TelemetryUsageInfo telemetry = TelemetryUsageInfo.getInstance();
telemetry.setSessionId(sessionId);
telemetry.setModuleId(MY_MODULE_ID);
telemetry.setCommand("AGENT_USAGE");
telemetry.setObjectId(agentId);
telemetry.setObjecttype("AGENT");
telemetry.setClassname(MyTelemetryHook.class.getName());
telemetry.setTimeMillis(System.currentTimeMillis()); // optional, auto-filled otherwise
telemetry.setJsonObject(jsonMetadata);
telemetry.saveUsageAudit();
Always wrap the call in try/finally and clear the thread-local instance:
try {
// configure telemetry
telemetry.saveUsageAudit();
} catch (Exception e) {
logger.error("Telemetry failed", e);
} finally {
TelemetryUsageInfo.clear();
}
Use case: track REST/API calls
The reference REST service demonstrates how to track incoming HTTP traffic:
public void trackAPICall(HttpServletRequest request, String apiEndpoint) {
try {
VariablesSecureApp vars = new VariablesSecureApp(request);
String sessionId = vars.getSessionValue("#AD_Session_ID");
JSONObject metadata = new JSONObject();
metadata.put("endpoint", apiEndpoint);
metadata.put("method", request.getMethod());
metadata.put("timestamp", System.currentTimeMillis());
TelemetryUsageInfo telemetry = TelemetryUsageInfo.getInstance();
telemetry.setSessionId(sessionId);
telemetry.setCommand("API_CALL");
telemetry.setObjectId(apiEndpoint);
telemetry.setObjecttype("REST");
telemetry.setClassname(MyRestService.class.getName());
telemetry.setJsonObject(metadata);
telemetry.saveUsageAudit();
} catch (Exception e) {
logger.error("Failed to track API call", e);
} finally {
TelemetryUsageInfo.clear();
}
}
Use case: measure process execution time
Wrap long-running commands to measure latency and persist performance data:
public void trackCommandExecution(String command, String objectId) {
long start = System.currentTimeMillis();
try {
TelemetryUsageInfo telemetry = TelemetryUsageInfo.getInstance();
telemetry.setSessionId(getCurrentSessionId());
telemetry.setCommand(command);
executeCommand(command); // your business logic
long elapsed = System.currentTimeMillis() - start;
JSONObject metadata = new JSONObject();
metadata.put("execution_time_ms", elapsed);
metadata.put("status", "success");
telemetry.setTimeMillis(elapsed);
telemetry.setObjectId(objectId);
telemetry.setObjecttype("P");
telemetry.setJsonObject(metadata);
telemetry.saveUsageAudit();
} catch (Exception e) {
logger.error("Error tracking command execution", e);
} finally {
TelemetryUsageInfo.clear();
}
}
Use case: feature adoption tracking
Collect structured metadata for UI or Copilot features to understand adoption patterns:
public void trackFeatureUsage(String featureId, Map<String, Object> featureData) {
try {
TelemetryUsageInfo telemetry = TelemetryUsageInfo.getInstance();
telemetry.setCommand("FEATURE_USAGE");
telemetry.setObjectId(featureId);
telemetry.setObjecttype("F");
JSONObject json = new JSONObject();
json.put("feature_id", featureId);
json.put("timestamp", System.currentTimeMillis());
for (Map.Entry<String, Object> entry : featureData.entrySet()) {
json.put(entry.getKey(), entry.getValue());
}
telemetry.setJsonObject(json);
telemetry.saveUsageAudit();
} catch (Exception e) {
logger.error("Error tracking feature usage", e);
} finally {
TelemetryUsageInfo.clear();
}
}
Best practices
- Clear thread-locals: Always call
TelemetryUsageInfo.clear()in afinallyblock, especially inside thread pools, to prevent memory leaks. - Set the session early: Populate the session ID as soon as you enter the request, then reuse the same
TelemetryUsageInfoinstance. - Use meaningful commands: Prefer explicit verbs like
AGENT_EXECUTIONorREPORT_GENERATIONover generic names. - Structure JSON consistently: Reuse common keys (
operation_type,status,execution_time_ms) so downstream analytics stay predictable. - Fail safe: Log telemetry exceptions but never propagate them to the end user; telemetry should not block functional flows.
Troubleshooting
- Nothing is saved: Enable debug logs and print the session, command, user, module, and object values before calling
saveUsageAudit()to find missing fields. - Memory leaks: Double-check that
TelemetryUsageInfo.clear()executes in every path, including error branches, when using executors. - JSON errors: Wrap
JSONObjectmutations in try/catch blocks and make sure the metadata only contains serializable values.
With these steps you can extend any Etendo module with reliable telemetry that surfaces how your features perform in production without impacting end-user workflows.
This work is licensed under CC BY-SA 2.5 ES by Futit Services S.L.