Skip to content

Commit

Permalink
feat(server): unified metrics endpoint (#1616)
Browse files Browse the repository at this point in the history
* draft of unified metrics endpoint

* ok, empty state works

* I think it's there?

* prefix with 'fake'

* some feedback

* continue feedback

* test refactor

* remove unused imports

* bad refactor

* partial feedback

* move time range parsing logic

* full union

* feedback
  • Loading branch information
daniellacosse authored Nov 22, 2024
1 parent f558117 commit 9d9f76b
Show file tree
Hide file tree
Showing 8 changed files with 433 additions and 10 deletions.
8 changes: 6 additions & 2 deletions src/shadowbox/infrastructure/prometheus_scraper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@ interface QueryResult {
error: string;
}

export class PrometheusClient {
export interface PrometheusClient {
query(query: string): Promise<QueryResultData>;
}

export class ApiPrometheusClient implements PrometheusClient {
constructor(private address: string) {}

query(query: string): Promise<QueryResultData> {
Expand Down Expand Up @@ -101,7 +105,7 @@ async function spawnPrometheusSubprocess(
prometheusEndpoint: string
): Promise<child_process.ChildProcess> {
logging.info('======== Starting Prometheus ========');
logging.info(`${binaryFilename} ${processArgs.map(a => `"${a}"`).join(' ')}`);
logging.info(`${binaryFilename} ${processArgs.map((a) => `"${a}"`).join(' ')}`);
const runProcess = child_process.spawn(binaryFilename, processArgs);
runProcess.on('error', (error) => {
logging.error(`Error spawning Prometheus: ${error}`);
Expand Down
55 changes: 54 additions & 1 deletion src/shadowbox/server/api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,60 @@ paths:
responses:
'204':
description: Access key limit deleted successfully.

/experimental/server/metrics:
get:
tags: Server
parameters:
- in: query
name: since
description: the range of time to return data for
schema:
type: string
responses:
'200':
description: Display server metric information
content:
application/json:
schema:
type: object
properties:
server:
type: array
items:
type: object
properties:
location:
type: string
asn:
type: number
asOrg:
type: string
tunnelTime:
type: object
properties:
seconds: number
dataTransferred:
type: object
properties:
bytes: number
accessKeys:
type: array
items:
type: object
properties:
accessKeyId:
type: string
tunnelTime:
type: object
properties:
seconds: number
dataTransferred:
type: object
properties:
bytes: number
examples:
'0':
value: '{"server":[{"location":"US","asn":null,"asOrg":null,"tunnelTime":{"seconds":100},"dataTransferred":{"bytes":100}}],"accessKeys":[{"accessKeyId":"0","tunnelTime":{"seconds":100},"dataTransferred":{"bytes":100}}]}'
/name:
put:
description: Renames the server
Expand Down
4 changes: 2 additions & 2 deletions src/shadowbox/server/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {RealClock} from '../infrastructure/clock';
import {PortProvider} from '../infrastructure/get_port';
import * as json_config from '../infrastructure/json_config';
import * as logging from '../infrastructure/logging';
import {PrometheusClient, startPrometheus} from '../infrastructure/prometheus_scraper';
import {ApiPrometheusClient, startPrometheus} from '../infrastructure/prometheus_scraper';
import {RolloutTracker} from '../infrastructure/rollout';
import * as version from './version';

Expand Down Expand Up @@ -197,7 +197,7 @@ async function main() {
prometheusEndpoint
);

const prometheusClient = new PrometheusClient(prometheusEndpoint);
const prometheusClient = new ApiPrometheusClient(prometheusEndpoint);
if (!serverConfig.data().portForNewAccessKeys) {
serverConfig.data().portForNewAccessKeys = await portProvider.reserveNewPort();
serverConfig.write();
Expand Down
191 changes: 191 additions & 0 deletions src/shadowbox/server/manager_metrics.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,200 @@
// limitations under the License.

import {PrometheusManagerMetrics} from './manager_metrics';
import {PrometheusClient, QueryResultData} from '../infrastructure/prometheus_scraper';
import {FakePrometheusClient} from './mocks/mocks';

export class QueryMapPrometheusClient implements PrometheusClient {
constructor(private queryMap: {[query: string]: QueryResultData}) {}

async query(_query: string): Promise<QueryResultData> {
return this.queryMap[_query];
}
}

describe('PrometheusManagerMetrics', () => {
it('getServerMetrics', async (done) => {
const managerMetrics = new PrometheusManagerMetrics(
new QueryMapPrometheusClient({
'sum(increase(shadowsocks_data_bytes_per_location{dir=~"c<p|p>t"}[0s])) by (location, asn, asorg)':
{
resultType: 'vector',
result: [
{
metric: {
location: 'US',
asn: '49490',
asorg: null,
},
value: [null, '1000'],
},
],
},
'sum(increase(shadowsocks_tunnel_time_seconds_per_location[0s])) by (location, asn, asorg)':
{
resultType: 'vector',
result: [
{
metric: {
location: 'US',
asn: '49490',
asorg: null,
},
value: [null, '1000'],
},
],
},
'sum(increase(shadowsocks_data_bytes{dir=~"c<p|p>t"}[0s])) by (access_key)': {
resultType: 'vector',
result: [
{
metric: {
access_key: '0',
},
value: [null, '1000'],
},
],
},
'sum(increase(shadowsocks_tunnel_time_seconds[0s])) by (access_key)': {
resultType: 'vector',
result: [
{
metric: {
access_key: '0',
},
value: [null, '1000'],
},
],
},
})
);

const serverMetrics = await managerMetrics.getServerMetrics({seconds: 0});

expect(JSON.stringify(serverMetrics, null, 2)).toEqual(`{
"server": [
{
"location": "US",
"asn": 49490,
"asOrg": "null",
"tunnelTime": {
"seconds": 1000
},
"dataTransferred": {
"bytes": 1000
}
}
],
"accessKeys": [
{
"accessKeyId": 0,
"tunnelTime": {
"seconds": 1000
},
"dataTransferred": {
"bytes": 1000
}
}
]
}`);
done();
});

it('getServerMetrics - does a full outer join on metric data', async (done) => {
const managerMetrics = new PrometheusManagerMetrics(
new QueryMapPrometheusClient({
'sum(increase(shadowsocks_data_bytes_per_location{dir=~"c<p|p>t"}[0s])) by (location, asn, asorg)':
{
resultType: 'vector',
result: [
{
metric: {
location: 'US',
asn: '49490',
asorg: null,
},
value: [null, '1000'],
},
],
},
'sum(increase(shadowsocks_tunnel_time_seconds_per_location[0s])) by (location, asn, asorg)':
{
resultType: 'vector',
result: [
{
metric: {
location: 'CA',
asn: '53520',
asorg: null,
},
value: [null, '1000'],
},
],
},
'sum(increase(shadowsocks_data_bytes{dir=~"c<p|p>t"}[0s])) by (access_key)': {
resultType: 'vector',
result: [
{
metric: {
access_key: '0',
},
value: [null, '1000'],
},
],
},
'sum(increase(shadowsocks_tunnel_time_seconds[0s])) by (access_key)': {
resultType: 'vector',
result: [
{
metric: {
access_key: '1',
},
value: [null, '1000'],
},
],
},
})
);

const serverMetrics = await managerMetrics.getServerMetrics({seconds: 0});

expect(JSON.stringify(serverMetrics, null, 2)).toEqual(`{
"server": [
{
"location": "CA",
"asn": 53520,
"asOrg": "null",
"tunnelTime": {
"seconds": 1000
}
},
{
"location": "US",
"asn": 49490,
"asOrg": "null",
"dataTransferred": {
"bytes": 1000
}
}
],
"accessKeys": [
{
"accessKeyId": 1,
"tunnelTime": {
"seconds": 1000
}
},
{
"accessKeyId": 0,
"dataTransferred": {
"bytes": 1000
}
}
]
}`);
done();
});

it('getOutboundByteTransfer', async (done) => {
const managerMetrics = new PrometheusManagerMetrics(
new FakePrometheusClient({'access-key-1': 1000, 'access-key-2': 10000})
Expand Down
Loading

0 comments on commit 9d9f76b

Please sign in to comment.