mirror of
https://github.com/OutlineFoundation/outline-server.git
synced 2026-08-04 14:37:34 +00:00
Merge pull request #275 from Jigsaw-Code/fortuna-go-only
Remove ss-libev
This commit is contained in:
commit
907faae795
8 changed files with 11 additions and 1281 deletions
|
|
@ -33,7 +33,6 @@ COPY src/shadowbox/scripts/update_mmdb.sh /etc/periodic/weekly/update_mmdb
|
|||
# TODO: remove the ACL file, used to prevent access to localhost and LAN, when migrating to shadowsocks-go.
|
||||
COPY src/shadowbox/shadowbox.acl /root/shadowbox/shadowsocks.acl
|
||||
|
||||
RUN sh ./scripts/install_shadowsocks.sh 3.2.0
|
||||
RUN /etc/periodic/weekly/update_mmdb
|
||||
|
||||
WORKDIR /root/shadowbox
|
||||
|
|
|
|||
|
|
@ -1,45 +0,0 @@
|
|||
// Copyright 2018 The Outline Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import * as ip_location from './ip_location';
|
||||
|
||||
function testIpLocationService(name: string, service: ip_location.IpLocationService) {
|
||||
describe(name, () => {
|
||||
it('returns ZZ on unknown country', (done) => {
|
||||
service.countryForIp('127.0.0.1')
|
||||
.then((countryCode) => {
|
||||
expect(countryCode).toEqual('ZZ');
|
||||
done();
|
||||
})
|
||||
.catch((e) => {
|
||||
done.fail(e);
|
||||
});
|
||||
});
|
||||
it('returns AU for 1.0.0.1', (done) => {
|
||||
service.countryForIp('1.0.0.1')
|
||||
.then((countryCode) => {
|
||||
expect(countryCode).toEqual('AU');
|
||||
done();
|
||||
})
|
||||
.catch((e) => {
|
||||
done.fail(e);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
testIpLocationService(
|
||||
'MmdbLocationService',
|
||||
new ip_location.MmdbLocationService(
|
||||
'third_party/maxmind/GeoLite2-Country_20180327/GeoLite2-Country.mmdb'));
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
// Copyright 2018 The Outline Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import * as maxmind from 'maxmind';
|
||||
|
||||
export interface IpLocationService {
|
||||
// Returns the 2-digit country code for the IP address.
|
||||
countryForIp(ipAddress: string): Promise<string>;
|
||||
}
|
||||
|
||||
// An IpLocationService that uses the node-maxmind package.
|
||||
// The database is downloaded by scripts/update_mmdb.sh.
|
||||
// The Dockerfile runs this script on boot and configures the system to run it weekly.
|
||||
export class MmdbLocationService implements IpLocationService {
|
||||
private readonly db: Promise<maxmind.Reader>;
|
||||
|
||||
constructor(filename: string) {
|
||||
this.db = new Promise<maxmind.Reader>((fulfill, reject) => {
|
||||
// TODO: Change type to maxmind.Options once the type definition is updated
|
||||
// with these fields.
|
||||
const options: {} = {watchForUpdates: true, watchForUpdatesNonPersistent: true};
|
||||
maxmind.open(filename, options, (err, reader) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
fulfill(reader);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
countryForIp(ipAddress) {
|
||||
return this.db.then((reader) => {
|
||||
if (!reader) {
|
||||
throw new Error('MMDB reader is not valid');
|
||||
}
|
||||
if (!maxmind.validate(ipAddress)) {
|
||||
throw new Error('Invalid IP address');
|
||||
}
|
||||
const result = reader.get(ipAddress);
|
||||
return (result && result.country && result.country.iso_code) || 'ZZ';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -11,9 +11,7 @@
|
|||
],
|
||||
"dependencies": {
|
||||
"ShadowsocksConfig": "Jigsaw-Code/outline-shadowsocksconfig#^v0.0.8",
|
||||
"ipaddr.js": "^1.4.0",
|
||||
"js-yaml": "^3.12.0",
|
||||
"maxmind": "^2.7.0",
|
||||
"prom-client": "^11.1.3",
|
||||
"randomstring": "^1.1.5",
|
||||
"request-lite": "^2.40.1",
|
||||
|
|
@ -22,7 +20,6 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@types/js-yaml": "^3.11.2",
|
||||
"@types/maxmind": "^2",
|
||||
"@types/node": "^8",
|
||||
"@types/randomstring": "^1.1.6",
|
||||
"@types/restify": "^2.0.41"
|
||||
|
|
|
|||
|
|
@ -1,71 +0,0 @@
|
|||
#!/bin/bash -eu
|
||||
#
|
||||
# Copyright 2018 The Outline Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# TODO(fortuna): Delete once outline-ss-server rolls out
|
||||
|
||||
VERSION=$1
|
||||
DOWNLOAD_URL=https://github.com/shadowsocks/shadowsocks-libev/releases/download/v${VERSION}/shadowsocks-libev-${VERSION}.tar.gz
|
||||
BUILD_DIR=/src/shadowsocks-libev
|
||||
|
||||
set -ex
|
||||
|
||||
# Install runtime dependencies
|
||||
apk add --no-cache libev c-ares libsodium mbedtls pcre
|
||||
|
||||
# Install build dependencies
|
||||
apk add --no-cache --virtual BUILD_DEPS \
|
||||
autoconf automake build-base gettext-dev libev-dev libsodium-dev libtool \
|
||||
linux-headers mbedtls-dev openssl-dev pcre-dev tar c-ares-dev
|
||||
|
||||
# Build.
|
||||
mkdir -p $BUILD_DIR
|
||||
cd $BUILD_DIR
|
||||
curl -sSL $DOWNLOAD_URL | tar xz --strip 1
|
||||
|
||||
./configure --disable-documentation
|
||||
make install
|
||||
|
||||
# Other licenses and/or source.
|
||||
# Alpine does not always include LICENSE files and has no equivalent of
|
||||
# Debian's "apt source" command. So, we have to manually roll something.
|
||||
# We'll place licenses in the root folder of the image, named LICENSE.xxx,
|
||||
# and sources under /src.
|
||||
|
||||
# libev (BSD or GPL2):
|
||||
# http://software.schmorp.de/pkg/libev.html
|
||||
curl -sS http://cvs.schmorp.de/libev/LICENSE > /LICENSE.libev
|
||||
|
||||
# c-ares (MIT):
|
||||
# https://c-ares.haxx.se/
|
||||
curl -sS https://c-ares.haxx.se/license.html > /LICENSE.c-ares.html
|
||||
|
||||
# libsodium (ISC):
|
||||
# https://libsodium.org/
|
||||
curl -sS https://raw.githubusercontent.com/jedisct1/libsodium/master/LICENSE > /LICENSE.libsodium
|
||||
|
||||
# mbedtls (Apache):
|
||||
# https://tls.mbed.org/
|
||||
curl -sS https://raw.githubusercontent.com/ARMmbed/mbedtls/development/apache-2.0.txt > /LICENSE.mbedtls
|
||||
|
||||
# pcre (BSD):
|
||||
# http://www.pcre.org/
|
||||
curl -sS http://www.pcre.org/licence.txt > /LICENSE.pcre
|
||||
|
||||
# Clean shadowsocks-libev's folder, leaving the source in the image.
|
||||
make clean
|
||||
|
||||
# Remove build dependencies.
|
||||
apk del BUILD_DEPS
|
||||
|
|
@ -1,231 +0,0 @@
|
|||
// Copyright 2018 The Outline Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import * as child_process from 'child_process';
|
||||
import * as dgram from 'dgram';
|
||||
import * as dns from 'dns';
|
||||
|
||||
import {IpLocationService} from '../infrastructure/ip_location';
|
||||
import * as logging from '../infrastructure/logging';
|
||||
import {AccessKey, ShadowsocksServer} from '../model/shadowsocks_server';
|
||||
|
||||
import {UsageMetricsWriter} from './shared_metrics';
|
||||
|
||||
const SHADOWSOCKS_ACL_PATH = '/root/shadowbox/shadowsocks.acl';
|
||||
|
||||
export async function createLibevShadowsocksServer(
|
||||
publicAddress: string, metricsSocketPort: number, ipLocation: IpLocationService,
|
||||
usageWriter: UsageMetricsWriter, verbose: boolean) {
|
||||
const metricsSocket = await createBoundUdpSocket(metricsSocketPort);
|
||||
return new LibevShadowsocksServer(publicAddress, metricsSocket, ipLocation, usageWriter, verbose);
|
||||
}
|
||||
|
||||
// Runs shadowsocks-libev server instances.
|
||||
// TODO(fortuna): Delete once outline-ss-server rolls out
|
||||
export class LibevShadowsocksServer implements ShadowsocksServer {
|
||||
private portId = new Map<number, string>();
|
||||
private portInboundBytes = new Map<number, number>();
|
||||
private portIps = new Map<number, string[]>();
|
||||
private keyProcess = new Map<string, child_process.ChildProcess>();
|
||||
private keys = new Map<string, AccessKey>();
|
||||
|
||||
constructor(
|
||||
private readonly publicAddress: string, private readonly metricsSocket: dgram.Socket,
|
||||
ipLocation: IpLocationService, usageWriter: UsageMetricsWriter,
|
||||
private readonly verbose: boolean) {
|
||||
metricsSocket.on('message', (buf: Buffer) => {
|
||||
let metricsMessage;
|
||||
try {
|
||||
metricsMessage = parseMetricsMessage(buf);
|
||||
} catch (err) {
|
||||
logging.error(`Error parsing metrics message ${buf}: ${err.stack}`);
|
||||
return;
|
||||
}
|
||||
const accessKeyId = this.portId.get(metricsMessage.portNumber);
|
||||
if (accessKeyId === undefined) {
|
||||
// Access key has been deleted, and we no longer have the portInboundBytes. Ignore.
|
||||
return;
|
||||
}
|
||||
let previousTotalInboundBytes = this.portInboundBytes.get(metricsMessage.portNumber) || 0;
|
||||
if (previousTotalInboundBytes > metricsMessage.totalInboundBytes) {
|
||||
// totalInboundBytes is a counter that monotonically increases. A drop means
|
||||
// ss-server got restarted, so we set the previous value to zero.
|
||||
previousTotalInboundBytes = 0;
|
||||
}
|
||||
const dataDelta = metricsMessage.totalInboundBytes - previousTotalInboundBytes;
|
||||
if (dataDelta === 0) {
|
||||
return;
|
||||
}
|
||||
this.portInboundBytes.set(metricsMessage.portNumber, metricsMessage.totalInboundBytes);
|
||||
getConnectedClientIPAddresses(metricsMessage.portNumber)
|
||||
.catch((e) => {
|
||||
logging.error(
|
||||
`Unable to get client IP for port ${metricsMessage.portNumber}: ${e.stack}`);
|
||||
return [];
|
||||
})
|
||||
.then((ipAddresses: string[]) => {
|
||||
// We keep using the same IP addresses if we don't see any IP for a port.
|
||||
// This may happen if getConnectedClientIPAddresses runs when there's no TCP
|
||||
// connection open at that moment.
|
||||
if (ipAddresses && ipAddresses.length > 0) {
|
||||
this.portIps.set(metricsMessage.portNumber, ipAddresses);
|
||||
} else {
|
||||
ipAddresses = this.portIps.get(metricsMessage.portNumber) || [];
|
||||
}
|
||||
return Promise.all(ipAddresses.map((ipAddress) => {
|
||||
return ipLocation.countryForIp(ipAddress).catch((e) => {
|
||||
logging.error(`failed to get country for IP: ${e.stack}`);
|
||||
return 'ZZ';
|
||||
});
|
||||
}));
|
||||
})
|
||||
.then((countries: string[]) => {
|
||||
const dedupedCountries = [...new Set(countries)].sort();
|
||||
usageWriter.writeBytesTransferred(accessKeyId || '', dataDelta, dedupedCountries);
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
logging.error(`Unable to write bytes transferred: ${err.stack}`);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Update spawns the ss-libev subprocess and returns, likely before the instances
|
||||
// are ready and serving.
|
||||
update(newKeys: AccessKey[]): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const oldKeyIds = this.keys.keys();
|
||||
const newKeyIds = new Set(newKeys.map(k => k.id));
|
||||
// Start keys that were added
|
||||
for (const key of newKeys) {
|
||||
if (this.keys.has(key.id)) {
|
||||
continue;
|
||||
}
|
||||
this.startInstance(key);
|
||||
}
|
||||
// Stop keys that were removed.
|
||||
for (const oldKeyId of oldKeyIds) {
|
||||
if (!newKeyIds.has(oldKeyId)) {
|
||||
this.stopInstance(oldKeyId);
|
||||
}
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
private startInstance(key: AccessKey): child_process.ChildProcess {
|
||||
logging.info(`Starting server on port ${key.port}`);
|
||||
this.keys.set(key.id, key);
|
||||
this.portId.set(key.port, key.id);
|
||||
|
||||
const metricsAddress = this.metricsSocket.address();
|
||||
const commandArguments = [
|
||||
'-m', key.cipher, // Encryption method
|
||||
'-u', // Allow UDP
|
||||
'--fast-open', // Allow TCP fast open
|
||||
'-p', key.port.toString(), '-k', key.secret, '--manager-address',
|
||||
`${metricsAddress.address}:${metricsAddress.port}`, '--acl', SHADOWSOCKS_ACL_PATH
|
||||
];
|
||||
logging.info('starting ss-server with args: ' + commandArguments.join(' '));
|
||||
// Add the system DNS servers.
|
||||
// TODO(fortuna): Add dns.getServers to @types/node.
|
||||
for (const dnsServer of dns.getServers()) {
|
||||
commandArguments.push('-d');
|
||||
commandArguments.push(dnsServer);
|
||||
}
|
||||
if (this.verbose) {
|
||||
// Make the Shadowsocks output verbose in debug mode.
|
||||
commandArguments.push('-v');
|
||||
}
|
||||
const childProcess = child_process.spawn('ss-server', commandArguments);
|
||||
this.keyProcess.set(key.id, childProcess);
|
||||
|
||||
childProcess.on('error', (error) => {
|
||||
logging.error(`Error spawning server on port ${key.port}: ${error}`);
|
||||
});
|
||||
// TODO(fortuna): Add restart logic.
|
||||
childProcess.on('exit', (code, signal) => {
|
||||
logging.info(`Server on port ${key.port} has exited. Code: ${code}, Signal: ${signal}`);
|
||||
});
|
||||
// This exposes the ss-server output on the docker logs.
|
||||
// TODO(fortuna): Consider saving the output and expose it through the manager service.
|
||||
childProcess.stdout.pipe(process.stdout);
|
||||
childProcess.stderr.pipe(process.stderr);
|
||||
return childProcess;
|
||||
}
|
||||
|
||||
private stopInstance(keyId: string) {
|
||||
this.keyProcess.get(keyId).kill();
|
||||
this.keyProcess.delete(keyId);
|
||||
const key = this.keys.get(keyId);
|
||||
this.keys.delete(keyId);
|
||||
this.portId.delete(key.port);
|
||||
this.portIps.delete(key.port);
|
||||
this.portInboundBytes.delete(key.port);
|
||||
}
|
||||
}
|
||||
|
||||
function getConnectedClientIPAddresses(portNumber: number): Promise<string[]> {
|
||||
const lsofCommand = `lsof -i tcp:${portNumber} -n -P -Fn ` +
|
||||
' | grep \'\\->\'' + // only look at connection lines (e.g. skips "p8855" and "f60")
|
||||
' | sed \'s/:\\d*$//g\'' + // remove p
|
||||
' | sed \'s/n\\S*->//g\'' + // remove first part of address
|
||||
' | sed \'s/\\[//g\'' + // remove [] (used by ipv6)
|
||||
' | sed \'s/\\]//g\'' + // remove ] (used by ipv6)
|
||||
' | sort | uniq'; // remove duplicates
|
||||
return execCmd(lsofCommand).then((output: string) => {
|
||||
return output.trim().split('\n').map((e) => e.trim()).filter(Boolean);
|
||||
});
|
||||
}
|
||||
|
||||
function execCmd(cmd: string): Promise<string> {
|
||||
return new Promise((fulfill, reject) => {
|
||||
child_process.exec(cmd, (error: child_process.ExecError, stdout: string, stderr: string) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
fulfill(stdout.trim());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
interface MetricsMessage {
|
||||
portNumber: number;
|
||||
totalInboundBytes: number;
|
||||
}
|
||||
|
||||
function parseMetricsMessage(buf): MetricsMessage {
|
||||
const jsonString = buf.toString()
|
||||
.substr('stat: '.length) // remove leading "stat: "
|
||||
.replace(/\0/g, ''); // remove trailing null terminator
|
||||
// statObj is in the form {"port#": totalInboundBytes}, where
|
||||
// there is always only 1 port# per JSON object. If there are multiple
|
||||
// ss-servers communicating to the same manager, we will get multiple
|
||||
// message events.
|
||||
const statObj = JSON.parse(jsonString);
|
||||
// Object.keys is used here because node doesn't support Object.values.
|
||||
const portNumber = parseInt(Object.keys(statObj)[0], 10);
|
||||
const totalInboundBytes = statObj[portNumber];
|
||||
return {portNumber, totalInboundBytes};
|
||||
}
|
||||
|
||||
// Creates a bound UDP socket on a random unused port.
|
||||
function createBoundUdpSocket(portNumber: number): Promise<dgram.Socket> {
|
||||
const socket = dgram.createSocket('udp4');
|
||||
return new Promise((fulfill, reject) => {
|
||||
socket.bind(portNumber, 'localhost', () => {
|
||||
return fulfill(socket);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -21,21 +21,18 @@ import * as restify from 'restify';
|
|||
|
||||
import {RealClock} from '../infrastructure/clock';
|
||||
import {PortProvider} from '../infrastructure/get_port';
|
||||
import * as ip_location from '../infrastructure/ip_location';
|
||||
import * as json_config from '../infrastructure/json_config';
|
||||
import * as logging from '../infrastructure/logging';
|
||||
import {PrometheusClient, runPrometheusScraper} from '../infrastructure/prometheus_scraper';
|
||||
import {RolloutTracker} from '../infrastructure/rollout';
|
||||
import {AccessKeyId} from '../model/access_key';
|
||||
import {ShadowsocksServer} from '../model/shadowsocks_server';
|
||||
|
||||
import {createLibevShadowsocksServer} from './libev_shadowsocks_server';
|
||||
import {LegacyManagerMetrics, LegacyManagerMetricsJson, PrometheusManagerMetrics} from './manager_metrics';
|
||||
import {bindService, ShadowsocksManagerService} from './manager_service';
|
||||
import {OutlineShadowsocksServer} from './outline_shadowsocks_server';
|
||||
import {AccessKeyConfigJson, ServerAccessKeyRepository} from './server_access_key';
|
||||
import * as server_config from './server_config';
|
||||
import {createPrometheusUsageMetricsWriter, OutlineSharedMetricsPublisher, PrometheusUsageMetrics, RestMetricsCollectorClient, SharedMetricsPublisher} from './shared_metrics';
|
||||
import {OutlineSharedMetricsPublisher, PrometheusUsageMetrics, RestMetricsCollectorClient, SharedMetricsPublisher} from './shared_metrics';
|
||||
|
||||
const DEFAULT_STATE_DIR = '/root/shadowbox/persisted-state';
|
||||
const MAX_STATS_FILE_AGE_MS = 5000;
|
||||
|
|
@ -81,6 +78,7 @@ function reserveAccessKeyPorts(
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Get rid of this after 30 days of everyone's migration to Prometheus.
|
||||
function createLegacyManagerMetrics(configFilename: string): LegacyManagerMetrics {
|
||||
const metricsConfig = readMetricsConfig(configFilename);
|
||||
return new LegacyManagerMetrics(
|
||||
|
|
@ -160,23 +158,14 @@ async function main() {
|
|||
]
|
||||
};
|
||||
|
||||
const rollouts = createRolloutTracker(serverConfig);
|
||||
let shadowsocksServer: ShadowsocksServer;
|
||||
if (rollouts.isRolloutEnabled('outline-ss-server', 100)) {
|
||||
const ssMetricsLocation = `localhost:${ssMetricsPort}`;
|
||||
logging.info(`outline-ss-server metrics is at ${ssMetricsLocation}`);
|
||||
prometheusConfigJson.scrape_configs.push(
|
||||
{job_name: 'outline-server-ss', static_configs: [{targets: [ssMetricsLocation]}]});
|
||||
shadowsocksServer =
|
||||
new OutlineShadowsocksServer(
|
||||
getPersistentFilename('outline-ss-server/config.yml'), verbose, ssMetricsLocation)
|
||||
.enableCountryMetrics(MMDB_LOCATION);
|
||||
} else {
|
||||
const ipLocation = new ip_location.MmdbLocationService(MMDB_LOCATION);
|
||||
const metricsWriter = createPrometheusUsageMetricsWriter(prometheus.register);
|
||||
shadowsocksServer = await createLibevShadowsocksServer(
|
||||
proxyHostname, await portProvider.reserveNewPort(), ipLocation, metricsWriter, verbose);
|
||||
}
|
||||
const ssMetricsLocation = `localhost:${ssMetricsPort}`;
|
||||
logging.info(`outline-ss-server metrics is at ${ssMetricsLocation}`);
|
||||
prometheusConfigJson.scrape_configs.push(
|
||||
{job_name: 'outline-server-ss', static_configs: [{targets: [ssMetricsLocation]}]});
|
||||
const shadowsocksServer =
|
||||
new OutlineShadowsocksServer(
|
||||
getPersistentFilename('outline-ss-server/config.yml'), verbose, ssMetricsLocation)
|
||||
.enableCountryMetrics(MMDB_LOCATION);
|
||||
runPrometheusScraper(
|
||||
[
|
||||
'--storage.tsdb.retention', '31d', '--storage.tsdb.path',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue