Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(server): add ASN metric to opt-in server usage report #1550

Open
wants to merge 3 commits into
base: sbruens/refactor-tests
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
35 changes: 25 additions & 10 deletions src/shadowbox/server/shared_metrics.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {AccessKeyConfigJson} from './server_access_key';

import {ServerConfigJson} from './server_config';
import {
CountryUsage,
LocationUsage,
DailyFeatureMetricsReportJson,
HourlyServerMetricsReportJson,
MetricsCollectorClient,
Expand Down Expand Up @@ -89,7 +89,7 @@ describe('OutlineSharedMetricsPublisher', () => {

describe('for server usage', () => {
it('is sending correct reports', async () => {
usageMetrics.countryUsage = [
usageMetrics.locationUsage = [
{country: 'AA', inboundBytes: 11},
{country: 'BB', inboundBytes: 11},
{country: 'CC', inboundBytes: 22},
Expand All @@ -114,16 +114,31 @@ describe('OutlineSharedMetricsPublisher', () => {
});
});

it('sends ASN data if present', async () => {
usageMetrics.locationUsage = [
{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']},
]);
});

it('resets metrics to avoid double reporting', async () => {
usageMetrics.countryUsage = [
usageMetrics.locationUsage = [
{country: 'AA', inboundBytes: 11},
{country: 'BB', inboundBytes: 11},
];
clock.nowMs += 60 * 60 * 1000;
startTime = clock.nowMs;
await clock.runCallbacks();
usageMetrics.countryUsage = [
...usageMetrics.countryUsage,
usageMetrics.locationUsage = [
...usageMetrics.locationUsage,
{country: 'CC', inboundBytes: 22},
{country: 'DD', inboundBytes: 22},
];
Expand All @@ -138,7 +153,7 @@ describe('OutlineSharedMetricsPublisher', () => {
});

it('ignores sanctioned countries', async () => {
usageMetrics.countryUsage = [
usageMetrics.locationUsage = [
{country: 'AA', inboundBytes: 11},
{country: 'SY', inboundBytes: 11},
{country: 'CC', inboundBytes: 22},
Expand Down Expand Up @@ -222,14 +237,14 @@ class FakeMetricsCollector implements MetricsCollectorClient {
}

class ManualUsageMetrics implements UsageMetrics {
public countryUsage = [] as CountryUsage[];
public locationUsage = [] as LocationUsage[];

getCountryUsage(): Promise<CountryUsage[]> {
return Promise.resolve(this.countryUsage);
getLocationUsage(): Promise<LocationUsage[]> {
return Promise.resolve(this.locationUsage);
}

reset() {
this.countryUsage = [] as CountryUsage[];
this.locationUsage = [] as LocationUsage[];
}
}

Expand Down
39 changes: 23 additions & 16 deletions src/shadowbox/server/shared_metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 LocationUsage {
country: string;
asn?: number;
inboundBytes: number;
}

Expand All @@ -44,6 +45,7 @@ export interface HourlyServerMetricsReportJson {
// Field renames will break backwards-compatibility.
export interface HourlyUserMetricsReportJson {
countries: string[];
asn?: number;
bytesTransferred: number;
}

Expand All @@ -70,7 +72,7 @@ export interface SharedMetricsPublisher {
}

export interface UsageMetrics {
getCountryUsage(): Promise<CountryUsage[]>;
getLocationUsage(): Promise<LocationUsage[]>;
reset();
}

Expand All @@ -80,17 +82,18 @@ export class PrometheusUsageMetrics implements UsageMetrics {

constructor(private prometheusClient: PrometheusClient) {}

async getCountryUsage(): Promise<CountryUsage[]> {
async getLocationUsage(): Promise<LocationUsage[]> {
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 LocationUsage[];
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;
}
Expand Down Expand Up @@ -163,7 +166,7 @@ export class OutlineSharedMetricsPublisher implements SharedMetricsPublisher {
return;
}
try {
await this.reportServerUsageMetrics(await usageMetrics.getCountryUsage());
await this.reportServerUsageMetrics(await usageMetrics.getLocationUsage());
usageMetrics.reset();
} catch (err) {
logging.error(`Failed to report server usage metrics: ${err}`);
Expand Down Expand Up @@ -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: LocationUsage[]): 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,
Expand Down