Skip to content

Commit 97a94df

Browse files
netomijanbro
andauthored
Add download metrics for Prometheus (#1607)
Co-authored-by: Alejandro Munoz <[email protected]>
1 parent 64720cc commit 97a94df

File tree

7 files changed

+159
-5
lines changed

7 files changed

+159
-5
lines changed
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/********************************************************************************
2+
* Copyright (c) 2026 Contributors to the Eclipse Foundation
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information regarding copyright ownership.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* https://www.eclipse.org/legal/epl-2.0
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
********************************************************************************/
13+
package org.eclipse.openvsx.metrics;
14+
15+
import io.micrometer.core.instrument.Counter;
16+
import io.micrometer.core.instrument.MeterRegistry;
17+
import io.micrometer.core.instrument.Tags;
18+
import org.eclipse.openvsx.entities.Extension;
19+
import org.eclipse.openvsx.entities.ExtensionVersion;
20+
import org.eclipse.openvsx.entities.FileResource;
21+
import org.slf4j.Logger;
22+
import org.slf4j.LoggerFactory;
23+
import org.springframework.stereotype.Service;
24+
25+
/**
26+
* Service for recording extension download metrics to Prometheus.
27+
*
28+
* Metrics recorded:
29+
* - openvsx_extension_downloads_total: Counter with namespace, extension tags
30+
* - openvsx_namespace_downloads_total: Counter with namespace tag
31+
*/
32+
@Service
33+
public class ExtensionDownloadMetrics {
34+
35+
private static final Logger logger = LoggerFactory.getLogger(ExtensionDownloadMetrics.class);
36+
37+
private static final String EXTENSION_DOWNLOADS_METRIC = "openvsx_extension_downloads_total";
38+
private static final String NAMESPACE_DOWNLOADS_METRIC = "openvsx_namespace_downloads_total";
39+
40+
private final MeterRegistry meterRegistry;
41+
42+
public ExtensionDownloadMetrics(MeterRegistry meterRegistry) {
43+
this.meterRegistry = meterRegistry;
44+
}
45+
46+
/**
47+
* Records a download for the given file resource.
48+
* Only records metrics for DOWNLOAD type resources (VSIX files).
49+
*/
50+
public void recordDownload(FileResource resource) {
51+
if (resource == null) {
52+
logger.debug("Skipping metrics: null resource");
53+
return;
54+
}
55+
56+
// Only track actual extension downloads (VSIX files)
57+
if (!FileResource.DOWNLOAD.equals(resource.getType())) {
58+
logger.debug("Skipping metrics: resource type is {}, not DOWNLOAD", resource.getType());
59+
return;
60+
}
61+
62+
ExtensionVersion extVersion = resource.getExtension();
63+
if (extVersion == null) {
64+
logger.warn("Skipping metrics: resource has no extension version");
65+
return;
66+
}
67+
68+
Extension extension = extVersion.getExtension();
69+
if (extension == null || extension.getNamespace() == null) {
70+
logger.warn("Skipping metrics: extension or namespace is null");
71+
return;
72+
}
73+
74+
String namespace = extension.getNamespace().getName();
75+
String extensionName = extension.getName();
76+
77+
// Record extension-level download
78+
recordExtensionDownload(namespace, extensionName);
79+
80+
// Record namespace-level download for efficient namespace aggregation
81+
recordNamespaceDownload(namespace);
82+
83+
logger.debug("Recorded download metrics for {}.{}", namespace, extensionName);
84+
}
85+
86+
/**
87+
* Records extension-level download metric.
88+
* Tags: namespace, extension
89+
*/
90+
private void recordExtensionDownload(String namespace, String extensionName) {
91+
try {
92+
Counter.builder(EXTENSION_DOWNLOADS_METRIC)
93+
.description("Total extension downloads by namespace and extension")
94+
.tags(Tags.of(
95+
"namespace", sanitizeLabel(namespace),
96+
"extension", sanitizeLabel(extensionName)
97+
))
98+
.register(meterRegistry)
99+
.increment();
100+
} catch (Exception e) {
101+
logger.error("Failed to record extension download metric: {}", e.getMessage());
102+
}
103+
}
104+
105+
/**
106+
* Records namespace-level download metric.
107+
* Separate metric for efficient namespace-only queries.
108+
*/
109+
private void recordNamespaceDownload(String namespace) {
110+
try {
111+
Counter.builder(NAMESPACE_DOWNLOADS_METRIC)
112+
.description("Total downloads by namespace")
113+
.tag("namespace", sanitizeLabel(namespace))
114+
.register(meterRegistry)
115+
.increment();
116+
} catch (Exception e) {
117+
logger.error("Failed to record namespace download metric: {}", e.getMessage());
118+
}
119+
}
120+
121+
/**
122+
* Sanitizes label values for Prometheus compatibility.
123+
* Prometheus labels should not contain special characters.
124+
*/
125+
private String sanitizeLabel(String value) {
126+
if (value == null || value.isEmpty()) {
127+
return "unknown";
128+
}
129+
// Replace any problematic characters with underscores
130+
// Keep alphanumeric, dash, underscore, and dot
131+
return value.replaceAll("[^a-zA-Z0-9._-]", "_");
132+
}
133+
}

server/src/main/java/org/eclipse/openvsx/storage/StorageUtilService.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import org.eclipse.openvsx.entities.ExtensionVersion;
2020
import org.eclipse.openvsx.entities.FileResource;
2121
import org.eclipse.openvsx.entities.Namespace;
22+
import org.eclipse.openvsx.metrics.ExtensionDownloadMetrics;
2223
import org.eclipse.openvsx.repositories.RepositoryService;
2324
import org.eclipse.openvsx.search.SearchUtilService;
2425
import org.eclipse.openvsx.storage.log.DownloadCountService;
@@ -54,6 +55,7 @@ public class StorageUtilService implements IStorageService {
5455
private final LocalStorageService localStorage;
5556
private final AwsStorageService awsStorage;
5657
private final DownloadCountService downloadCountService;
58+
private final ExtensionDownloadMetrics downloadMetrics;
5759
private final SearchUtilService search;
5860
private final CacheService cache;
5961
private final EntityManager entityManager;
@@ -75,6 +77,7 @@ public StorageUtilService(
7577
LocalStorageService localStorage,
7678
AwsStorageService awsStorage,
7779
DownloadCountService downloadCountService,
80+
ExtensionDownloadMetrics downloadMetrics,
7881
SearchUtilService search,
7982
CacheService cache,
8083
EntityManager entityManager,
@@ -87,6 +90,7 @@ public StorageUtilService(
8790
this.localStorage = localStorage;
8891
this.awsStorage = awsStorage;
8992
this.downloadCountService = downloadCountService;
93+
this.downloadMetrics = downloadMetrics;
9094
this.search = search;
9195
this.cache = cache;
9296
this.entityManager = entityManager;
@@ -278,6 +282,8 @@ public Map<Long, Map<String, String>> getFileUrls(Collection<ExtensionVersion> e
278282

279283
@Transactional
280284
public void increaseDownloadCount(FileResource resource) {
285+
downloadMetrics.recordDownload(resource);
286+
281287
if(downloadCountService.isEnabled(resource)) {
282288
// don't count downloads twice
283289
return;

server/src/test/java/org/eclipse/openvsx/RegistryAPITest.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import org.eclipse.openvsx.security.OAuth2UserServices;
3535
import org.eclipse.openvsx.security.SecurityConfig;
3636
import org.eclipse.openvsx.storage.*;
37+
import org.eclipse.openvsx.metrics.ExtensionDownloadMetrics;
3738
import org.eclipse.openvsx.storage.log.DownloadCountService;
3839
import org.eclipse.openvsx.util.TargetPlatform;
3940
import org.eclipse.openvsx.util.VersionAlias;
@@ -87,7 +88,7 @@
8788
@AutoConfigureWebClient
8889
@MockitoBean(types = {
8990
ClientRegistrationRepository.class, UpstreamRegistryService.class, GoogleCloudStorageService.class,
90-
AzureBlobStorageService.class, AwsStorageService.class, VSCodeIdService.class, DownloadCountService.class,
91+
AzureBlobStorageService.class, AwsStorageService.class, VSCodeIdService.class, DownloadCountService.class, ExtensionDownloadMetrics.class,
9192
CacheService.class, EclipseService.class, PublishExtensionVersionService.class, SimpleMeterRegistry.class,
9293
JobRequestScheduler.class, ExtensionControlService.class, FileCacheDurationConfig.class, CdnServiceConfig.class,
9394
ExtensionScanPersistenceService.class
@@ -2637,6 +2638,7 @@ StorageUtilService storageUtilService(
26372638
LocalStorageService localStorage,
26382639
AwsStorageService awsStorage,
26392640
DownloadCountService downloadCountService,
2641+
ExtensionDownloadMetrics downloadMetrics,
26402642
SearchUtilService search,
26412643
CacheService cache,
26422644
EntityManager entityManager,
@@ -2650,6 +2652,7 @@ StorageUtilService storageUtilService(
26502652
localStorage,
26512653
awsStorage,
26522654
downloadCountService,
2655+
downloadMetrics,
26532656
search,
26542657
cache,
26552658
entityManager,

server/src/test/java/org/eclipse/openvsx/adapter/VSCodeAPITest.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.eclipse.openvsx.security.OAuth2UserServices;
3030
import org.eclipse.openvsx.security.SecurityConfig;
3131
import org.eclipse.openvsx.storage.*;
32+
import org.eclipse.openvsx.metrics.ExtensionDownloadMetrics;
3233
import org.eclipse.openvsx.storage.log.DownloadCountService;
3334
import org.eclipse.openvsx.util.TargetPlatform;
3435
import org.eclipse.openvsx.util.VersionService;
@@ -72,7 +73,7 @@
7273
@AutoConfigureWebClient
7374
@MockitoBean( types = {
7475
ClientRegistrationRepository.class, GoogleCloudStorageService.class, AzureBlobStorageService.class,
75-
AwsStorageService.class, DownloadCountService.class, CacheService.class, UpstreamVSCodeService.class,
76+
AwsStorageService.class, DownloadCountService.class, ExtensionDownloadMetrics.class, CacheService.class, UpstreamVSCodeService.class,
7677
VSCodeIdService.class, EclipseService.class, ExtensionValidator.class, SimpleMeterRegistry.class,
7778
FileCacheDurationConfig.class, CdnServiceConfig.class
7879
})
@@ -1094,6 +1095,7 @@ StorageUtilService storageUtilService(
10941095
LocalStorageService localStorage,
10951096
AwsStorageService awsStorage,
10961097
DownloadCountService downloadCountService,
1098+
ExtensionDownloadMetrics downloadMetrics,
10971099
SearchUtilService search,
10981100
CacheService cache,
10991101
EntityManager entityManager,
@@ -1107,6 +1109,7 @@ StorageUtilService storageUtilService(
11071109
localStorage,
11081110
awsStorage,
11091111
downloadCountService,
1112+
downloadMetrics,
11101113
search,
11111114
cache,
11121115
entityManager,

server/src/test/java/org/eclipse/openvsx/admin/AdminAPITest.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import org.eclipse.openvsx.security.OAuth2UserServices;
3636
import org.eclipse.openvsx.security.SecurityConfig;
3737
import org.eclipse.openvsx.storage.*;
38+
import org.eclipse.openvsx.metrics.ExtensionDownloadMetrics;
3839
import org.eclipse.openvsx.storage.log.DownloadCountService;
3940
import org.eclipse.openvsx.util.TargetPlatform;
4041
import org.eclipse.openvsx.util.VersionService;
@@ -74,7 +75,7 @@
7475
@AutoConfigureWebClient
7576
@MockitoBean(types = {
7677
ClientRegistrationRepository.class, UpstreamRegistryService.class, GoogleCloudStorageService.class,
77-
AzureBlobStorageService.class, AwsStorageService.class, VSCodeIdService.class, DownloadCountService.class,
78+
AzureBlobStorageService.class, AwsStorageService.class, VSCodeIdService.class, DownloadCountService.class, ExtensionDownloadMetrics.class,
7879
CacheService.class, PublishExtensionVersionHandler.class, SearchUtilService.class, EclipseService.class,
7980
SimpleMeterRegistry.class, FileCacheDurationConfig.class, MailService.class, CdnServiceConfig.class,
8081
ExtensionScanService.class, ExtensionScanPersistenceService.class
@@ -1590,6 +1591,7 @@ StorageUtilService storageUtilService(
15901591
LocalStorageService localStorage,
15911592
AwsStorageService awsStorage,
15921593
DownloadCountService downloadCountService,
1594+
ExtensionDownloadMetrics downloadMetrics,
15931595
SearchUtilService search,
15941596
CacheService cache,
15951597
EntityManager entityManager,
@@ -1603,6 +1605,7 @@ StorageUtilService storageUtilService(
16031605
localStorage,
16041606
awsStorage,
16051607
downloadCountService,
1608+
downloadMetrics,
16061609
search,
16071610
cache,
16081611
entityManager,

server/src/test/java/org/eclipse/openvsx/eclipse/EclipseServiceTest.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.eclipse.openvsx.scanning.ExtensionScanService;
2727
import org.eclipse.openvsx.search.SearchUtilService;
2828
import org.eclipse.openvsx.storage.*;
29+
import org.eclipse.openvsx.metrics.ExtensionDownloadMetrics;
2930
import org.eclipse.openvsx.storage.log.DownloadCountService;
3031
import org.eclipse.openvsx.util.ErrorResultException;
3132
import org.eclipse.openvsx.util.TargetPlatform;
@@ -59,7 +60,7 @@
5960
@ExtendWith(SpringExtension.class)
6061
@MockitoBean(types = {
6162
EntityManager.class, SearchUtilService.class, GoogleCloudStorageService.class, AzureBlobStorageService.class,
62-
AwsStorageService.class, VSCodeIdService.class, DownloadCountService.class, CacheService.class,
63+
AwsStorageService.class, VSCodeIdService.class, DownloadCountService.class, ExtensionDownloadMetrics.class, CacheService.class,
6364
UserService.class, PublishExtensionVersionHandler.class, SimpleMeterRegistry.class, FileCacheDurationConfig.class,
6465
JobRequestScheduler.class, CdnServiceConfig.class, ExtensionScanService.class, ExtensionScanPersistenceService.class
6566
})
@@ -440,6 +441,7 @@ StorageUtilService storageUtilService(
440441
LocalStorageService localStorage,
441442
AwsStorageService awsStorage,
442443
DownloadCountService downloadCountService,
444+
ExtensionDownloadMetrics downloadMetrics,
443445
SearchUtilService search,
444446
CacheService cache,
445447
EntityManager entityManager,
@@ -453,6 +455,7 @@ StorageUtilService storageUtilService(
453455
localStorage,
454456
awsStorage,
455457
downloadCountService,
458+
downloadMetrics,
456459
search,
457460
cache,
458461
entityManager,

server/src/test/java/org/eclipse/openvsx/storage/StorageUtilServiceTest.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import org.eclipse.openvsx.entities.*;
1717
import org.eclipse.openvsx.repositories.RepositoryService;
1818
import org.eclipse.openvsx.search.SearchUtilService;
19+
import org.eclipse.openvsx.metrics.ExtensionDownloadMetrics;
1920
import org.eclipse.openvsx.storage.log.DownloadCountService;
2021
import org.junit.jupiter.api.Test;
2122
import org.junit.jupiter.api.extension.ExtendWith;
@@ -37,7 +38,7 @@
3738
@ExtendWith(SpringExtension.class)
3839
@MockitoBean(types = {
3940
EntityManager.class, SearchUtilService.class, GoogleCloudStorageService.class,
40-
DownloadCountService.class, CacheService.class, UserService.class, FileCacheDurationConfig.class,
41+
DownloadCountService.class, ExtensionDownloadMetrics.class, CacheService.class, UserService.class, FileCacheDurationConfig.class,
4142
FilesCacheKeyGenerator.class, RepositoryService.class, LocalStorageService.class
4243
})
4344
@ContextConfiguration(classes = StorageUtilServiceTest.TestConfig.class)
@@ -173,6 +174,7 @@ StorageUtilService storageUtilService(
173174
LocalStorageService localStorage,
174175
AwsStorageService awsStorage,
175176
DownloadCountService downloadCountService,
177+
ExtensionDownloadMetrics downloadMetrics,
176178
SearchUtilService search,
177179
CacheService cache,
178180
EntityManager entityManager,
@@ -186,6 +188,7 @@ StorageUtilService storageUtilService(
186188
localStorage,
187189
awsStorage,
188190
downloadCountService,
191+
downloadMetrics,
189192
search,
190193
cache,
191194
entityManager,

0 commit comments

Comments
 (0)