The dotCMS telemetry system collects and reports metrics about system usage, configuration, and features. The system has been refactored to use CDI (Contexts and Dependency Injection) for automatic discovery and management of metrics, replacing static factory patterns.
The base interface for all metrics. Implementations must provide:
- Metadata: Name, description, category, and feature
- Value Calculation: Logic to compute the metric value
All concrete MetricType implementations must be annotated with @ApplicationScoped to be discovered by CDI.
CDI-managed service (@ApplicationScoped) that collects all metrics and generates MetricsSnapshot objects.
Key Features:
- Automatically discovers all
@ApplicationScoped MetricTypeimplementations via@Inject Instance<MetricType> - Aggregates metrics into
MetricsSnapshotfor reporting - Handles errors gracefully, collecting failed metrics separately
- Used by telemetry endpoints and scheduled jobs
Usage:
@Inject
private MetricStatsCollector collector;
MetricsSnapshot snapshot = collector.getStats();CDI-managed service that provides dashboard-specific metrics. Only metrics annotated with @DashboardMetric are included.
Key Features:
- Filters metrics by
@DashboardMetricannotation - Sorts metrics by priority (from annotation)
- Supports category-based filtering
- Used by
UsageResourcefor dashboard display
Usage:
@Inject
private DashboardMetricsProvider provider;
List<MetricType> dashboardMetrics = provider.getDashboardMetrics();
List<MetricType> contentMetrics = provider.getDashboardMetricsByCategory("content");Marker annotation to include metrics in the Usage API dashboard endpoint (/v1/usage/summary).
Attributes:
category(String): Optional grouping for organizing metrics (e.g., "content", "site", "user", "system")priority(int): Display order (lower values appear first, default: 0)
Purpose: "What to show" - Declares display intent and metadata for dashboard presentation.
Example:
@ApplicationScoped
@MetricsProfile({ProfileType.MINIMAL, ProfileType.STANDARD, ProfileType.FULL}) // Performance control
@DashboardMetric(category = "content", priority = 1) // Display intent
public class TotalContentsDatabaseMetricType implements DBMetricType {
// ...
}Benefits:
- Declarative configuration (no code changes needed to add/remove metrics from dashboard)
- Automatic discovery via CDI
- No hardcoded metric lists in API code
Required annotation that declares which performance profiles a metric supports (MINIMAL, STANDARD, FULL).
Purpose: "When to collect" - Controls performance by restricting metric collection based on active profile.
Attributes:
value(ProfileType[]): Array of profiles this metric supports
Example:
@ApplicationScoped
@MetricsProfile({ProfileType.MINIMAL, ProfileType.STANDARD, ProfileType.FULL}) // Supports MINIMAL, STANDARD, and FULL
@DashboardMetric(category = "site", priority = 1)
public class TotalSitesDatabaseMetricType implements DBMetricType {
// ...
}Important: All metrics must have @MetricsProfile annotation. Metrics without this annotation will be excluded from collection. An error will be logged for missing @MetricsProfile annotations.
These annotations work together in a two-stage filtering process:
-
Stage 1:
@DashboardMetricFilter- Filters metrics for dashboard display
- Provides display metadata (category, priority)
- Purpose: "What to show"
-
Stage 2:
@MetricsProfileFilter- Filters metrics by active performance profile
- Controls when metrics are collected
- Purpose: "When to collect"
- Required: All metrics must have this annotation
Best Practice: Always use both annotations together:
@ApplicationScoped
@MetricsProfile({ProfileType.MINIMAL, ProfileType.STANDARD, ProfileType.FULL}) // Performance: supports all profiles
@DashboardMetric(category = "site", priority = 1) // Display: site category, priority 1
public class TotalSitesDatabaseMetricType implements DBMetricType {
// ...
}Profile Guidelines:
- MINIMAL: Fast, simple queries (< 500ms). Core metrics only (10-15 metrics).
- STANDARD: Moderate complexity (~50 metrics). Includes MINIMAL plus additional valuable metrics.
- FULL: All metrics (128+). Background collection only.
package com.dotcms.telemetry.collectors.example;
import com.dotcms.telemetry.MetricCategory;
import com.dotcms.telemetry.MetricFeature;
import com.dotcms.telemetry.MetricType;
import com.dotcms.telemetry.MetricValue;
import com.dotcms.telemetry.collectors.DBMetricType;
import javax.enterprise.context.ApplicationScoped;
import java.util.Optional;
@ApplicationScoped
public class MyNewMetricType implements DBMetricType {
@Override
public String getName() {
return "MY_NEW_METRIC";
}
@Override
public String getDescription() {
return "Description of my new metric";
}
@Override
public MetricCategory getCategory() {
return MetricCategory.DIFFERENTIATING_FEATURES;
}
@Override
public MetricFeature getFeature() {
return MetricFeature.CONTENTLETS;
}
@Override
public String getSqlQuery() {
return "SELECT COUNT(*) AS value FROM my_table";
}
}All metrics must have @MetricsProfile annotation. To include the metric in the Usage API dashboard, add both @MetricsProfile and @DashboardMetric annotations:
@ApplicationScoped
@MetricsProfile({ProfileType.STANDARD, ProfileType.FULL}) // Performance control (required)
@DashboardMetric(category = "content", priority = 5) // Display intent (optional)
public class MyNewMetricType implements DBMetricType {
// ...
}Important: All metrics require @MetricsProfile annotation. Metrics without this annotation will be excluded from collection. For dashboard metrics, also include @DashboardMetric to control display.
The metric will automatically:
- Be discovered by
MetricStatsCollectorfor general telemetry - Be filtered by active profile via
ProfileFilter - Be included in dashboard via
DashboardMetricsProvider(if profile matches) - Appear in
/v1/usage/summaryendpoint response - Be sorted by priority within its category
For common patterns, extend base classes:
DBMetricType: For database query-based metrics- Abstract classes: Various abstract base classes for specific metric families (see package structure)
Note: API request counting metrics are not part of the telemetry system. For request-level metrics, use Micrometer Counters (see com.dotcms.metrics.binders.HttpRequestMetrics).
The profile system controls which metrics are collected based on performance requirements. This enables fast dashboard loading while still collecting comprehensive metrics for background jobs.
- MINIMAL: 10-15 core metrics, < 5 seconds total collection time. Used for fast dashboard loading.
- STANDARD: ~50 metrics, < 15 seconds total collection time. Future implementation.
- FULL: All 128+ metrics. Background collection only. Used by scheduled cron jobs.
Two separate profile settings:
# Dashboard/API profile (default: MINIMAL)
telemetry.default.profile=MINIMAL
# Cron job profile (default: FULL)
telemetry.cron.profile=FULL-
Dashboard/API: Uses
telemetry.default.profile(typically MINIMAL)- Fast loading with core metrics only
- Filtered via
DashboardMetricsProvider
-
Cron Jobs: Uses
telemetry.cron.profile(typically FULL)- Collects all metrics for persistence
- Filtered via
MetricStatsCollector.getStatsAndCleanUp(ProfileType)
All metrics must include @MetricsProfile annotation. Dashboard metrics should also include @DashboardMetric:
@ApplicationScoped
@MetricsProfile({ProfileType.MINIMAL, ProfileType.STANDARD, ProfileType.FULL}) // Fast, core metric
@DashboardMetric(category = "site", priority = 1)
public class TotalSitesDatabaseMetricType implements DBMetricType {
// ...
}
@ApplicationScoped
@MetricsProfile({ProfileType.STANDARD, ProfileType.FULL}) // More complex metric
@DashboardMetric(category = "content", priority = 2)
public class RecentlyEditedContentDatabaseMetricType implements DBMetricType {
// ...
}Profile Selection Guidelines:
- MINIMAL: Simple queries (< 500ms), single table, core business value
- STANDARD: Moderate complexity, simple joins, valuable but not critical
- FULL: Complex aggregations, multi-table queries, comprehensive analysis
- Always annotate the metric class with
@MetricsProfile(required for all metrics) - Optionally annotate with
@DashboardMetricto include in dashboard - Optionally set
categoryandpriorityin@DashboardMetric - No code changes needed in
UsageResourceor other API classes
Remove the @DashboardMetric annotation. The metric will still be available via general telemetry endpoints but won't appear in the dashboard.
Use the category attribute to group related metrics:
"content": Content-related metrics"site": Site-related metrics"user": User-related metrics"system": System configuration metrics
Use priority to control display order within categories (lower = first).
The telemetry system includes comprehensive performance monitoring and timeout protection to ensure system stability and provide visibility into metric collection performance.
MetricTiming
Captures execution timing information for each metric:
durationMs: Total execution time including cache lookupcacheHit: Whether the value was served from cachecomputationMs: Actual computation time (0 for cache hits)timedOut: Whether the metric exceeded timeout thresholdslow: Whether the metric exceeded slow threshold
Example timing data:
{
"metricName": "COUNT_OF_SITES",
"durationMs": 5,
"cacheHit": true,
"computationMs": 0,
"timedOut": false,
"slow": false
}For cache misses:
{
"metricName": "COMPLEX_QUERY",
"durationMs": 820,
"cacheHit": false,
"computationMs": 800,
"timedOut": false,
"slow": true
}Global Timeout All metric collections are protected by a global timeout to prevent runaway queries from locking the database:
# Global timeout for any single metric (hard limit)
telemetry.metric.timeout.seconds=2
# Total timeout for entire collection process
telemetry.collection.timeout.seconds=30
# Warning threshold for slow metrics
telemetry.metric.slow.threshold.ms=500How It Works:
- Each metric collection runs in a timeout-enforced executor
- If a metric exceeds the timeout, it's cancelled and logged as an error
- Slow metrics (above threshold but not timed out) are logged as warnings
- Timing data is collected regardless of success/failure
Implementation: See MetricStatsCollector.java:123-225 for timeout enforcement logic.
The timing data provides full visibility into cache effectiveness:
Cache Hit Rate:
cacheHits / totalMetrics * 100
Time Savings:
sum(computationMs for cache misses) - sum(durationMs for cache hits)
Slow Metrics by Cache Status:
- Cache hits that are slow: Check cache configuration
- Cache misses that are slow: Optimize underlying query
Timeout Settings:
# Global timeout (no exceptions)
telemetry.metric.timeout.seconds=2
# Total collection timeout
telemetry.collection.timeout.seconds=30
# Slow metric warning threshold
telemetry.metric.slow.threshold.ms=500Best Practices:
- Default timeout (2s): Protects database from long-running queries
- No per-metric exceptions: Consistency and predictability
- Slow threshold (500ms): Identifies optimization opportunities
- Cache slow metrics: Metrics consistently over 500ms should be cached
/api/v1/telemetry/stats: Returns all metrics with timing information- Query Parameters:
metricNames: Comma-separated list of specific metrics to retrieveprofile: Profile type (MINIMAL, STANDARD, FULL)bypassCache: Set totrueto force fresh computation bypassing cache
- Uses
MetricStatsCollectorto collect all discovered metrics - Returns
MetricsSnapshotwith metrics, errors, and timing data
- Query Parameters:
Cache Bypass for Diagnostics:
# Normal operation (uses cache)
curl -H "Authorization: Basic $(echo -n 'admin@dotcms.com:admin' | base64)" \
"http://localhost:8080/api/v1/telemetry/stats"
# Bypass cache for diagnostics
curl -H "Authorization: Basic $(echo -n 'admin@dotcms.com:admin' | base64)" \
"http://localhost:8080/api/v1/telemetry/stats?bypassCache=true"Use Cases for Cache Bypass:
- Diagnosing slow metrics: See actual database query times
- Testing optimizations: Verify performance improvements
- Cache verification: Compare cached vs fresh values
- Performance baselines: Establish actual query times
/api/v1/usage/summary: Returns dashboard-specific metrics- Uses
DashboardMetricsProviderto collect only@DashboardMetricannotated metrics - Returns
UsageSummaryoptimized for dashboard consumption - Includes content, site, user, and system metrics
- All
@ApplicationScopedbeans implementingMetricTypeare automatically discovered by CDI MetricStatsCollectorinjectsInstance<MetricType>to access all metricsDashboardMetricsProviderfilters the collection for@DashboardMetricannotated types- No manual registration or factory methods required
- Concrete implementations: Must be annotated with
@ApplicationScoped - Abstract classes: Should NOT be annotated (they're not instantiated)
- Interface implementations: Must be concrete classes (interfaces can't be CDI beans)
The refactoring moved from:
- Static factory methods with hardcoded metric lists
- Manual instantiation with
new - Required code changes to add/remove metrics
To:
- CDI-based automatic discovery
- Declarative configuration via annotations
- No code changes needed to add/remove metrics
All call sites have been updated to use CDI:
TelemetryResource: UsesCDIUtils.getBeanThrows(MetricStatsCollector.class)MetricsStatsJob: UsesCDIUtils.getBeanThrows(MetricStatsCollector.class)UsageResource: UsesCDIUtils.getBeanThrows(DashboardMetricsProvider.class)
No static method wrappers are needed - all call sites have been migrated to CDI.
com.dotcms.telemetry/
├── MetricType.java # Base interface
├── Metric.java # Metric metadata
├── MetricValue.java # Metric value wrapper
├── MetricsSnapshot.java # Collection of metrics
├── DashboardMetric.java # Dashboard marker annotation
├── MetricCategory.java # Metric categories enum
├── MetricFeature.java # Feature enums
├── collectors/
│ ├── MetricStatsCollector.java # Main collector (CDI)
│ ├── DashboardMetricsProvider.java # Dashboard provider (CDI)
│ ├── DBMetricType.java # Database metric base class
│ ├── ai/ # AI metrics
│ ├── container/ # Container metrics
│ ├── content/ # Content metrics
│ ├── experiment/ # Experiment metrics
│ ├── language/ # Language metrics
│ ├── site/ # Site metrics
│ ├── template/ # Template metrics
│ ├── theme/ # Theme metrics
│ ├── user/ # User metrics
│ └── workflow/ # Workflow metrics
├── rest/
│ └── TelemetryResource.java # Telemetry REST endpoint
└── job/
└── MetricsStatsJob.java # Scheduled metric collection job
The telemetry system includes a configuration-driven caching layer to improve performance for dashboard loading while maintaining data freshness.
Configuration-Driven (Not Annotation-Based)
The caching system uses properties instead of annotations for several key reasons:
- Runtime Configurable: Change caching behavior without code changes or redeployment
- Environment-Specific: Different cache settings per environment (dev/staging/prod)
- Future-Proof: Works for both code-based metrics and future configuration-based metrics
- Separation of Concerns: Metrics compute values; cache manager handles caching
CDI-managed service that provides transparent caching for metrics using DynamicTTLCache (Caffeine-based).
Key Features:
- Configuration-driven: All caching controlled via properties
- Per-metric TTL: Different cache durations per metric
- Works by metric name: Supports future config-based metrics
- Transparent to metrics: Metrics don't know about caching
Usage:
@ApplicationScoped
public class MetricStatsCollector {
@Inject
private MetricCacheManager cacheManager;
private Optional<MetricValue> getMetricValue(MetricType metricType) {
return cacheManager.get(
metricType.getName(),
() -> computeMetricValue(metricType)
);
}
}Reads caching configuration from properties and provides cache settings to MetricCacheManager.
Global settings:
# Enable/disable caching globally
telemetry.cache.enabled=true
# Default TTL for all cached metrics (seconds)
telemetry.cache.default.ttl.seconds=300
# Maximum cache size (number of entries)
telemetry.cache.max.size=1000Per-metric overrides:
# Override caching for specific metrics
telemetry.cache.metric.COUNT_OF_SITES.enabled=true
telemetry.cache.metric.COUNT_OF_SITES.ttl.seconds=600
# Disable caching for specific metrics
telemetry.cache.metric.COUNT_OF_USERS.enabled=false- Cache Check:
MetricCacheManagerchecks if caching is enabled for the metric - Cache Hit: If cached value exists and not expired, returns immediately
- Cache Miss: Computes value using supplier, caches with configured TTL
- No Cache: If caching disabled, always computes fresh value
See MetricCacheManager.java:87-114 for implementation.
Uses DynamicTTLCache (Caffeine-based) instead of DotCache:
- ✅ Simpler: No need to register cache regions in enum
- ✅ Per-key TTL: Different expiration times per metric
- ✅ High Performance: Caffeine is optimized for in-memory caching
- ✅ Sufficient: Metrics are database-sourced (same value on all instances)
Dashboard Performance (MINIMAL Profile):
- Target: < 5 seconds total load time
- Strategy: Annotate only 10-15 core metrics with
@MetricsProfile(ProfileType.MINIMAL) - Enable caching for slow metrics (>500ms execution time)
- Set appropriate TTL based on data freshness requirements
Example slow metric configuration:
# Cache expensive query for 10 minutes
telemetry.cache.metric.COUNT_OF_EXPERIMENTS.enabled=true
telemetry.cache.metric.COUNT_OF_EXPERIMENTS.ttl.seconds=600- Use appropriate base classes: Extend
DBMetricTypewhen applicable. For request counting metrics, use Micrometer Counters instead (seecom.dotcms.metrics.binders.HttpRequestMetrics) - Provide clear descriptions: Help users understand what the metric measures
- Choose correct category/feature: Ensures proper classification
- Handle errors gracefully: Return
Optional.empty()on failure rather than throwing
- Use meaningful categories: Group related metrics together
- Set appropriate priorities: Lower numbers for more important metrics
- Keep dashboard focused: Only annotate metrics relevant to dashboard users
- Document categories: Use consistent category names across metrics
- Always annotate concrete classes:
@ApplicationScopedis required for discovery - Don't annotate abstract classes: They're not instantiated by CDI
- Use
Instance<T>for collections: Allows iteration over all beans of a type - Handle empty collections: CDI injects empty
Instanceif no beans found (never null)
- JavaDoc: All classes have comprehensive JavaDoc with usage examples
- REST API Patterns: See
docs/backend/REST_API_PATTERNS.mdfor API design guidelines
All metrics are defined in the codebase under com.dotcms.telemetry.collectors.*. To discover available metrics:
- Programmatically: Use
MetricStatsCollectororDashboardMetricsProviderto get all discovered metrics - In Code: Browse the
com.dotcms.telemetry.collectorspackage structure - At Runtime: Query the
/api/v1/telemetry/statsendpoint to see all collected metrics
The code is the source of truth for metric definitions. Each MetricType implementation provides:
getName(): The metric identifiergetDescription(): What the metric measuresgetFeature(): The feature categorygetCategory(): The metric category
Last Updated: December 2024 Issues:
- #33979 - Refactor telemetry API to use CDI dependency injection
- #33980 - Design and implement flexible caching and metric profiling
- #33986 - Implement performance timing infrastructure and timeout handling
- #33987 - Implement minimal profile with core metrics for fast dashboard loading