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 tunnel time metric to opt-in server usage report #1551

Merged
merged 11 commits into from
Oct 18, 2024
2 changes: 1 addition & 1 deletion src/shadowbox/server/mocks/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export class FakePrometheusClient extends PrometheusClient {
const bytesTransferred = this.bytesTransferredById[accessKeyId] || 0;
queryResultData.result.push({
metric: {access_key: accessKeyId},
value: [bytesTransferred, `${bytesTransferred}`],
value: [Date.now() / 1000, `${bytesTransferred}`],
});
}
return queryResultData;
Expand Down
70 changes: 35 additions & 35 deletions src/shadowbox/server/shared_metrics.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,11 @@ describe('OutlineSharedMetricsPublisher', () => {

publisher.startSharing();
usageMetrics.reportedUsage = [
{country: 'AA', inboundBytes: 11},
{country: 'BB', inboundBytes: 11},
{country: 'CC', inboundBytes: 22},
{country: 'AA', inboundBytes: 33},
{country: 'DD', inboundBytes: 33},
{country: 'AA', inboundBytes: 11, tunnelTimeSec: 99},
{country: 'BB', inboundBytes: 11, tunnelTimeSec: 88},
{country: 'CC', inboundBytes: 22, tunnelTimeSec: 77},
{country: 'AA', inboundBytes: 33, tunnelTimeSec: 66},
{country: 'DD', inboundBytes: 33, tunnelTimeSec: 55},
];

clock.nowMs += 60 * 60 * 1000;
Expand All @@ -93,18 +93,18 @@ describe('OutlineSharedMetricsPublisher', () => {
startUtcMs: startTime,
endUtcMs: clock.nowMs,
userReports: [
{bytesTransferred: 11, countries: ['AA']},
{bytesTransferred: 11, countries: ['BB']},
{bytesTransferred: 22, countries: ['CC']},
{bytesTransferred: 33, countries: ['AA']},
{bytesTransferred: 33, countries: ['DD']},
{bytesTransferred: 11, countries: ['AA'], tunnelTimeSec: 99},
{bytesTransferred: 11, countries: ['BB'], tunnelTimeSec: 88},
{bytesTransferred: 22, countries: ['CC'], tunnelTimeSec: 77},
{bytesTransferred: 33, countries: ['AA'], tunnelTimeSec: 66},
{bytesTransferred: 33, countries: ['DD'], tunnelTimeSec: 55},
],
});

startTime = clock.nowMs;
usageMetrics.reportedUsage = [
{country: 'EE', inboundBytes: 44},
{country: 'FF', inboundBytes: 55},
{country: 'EE', inboundBytes: 44, tunnelTimeSec: 11},
{country: 'FF', inboundBytes: 55, tunnelTimeSec: 22},
];

clock.nowMs += 60 * 60 * 1000;
Expand All @@ -114,8 +114,8 @@ describe('OutlineSharedMetricsPublisher', () => {
startUtcMs: startTime,
endUtcMs: clock.nowMs,
userReports: [
{bytesTransferred: 44, countries: ['EE']},
{bytesTransferred: 55, countries: ['FF']},
{bytesTransferred: 44, countries: ['EE'], tunnelTimeSec: 11},
{bytesTransferred: 55, countries: ['FF'], tunnelTimeSec: 22},
],
});

Expand All @@ -137,15 +137,15 @@ describe('OutlineSharedMetricsPublisher', () => {
publisher.startSharing();

usageMetrics.reportedUsage = [
{country: 'DD', asn: 999, inboundBytes: 44},
{country: 'EE', inboundBytes: 55},
{country: 'DD', inboundBytes: 44, tunnelTimeSec: 11, asn: 999},
{country: 'EE', inboundBytes: 55, tunnelTimeSec: 22},
];
clock.nowMs += 60 * 60 * 1000;
await clock.runCallbacks();

expect(metricsCollector.collectedServerUsageReport.userReports).toEqual([
{bytesTransferred: 44, countries: ['DD'], asn: 999},
{bytesTransferred: 55, countries: ['EE']},
{bytesTransferred: 44, tunnelTimeSec: 11, countries: ['DD'], asn: 999},
{bytesTransferred: 55, tunnelTimeSec: 22, countries: ['EE']},
]);
publisher.stopSharing();
});
Expand All @@ -165,15 +165,15 @@ describe('OutlineSharedMetricsPublisher', () => {
publisher.startSharing();

usageMetrics.reportedUsage = [
{country: 'DD', asn: 999, inboundBytes: 44},
{country: 'DD', asn: 888, inboundBytes: 55},
{country: 'DD', asn: 999, tunnelTimeSec: 11, inboundBytes: 44},
{country: 'DD', asn: 888, tunnelTimeSec: 22, 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},
{bytesTransferred: 44, tunnelTimeSec: 11, countries: ['DD'], asn: 999},
{bytesTransferred: 55, tunnelTimeSec: 22, countries: ['DD'], asn: 888},
]);
publisher.stopSharing();
});
Expand All @@ -193,15 +193,15 @@ describe('OutlineSharedMetricsPublisher', () => {
publisher.startSharing();

usageMetrics.reportedUsage = [
{country: 'DD', asn: 999, inboundBytes: 44},
{country: 'EE', asn: 999, inboundBytes: 66},
{country: 'DD', asn: 999, tunnelTimeSec: 11, inboundBytes: 44},
{country: 'EE', asn: 999, tunnelTimeSec: 22, inboundBytes: 55},
];
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},
{bytesTransferred: 44, tunnelTimeSec: 11, countries: ['DD'], asn: 999},
{bytesTransferred: 55, tunnelTimeSec: 22, countries: ['EE'], asn: 999},
]);
publisher.stopSharing();
});
Expand All @@ -222,11 +222,11 @@ describe('OutlineSharedMetricsPublisher', () => {

publisher.startSharing();
usageMetrics.reportedUsage = [
{country: 'AA', inboundBytes: 11},
{country: 'SY', inboundBytes: 11},
{country: 'CC', inboundBytes: 22},
{country: 'AA', inboundBytes: 33},
{country: 'DD', inboundBytes: 33},
{country: 'AA', tunnelTimeSec: 99, inboundBytes: 11},
{country: 'SY', tunnelTimeSec: 88, inboundBytes: 11},
{country: 'CC', tunnelTimeSec: 77, inboundBytes: 22},
{country: 'AA', tunnelTimeSec: 66, inboundBytes: 33},
{country: 'DD', tunnelTimeSec: 55, inboundBytes: 33},
];

clock.nowMs += 60 * 60 * 1000;
Expand All @@ -236,10 +236,10 @@ describe('OutlineSharedMetricsPublisher', () => {
startUtcMs: startTime,
endUtcMs: clock.nowMs,
userReports: [
{bytesTransferred: 11, countries: ['AA']},
{bytesTransferred: 22, countries: ['CC']},
{bytesTransferred: 33, countries: ['AA']},
{bytesTransferred: 33, countries: ['DD']},
{bytesTransferred: 11, tunnelTimeSec: 99, countries: ['AA']},
{bytesTransferred: 22, tunnelTimeSec: 77, countries: ['CC']},
{bytesTransferred: 33, tunnelTimeSec: 66, countries: ['AA']},
{bytesTransferred: 33, tunnelTimeSec: 55, countries: ['DD']},
],
});
publisher.stopSharing();
Expand Down
59 changes: 46 additions & 13 deletions src/shadowbox/server/shared_metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export interface ReportedUsage {
country: string;
asn?: number;
inboundBytes: number;
tunnelTimeSec: number;
}

// JSON format for the published report.
Expand All @@ -47,6 +48,7 @@ export interface HourlyUserMetricsReportJson {
countries: string[];
asn?: number;
bytesTransferred: number;
tunnelTimeSec: number;
}

// JSON format for the feature metrics report.
Expand Down Expand Up @@ -84,18 +86,48 @@ export class PrometheusUsageMetrics implements UsageMetrics {

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, asn)`
);
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, asn});
// Return both data bytes and tunnel time information with a single
// Prometheus query, by using a custom "metric_type" label.
const queryResponse = await this.prometheusClient.query(`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is an ok solution, though I don't consider good practice to mix data of different types. Here we are creating a single time series with different units and meaning. And later split them again. I'd rather keep them as separate time series, as it's a lot clearer.
I also wonder if this approach affects performance somehow.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fortuna Ok let me try something else. How about this?

label_replace(
sum(increase(shadowsocks_data_bytes_per_location{dir=~"p>t|p<t"}[${timeDeltaSecs}s]))
by (location, asn),
"metric_type", "inbound_bytes", "", ""
) or
label_replace(
sum(increase(shadowsocks_tunnel_time_seconds_per_location[${timeDeltaSecs}s]))
by (location, asn),
"metric_type", "tunnel_time", "", ""
)
`);

const usage = new Map<string, ReportedUsage>();
for (const result of queryResponse.result) {
const country = result.metric['location'] || '';
const asn = result.metric['asn'] ? Number(result.metric['asn']) : undefined;

// Get or create an entry for the country+ASN combination.
const key = `${country}-${asn}`;
let entry: ReportedUsage;
if (usage.has(key)) {
entry = usage.get(key);
} else {
entry = {
country,
asn,
inboundBytes: 0,
tunnelTimeSec: 0,
};
}

if (result.metric['metric_type'] === 'inbound_bytes') {
entry.inboundBytes = Math.round(parseFloat(result.value[1]));
} else if (result.metric['metric_type'] === 'tunnel_time') {
entry.tunnelTimeSec = Math.round(parseFloat(result.value[1]));
}
usage.set(key, entry);
}
return usage;
return Array.from(usage.values());
}

reset() {
Expand Down Expand Up @@ -205,7 +237,7 @@ export class OutlineSharedMetricsPublisher implements SharedMetricsPublisher {

const userReports: HourlyUserMetricsReportJson[] = [];
for (const locationUsage of locationUsageMetrics) {
if (locationUsage.inboundBytes === 0) {
if (locationUsage.inboundBytes === 0 && locationUsage.tunnelTimeSec === 0) {
continue;
}
if (isSanctionedCountry(locationUsage.country)) {
Expand All @@ -215,8 +247,9 @@ export class OutlineSharedMetricsPublisher implements SharedMetricsPublisher {
// It's used to differentiate the row from the legacy key usage rows.
const country = locationUsage.country || 'ZZ';
const report: HourlyUserMetricsReportJson = {
bytesTransferred: locationUsage.inboundBytes,
countries: [country],
bytesTransferred: locationUsage.inboundBytes,
tunnelTimeSec: locationUsage.tunnelTimeSec,
};
if (locationUsage.asn) {
report.asn = locationUsage.asn;
Expand Down
Loading