mirror of
https://github.com/OutlineFoundation/outline-server.git
synced 2026-05-13 13:58:57 +00:00
feat(server): add ASN metric to opt-in server usage report (#1610)
* feat(server): add ASN metric to opt-in server usage report * Rename `LocationUsage` to `ReportedUsage`. * Add test cases for different ASN+country combinations.
This commit is contained in:
parent
91046e30d2
commit
845c02373b
2 changed files with 117 additions and 25 deletions
|
|
@ -20,7 +20,7 @@ import {AccessKeyConfigJson} from './server_access_key';
|
|||
|
||||
import {ServerConfigJson} from './server_config';
|
||||
import {
|
||||
CountryUsage,
|
||||
ReportedUsage,
|
||||
DailyFeatureMetricsReportJson,
|
||||
HourlyServerMetricsReportJson,
|
||||
MetricsCollectorClient,
|
||||
|
|
@ -78,7 +78,7 @@ describe('OutlineSharedMetricsPublisher', () => {
|
|||
);
|
||||
|
||||
publisher.startSharing();
|
||||
usageMetrics.countryUsage = [
|
||||
usageMetrics.reportedUsage = [
|
||||
{country: 'AA', inboundBytes: 11},
|
||||
{country: 'BB', inboundBytes: 11},
|
||||
{country: 'CC', inboundBytes: 22},
|
||||
|
|
@ -102,7 +102,7 @@ describe('OutlineSharedMetricsPublisher', () => {
|
|||
});
|
||||
|
||||
startTime = clock.nowMs;
|
||||
usageMetrics.countryUsage = [
|
||||
usageMetrics.reportedUsage = [
|
||||
{country: 'EE', inboundBytes: 44},
|
||||
{country: 'FF', inboundBytes: 55},
|
||||
];
|
||||
|
|
@ -121,6 +121,91 @@ describe('OutlineSharedMetricsPublisher', () => {
|
|||
|
||||
publisher.stopSharing();
|
||||
});
|
||||
|
||||
it('reports ASN metrics correctly', async () => {
|
||||
const clock = new ManualClock();
|
||||
const serverConfig = new InMemoryConfig<ServerConfigJson>({serverId: 'server-id'});
|
||||
const usageMetrics = new ManualUsageMetrics();
|
||||
const metricsCollector = new FakeMetricsCollector();
|
||||
const publisher = new OutlineSharedMetricsPublisher(
|
||||
clock,
|
||||
serverConfig,
|
||||
null,
|
||||
usageMetrics,
|
||||
metricsCollector
|
||||
);
|
||||
publisher.startSharing();
|
||||
|
||||
usageMetrics.reportedUsage = [
|
||||
{country: 'DD', asn: 999, inboundBytes: 44},
|
||||
{country: 'EE', inboundBytes: 55},
|
||||
];
|
||||
clock.nowMs += 60 * 60 * 1000;
|
||||
await clock.runCallbacks();
|
||||
|
||||
expect(metricsCollector.collectedServerUsageReport.userReports).toEqual([
|
||||
{bytesTransferred: 44, countries: ['DD'], asn: 999},
|
||||
{bytesTransferred: 55, countries: ['EE']},
|
||||
]);
|
||||
publisher.stopSharing();
|
||||
});
|
||||
|
||||
it('reports different ASNs in the same country correctly', async () => {
|
||||
const clock = new ManualClock();
|
||||
const serverConfig = new InMemoryConfig<ServerConfigJson>({serverId: 'server-id'});
|
||||
const usageMetrics = new ManualUsageMetrics();
|
||||
const metricsCollector = new FakeMetricsCollector();
|
||||
const publisher = new OutlineSharedMetricsPublisher(
|
||||
clock,
|
||||
serverConfig,
|
||||
null,
|
||||
usageMetrics,
|
||||
metricsCollector
|
||||
);
|
||||
publisher.startSharing();
|
||||
|
||||
usageMetrics.reportedUsage = [
|
||||
{country: 'DD', asn: 999, inboundBytes: 44},
|
||||
{country: 'DD', asn: 888, inboundBytes: 55},
|
||||
];
|
||||
clock.nowMs += 60 * 60 * 1000;
|
||||
await clock.runCallbacks();
|
||||
|
||||
expect(metricsCollector.collectedServerUsageReport.userReports).toEqual([
|
||||
{bytesTransferred: 44, countries: ['DD'], asn: 999},
|
||||
{bytesTransferred: 55, countries: ['DD'], asn: 888},
|
||||
]);
|
||||
publisher.stopSharing();
|
||||
});
|
||||
|
||||
it('reports the same ASNs across different countries correctly', async () => {
|
||||
const clock = new ManualClock();
|
||||
const serverConfig = new InMemoryConfig<ServerConfigJson>({serverId: 'server-id'});
|
||||
const usageMetrics = new ManualUsageMetrics();
|
||||
const metricsCollector = new FakeMetricsCollector();
|
||||
const publisher = new OutlineSharedMetricsPublisher(
|
||||
clock,
|
||||
serverConfig,
|
||||
null,
|
||||
usageMetrics,
|
||||
metricsCollector
|
||||
);
|
||||
publisher.startSharing();
|
||||
|
||||
usageMetrics.reportedUsage = [
|
||||
{country: 'DD', asn: 999, inboundBytes: 44},
|
||||
{country: 'EE', asn: 999, inboundBytes: 66},
|
||||
];
|
||||
clock.nowMs += 60 * 60 * 1000;
|
||||
await clock.runCallbacks();
|
||||
|
||||
expect(metricsCollector.collectedServerUsageReport.userReports).toEqual([
|
||||
{bytesTransferred: 44, countries: ['DD'], asn: 999},
|
||||
{bytesTransferred: 66, countries: ['EE'], asn: 999},
|
||||
]);
|
||||
publisher.stopSharing();
|
||||
});
|
||||
|
||||
it('ignores sanctioned countries', async () => {
|
||||
const clock = new ManualClock();
|
||||
const startTime = clock.nowMs;
|
||||
|
|
@ -136,7 +221,7 @@ describe('OutlineSharedMetricsPublisher', () => {
|
|||
);
|
||||
|
||||
publisher.startSharing();
|
||||
usageMetrics.countryUsage = [
|
||||
usageMetrics.reportedUsage = [
|
||||
{country: 'AA', inboundBytes: 11},
|
||||
{country: 'SY', inboundBytes: 11},
|
||||
{country: 'CC', inboundBytes: 22},
|
||||
|
|
@ -257,13 +342,13 @@ class FakeMetricsCollector implements MetricsCollectorClient {
|
|||
}
|
||||
|
||||
class ManualUsageMetrics implements UsageMetrics {
|
||||
countryUsage = [] as CountryUsage[];
|
||||
reportedUsage = [] as ReportedUsage[];
|
||||
|
||||
getCountryUsage(): Promise<CountryUsage[]> {
|
||||
return Promise.resolve(this.countryUsage);
|
||||
getReportedUsage(): Promise<ReportedUsage[]> {
|
||||
return Promise.resolve(this.reportedUsage);
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.countryUsage = [] as CountryUsage[];
|
||||
this.reportedUsage = [] as ReportedUsage[];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,8 +26,9 @@ const MS_PER_HOUR = 60 * 60 * 1000;
|
|||
const MS_PER_DAY = 24 * MS_PER_HOUR;
|
||||
const SANCTIONED_COUNTRIES = new Set(['CU', 'KP', 'SY']);
|
||||
|
||||
export interface CountryUsage {
|
||||
export interface ReportedUsage {
|
||||
country: string;
|
||||
asn?: number;
|
||||
inboundBytes: number;
|
||||
}
|
||||
|
||||
|
|
@ -44,6 +45,7 @@ export interface HourlyServerMetricsReportJson {
|
|||
// Field renames will break backwards-compatibility.
|
||||
export interface HourlyUserMetricsReportJson {
|
||||
countries: string[];
|
||||
asn?: number;
|
||||
bytesTransferred: number;
|
||||
}
|
||||
|
||||
|
|
@ -70,7 +72,7 @@ export interface SharedMetricsPublisher {
|
|||
}
|
||||
|
||||
export interface UsageMetrics {
|
||||
getCountryUsage(): Promise<CountryUsage[]>;
|
||||
getReportedUsage(): Promise<ReportedUsage[]>;
|
||||
reset();
|
||||
}
|
||||
|
||||
|
|
@ -80,17 +82,18 @@ export class PrometheusUsageMetrics implements UsageMetrics {
|
|||
|
||||
constructor(private prometheusClient: PrometheusClient) {}
|
||||
|
||||
async getCountryUsage(): Promise<CountryUsage[]> {
|
||||
async getReportedUsage(): Promise<ReportedUsage[]> {
|
||||
const timeDeltaSecs = Math.round((Date.now() - this.resetTimeMs) / 1000);
|
||||
// We measure the traffic to and from the target, since that's what we are protecting.
|
||||
const result = await this.prometheusClient.query(
|
||||
`sum(increase(shadowsocks_data_bytes_per_location{dir=~"p>t|p<t"}[${timeDeltaSecs}s])) by (location)`
|
||||
`sum(increase(shadowsocks_data_bytes_per_location{dir=~"p>t|p<t"}[${timeDeltaSecs}s])) by (location, asn)`
|
||||
);
|
||||
const usage = [] as CountryUsage[];
|
||||
const usage = [] as ReportedUsage[];
|
||||
for (const entry of result.result) {
|
||||
const country = entry.metric['location'] || '';
|
||||
const asn = entry.metric['asn'] ? Number(entry.metric['asn']) : undefined;
|
||||
const inboundBytes = Math.round(parseFloat(entry.value[1]));
|
||||
usage.push({country, inboundBytes});
|
||||
usage.push({country, inboundBytes, asn});
|
||||
}
|
||||
return usage;
|
||||
}
|
||||
|
|
@ -105,7 +108,7 @@ export interface MetricsCollectorClient {
|
|||
collectFeatureMetrics(reportJson: DailyFeatureMetricsReportJson): Promise<void>;
|
||||
}
|
||||
|
||||
export class RestMetricsCollectorClient {
|
||||
export class RestMetricsCollectorClient implements MetricsCollectorClient {
|
||||
constructor(private serviceUrl: string) {}
|
||||
|
||||
collectServerUsageMetrics(reportJson: HourlyServerMetricsReportJson): Promise<void> {
|
||||
|
|
@ -163,7 +166,7 @@ export class OutlineSharedMetricsPublisher implements SharedMetricsPublisher {
|
|||
return;
|
||||
}
|
||||
try {
|
||||
await this.reportServerUsageMetrics(await usageMetrics.getCountryUsage());
|
||||
await this.reportServerUsageMetrics(await usageMetrics.getReportedUsage());
|
||||
usageMetrics.reset();
|
||||
} catch (err) {
|
||||
logging.error(`Failed to report server usage metrics: ${err}`);
|
||||
|
|
@ -197,24 +200,28 @@ export class OutlineSharedMetricsPublisher implements SharedMetricsPublisher {
|
|||
return this.serverConfig.data().metricsEnabled || false;
|
||||
}
|
||||
|
||||
private async reportServerUsageMetrics(countryUsageMetrics: CountryUsage[]): Promise<void> {
|
||||
private async reportServerUsageMetrics(locationUsageMetrics: ReportedUsage[]): Promise<void> {
|
||||
const reportEndTimestampMs = this.clock.now();
|
||||
|
||||
const userReports = [] as HourlyUserMetricsReportJson[];
|
||||
for (const countryUsage of countryUsageMetrics) {
|
||||
if (countryUsage.inboundBytes === 0) {
|
||||
const userReports: HourlyUserMetricsReportJson[] = [];
|
||||
for (const locationUsage of locationUsageMetrics) {
|
||||
if (locationUsage.inboundBytes === 0) {
|
||||
continue;
|
||||
}
|
||||
if (isSanctionedCountry(countryUsage.country)) {
|
||||
if (isSanctionedCountry(locationUsage.country)) {
|
||||
continue;
|
||||
}
|
||||
// Make sure to always set a country, which is required by the metrics server validation.
|
||||
// It's used to differentiate the row from the legacy key usage rows.
|
||||
const country = countryUsage.country || 'ZZ';
|
||||
userReports.push({
|
||||
bytesTransferred: countryUsage.inboundBytes,
|
||||
const country = locationUsage.country || 'ZZ';
|
||||
const report: HourlyUserMetricsReportJson = {
|
||||
bytesTransferred: locationUsage.inboundBytes,
|
||||
countries: [country],
|
||||
});
|
||||
};
|
||||
if (locationUsage.asn) {
|
||||
report.asn = locationUsage.asn;
|
||||
}
|
||||
userReports.push(report);
|
||||
}
|
||||
const report = {
|
||||
serverId: this.serverConfig.data().serverId,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue