From 4e8101de7fe149d69af75d9664f4dff177feda0f Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Thu, 23 Nov 2023 11:52:00 +0200 Subject: [PATCH 01/67] feat(telemetry): add opentelemetry and instrument rafiki. localenv now uses otel collector in order to send metrics to AMP Co-authored-by: JoblersTune --- localenv/.gitignore | 1 + localenv/cloud-nine-wallet/.env.example | 2 + localenv/cloud-nine-wallet/docker-compose.yml | 18 ++ localenv/cloud-nine-wallet/seed.yml | 6 +- localenv/collector/otel-collector-config.yaml | 40 +++ localenv/happy-life-bank/.env.example | 2 + localenv/happy-life-bank/docker-compose.yml | 21 ++ localenv/happy-life-bank/seed.yml | 8 +- packages/backend/package.json | 4 + .../backend/src/accounting/psql/service.ts | 3 + packages/backend/src/accounting/service.ts | 40 ++- .../src/accounting/tigerbeetle/service.ts | 3 + packages/backend/src/app.ts | 14 +- packages/backend/src/config/app.ts | 9 + packages/backend/src/index.ts | 21 ++ .../open_payments/payment/outgoing/service.ts | 3 + packages/backend/src/telemetry/meter.ts | 96 +++++++ pnpm-lock.yaml | 255 +++++++++++++++++- 18 files changed, 530 insertions(+), 16 deletions(-) create mode 100644 localenv/.gitignore create mode 100644 localenv/cloud-nine-wallet/.env.example create mode 100644 localenv/collector/otel-collector-config.yaml create mode 100644 localenv/happy-life-bank/.env.example create mode 100644 packages/backend/src/telemetry/meter.ts diff --git a/localenv/.gitignore b/localenv/.gitignore new file mode 100644 index 0000000000..4c49bd78f1 --- /dev/null +++ b/localenv/.gitignore @@ -0,0 +1 @@ +.env diff --git a/localenv/cloud-nine-wallet/.env.example b/localenv/cloud-nine-wallet/.env.example new file mode 100644 index 0000000000..6b6d9295fa --- /dev/null +++ b/localenv/cloud-nine-wallet/.env.example @@ -0,0 +1,2 @@ +AWS_ACCESS_KEY_ID=OPENTELEMETRY_COLLECTOR_ACCESS_KEY +AWS_SECRET_ACCESS_KEY=OPENTELEMETRY_COLLECTOR_SCCESS_KEY diff --git a/localenv/cloud-nine-wallet/docker-compose.yml b/localenv/cloud-nine-wallet/docker-compose.yml index fc8b22641e..85a2847b19 100644 --- a/localenv/cloud-nine-wallet/docker-compose.yml +++ b/localenv/cloud-nine-wallet/docker-compose.yml @@ -59,6 +59,10 @@ services: REDIS_URL: redis://shared-redis:6379/0 WALLET_ADDRESS_URL: ${CLOUD_NINE_WALLET_ADDRESS_URL:-https://cloud-nine-wallet-backend/.well-known/pay} ILP_CONNECTOR_ADDRESS: ${CLOUD_NINE_CONNECTOR_URL} + ENABLE_TELEMETRY: true + OPEN_TELEMETRY_COLLECTOR_URL: http://cloud-nine-otel-collector:4317 + OPEN_TELEMETRY_EXPORT_INTERVAL: 15000 + TELEMETRY_SERVICE_NAME: CLOUD-NINE depends_on: - shared-database - shared-redis @@ -117,6 +121,20 @@ services: ENABLE_INSECURE_MESSAGE_COOKIE: true depends_on: - cloud-nine-backend + cloud-nine-otel-collector: + image: otel/opentelemetry-collector-contrib:latest + command: ["--config=/etc/otel-collector-config.yaml", ""] + environment: + - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID-''} + - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY-''} + volumes: + - ../collector/otel-collector-config.yaml:/etc/otel-collector-config.yaml + networks: + - rafiki + expose: + - 4317 + ports: + - "13133:13133" # health_check extension volumes: database-data: # named volumes can be managed easier using docker-compose diff --git a/localenv/cloud-nine-wallet/seed.yml b/localenv/cloud-nine-wallet/seed.yml index bdf6e825ec..bb8d40cca8 100644 --- a/localenv/cloud-nine-wallet/seed.yml +++ b/localenv/cloud-nine-wallet/seed.yml @@ -29,18 +29,18 @@ accounts: - name: 'Grace Franklin' path: accounts/gfranklin id: 742ab7cd-1624-4d2e-af6e-e15a71638669 - initialBalance: 50000 + initialBalance: 4000 postmanEnvVar: gfranklinWalletAddress assetCode: USD - name: 'Bert Hamchest' id: a9adbe1a-df31-4766-87c9-d2cb2e636a9b - initialBalance: 50000 + initialBalance: 400000 path: accounts/bhamchest postmanEnvVar: bhamchestWalletAddress assetCode: USD - name: "World's Best Donut Co" id: 5726eefe-8737-459d-a36b-0acce152cb90 - initialBalance: 2000 + initialBalance: 200000 path: accounts/wbdc postmanEnvVar: wbdcWalletAddress assetCode: USD diff --git a/localenv/collector/otel-collector-config.yaml b/localenv/collector/otel-collector-config.yaml new file mode 100644 index 0000000000..dd0c35cfd2 --- /dev/null +++ b/localenv/collector/otel-collector-config.yaml @@ -0,0 +1,40 @@ +extensions: + sigv4auth: + assume_role: + arn: 'arn:aws:iam::145795521759:role/PrometheusRemoteWrite' + sts_region: 'eu-west-2' + +receivers: + otlp: + protocols: + grpc: + http: + cors: + allowed_origins: + - http://* + - https://* + +processors: + batch: + +exporters: + logging: + verbosity: 'normal' + prometheusremotewrite: + endpoint: 'https://aps-workspaces.eu-west-2.amazonaws.com/workspaces/ws-7c8fd050-cb3b-451f-9971-1e6099db2e8f/api/v1/remote_write' + auth: + authenticator: sigv4auth + +service: + telemetry: + logs: + level: 'debug' + metrics: + level: 'detailed' + address: 0.0.0.0:8888 + extensions: [sigv4auth] + pipelines: + metrics: + receivers: [otlp] + processors: [batch] + exporters: [logging, prometheusremotewrite] diff --git a/localenv/happy-life-bank/.env.example b/localenv/happy-life-bank/.env.example new file mode 100644 index 0000000000..6b6d9295fa --- /dev/null +++ b/localenv/happy-life-bank/.env.example @@ -0,0 +1,2 @@ +AWS_ACCESS_KEY_ID=OPENTELEMETRY_COLLECTOR_ACCESS_KEY +AWS_SECRET_ACCESS_KEY=OPENTELEMETRY_COLLECTOR_SCCESS_KEY diff --git a/localenv/happy-life-bank/docker-compose.yml b/localenv/happy-life-bank/docker-compose.yml index e4495d0e07..22ad9e5b1b 100644 --- a/localenv/happy-life-bank/docker-compose.yml +++ b/localenv/happy-life-bank/docker-compose.yml @@ -51,8 +51,14 @@ services: EXCHANGE_RATES_URL: http://happy-life-bank/rates REDIS_URL: redis://shared-redis:6379/1 WALLET_ADDRESS_URL: ${HAPPY_LIFE_BANK_WALLET_ADDRESS_URL:-https://happy-life-bank-backend/.well-known/pay} + ENABLE_TELEMETRY: true + OPEN_TELEMETRY_COLLECTOR_URL: http://happy-life-otel-collector:4317 + OPEN_TELEMETRY_EXPORT_INTERVAL: 15000 + TELEMETRY_SERVICE_NAME: HAPPY-LIFE + depends_on: - cloud-nine-backend + # - happy-life-otel-collector happy-life-auth: hostname: happy-life-bank-auth image: rafiki-auth @@ -86,6 +92,21 @@ services: depends_on: - cloud-nine-admin - happy-life-backend + happy-life-otel-collector: + image: otel/opentelemetry-collector-contrib:latest + command: ["--config=/etc/otel-collector-config.yaml", ""] + environment: + - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID-''} + - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY-''} + volumes: + - ../collector/otel-collector-config.yaml:/etc/otel-collector-config.yaml + networks: + - rafiki + expose: + - 4317 + ports: + - "13132:13133" # health_check extension + networks: rafiki: diff --git a/localenv/happy-life-bank/seed.yml b/localenv/happy-life-bank/seed.yml index e86869588b..acced59f02 100644 --- a/localenv/happy-life-bank/seed.yml +++ b/localenv/happy-life-bank/seed.yml @@ -29,12 +29,12 @@ accounts: - name: 'Philip Fry' path: accounts/pfry id: 97a3a431-8ee1-48fc-ac85-70e2f5eba8e5 - initialBalance: 50000 + initialBalance: 1 postmanEnvVar: pfryWalletAddress assetCode: USD - name: 'PlanEx Corp' id: a455cc54-b583-455b-836a-e5275c5c05b7 - initialBalance: 2000 + initialBalance: 200000 path: accounts/planex postmanEnvVar: planexWalletAddress assetCode: USD @@ -48,12 +48,12 @@ accounts: - name: 'Lars' path: accounts/lars id: fd4ecbc9-205d-4ecd-a030-507d6ce2bde6 - initialBalance: 50000 + initialBalance: 500000 assetCode: EUR - name: 'David' path: accounts/david id: 60257507-3191-4507-9d77-9071fd6b3c30 - initialBalance: 150000 + initialBalance: 15000000 assetCode: MXN rates: EUR: diff --git a/packages/backend/package.json b/packages/backend/package.json index 42af781815..6c67965f8c 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -56,6 +56,10 @@ "@interledger/pay": "0.4.0-alpha.9", "@interledger/stream-receiver": "^0.3.3-alpha.3", "@koa/router": "^12.0.0", + "@opentelemetry/api": "^1.6.0", + "@opentelemetry/exporter-metrics-otlp-grpc": "^0.43.0", + "@opentelemetry/resources": "^1.17.0", + "@opentelemetry/sdk-metrics": "^1.17.0", "ajv": "^8.12.0", "axios": "1.6.7", "base64url": "^3.0.1", diff --git a/packages/backend/src/accounting/psql/service.ts b/packages/backend/src/accounting/psql/service.ts index b876048cbd..0c6488b719 100644 --- a/packages/backend/src/accounting/psql/service.ts +++ b/packages/backend/src/accounting/psql/service.ts @@ -33,8 +33,10 @@ import { voidTransfers } from './ledger-transfer' import { LedgerTransfer, LedgerTransferType } from './ledger-transfer/model' +import { TelemetryService } from '../../telemetry/meter' export interface ServiceDependencies extends BaseService { + telemetry?: TelemetryService knex: TransactionOrKnex withdrawalThrottleDelay?: number } @@ -201,6 +203,7 @@ export async function createTransfer( args: TransferOptions ): Promise { return createAccountToAccountTransfer({ + telemetry: deps.telemetry, transferArgs: args, withdrawalThrottleDelay: deps.withdrawalThrottleDelay, voidTransfers: async (transferRefs) => voidTransfers(deps, transferRefs), diff --git a/packages/backend/src/accounting/service.ts b/packages/backend/src/accounting/service.ts index 6ed5d482d7..af9bbf483b 100644 --- a/packages/backend/src/accounting/service.ts +++ b/packages/backend/src/accounting/service.ts @@ -1,5 +1,6 @@ import { TransactionOrKnex } from 'objection' import { isTransferError, TransferError } from './errors' +import { Metrics, TelemetryService } from '../telemetry/meter' export enum LiquidityAccountType { ASSET = 'ASSET', @@ -13,6 +14,8 @@ export interface LiquidityAccount { id: string asset: { id: string + code?: string + scale?: number ledger: number onDebit?: (options: OnDebitOptions) => Promise } @@ -95,6 +98,7 @@ interface CreateAccountToAccountTransferArgs { transfers: TransferToCreate[] ): Promise withdrawalThrottleDelay?: number + telemetry?: TelemetryService } export async function createAccountToAccountTransfer( @@ -107,7 +111,8 @@ export async function createAccountToAccountTransfer( getAccountReceived, getAccountBalance, withdrawalThrottleDelay, - transferArgs + transferArgs, + telemetry } = args const { sourceAccount, destinationAccount, sourceAmount, destinationAmount } = @@ -182,6 +187,39 @@ export async function createAccountToAccountTransfer( totalReceived, withdrawalThrottleDelay }) + + telemetry?.getCounter(Metrics.TRANSACTIONS_TOTAL)?.add(1, { + source: telemetry?.getServiceName() ?? 'Rafiki', + asset_code: destinationAccount.asset.code + }) + + console.log( + `######################## [TELEMETRY]Gathering Transaction amount ####################` + ) + + const scalingFactor = destinationAccount.asset.scale + ? Math.pow(10, 4 - destinationAccount.asset.scale) + : undefined + console.log( + `scaling factor is: Math.pow(10 , 4 - ${destinationAccount.asset.scale}) === ${scalingFactor}` + ) + + const totalReceivedInAssetScale4 = + Number(totalReceived) * Number(scalingFactor) + + console.log( + `totalReceivedInAssetScale4 = totalReceived(${totalReceived}) * scalingFactor(${scalingFactor})` + ) + + console.log( + `totalReceivedInAssetScale4 is ${totalReceivedInAssetScale4}` + ) + telemetry + ?.getCounter(Metrics.TRANSACTIONS_AMOUNT) + ?.add(totalReceivedInAssetScale4, { + asset_code: destinationAccount.asset.code, + source: telemetry?.getServiceName() ?? 'Rafiki' + }) } }, void: async (): Promise => { diff --git a/packages/backend/src/accounting/tigerbeetle/service.ts b/packages/backend/src/accounting/tigerbeetle/service.ts index 986a633c0b..331458f407 100644 --- a/packages/backend/src/accounting/tigerbeetle/service.ts +++ b/packages/backend/src/accounting/tigerbeetle/service.ts @@ -26,6 +26,7 @@ import { TransferOptions, Withdrawal } from '../service' +import { TelemetryService } from '../../telemetry/meter' export enum TigerbeetleAccountCode { LIQUIDITY_WEB_MONETIZATION = 1, @@ -48,6 +49,7 @@ export const convertToTigerbeetleAccountCode: { } export interface ServiceDependencies extends BaseService { + telemetry?: TelemetryService tigerbeetle: Client withdrawalThrottleDelay?: number } @@ -216,6 +218,7 @@ export async function createTransfer( args: TransferOptions ): Promise { return createAccountToAccountTransfer({ + telemetry: deps.telemetry, transferArgs: args, withdrawalThrottleDelay: deps.withdrawalThrottleDelay, voidTransfers: async (transferIds) => { diff --git a/packages/backend/src/app.ts b/packages/backend/src/app.ts index ce25313d3c..9d9dc5344e 100644 --- a/packages/backend/src/app.ts +++ b/packages/backend/src/app.ts @@ -83,7 +83,7 @@ import { Rafiki as ConnectorApp } from './payment-method/ilp/connector/core' import { AxiosInstance } from 'axios' import { PaymentMethodHandlerService } from './payment-method/handler/service' import { IlpPaymentService } from './payment-method/ilp/service' - +import { TelemetryService } from './telemetry/meter' export interface AppContextData { logger: Logger container: AppContainer @@ -204,6 +204,7 @@ const WALLET_ADDRESS_PATH = '/:walletAddressPath+' export interface AppServices { logger: Promise + telemetry: Promise knex: Promise axios: Promise config: Promise @@ -282,6 +283,17 @@ export class App { } } + // let counter = 0 + // const telemetry = await this.container.use('telemetry') + // setInterval(() => { + // counter++ + // console.log( + // `HERE ---------------------------####################---------------` + // ) + // console.log(`counter: ${counter}`) + // telemetry.getCounter('test')?.add(1) + // }, 10000) + public async startAdminServer(port: number): Promise { const koa = await this.createKoaServer() const httpServer = http.createServer(koa.callback()) diff --git a/packages/backend/src/config/app.ts b/packages/backend/src/config/app.ts index 101537a233..98ea3edbbd 100644 --- a/packages/backend/src/config/app.ts +++ b/packages/backend/src/config/app.ts @@ -32,6 +32,15 @@ dotenv.config({ export const Config = { logLevel: envString('LOG_LEVEL', 'info'), + enableTelemetry: envBool('ENABLE_TELEMETRY', true), + telemetryServiceName: envString('TELEMETRY_SERVICE_NAME', 'Rafiki'), + openTelemetryCollectorUrl: envString( + 'OPEN_TELEMETRY_COLLECTOR_URL', + 'http://otel-collector:4317' + ), + openTelemetryExportInterval: envInt('OPEN_TELEMETRY_EXPORT_INTERVAL', 60000), + // publicHost is for open payments URLs. + publicHost: envString('PUBLIC_HOST', 'http://127.0.0.1:3001'), adminPort: envInt('ADMIN_PORT', 3001), openPaymentsUrl: envString('OPEN_PAYMENTS_URL', 'http://127.0.0.1:3000'), openPaymentsPort: envInt('OPEN_PAYMENTS_PORT', 3003), diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 75bc10f452..21c425d2be 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -48,6 +48,7 @@ import { createAutoPeeringRoutes } from './payment-method/ilp/auto-peering/route import axios from 'axios' import { createIlpPaymentService } from './payment-method/ilp/service' import { createPaymentMethodHandlerService } from './payment-method/handler/service' +import { TelemetryService, createTelemetryService } from './telemetry/meter' BigInt.prototype.toJSON = function () { return this.toString() @@ -127,6 +128,19 @@ export function initIocContainer( replica_addresses: config.tigerbeetleReplicaAddresses }) }) + + if (config.enableTelemetry) { + container.singleton('telemetry', async (deps) => { + const config = await deps.use('config') + return createTelemetryService({ + logger: await deps.use('logger'), + serviceName: config.telemetryServiceName, + collectorUrl: config.openTelemetryCollectorUrl, + exportIntervalMillis: config.openTelemetryExportInterval + }) + }) + } + container.singleton('openApi', async () => { const resourceServerSpec = await createOpenAPI( path.resolve(__dirname, './openapi/resource-server.yaml') @@ -184,10 +198,16 @@ export function initIocContainer( const knex = await deps.use('knex') const config = await deps.use('config') + let telemetry: TelemetryService | undefined + if (config.enableTelemetry) { + telemetry = await deps.use('telemetry') + } + if (config.useTigerbeetle) { const tigerbeetle = await deps.use('tigerbeetle') return createTigerbeetleAccountingService({ + telemetry, logger, knex, tigerbeetle, @@ -196,6 +216,7 @@ export function initIocContainer( } return createPsqlAccountingService({ + telemetry, logger, knex, withdrawalThrottleDelay: config.withdrawalThrottleDelay diff --git a/packages/backend/src/open_payments/payment/outgoing/service.ts b/packages/backend/src/open_payments/payment/outgoing/service.ts index 0826769f44..19ada24203 100644 --- a/packages/backend/src/open_payments/payment/outgoing/service.ts +++ b/packages/backend/src/open_payments/payment/outgoing/service.ts @@ -391,6 +391,9 @@ async function fundPayment( return error } await payment.$query(trx).patch({ state: OutgoingPaymentState.Sending }) + + //Transactions total metric mighe need to be used here + return await addSentAmount(deps, payment) }) } diff --git a/packages/backend/src/telemetry/meter.ts b/packages/backend/src/telemetry/meter.ts new file mode 100644 index 0000000000..1f6049f779 --- /dev/null +++ b/packages/backend/src/telemetry/meter.ts @@ -0,0 +1,96 @@ +import { + Counter, + DiagConsoleLogger, + DiagLogLevel, + MetricOptions, + ValueType, + diag, + metrics +} from '@opentelemetry/api' +import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-grpc' +import { Resource } from '@opentelemetry/resources' +import { + MeterProvider, + PeriodicExportingMetricReader +} from '@opentelemetry/sdk-metrics' + +import { BaseService } from '../shared/baseService' + +export interface TelemetryService { + getCounter(name: string): Counter | undefined + getServiceName(): string | undefined +} + +interface TelemetryServiceDependencies extends BaseService { + serviceName?: string + collectorUrl?: string + exportIntervalMillis?: number +} + +export enum Metrics { + TRANSACTIONS_TOTAL = 'transactions_total', + TRANSACTIONS_AMOUNT = 'transactions_amount' +} + +export function createTelemetryService( + deps: TelemetryServiceDependencies +): TelemetryService { + return new TelemetryServiceImpl(deps) +} + +class TelemetryServiceImpl implements TelemetryService { + private serviceName: string | undefined + + private counters = new Map() + constructor(private deps: TelemetryServiceDependencies) { + diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.DEBUG) + this.serviceName = deps.serviceName + console.log( + `serviceName: ${deps.serviceName}, collectorUrl: ${deps.collectorUrl} }` + ) + + const meterProvider = new MeterProvider({ + resource: new Resource({ 'service.name': 'RAFIKI_NETWORK' }) + }) + + const metricExporter = new OTLPMetricExporter({ + url: deps.collectorUrl ?? 'http://otel-collector:4317' + }) + + const metricReader = new PeriodicExportingMetricReader({ + exporter: metricExporter, + exportIntervalMillis: deps.exportIntervalMillis ?? 60000 + }) + + meterProvider.addMetricReader(metricReader) + + metrics.setGlobalMeterProvider(meterProvider) + + //* init counters + this.createCounter(Metrics.TRANSACTIONS_TOTAL, { + description: 'Count of funded transactions' + }) + + this.createCounter(Metrics.TRANSACTIONS_AMOUNT, { + description: + 'Amount sent through the network. Asset Code & Asset Scale are sent as attributes', + valueType: ValueType.DOUBLE + }) + } + + private createCounter( + name: string, + options: MetricOptions | undefined + ): void { + const counter = metrics.getMeter('Rafiki').createCounter(name, options) + this.counters.set(name, counter) + } + + public getCounter(name: string): Counter | undefined { + return this.counters.get(name) + } + + public getServiceName(): string | undefined { + return this.serviceName + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fda482c85b..76165ec981 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -286,6 +286,18 @@ importers: '@koa/router': specifier: ^12.0.0 version: 12.0.0 + '@opentelemetry/api': + specifier: ^1.6.0 + version: 1.7.0 + '@opentelemetry/exporter-metrics-otlp-grpc': + specifier: ^0.43.0 + version: 0.43.0(@opentelemetry/api@1.7.0) + '@opentelemetry/resources': + specifier: ^1.17.0 + version: 1.18.1(@opentelemetry/api@1.7.0) + '@opentelemetry/sdk-metrics': + specifier: ^1.17.0 + version: 1.18.1(@opentelemetry/api@1.7.0) ajv: specifier: ^8.12.0 version: 8.12.0 @@ -3146,6 +3158,25 @@ packages: dependencies: graphql: 16.8.1 + /@grpc/grpc-js@1.9.11: + resolution: {integrity: sha512-QDhMfbTROOXUhLHMroow8f3EHiCKUOh6UwxMP5S3EuXMnWMNSVIhatGZRwkpg9OUTYdZPsDUVH3cOAkWhGFUJw==} + engines: {node: ^8.13.0 || >=10.10.0} + dependencies: + '@grpc/proto-loader': 0.7.10 + '@types/node': 18.18.5 + dev: false + + /@grpc/proto-loader@0.7.10: + resolution: {integrity: sha512-CAqDfoaQ8ykFd9zqBDn4k6iWT9loLAlc2ETmDFS9JCD70gDcnA4L3AFEo2iV7KyAtAAHFW9ftq1Fz+Vsgq80RQ==} + engines: {node: '>=6'} + hasBin: true + dependencies: + lodash.camelcase: 4.3.0 + long: 5.2.3 + protobufjs: 7.2.5 + yargs: 17.7.2 + dev: false + /@headlessui/react@1.7.18(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-4i5DOrzwN4qSgNsL4Si61VMkUcWbcSKueUV7sFhpHzQcSShdlHENE5+QBntMSRvHt8NyoFO2AGG8si9lq+w4zQ==} engines: {node: '>=10'} @@ -3686,6 +3717,187 @@ packages: which: 3.0.1 dev: true + /@opentelemetry/api-logs@0.43.0: + resolution: {integrity: sha512-0CXMOYPXgAdLM2OzVkiUfAL6QQwWVhnMfUXCqLsITY42FZ9TxAhZIHkoc4mfVxvPuXsBnRYGR8UQZX86p87z4A==} + engines: {node: '>=14'} + dependencies: + '@opentelemetry/api': 1.7.0 + dev: false + + /@opentelemetry/api@1.7.0: + resolution: {integrity: sha512-AdY5wvN0P2vXBi3b29hxZgSFvdhdxPB9+f0B6s//P9Q8nibRWeA3cHm8UmLpio9ABigkVHJ5NMPk+Mz8VCCyrw==} + engines: {node: '>=8.0.0'} + dev: false + + /@opentelemetry/core@1.17.0(@opentelemetry/api@1.7.0): + resolution: {integrity: sha512-tfnl3h+UefCgx1aeN2xtrmr6BmdWGKXypk0pflQR0urFS40aE88trnkOMc2HTJZbMrqEEl4HsaBeFhwLVXsrJg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.7.0' + dependencies: + '@opentelemetry/api': 1.7.0 + '@opentelemetry/semantic-conventions': 1.17.0 + dev: false + + /@opentelemetry/core@1.18.1(@opentelemetry/api@1.7.0): + resolution: {integrity: sha512-kvnUqezHMhsQvdsnhnqTNfAJs3ox/isB0SVrM1dhVFw7SsB7TstuVa6fgWnN2GdPyilIFLUvvbTZoVRmx6eiRg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.8.0' + dependencies: + '@opentelemetry/api': 1.7.0 + '@opentelemetry/semantic-conventions': 1.18.1 + dev: false + + /@opentelemetry/exporter-metrics-otlp-grpc@0.43.0(@opentelemetry/api@1.7.0): + resolution: {integrity: sha512-m7HtZAvfFt1YDjjzVf/kLr2pyuFth3NU3pfqs41zfYB5o/n/RbxRhVLphRzr6qLDccqsL0mxn1e6tkUuIn/Hfg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@grpc/grpc-js': 1.9.11 + '@opentelemetry/api': 1.7.0 + '@opentelemetry/core': 1.17.0(@opentelemetry/api@1.7.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.43.0(@opentelemetry/api@1.7.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.43.0(@opentelemetry/api@1.7.0) + '@opentelemetry/otlp-transformer': 0.43.0(@opentelemetry/api@1.7.0) + '@opentelemetry/resources': 1.17.0(@opentelemetry/api@1.7.0) + '@opentelemetry/sdk-metrics': 1.17.0(@opentelemetry/api@1.7.0) + dev: false + + /@opentelemetry/exporter-metrics-otlp-http@0.43.0(@opentelemetry/api@1.7.0): + resolution: {integrity: sha512-k0KHKLS/xEWI4e5xrsnHpRk7Adj7JSFbFeKF4ti1d9soek3y85ZC2fTzDQC+ysUYo/lccoAXGR/gjcYgQOe7pg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.7.0 + '@opentelemetry/core': 1.17.0(@opentelemetry/api@1.7.0) + '@opentelemetry/otlp-exporter-base': 0.43.0(@opentelemetry/api@1.7.0) + '@opentelemetry/otlp-transformer': 0.43.0(@opentelemetry/api@1.7.0) + '@opentelemetry/resources': 1.17.0(@opentelemetry/api@1.7.0) + '@opentelemetry/sdk-metrics': 1.17.0(@opentelemetry/api@1.7.0) + dev: false + + /@opentelemetry/otlp-exporter-base@0.43.0(@opentelemetry/api@1.7.0): + resolution: {integrity: sha512-LXNtRFVuPRXB9q0qdvrLikQ3NtT9Jmv255Idryz3RJPhOh/Fa03sBASQoj3D55OH3xazmA90KFHfhJ/d8D8y4A==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + dependencies: + '@opentelemetry/api': 1.7.0 + '@opentelemetry/core': 1.17.0(@opentelemetry/api@1.7.0) + dev: false + + /@opentelemetry/otlp-grpc-exporter-base@0.43.0(@opentelemetry/api@1.7.0): + resolution: {integrity: sha512-oOpqtDJo9BBa1+nD6ID1qZ55ZdTwEwSSn2idMobw8jmByJKaanVLdr9SJKsn5T9OBqo/c5QY2brMf0TNZkobJQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + dependencies: + '@grpc/grpc-js': 1.9.11 + '@opentelemetry/api': 1.7.0 + '@opentelemetry/core': 1.17.0(@opentelemetry/api@1.7.0) + '@opentelemetry/otlp-exporter-base': 0.43.0(@opentelemetry/api@1.7.0) + protobufjs: 7.2.5 + dev: false + + /@opentelemetry/otlp-transformer@0.43.0(@opentelemetry/api@1.7.0): + resolution: {integrity: sha512-KXYmgzWdVBOD5NvPmGW1nEMJjyQ8gK3N8r6pi4HvmEhTp0v4T13qDSax4q0HfsqmbPJR355oqQSJUnu1dHNutw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.7.0' + dependencies: + '@opentelemetry/api': 1.7.0 + '@opentelemetry/api-logs': 0.43.0 + '@opentelemetry/core': 1.17.0(@opentelemetry/api@1.7.0) + '@opentelemetry/resources': 1.17.0(@opentelemetry/api@1.7.0) + '@opentelemetry/sdk-logs': 0.43.0(@opentelemetry/api-logs@0.43.0)(@opentelemetry/api@1.7.0) + '@opentelemetry/sdk-metrics': 1.17.0(@opentelemetry/api@1.7.0) + '@opentelemetry/sdk-trace-base': 1.17.0(@opentelemetry/api@1.7.0) + dev: false + + /@opentelemetry/resources@1.17.0(@opentelemetry/api@1.7.0): + resolution: {integrity: sha512-+u0ciVnj8lhuL/qGRBPeVYvk7fL+H/vOddfvmOeJaA1KC+5/3UED1c9KoZQlRsNT5Kw1FaK8LkY2NVLYfOVZQw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.7.0' + dependencies: + '@opentelemetry/api': 1.7.0 + '@opentelemetry/core': 1.17.0(@opentelemetry/api@1.7.0) + '@opentelemetry/semantic-conventions': 1.17.0 + dev: false + + /@opentelemetry/resources@1.18.1(@opentelemetry/api@1.7.0): + resolution: {integrity: sha512-JjbcQLYMttXcIabflLRuaw5oof5gToYV9fuXbcsoOeQ0BlbwUn6DAZi++PNsSz2jjPeASfDls10iaO/8BRIPRA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.8.0' + dependencies: + '@opentelemetry/api': 1.7.0 + '@opentelemetry/core': 1.18.1(@opentelemetry/api@1.7.0) + '@opentelemetry/semantic-conventions': 1.18.1 + dev: false + + /@opentelemetry/sdk-logs@0.43.0(@opentelemetry/api-logs@0.43.0)(@opentelemetry/api@1.7.0): + resolution: {integrity: sha512-JyJ2BBRKm37Mc4cSEhFmsMl5ASQn1dkGhEWzAAMSlhPtLRTv5PfvJwhR+Mboaic/eDLAlciwsgijq8IFlf6IgQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.4.0 <1.7.0' + '@opentelemetry/api-logs': '>=0.39.1' + dependencies: + '@opentelemetry/api': 1.7.0 + '@opentelemetry/api-logs': 0.43.0 + '@opentelemetry/core': 1.17.0(@opentelemetry/api@1.7.0) + '@opentelemetry/resources': 1.17.0(@opentelemetry/api@1.7.0) + dev: false + + /@opentelemetry/sdk-metrics@1.17.0(@opentelemetry/api@1.7.0): + resolution: {integrity: sha512-HlWM27yGmYuwCoVRe3yg2PqKnIsq0kEF0HQgvkeDWz2NYkq9fFaSspR6kvjxUTbghAlZrabiqbgyKoYpYaXS3w==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.7.0' + dependencies: + '@opentelemetry/api': 1.7.0 + '@opentelemetry/core': 1.17.0(@opentelemetry/api@1.7.0) + '@opentelemetry/resources': 1.17.0(@opentelemetry/api@1.7.0) + lodash.merge: 4.6.2 + dev: false + + /@opentelemetry/sdk-metrics@1.18.1(@opentelemetry/api@1.7.0): + resolution: {integrity: sha512-TEFgeNFhdULBYiCoHbz31Y4PDsfjjxRp8Wmdp6ybLQZPqMNEb+dRq+XN8Xw3ivIgTaf9gYsomgV5ensX99RuEQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.8.0' + dependencies: + '@opentelemetry/api': 1.7.0 + '@opentelemetry/core': 1.18.1(@opentelemetry/api@1.7.0) + '@opentelemetry/resources': 1.18.1(@opentelemetry/api@1.7.0) + lodash.merge: 4.6.2 + dev: false + + /@opentelemetry/sdk-trace-base@1.17.0(@opentelemetry/api@1.7.0): + resolution: {integrity: sha512-2T5HA1/1iE36Q9eg6D4zYlC4Y4GcycI1J6NsHPKZY9oWfAxWsoYnRlkPfUqyY5XVtocCo/xHpnJvGNHwzT70oQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.7.0' + dependencies: + '@opentelemetry/api': 1.7.0 + '@opentelemetry/core': 1.17.0(@opentelemetry/api@1.7.0) + '@opentelemetry/resources': 1.17.0(@opentelemetry/api@1.7.0) + '@opentelemetry/semantic-conventions': 1.17.0 + dev: false + + /@opentelemetry/semantic-conventions@1.17.0: + resolution: {integrity: sha512-+fguCd2d8d2qruk0H0DsCEy2CTK3t0Tugg7MhZ/UQMvmewbZLNnJ6heSYyzIZWG5IPfAXzoj4f4F/qpM7l4VBA==} + engines: {node: '>=14'} + dev: false + + /@opentelemetry/semantic-conventions@1.18.1: + resolution: {integrity: sha512-+NLGHr6VZwcgE/2lw8zDIufOCGnzsA5CbQIMleXZTrgkBd0TanCX+MiDYJ1TOS4KL/Tqk0nFRxawnaYr6pkZkA==} + engines: {node: '>=14'} + dev: false + /@pagefind/darwin-arm64@1.0.3: resolution: {integrity: sha512-vsHDtvao3W4iFCxVc4S0BVhpj3E2MAoIVM7RmuQfGp1Ng22nGLRaMP6FguLO8TMabRJdvp4SVr227hL4WGKOHA==} cpu: [arm64] @@ -5501,7 +5713,6 @@ packages: engines: {node: '>=8'} dependencies: color-convert: 2.0.1 - dev: true /ansi-styles@5.2.0: resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} @@ -6470,7 +6681,6 @@ packages: string-width: 4.2.3 strip-ansi: 6.0.1 wrap-ansi: 7.0.0 - dev: true /clone@1.0.4: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} @@ -8753,7 +8963,6 @@ packages: /get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} - dev: true /get-intrinsic@1.2.1: resolution: {integrity: sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==} @@ -10968,7 +11177,6 @@ packages: /lodash.camelcase@4.3.0: resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} - dev: true /lodash.clonedeep@4.5.0: resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} @@ -11049,6 +11257,10 @@ packages: /long@4.0.0: resolution: {integrity: sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==} + /long@5.2.3: + resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==} + dev: false + /longest-streak@3.0.1: resolution: {integrity: sha512-cHlYSUpL2s7Fb3394mYxwTYj8niTaNHUCLr0qdiCXQfSjfuA7CKofpX2uSwEfFDQ0EB7JcnMnm+GjbqqoinYYg==} @@ -13514,6 +13726,25 @@ packages: /property-information@6.1.1: resolution: {integrity: sha512-hrzC564QIl0r0vy4l6MvRLhafmUowhO/O3KgVSoXIbbA2Sz4j8HGpJc6T2cubRVwMwpdiG/vKGfhT4IixmKN9w==} + /protobufjs@7.2.5: + resolution: {integrity: sha512-gGXRSXvxQ7UiPgfw8gevrfRWcTlSbOFg+p/N+JVJEK5VhueL2miT6qTymqAmjr1Q5WbOCyJbyrk6JfWKwlFn6A==} + engines: {node: '>=12.0.0'} + requiresBuild: true + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 18.18.5 + long: 5.2.3 + dev: false + /proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -13991,7 +14222,6 @@ packages: /require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} - dev: true /require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} @@ -16070,7 +16300,6 @@ packages: ansi-styles: 4.3.0 string-width: 4.2.3 strip-ansi: 6.0.1 - dev: true /wrap-ansi@8.1.0: resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} @@ -16162,7 +16391,6 @@ packages: /y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} - dev: true /yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -16220,6 +16448,19 @@ packages: yargs-parser: 21.1.1 dev: true + /yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + dependencies: + cliui: 8.0.1 + escalade: 3.1.1 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + dev: false + /ylru@1.3.2: resolution: {integrity: sha512-RXRJzMiK6U2ye0BlGGZnmpwJDPgakn6aNQ0A7gHRbD4I0uvK4TW6UqkK1V0pp9jskjJBAXd3dRrbzWkqJ+6cxA==} engines: {node: '>= 4.0.0'} From 4ee5dda078c7ae377b5e51b70e64330ffa63d226 Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Fri, 24 Nov 2023 19:05:58 +0200 Subject: [PATCH 02/67] feat(telemetry): move transaction_amount metric into ilp connector. extract metric collection logic from spread codebase into "collector" functions in the telemetry folder. (this approach intends using callbacks that provide the necessary data after bussiness logic is executed, in order to separate telemetry logic out of the bussiness. The data is then fed into the "collector" functions) Increased seed liquidity for localenv --- localenv/cloud-nine-wallet/seed.yml | 27 +++--- localenv/happy-life-bank/seed.yml | 27 +++--- packages/backend/src/accounting/service.ts | 53 ++++-------- packages/backend/src/index.ts | 82 +++++++++++-------- .../ilp/connector/core/rafiki.ts | 21 ++++- packages/backend/src/telemetry/collectors.ts | 59 +++++++++++++ 6 files changed, 167 insertions(+), 102 deletions(-) create mode 100644 packages/backend/src/telemetry/collectors.ts diff --git a/localenv/cloud-nine-wallet/seed.yml b/localenv/cloud-nine-wallet/seed.yml index bb8d40cca8..c30eeb475f 100644 --- a/localenv/cloud-nine-wallet/seed.yml +++ b/localenv/cloud-nine-wallet/seed.yml @@ -5,42 +5,41 @@ self: assets: - code: USD scale: 2 - liquidity: 1000000 - liquidityThreshold: 100000 + liquidity: 100000000 + liquidityThreshold: 10000000 - code: EUR scale: 2 - liquidity: 1000000 - liquidityThreshold: 100000 + liquidity: 100000000 + liquidityThreshold: 10000000 - code: MXN scale: 2 - liquidity: 1000000 - liquidityThreshold: 100000 + liquidity: 100000000 + liquidityThreshold: 10000000 - code: JPY scale: 0 - liquidity: 10000 - liquidityThreshold: 1000 -peeringAsset: 'USD' + liquidity: 1000000 + liquidityThreshold: 100000 peers: - - initialLiquidity: '100000' + - initialLiquidity: '10000000' peerUrl: http://happy-life-bank-backend:3002 peerIlpAddress: test.happy-life-bank - liquidityThreshold: 10000 + liquidityThreshold: 1000000 accounts: - name: 'Grace Franklin' path: accounts/gfranklin id: 742ab7cd-1624-4d2e-af6e-e15a71638669 - initialBalance: 4000 + initialBalance: 40000000 postmanEnvVar: gfranklinWalletAddress assetCode: USD - name: 'Bert Hamchest' id: a9adbe1a-df31-4766-87c9-d2cb2e636a9b - initialBalance: 400000 + initialBalance: 40000000 path: accounts/bhamchest postmanEnvVar: bhamchestWalletAddress assetCode: USD - name: "World's Best Donut Co" id: 5726eefe-8737-459d-a36b-0acce152cb90 - initialBalance: 200000 + initialBalance: 20000000 path: accounts/wbdc postmanEnvVar: wbdcWalletAddress assetCode: USD diff --git a/localenv/happy-life-bank/seed.yml b/localenv/happy-life-bank/seed.yml index acced59f02..c3e147f2d1 100644 --- a/localenv/happy-life-bank/seed.yml +++ b/localenv/happy-life-bank/seed.yml @@ -5,26 +5,25 @@ self: assets: - code: USD scale: 2 - liquidity: 1000000 - liquidityThreshold: 100000 + liquidity: 10000000000 + liquidityThreshold: 100000000 - code: EUR scale: 2 - liquidity: 1000000 - liquidityThreshold: 100000 + liquidity: 10000000000 + liquidityThreshold: 1000000 - code: MXN scale: 2 - liquidity: 1000000 - liquidityThreshold: 100000 + liquidity: 10000000000 + liquidityThreshold: 10000000 - code: JPY scale: 0 - liquidity: 10000 - liquidityThreshold: 1000 -peeringAsset: 'USD' + liquidity: 1000000000 + liquidityThreshold: 1000000 peers: - initialLiquidity: '1000000000000' peerUrl: http://cloud-nine-wallet-backend:3002 peerIlpAddress: test.cloud-nine-wallet - liquidityThreshold: 100000 + liquidityThreshold: 1000000 accounts: - name: 'Philip Fry' path: accounts/pfry @@ -34,26 +33,26 @@ accounts: assetCode: USD - name: 'PlanEx Corp' id: a455cc54-b583-455b-836a-e5275c5c05b7 - initialBalance: 200000 + initialBalance: 2000000 path: accounts/planex postmanEnvVar: planexWalletAddress assetCode: USD - name: 'Alice Smith' path: accounts/asmith id: f47ac10b-58cc-4372-a567-0e02b2c3d479 - initialBalance: 500 + initialBalance: 5000000 postmanEnvVar: asmithWalletAddress skipWalletAddressCreation: true assetCode: USD - name: 'Lars' path: accounts/lars id: fd4ecbc9-205d-4ecd-a030-507d6ce2bde6 - initialBalance: 500000 + initialBalance: 50000000 assetCode: EUR - name: 'David' path: accounts/david id: 60257507-3191-4507-9d77-9071fd6b3c30 - initialBalance: 15000000 + initialBalance: 1500000000 assetCode: MXN rates: EUR: diff --git a/packages/backend/src/accounting/service.ts b/packages/backend/src/accounting/service.ts index af9bbf483b..1c6ad5a058 100644 --- a/packages/backend/src/accounting/service.ts +++ b/packages/backend/src/accounting/service.ts @@ -1,6 +1,7 @@ import { TransactionOrKnex } from 'objection' +import { collectTransactionCountMetric } from '../telemetry/collectors' +import { TelemetryService } from '../telemetry/meter' import { isTransferError, TransferError } from './errors' -import { Metrics, TelemetryService } from '../telemetry/meter' export enum LiquidityAccountType { ASSET = 'ASSET', @@ -10,13 +11,16 @@ export enum LiquidityAccountType { WEB_MONETIZATION = 'WEB_MONETIZATION' } +export type LiquidityAccountAsset = { + id: string + code?: string + scale?: number + ledger: number +} + export interface LiquidityAccount { id: string - asset: { - id: string - code?: string - scale?: number - ledger: number + asset: LiquidityAccountAsset & { onDebit?: (options: OnDebitOptions) => Promise } onCredit?: (options: OnCreditOptions) => Promise @@ -188,38 +192,11 @@ export async function createAccountToAccountTransfer( withdrawalThrottleDelay }) - telemetry?.getCounter(Metrics.TRANSACTIONS_TOTAL)?.add(1, { - source: telemetry?.getServiceName() ?? 'Rafiki', - asset_code: destinationAccount.asset.code - }) - - console.log( - `######################## [TELEMETRY]Gathering Transaction amount ####################` - ) - - const scalingFactor = destinationAccount.asset.scale - ? Math.pow(10, 4 - destinationAccount.asset.scale) - : undefined - console.log( - `scaling factor is: Math.pow(10 , 4 - ${destinationAccount.asset.scale}) === ${scalingFactor}` - ) - - const totalReceivedInAssetScale4 = - Number(totalReceived) * Number(scalingFactor) - - console.log( - `totalReceivedInAssetScale4 = totalReceived(${totalReceived}) * scalingFactor(${scalingFactor})` - ) - - console.log( - `totalReceivedInAssetScale4 is ${totalReceivedInAssetScale4}` - ) - telemetry - ?.getCounter(Metrics.TRANSACTIONS_AMOUNT) - ?.add(totalReceivedInAssetScale4, { - asset_code: destinationAccount.asset.code, - source: telemetry?.getServiceName() ?? 'Rafiki' - }) + telemetry && + collectTransactionCountMetric( + telemetry, + destinationAccount.asset.code + ) } }, void: async (): Promise => { diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 21c425d2be..de666671d9 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -1,54 +1,56 @@ -import path from 'path' -import createLogger from 'pino' -import { knex } from 'knex' -import { Model } from 'objection' import { Ioc, IocContract } from '@adonisjs/fold' import { Redis } from 'ioredis' +import { knex } from 'knex' +import { Model } from 'objection' +import path from 'path' +import createLogger from 'pino' import { createClient } from 'tigerbeetle-node' import { createClient as createIntrospectionClient } from 'token-introspection' +import { createAuthenticatedClient as createOpenPaymentsClient } from '@interledger/open-payments' +import { createOpenAPI } from '@interledger/openapi' +import { StreamServer } from '@interledger/stream-receiver' +import axios from 'axios' +import { createAccountingService as createPsqlAccountingService } from './accounting/psql/service' +import { createAccountingService as createTigerbeetleAccountingService } from './accounting/tigerbeetle/service' import { App, AppServices } from './app' +import { createAssetService } from './asset/service' import { Config } from './config/app' -import { createRatesService } from './rates/service' -import { createQuoteRoutes } from './open_payments/quote/routes' -import { createQuoteService } from './open_payments/quote/service' +import { createFeeService } from './fee/service' +import { createAuthServerService } from './open_payments/authServer/service' +import { createGrantService } from './open_payments/grant/service' +import { createCombinedPaymentService } from './open_payments/payment/combined/service' +import { createIncomingPaymentRoutes } from './open_payments/payment/incoming/routes' +import { createIncomingPaymentService } from './open_payments/payment/incoming/service' +import { createRemoteIncomingPaymentService } from './open_payments/payment/incoming_remote/service' import { createOutgoingPaymentRoutes } from './open_payments/payment/outgoing/routes' import { createOutgoingPaymentService } from './open_payments/payment/outgoing/service' +import { createQuoteRoutes } from './open_payments/quote/routes' +import { createQuoteService } from './open_payments/quote/service' +import { createReceiverService } from './open_payments/receiver/service' +import { createWalletAddressKeyRoutes } from './open_payments/wallet_address/key/routes' +import { createWalletAddressKeyService } from './open_payments/wallet_address/key/service' +import { createWalletAddressRoutes } from './open_payments/wallet_address/routes' +import { createWalletAddressService } from './open_payments/wallet_address/service' +import { createPaymentMethodHandlerService } from './payment-method/handler/service' +import { createAutoPeeringRoutes } from './payment-method/ilp/auto-peering/routes' +import { createAutoPeeringService } from './payment-method/ilp/auto-peering/service' +import { createConnectorService } from './payment-method/ilp/connector' import { - createIlpPlugin, IlpPlugin, - IlpPluginOptions + IlpPluginOptions, + createIlpPlugin } from './payment-method/ilp/ilp_plugin' import { createHttpTokenService } from './payment-method/ilp/peer-http-token/service' -import { createAssetService } from './asset/service' -import { createAccountingService as createTigerbeetleAccountingService } from './accounting/tigerbeetle/service' -import { createAccountingService as createPsqlAccountingService } from './accounting/psql/service' import { createPeerService } from './payment-method/ilp/peer/service' -import { createAuthServerService } from './open_payments/authServer/service' -import { createGrantService } from './open_payments/grant/service' +import { createIlpPaymentService } from './payment-method/ilp/service' import { createSPSPRoutes } from './payment-method/ilp/spsp/routes' -import { createWalletAddressService } from './open_payments/wallet_address/service' -import { createWalletAddressKeyRoutes } from './open_payments/wallet_address/key/routes' -import { createWalletAddressRoutes } from './open_payments/wallet_address/routes' -import { createIncomingPaymentRoutes } from './open_payments/payment/incoming/routes' -import { createIncomingPaymentService } from './open_payments/payment/incoming/service' -import { StreamServer } from '@interledger/stream-receiver' -import { createWebhookService } from './webhook/service' -import { createConnectorService } from './payment-method/ilp/connector' -import { createOpenAPI } from '@interledger/openapi' -import { createAuthenticatedClient as createOpenPaymentsClient } from '@interledger/open-payments' import { createStreamCredentialsService } from './payment-method/ilp/stream-credentials/service' -import { createWalletAddressKeyService } from './open_payments/wallet_address/key/service' -import { createReceiverService } from './open_payments/receiver/service' -import { createRemoteIncomingPaymentService } from './open_payments/payment/incoming_remote/service' -import { createCombinedPaymentService } from './open_payments/payment/combined/service' -import { createFeeService } from './fee/service' -import { createAutoPeeringService } from './payment-method/ilp/auto-peering/service' -import { createAutoPeeringRoutes } from './payment-method/ilp/auto-peering/routes' -import axios from 'axios' -import { createIlpPaymentService } from './payment-method/ilp/service' -import { createPaymentMethodHandlerService } from './payment-method/handler/service' +import { createRatesService } from './rates/service' +import { collectTransactionsAmountMetric } from './telemetry/collectors' import { TelemetryService, createTelemetryService } from './telemetry/meter' +import { createWebhookService } from './webhook/service' +import { IlpObservabilityParameters } from './payment-method/ilp/connector/core' BigInt.prototype.toJSON = function () { return this.toString() @@ -353,12 +355,22 @@ export function initIocContainer( container.singleton('makeIlpPlugin', async (deps) => { const connectorApp = await deps.use('connectorApp') + const telemetry = await deps.use('telemetry') + const observabilityCallback = (params: IlpObservabilityParameters) => { + collectTransactionsAmountMetric(telemetry, params) + } + return ({ sourceAccount, unfulfillable = false }: IlpPluginOptions): IlpPlugin => { return createIlpPlugin((data: Buffer): Promise => { - return connectorApp.handleIlpData(sourceAccount, unfulfillable, data) + return connectorApp.handleIlpData( + sourceAccount, + unfulfillable, + data, + observabilityCallback + ) }) } }) diff --git a/packages/backend/src/payment-method/ilp/connector/core/rafiki.ts b/packages/backend/src/payment-method/ilp/connector/core/rafiki.ts index 05081ee2b8..fef3abdeee 100644 --- a/packages/backend/src/payment-method/ilp/connector/core/rafiki.ts +++ b/packages/backend/src/payment-method/ilp/connector/core/rafiki.ts @@ -19,6 +19,7 @@ import { } from '../../../../accounting/errors' import { LiquidityAccount, + LiquidityAccountAsset, LiquidityAccountType, Transaction } from '../../../../accounting/service' @@ -26,6 +27,7 @@ import { AssetOptions } from '../../../../asset/service' import { WalletAddressService } from '../../../../open_payments/wallet_address/service' import { IncomingPaymentService } from '../../../../open_payments/payment/incoming/service' import { PeerService } from '../../peer/service' +import { TelemetryService } from '../../../../telemetry/meter' // Model classes that represent an Interledger sender, receiver, or // connector SHOULD implement this ConnectorAccount interface. @@ -71,6 +73,7 @@ export interface AccountingService { export interface RafikiServices { //router: Router accounting: AccountingService + telemetry?: TelemetryService walletAddresses: WalletAddressService logger: Logger incomingPayments: IncomingPaymentService @@ -111,6 +114,15 @@ export type ILPContext = { revertTotalReceived?: () => Promise state: T } +export type IlpObservabilityParameters = { + asset: LiquidityAccountAsset + amount: bigint + unfulfillable: boolean +} + +export type IlpObservabilityCallback = ( + params: IlpObservabilityParameters +) => void export class Rafiki { //private _router?: Router @@ -169,10 +181,17 @@ export class Rafiki { async handleIlpData( sourceAccount: IncomingAccount, unfulfillable: boolean, - rawPrepare: Buffer + rawPrepare: Buffer, + observabilityCallback: IlpObservabilityCallback ): Promise { const prepare = new ZeroCopyIlpPrepare(rawPrepare) const response = new IlpResponse() + + observabilityCallback({ + amount: BigInt(prepare.amount), + asset: sourceAccount.asset, + unfulfillable + }) await this.routes( { request: { prepare, rawPrepare }, diff --git a/packages/backend/src/telemetry/collectors.ts b/packages/backend/src/telemetry/collectors.ts new file mode 100644 index 0000000000..01ede655a4 --- /dev/null +++ b/packages/backend/src/telemetry/collectors.ts @@ -0,0 +1,59 @@ +import { IlpObservabilityParameters } from '../payment-method/ilp/connector/core/rafiki' +import { Metrics, TelemetryService } from './meter' + +export function collectTransactionsAmountMetric( + telemetry: TelemetryService, + params: IlpObservabilityParameters +): void { + const { asset, amount, unfulfillable } = params + + if (unfulfillable) { + console.log('unfulfillable') + //can collect metrics on unfulfillable packets here + return + } + + console.log( + `######################## [TELEMETRY]Gathering Transaction Amount Metric............` + ) + + const scalingFactor = asset.scale ? Math.pow(10, 4 - asset.scale) : undefined + console.log( + `scaling factor is: Math.pow(10 , 4 - ${asset.scale}) === ${scalingFactor}` + ) + + const totalReceivedInAssetScale4 = Number(amount) * Number(scalingFactor) + + console.log( + `totalReceivedInAssetScale4 (${totalReceivedInAssetScale4}) = totalReceived(${amount}) * scalingFactor(${scalingFactor})` + ) + + telemetry + ?.getCounter(Metrics.TRANSACTIONS_AMOUNT) + ?.add(totalReceivedInAssetScale4, { + asset_code: asset.code, + source: telemetry?.getServiceName() ?? 'Rafiki' + }) + + console.log( + '######################## [TELEMETRY] Transaction Amount Metric Collected ####################' + ) +} + +export function collectTransactionCountMetric( + telemetry: TelemetryService, + assetCode?: string +): void { + console.log( + `######################## [TELEMETRY]Gathering Transaction Count Metric..........` + ) + + telemetry?.getCounter(Metrics.TRANSACTIONS_TOTAL)?.add(1, { + source: telemetry?.getServiceName() ?? 'Rafiki', + asset_code: assetCode + }) + + console.log( + '######################## [TELEMETRY] Transaction Count Metric Collected ####################' + ) +} From ec31fad1f3ffec894ab75a1d3ed08f84418cf292 Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Fri, 24 Nov 2023 19:10:07 +0200 Subject: [PATCH 03/67] chore(telemetry): format --- packages/backend/src/telemetry/collectors.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/backend/src/telemetry/collectors.ts b/packages/backend/src/telemetry/collectors.ts index 01ede655a4..23907d95a5 100644 --- a/packages/backend/src/telemetry/collectors.ts +++ b/packages/backend/src/telemetry/collectors.ts @@ -8,8 +8,7 @@ export function collectTransactionsAmountMetric( const { asset, amount, unfulfillable } = params if (unfulfillable) { - console.log('unfulfillable') - //can collect metrics on unfulfillable packets here + //can collect metrics such as count of unfulfillable packets here return } From f0e9e33c597e73182aac858f649c5ac3c7621caf Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Fri, 24 Nov 2023 20:36:02 +0200 Subject: [PATCH 04/67] feat(telemetry): go back to usual DI implementation the collector functions are now part of TelemetryService --- packages/backend/src/accounting/service.ts | 7 +-- packages/backend/src/index.ts | 38 +++++------- .../ilp/connector/core/rafiki.ts | 30 +++++----- .../src/payment-method/ilp/connector/index.ts | 34 ++++++----- packages/backend/src/telemetry/collectors.ts | 58 ------------------- packages/backend/src/telemetry/meter.ts | 58 +++++++++++++++++++ 6 files changed, 107 insertions(+), 118 deletions(-) delete mode 100644 packages/backend/src/telemetry/collectors.ts diff --git a/packages/backend/src/accounting/service.ts b/packages/backend/src/accounting/service.ts index 1c6ad5a058..0b2b1f847c 100644 --- a/packages/backend/src/accounting/service.ts +++ b/packages/backend/src/accounting/service.ts @@ -1,5 +1,4 @@ import { TransactionOrKnex } from 'objection' -import { collectTransactionCountMetric } from '../telemetry/collectors' import { TelemetryService } from '../telemetry/meter' import { isTransferError, TransferError } from './errors' @@ -192,11 +191,7 @@ export async function createAccountToAccountTransfer( withdrawalThrottleDelay }) - telemetry && - collectTransactionCountMetric( - telemetry, - destinationAccount.asset.code - ) + telemetry?.collectTransactionCountMetric(destinationAccount.asset.code) } }, void: async (): Promise => { diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index de666671d9..2e5becd544 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -47,10 +47,8 @@ import { createIlpPaymentService } from './payment-method/ilp/service' import { createSPSPRoutes } from './payment-method/ilp/spsp/routes' import { createStreamCredentialsService } from './payment-method/ilp/stream-credentials/service' import { createRatesService } from './rates/service' -import { collectTransactionsAmountMetric } from './telemetry/collectors' import { TelemetryService, createTelemetryService } from './telemetry/meter' import { createWebhookService } from './webhook/service' -import { IlpObservabilityParameters } from './payment-method/ilp/connector/core' BigInt.prototype.toJSON = function () { return this.toString() @@ -353,34 +351,13 @@ export function initIocContainer( }) }) - container.singleton('makeIlpPlugin', async (deps) => { - const connectorApp = await deps.use('connectorApp') - const telemetry = await deps.use('telemetry') - const observabilityCallback = (params: IlpObservabilityParameters) => { - collectTransactionsAmountMetric(telemetry, params) - } - - return ({ - sourceAccount, - unfulfillable = false - }: IlpPluginOptions): IlpPlugin => { - return createIlpPlugin((data: Buffer): Promise => { - return connectorApp.handleIlpData( - sourceAccount, - unfulfillable, - data, - observabilityCallback - ) - }) - } - }) - container.singleton('connectorApp', async (deps) => { const config = await deps.use('config') return await createConnectorService({ logger: await deps.use('logger'), redis: await deps.use('redis'), accountingService: await deps.use('accountingService'), + telemetry: await deps.use('telemetry'), walletAddressService: await deps.use('walletAddressService'), incomingPaymentService: await deps.use('incomingPaymentService'), peerService: await deps.use('peerService'), @@ -390,6 +367,19 @@ export function initIocContainer( }) }) + container.singleton('makeIlpPlugin', async (deps) => { + const connectorApp = await deps.use('connectorApp') + + return ({ + sourceAccount, + unfulfillable = false + }: IlpPluginOptions): IlpPlugin => { + return createIlpPlugin((data: Buffer): Promise => { + return connectorApp.handleIlpData(sourceAccount, unfulfillable, data) + }) + } + }) + container.singleton('combinedPaymentService', async (deps) => { return await createCombinedPaymentService({ logger: await deps.use('logger'), diff --git a/packages/backend/src/payment-method/ilp/connector/core/rafiki.ts b/packages/backend/src/payment-method/ilp/connector/core/rafiki.ts index fef3abdeee..b082046b7a 100644 --- a/packages/backend/src/payment-method/ilp/connector/core/rafiki.ts +++ b/packages/backend/src/payment-method/ilp/connector/core/rafiki.ts @@ -1,18 +1,11 @@ import * as http from 'http' /* eslint-disable @typescript-eslint/no-explicit-any */ -import Koa, { Middleware } from 'koa' +import { StreamServer } from '@interledger/stream-receiver' +import { Errors } from 'ilp-packet' import { Redis } from 'ioredis' +import Koa, { Middleware } from 'koa' import { Logger } from 'pino' -import { Errors } from 'ilp-packet' -import { StreamServer } from '@interledger/stream-receiver' //import { Router } from './services/router' -import { - createIlpPacketMiddleware, - ZeroCopyIlpPrepare, - IlpResponse -} from './middleware/ilp-packet' -import { createTokenAuthMiddleware } from './middleware' -import { RatesService } from '../../../../rates/service' import { CreateAccountError, TransferError @@ -24,10 +17,17 @@ import { Transaction } from '../../../../accounting/service' import { AssetOptions } from '../../../../asset/service' -import { WalletAddressService } from '../../../../open_payments/wallet_address/service' import { IncomingPaymentService } from '../../../../open_payments/payment/incoming/service' -import { PeerService } from '../../peer/service' +import { WalletAddressService } from '../../../../open_payments/wallet_address/service' +import { RatesService } from '../../../../rates/service' import { TelemetryService } from '../../../../telemetry/meter' +import { PeerService } from '../../peer/service' +import { createTokenAuthMiddleware } from './middleware' +import { + IlpResponse, + ZeroCopyIlpPrepare, + createIlpPacketMiddleware +} from './middleware/ilp-packet' // Model classes that represent an Interledger sender, receiver, or // connector SHOULD implement this ConnectorAccount interface. @@ -181,17 +181,17 @@ export class Rafiki { async handleIlpData( sourceAccount: IncomingAccount, unfulfillable: boolean, - rawPrepare: Buffer, - observabilityCallback: IlpObservabilityCallback + rawPrepare: Buffer ): Promise { const prepare = new ZeroCopyIlpPrepare(rawPrepare) const response = new IlpResponse() - observabilityCallback({ + this.config.telemetry?.collectTransactionsAmountMetric({ amount: BigInt(prepare.amount), asset: sourceAccount.asset, unfulfillable }) + await this.routes( { request: { prepare, rawPrepare }, diff --git a/packages/backend/src/payment-method/ilp/connector/index.ts b/packages/backend/src/payment-method/ilp/connector/index.ts index b7fdfc9229..d539baf372 100644 --- a/packages/backend/src/payment-method/ilp/connector/index.ts +++ b/packages/backend/src/payment-method/ilp/connector/index.ts @@ -1,37 +1,39 @@ -import { Redis } from 'ioredis' import { StreamServer } from '@interledger/stream-receiver' +import { Redis } from 'ioredis' +import { AccountingService } from '../../../accounting/service' +import { IncomingPaymentService } from '../../../open_payments/payment/incoming/service' +import { WalletAddressService } from '../../../open_payments/wallet_address/service' +import { RatesService } from '../../../rates/service' +import { BaseService } from '../../../shared/baseService' +import { TelemetryService } from '../../../telemetry/meter' +import { PeerService } from '../peer/service' import { - createApp, - Rafiki, ILPContext, ILPMiddleware, + Rafiki, + createAccountMiddleware, + createApp, createBalanceMiddleware, - createIncomingErrorHandlerMiddleware, - createIldcpMiddleware, - createStreamController, - createOutgoingExpireMiddleware, createClientController, + createIldcpMiddleware, + createIncomingErrorHandlerMiddleware, createIncomingMaxPacketAmountMiddleware, createIncomingRateLimitMiddleware, createIncomingThroughputMiddleware, + createOutgoingExpireMiddleware, createOutgoingReduceExpiryMiddleware, createOutgoingThroughputMiddleware, createOutgoingValidateFulfillmentMiddleware, - createAccountMiddleware, - createStreamAddressMiddleware + createStreamAddressMiddleware, + createStreamController } from './core' -import { AccountingService } from '../../../accounting/service' -import { WalletAddressService } from '../../../open_payments/wallet_address/service' -import { IncomingPaymentService } from '../../../open_payments/payment/incoming/service' -import { PeerService } from '../peer/service' -import { RatesService } from '../../../rates/service' -import { BaseService } from '../../../shared/baseService' interface ServiceDependencies extends BaseService { redis: Redis ratesService: RatesService accountingService: AccountingService + telemetry: TelemetryService walletAddressService: WalletAddressService incomingPaymentService: IncomingPaymentService peerService: PeerService @@ -44,6 +46,7 @@ export async function createConnectorService({ redis, ratesService, accountingService, + telemetry, walletAddressService, incomingPaymentService, peerService, @@ -57,6 +60,7 @@ export async function createConnectorService({ service: 'ConnectorService' }), accounting: accountingService, + telemetry, walletAddresses: walletAddressService, incomingPayments: incomingPaymentService, peers: peerService, diff --git a/packages/backend/src/telemetry/collectors.ts b/packages/backend/src/telemetry/collectors.ts deleted file mode 100644 index 23907d95a5..0000000000 --- a/packages/backend/src/telemetry/collectors.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { IlpObservabilityParameters } from '../payment-method/ilp/connector/core/rafiki' -import { Metrics, TelemetryService } from './meter' - -export function collectTransactionsAmountMetric( - telemetry: TelemetryService, - params: IlpObservabilityParameters -): void { - const { asset, amount, unfulfillable } = params - - if (unfulfillable) { - //can collect metrics such as count of unfulfillable packets here - return - } - - console.log( - `######################## [TELEMETRY]Gathering Transaction Amount Metric............` - ) - - const scalingFactor = asset.scale ? Math.pow(10, 4 - asset.scale) : undefined - console.log( - `scaling factor is: Math.pow(10 , 4 - ${asset.scale}) === ${scalingFactor}` - ) - - const totalReceivedInAssetScale4 = Number(amount) * Number(scalingFactor) - - console.log( - `totalReceivedInAssetScale4 (${totalReceivedInAssetScale4}) = totalReceived(${amount}) * scalingFactor(${scalingFactor})` - ) - - telemetry - ?.getCounter(Metrics.TRANSACTIONS_AMOUNT) - ?.add(totalReceivedInAssetScale4, { - asset_code: asset.code, - source: telemetry?.getServiceName() ?? 'Rafiki' - }) - - console.log( - '######################## [TELEMETRY] Transaction Amount Metric Collected ####################' - ) -} - -export function collectTransactionCountMetric( - telemetry: TelemetryService, - assetCode?: string -): void { - console.log( - `######################## [TELEMETRY]Gathering Transaction Count Metric..........` - ) - - telemetry?.getCounter(Metrics.TRANSACTIONS_TOTAL)?.add(1, { - source: telemetry?.getServiceName() ?? 'Rafiki', - asset_code: assetCode - }) - - console.log( - '######################## [TELEMETRY] Transaction Count Metric Collected ####################' - ) -} diff --git a/packages/backend/src/telemetry/meter.ts b/packages/backend/src/telemetry/meter.ts index 1f6049f779..648d67083d 100644 --- a/packages/backend/src/telemetry/meter.ts +++ b/packages/backend/src/telemetry/meter.ts @@ -15,10 +15,13 @@ import { } from '@opentelemetry/sdk-metrics' import { BaseService } from '../shared/baseService' +import { IlpObservabilityParameters } from '../payment-method/ilp/connector/core' export interface TelemetryService { getCounter(name: string): Counter | undefined getServiceName(): string | undefined + collectTransactionsAmountMetric(params: IlpObservabilityParameters): void + collectTransactionCountMetric(assetCode?: string): void } interface TelemetryServiceDependencies extends BaseService { @@ -93,4 +96,59 @@ class TelemetryServiceImpl implements TelemetryService { public getServiceName(): string | undefined { return this.serviceName } + + public collectTransactionsAmountMetric( + params: IlpObservabilityParameters + ): void { + const { asset, amount, unfulfillable } = params + + if (unfulfillable || !amount) { + //can collect metrics such as count of unfulfillable packets here + return + } + + console.log( + `######################## [TELEMETRY]Gathering Transaction Amount Metric............` + ) + + const scalingFactor = asset.scale + ? Math.pow(10, 4 - asset.scale) + : undefined + console.log( + `scaling factor is: Math.pow(10 , 4 - ${asset.scale}) === ${scalingFactor}` + ) + + const totalReceivedInAssetScale4 = Number(amount) * Number(scalingFactor) + + console.log( + `totalReceivedInAssetScale4 (${totalReceivedInAssetScale4}) = totalReceived(${amount}) * scalingFactor(${scalingFactor})` + ) + + this?.getCounter(Metrics.TRANSACTIONS_AMOUNT)?.add( + totalReceivedInAssetScale4, + { + asset_code: asset.code, + source: this.getServiceName() ?? 'Rafiki' + } + ) + + console.log( + '######################## [TELEMETRY] Transaction Amount Metric Collected ####################' + ) + } + + public collectTransactionCountMetric(assetCode?: string): void { + console.log( + `######################## [TELEMETRY]Gathering Transaction Count Metric..........` + ) + + this.getCounter(Metrics.TRANSACTIONS_TOTAL)?.add(1, { + source: this.getServiceName() ?? 'Rafiki', + asset_code: assetCode + }) + + console.log( + '######################## [TELEMETRY] Transaction Count Metric Collected ####################' + ) + } } From 31402d6297356b31b7f9ba5132adb128b38e6567 Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Fri, 24 Nov 2023 22:57:11 +0200 Subject: [PATCH 05/67] feat(telemetry): remove collector methods from telemetry service & reintroduce logic in respective services --- packages/backend/src/accounting/service.ts | 7 ++- .../ilp/connector/core/rafiki.ts | 21 +++++-- packages/backend/src/telemetry/meter.ts | 58 ------------------- 3 files changed, 20 insertions(+), 66 deletions(-) diff --git a/packages/backend/src/accounting/service.ts b/packages/backend/src/accounting/service.ts index 0b2b1f847c..6a77dd7d51 100644 --- a/packages/backend/src/accounting/service.ts +++ b/packages/backend/src/accounting/service.ts @@ -1,5 +1,5 @@ import { TransactionOrKnex } from 'objection' -import { TelemetryService } from '../telemetry/meter' +import { Metrics, TelemetryService } from '../telemetry/meter' import { isTransferError, TransferError } from './errors' export enum LiquidityAccountType { @@ -191,7 +191,10 @@ export async function createAccountToAccountTransfer( withdrawalThrottleDelay }) - telemetry?.collectTransactionCountMetric(destinationAccount.asset.code) + telemetry?.getCounter(Metrics.TRANSACTIONS_TOTAL)?.add(1, { + source: telemetry.getServiceName() ?? 'Rafiki', + asset_code: destinationAccount.asset.code + }) } }, void: async (): Promise => { diff --git a/packages/backend/src/payment-method/ilp/connector/core/rafiki.ts b/packages/backend/src/payment-method/ilp/connector/core/rafiki.ts index b082046b7a..07b9467b58 100644 --- a/packages/backend/src/payment-method/ilp/connector/core/rafiki.ts +++ b/packages/backend/src/payment-method/ilp/connector/core/rafiki.ts @@ -20,7 +20,7 @@ import { AssetOptions } from '../../../../asset/service' import { IncomingPaymentService } from '../../../../open_payments/payment/incoming/service' import { WalletAddressService } from '../../../../open_payments/wallet_address/service' import { RatesService } from '../../../../rates/service' -import { TelemetryService } from '../../../../telemetry/meter' +import { Metrics, TelemetryService } from '../../../../telemetry/meter' import { PeerService } from '../../peer/service' import { createTokenAuthMiddleware } from './middleware' import { @@ -186,11 +186,20 @@ export class Rafiki { const prepare = new ZeroCopyIlpPrepare(rawPrepare) const response = new IlpResponse() - this.config.telemetry?.collectTransactionsAmountMetric({ - amount: BigInt(prepare.amount), - asset: sourceAccount.asset, - unfulfillable - }) + if (!unfulfillable && Number(prepare.amount)) { + const scalingFactor = sourceAccount.asset.scale + ? Math.pow(10, 4 - sourceAccount.asset.scale) + : undefined + const totalReceivedInAssetScale4 = + Number(prepare.amount) * Number(scalingFactor) + + this.config.telemetry + ?.getCounter(Metrics.TRANSACTIONS_AMOUNT) + ?.add(totalReceivedInAssetScale4, { + asset_code: sourceAccount.asset.code, + source: this.config.telemetry?.getServiceName() ?? 'Rafiki' + }) + } await this.routes( { diff --git a/packages/backend/src/telemetry/meter.ts b/packages/backend/src/telemetry/meter.ts index 648d67083d..1f6049f779 100644 --- a/packages/backend/src/telemetry/meter.ts +++ b/packages/backend/src/telemetry/meter.ts @@ -15,13 +15,10 @@ import { } from '@opentelemetry/sdk-metrics' import { BaseService } from '../shared/baseService' -import { IlpObservabilityParameters } from '../payment-method/ilp/connector/core' export interface TelemetryService { getCounter(name: string): Counter | undefined getServiceName(): string | undefined - collectTransactionsAmountMetric(params: IlpObservabilityParameters): void - collectTransactionCountMetric(assetCode?: string): void } interface TelemetryServiceDependencies extends BaseService { @@ -96,59 +93,4 @@ class TelemetryServiceImpl implements TelemetryService { public getServiceName(): string | undefined { return this.serviceName } - - public collectTransactionsAmountMetric( - params: IlpObservabilityParameters - ): void { - const { asset, amount, unfulfillable } = params - - if (unfulfillable || !amount) { - //can collect metrics such as count of unfulfillable packets here - return - } - - console.log( - `######################## [TELEMETRY]Gathering Transaction Amount Metric............` - ) - - const scalingFactor = asset.scale - ? Math.pow(10, 4 - asset.scale) - : undefined - console.log( - `scaling factor is: Math.pow(10 , 4 - ${asset.scale}) === ${scalingFactor}` - ) - - const totalReceivedInAssetScale4 = Number(amount) * Number(scalingFactor) - - console.log( - `totalReceivedInAssetScale4 (${totalReceivedInAssetScale4}) = totalReceived(${amount}) * scalingFactor(${scalingFactor})` - ) - - this?.getCounter(Metrics.TRANSACTIONS_AMOUNT)?.add( - totalReceivedInAssetScale4, - { - asset_code: asset.code, - source: this.getServiceName() ?? 'Rafiki' - } - ) - - console.log( - '######################## [TELEMETRY] Transaction Amount Metric Collected ####################' - ) - } - - public collectTransactionCountMetric(assetCode?: string): void { - console.log( - `######################## [TELEMETRY]Gathering Transaction Count Metric..........` - ) - - this.getCounter(Metrics.TRANSACTIONS_TOTAL)?.add(1, { - source: this.getServiceName() ?? 'Rafiki', - asset_code: assetCode - }) - - console.log( - '######################## [TELEMETRY] Transaction Count Metric Collected ####################' - ) - } } From 8cc1f989ab796f34ef404bffd0b5ac3d5cc4223e Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Mon, 27 Nov 2023 12:55:56 +0200 Subject: [PATCH 06/67] feat(telemetry): use already existing env variable (instance_name) --- localenv/cloud-nine-wallet/docker-compose.yml | 2 +- localenv/happy-life-bank/docker-compose.yml | 3 +-- packages/backend/src/config/app.ts | 1 - packages/backend/src/index.ts | 2 +- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/localenv/cloud-nine-wallet/docker-compose.yml b/localenv/cloud-nine-wallet/docker-compose.yml index 85a2847b19..118202d2b8 100644 --- a/localenv/cloud-nine-wallet/docker-compose.yml +++ b/localenv/cloud-nine-wallet/docker-compose.yml @@ -40,6 +40,7 @@ services: - rafiki environment: NODE_ENV: ${NODE_ENV:-development} + INSTANCE_NAME: CLOUD-NINE TRUST_PROXY: ${TRUST_PROXY} LOG_LEVEL: debug ADMIN_PORT: 3001 @@ -62,7 +63,6 @@ services: ENABLE_TELEMETRY: true OPEN_TELEMETRY_COLLECTOR_URL: http://cloud-nine-otel-collector:4317 OPEN_TELEMETRY_EXPORT_INTERVAL: 15000 - TELEMETRY_SERVICE_NAME: CLOUD-NINE depends_on: - shared-database - shared-redis diff --git a/localenv/happy-life-bank/docker-compose.yml b/localenv/happy-life-bank/docker-compose.yml index 22ad9e5b1b..5c7f4aa895 100644 --- a/localenv/happy-life-bank/docker-compose.yml +++ b/localenv/happy-life-bank/docker-compose.yml @@ -34,6 +34,7 @@ services: - rafiki environment: NODE_ENV: development + INSTANCE_NAME: HAPPY-LIFE LOG_LEVEL: debug ADMIN_PORT: 3001 CONNECTOR_PORT: 3002 @@ -54,11 +55,9 @@ services: ENABLE_TELEMETRY: true OPEN_TELEMETRY_COLLECTOR_URL: http://happy-life-otel-collector:4317 OPEN_TELEMETRY_EXPORT_INTERVAL: 15000 - TELEMETRY_SERVICE_NAME: HAPPY-LIFE depends_on: - cloud-nine-backend - # - happy-life-otel-collector happy-life-auth: hostname: happy-life-bank-auth image: rafiki-auth diff --git a/packages/backend/src/config/app.ts b/packages/backend/src/config/app.ts index 98ea3edbbd..eaaa6673c6 100644 --- a/packages/backend/src/config/app.ts +++ b/packages/backend/src/config/app.ts @@ -33,7 +33,6 @@ dotenv.config({ export const Config = { logLevel: envString('LOG_LEVEL', 'info'), enableTelemetry: envBool('ENABLE_TELEMETRY', true), - telemetryServiceName: envString('TELEMETRY_SERVICE_NAME', 'Rafiki'), openTelemetryCollectorUrl: envString( 'OPEN_TELEMETRY_COLLECTOR_URL', 'http://otel-collector:4317' diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 2e5becd544..02b2895d3b 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -134,7 +134,7 @@ export function initIocContainer( const config = await deps.use('config') return createTelemetryService({ logger: await deps.use('logger'), - serviceName: config.telemetryServiceName, + serviceName: config.instanceName, collectorUrl: config.openTelemetryCollectorUrl, exportIntervalMillis: config.openTelemetryExportInterval }) From cfcc091dec593b5bf9dfef3374612811fec57878 Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Mon, 27 Nov 2023 16:15:03 +0200 Subject: [PATCH 07/67] feat(telemetry): move transactions_total metric to outgoing payment instead of accounting. -this is so that both transaction_amount and transaction_total to metrics to be collected on the sender side of the transaction. remove Metrics enum add getOrCreate, so that Counters can be created in respective services instead of telemetryService constructor --- .../backend/src/accounting/psql/service.ts | 7 ++-- packages/backend/src/accounting/service.ts | 12 ++----- .../src/accounting/tigerbeetle/service.ts | 23 ++++++------- packages/backend/src/app.ts | 11 ------ packages/backend/src/index.ts | 22 ++++++------ .../payment/outgoing/lifecycle.ts | 9 +++++ .../open_payments/payment/outgoing/service.ts | 2 ++ .../ilp/connector/core/rafiki.ts | 13 ++++--- .../src/payment-method/ilp/connector/index.ts | 2 +- packages/backend/src/telemetry/meter.ts | 34 ++++++------------- 10 files changed, 57 insertions(+), 78 deletions(-) diff --git a/packages/backend/src/accounting/psql/service.ts b/packages/backend/src/accounting/psql/service.ts index 0c6488b719..c2952d477c 100644 --- a/packages/backend/src/accounting/psql/service.ts +++ b/packages/backend/src/accounting/psql/service.ts @@ -6,14 +6,14 @@ import { BaseService } from '../../shared/baseService' import { isTransferError, TransferError } from '../errors' import { AccountingService, + createAccountToAccountTransfer, Deposit, LiquidityAccount, LiquidityAccountType, Transaction, TransferOptions, TransferToCreate, - Withdrawal, - createAccountToAccountTransfer + Withdrawal } from '../service' import { getAccountBalances } from './balance' import { @@ -33,10 +33,8 @@ import { voidTransfers } from './ledger-transfer' import { LedgerTransfer, LedgerTransferType } from './ledger-transfer/model' -import { TelemetryService } from '../../telemetry/meter' export interface ServiceDependencies extends BaseService { - telemetry?: TelemetryService knex: TransactionOrKnex withdrawalThrottleDelay?: number } @@ -203,7 +201,6 @@ export async function createTransfer( args: TransferOptions ): Promise { return createAccountToAccountTransfer({ - telemetry: deps.telemetry, transferArgs: args, withdrawalThrottleDelay: deps.withdrawalThrottleDelay, voidTransfers: async (transferRefs) => voidTransfers(deps, transferRefs), diff --git a/packages/backend/src/accounting/service.ts b/packages/backend/src/accounting/service.ts index 6a77dd7d51..30491a5eb1 100644 --- a/packages/backend/src/accounting/service.ts +++ b/packages/backend/src/accounting/service.ts @@ -1,6 +1,5 @@ import { TransactionOrKnex } from 'objection' -import { Metrics, TelemetryService } from '../telemetry/meter' -import { isTransferError, TransferError } from './errors' +import { TransferError, isTransferError } from './errors' export enum LiquidityAccountType { ASSET = 'ASSET', @@ -101,7 +100,6 @@ interface CreateAccountToAccountTransferArgs { transfers: TransferToCreate[] ): Promise withdrawalThrottleDelay?: number - telemetry?: TelemetryService } export async function createAccountToAccountTransfer( @@ -114,8 +112,7 @@ export async function createAccountToAccountTransfer( getAccountReceived, getAccountBalance, withdrawalThrottleDelay, - transferArgs, - telemetry + transferArgs } = args const { sourceAccount, destinationAccount, sourceAmount, destinationAmount } = @@ -190,11 +187,6 @@ export async function createAccountToAccountTransfer( totalReceived, withdrawalThrottleDelay }) - - telemetry?.getCounter(Metrics.TRANSACTIONS_TOTAL)?.add(1, { - source: telemetry.getServiceName() ?? 'Rafiki', - asset_code: destinationAccount.asset.code - }) } }, void: async (): Promise => { diff --git a/packages/backend/src/accounting/tigerbeetle/service.ts b/packages/backend/src/accounting/tigerbeetle/service.ts index 331458f407..9d03024628 100644 --- a/packages/backend/src/accounting/tigerbeetle/service.ts +++ b/packages/backend/src/accounting/tigerbeetle/service.ts @@ -1,16 +1,8 @@ import { Client } from 'tigerbeetle-node' import { v4 as uuid } from 'uuid' -import { calculateBalance, createAccounts, getAccounts } from './accounts' -import { - areAllAccountExistsErrors, - TigerbeetleCreateAccountError, - TigerbeetleUnknownAccountError -} from './errors' -import { NewTransferOptions, createTransfers } from './transfers' import { BaseService } from '../../shared/baseService' import { validateId } from '../../shared/utils' -import { toTigerbeetleId } from './utils' import { AccountAlreadyExistsError, BalanceTransferError, @@ -18,15 +10,22 @@ import { } from '../errors' import { AccountingService, - createAccountToAccountTransfer, Deposit, LiquidityAccount, LiquidityAccountType, Transaction, TransferOptions, - Withdrawal + Withdrawal, + createAccountToAccountTransfer } from '../service' -import { TelemetryService } from '../../telemetry/meter' +import { calculateBalance, createAccounts, getAccounts } from './accounts' +import { + TigerbeetleCreateAccountError, + TigerbeetleUnknownAccountError, + areAllAccountExistsErrors +} from './errors' +import { NewTransferOptions, createTransfers } from './transfers' +import { toTigerbeetleId } from './utils' export enum TigerbeetleAccountCode { LIQUIDITY_WEB_MONETIZATION = 1, @@ -49,7 +48,6 @@ export const convertToTigerbeetleAccountCode: { } export interface ServiceDependencies extends BaseService { - telemetry?: TelemetryService tigerbeetle: Client withdrawalThrottleDelay?: number } @@ -218,7 +216,6 @@ export async function createTransfer( args: TransferOptions ): Promise { return createAccountToAccountTransfer({ - telemetry: deps.telemetry, transferArgs: args, withdrawalThrottleDelay: deps.withdrawalThrottleDelay, voidTransfers: async (transferIds) => { diff --git a/packages/backend/src/app.ts b/packages/backend/src/app.ts index 9d9dc5344e..429aa6a828 100644 --- a/packages/backend/src/app.ts +++ b/packages/backend/src/app.ts @@ -283,17 +283,6 @@ export class App { } } - // let counter = 0 - // const telemetry = await this.container.use('telemetry') - // setInterval(() => { - // counter++ - // console.log( - // `HERE ---------------------------####################---------------` - // ) - // console.log(`counter: ${counter}`) - // telemetry.getCounter('test')?.add(1) - // }, 10000) - public async startAdminServer(port: number): Promise { const koa = await this.createKoaServer() const httpServer = http.createServer(koa.callback()) diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 02b2895d3b..63c36b7879 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -47,7 +47,7 @@ import { createIlpPaymentService } from './payment-method/ilp/service' import { createSPSPRoutes } from './payment-method/ilp/spsp/routes' import { createStreamCredentialsService } from './payment-method/ilp/stream-credentials/service' import { createRatesService } from './rates/service' -import { TelemetryService, createTelemetryService } from './telemetry/meter' +import { createTelemetryService } from './telemetry/meter' import { createWebhookService } from './webhook/service' BigInt.prototype.toJSON = function () { @@ -198,16 +198,10 @@ export function initIocContainer( const knex = await deps.use('knex') const config = await deps.use('config') - let telemetry: TelemetryService | undefined - if (config.enableTelemetry) { - telemetry = await deps.use('telemetry') - } - if (config.useTigerbeetle) { const tigerbeetle = await deps.use('tigerbeetle') return createTigerbeetleAccountingService({ - telemetry, logger, knex, tigerbeetle, @@ -216,7 +210,6 @@ export function initIocContainer( } return createPsqlAccountingService({ - telemetry, logger, knex, withdrawalThrottleDelay: config.withdrawalThrottleDelay @@ -357,13 +350,15 @@ export function initIocContainer( logger: await deps.use('logger'), redis: await deps.use('redis'), accountingService: await deps.use('accountingService'), - telemetry: await deps.use('telemetry'), walletAddressService: await deps.use('walletAddressService'), incomingPaymentService: await deps.use('incomingPaymentService'), peerService: await deps.use('peerService'), ratesService: await deps.use('ratesService'), streamServer: await deps.use('streamServer'), - ilpAddress: config.ilpAddress + ilpAddress: config.ilpAddress, + telemetry: config.enableTelemetry + ? await deps.use('telemetry') + : undefined }) }) @@ -456,6 +451,7 @@ export function initIocContainer( }) container.singleton('outgoingPaymentService', async (deps) => { + const config = await deps.use('config') return await createOutgoingPaymentService({ logger: await deps.use('logger'), knex: await deps.use('knex'), @@ -465,9 +461,13 @@ export function initIocContainer( 'paymentMethodHandlerService' ), peerService: await deps.use('peerService'), - walletAddressService: await deps.use('walletAddressService') + walletAddressService: await deps.use('walletAddressService'), + telemetry: config.enableTelemetry + ? await deps.use('telemetry') + : undefined }) }) + container.singleton('outgoingPaymentRoutes', async (deps) => { return createOutgoingPaymentRoutes({ config: await deps.use('config'), diff --git a/packages/backend/src/open_payments/payment/outgoing/lifecycle.ts b/packages/backend/src/open_payments/payment/outgoing/lifecycle.ts index b98aa815b7..80464258b6 100644 --- a/packages/backend/src/open_payments/payment/outgoing/lifecycle.ts +++ b/packages/backend/src/open_payments/payment/outgoing/lifecycle.ts @@ -81,6 +81,15 @@ export async function handleSending( finalReceiveAmount: maxReceiveAmount }) + deps.telemetry + ?.getOrCreate('transactions_total', { + description: 'Count of funded transactions' + }) + .add(1, { + source: deps.telemetry?.getServiceName(), + asset_code: payment.sentAmount.assetCode + }) + await handleCompleted(deps, payment) } diff --git a/packages/backend/src/open_payments/payment/outgoing/service.ts b/packages/backend/src/open_payments/payment/outgoing/service.ts index 19ada24203..feb8d24493 100644 --- a/packages/backend/src/open_payments/payment/outgoing/service.ts +++ b/packages/backend/src/open_payments/payment/outgoing/service.ts @@ -35,6 +35,7 @@ import { Interval } from 'luxon' import { knex } from 'knex' import { AccountAlreadyExistsError } from '../../../accounting/errors' import { PaymentMethodHandlerService } from '../../../payment-method/handler/service' +import { TelemetryService } from '../../../telemetry/meter' export interface OutgoingPaymentService extends WalletAddressSubresourceService { @@ -54,6 +55,7 @@ export interface ServiceDependencies extends BaseService { peerService: PeerService paymentMethodHandlerService: PaymentMethodHandlerService walletAddressService: WalletAddressService + telemetry?: TelemetryService } export async function createOutgoingPaymentService( diff --git a/packages/backend/src/payment-method/ilp/connector/core/rafiki.ts b/packages/backend/src/payment-method/ilp/connector/core/rafiki.ts index 07b9467b58..cf4f24190a 100644 --- a/packages/backend/src/payment-method/ilp/connector/core/rafiki.ts +++ b/packages/backend/src/payment-method/ilp/connector/core/rafiki.ts @@ -6,6 +6,7 @@ import { Redis } from 'ioredis' import Koa, { Middleware } from 'koa' import { Logger } from 'pino' //import { Router } from './services/router' +import { ValueType } from '@opentelemetry/api' import { CreateAccountError, TransferError @@ -20,7 +21,7 @@ import { AssetOptions } from '../../../../asset/service' import { IncomingPaymentService } from '../../../../open_payments/payment/incoming/service' import { WalletAddressService } from '../../../../open_payments/wallet_address/service' import { RatesService } from '../../../../rates/service' -import { Metrics, TelemetryService } from '../../../../telemetry/meter' +import { TelemetryService } from '../../../../telemetry/meter' import { PeerService } from '../../peer/service' import { createTokenAuthMiddleware } from './middleware' import { @@ -194,10 +195,14 @@ export class Rafiki { Number(prepare.amount) * Number(scalingFactor) this.config.telemetry - ?.getCounter(Metrics.TRANSACTIONS_AMOUNT) - ?.add(totalReceivedInAssetScale4, { + ?.getOrCreate('transactions_amount', { + description: + 'Amount sent through the network. Asset Code & Asset Scale are sent as attributes', + valueType: ValueType.DOUBLE + }) + .add(totalReceivedInAssetScale4, { asset_code: sourceAccount.asset.code, - source: this.config.telemetry?.getServiceName() ?? 'Rafiki' + source: this.config.telemetry?.getServiceName() }) } diff --git a/packages/backend/src/payment-method/ilp/connector/index.ts b/packages/backend/src/payment-method/ilp/connector/index.ts index d539baf372..11d496cfa9 100644 --- a/packages/backend/src/payment-method/ilp/connector/index.ts +++ b/packages/backend/src/payment-method/ilp/connector/index.ts @@ -33,7 +33,7 @@ interface ServiceDependencies extends BaseService { redis: Redis ratesService: RatesService accountingService: AccountingService - telemetry: TelemetryService + telemetry?: TelemetryService walletAddressService: WalletAddressService incomingPaymentService: IncomingPaymentService peerService: PeerService diff --git a/packages/backend/src/telemetry/meter.ts b/packages/backend/src/telemetry/meter.ts index 1f6049f779..c8c46e8310 100644 --- a/packages/backend/src/telemetry/meter.ts +++ b/packages/backend/src/telemetry/meter.ts @@ -3,7 +3,6 @@ import { DiagConsoleLogger, DiagLogLevel, MetricOptions, - ValueType, diag, metrics } from '@opentelemetry/api' @@ -17,21 +16,16 @@ import { import { BaseService } from '../shared/baseService' export interface TelemetryService { - getCounter(name: string): Counter | undefined + getOrCreate(name: string, options?: MetricOptions): Counter getServiceName(): string | undefined } interface TelemetryServiceDependencies extends BaseService { - serviceName?: string + serviceName: string collectorUrl?: string exportIntervalMillis?: number } -export enum Metrics { - TRANSACTIONS_TOTAL = 'transactions_total', - TRANSACTIONS_AMOUNT = 'transactions_amount' -} - export function createTelemetryService( deps: TelemetryServiceDependencies ): TelemetryService { @@ -39,7 +33,7 @@ export function createTelemetryService( } class TelemetryServiceImpl implements TelemetryService { - private serviceName: string | undefined + private serviceName: string private counters = new Map() constructor(private deps: TelemetryServiceDependencies) { @@ -65,29 +59,23 @@ class TelemetryServiceImpl implements TelemetryService { meterProvider.addMetricReader(metricReader) metrics.setGlobalMeterProvider(meterProvider) - - //* init counters - this.createCounter(Metrics.TRANSACTIONS_TOTAL, { - description: 'Count of funded transactions' - }) - - this.createCounter(Metrics.TRANSACTIONS_AMOUNT, { - description: - 'Amount sent through the network. Asset Code & Asset Scale are sent as attributes', - valueType: ValueType.DOUBLE - }) } private createCounter( name: string, options: MetricOptions | undefined - ): void { + ): Counter { const counter = metrics.getMeter('Rafiki').createCounter(name, options) this.counters.set(name, counter) + return counter } - public getCounter(name: string): Counter | undefined { - return this.counters.get(name) + public getOrCreate(name: string, options?: MetricOptions): Counter { + const existing = this.counters.get(name) + if (existing) { + return existing + } + return this.createCounter(name, options) } public getServiceName(): string | undefined { From fdd41d7a693e69a9273b11f061156f3593f17788 Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Mon, 27 Nov 2023 17:43:14 +0200 Subject: [PATCH 08/67] feat(telemetry): telemetry now has it's own middleware instead of injecting logic into handleIlpData --- .../connector/core/middleware/telemetry.ts | 38 +++++++++++++++++++ .../ilp/connector/core/rafiki.ts | 20 ---------- .../src/payment-method/ilp/connector/index.ts | 3 ++ 3 files changed, 41 insertions(+), 20 deletions(-) create mode 100644 packages/backend/src/payment-method/ilp/connector/core/middleware/telemetry.ts diff --git a/packages/backend/src/payment-method/ilp/connector/core/middleware/telemetry.ts b/packages/backend/src/payment-method/ilp/connector/core/middleware/telemetry.ts new file mode 100644 index 0000000000..1497e52a39 --- /dev/null +++ b/packages/backend/src/payment-method/ilp/connector/core/middleware/telemetry.ts @@ -0,0 +1,38 @@ +import { ValueType } from '@opentelemetry/api' +import { ILPContext, ILPMiddleware } from '../rafiki' + +export function createTelemetryMiddleware(): ILPMiddleware { + return async ( + { request, services, accounts, state }: ILPContext, + next: () => Promise + ): Promise => { + console.log('IN TELEMETRY MIDDLEWARE') + const { amount } = request.prepare + if (state.unfulfillable || !Number(amount)) { + await next() + return + } + const { code, scale } = accounts.incoming.asset + + const scalingFactor = scale ? Math.pow(10, 4 - scale) : undefined + const totalReceivedInAssetScale4 = Number(amount) * Number(scalingFactor) + + services.telemetry + ?.getOrCreate('transactions_amount', { + description: + 'Amount sent through the network. Asset Code & Asset Scale are sent as attributes', + valueType: ValueType.DOUBLE + }) + .add(totalReceivedInAssetScale4, { + asset_code: code, + source: services.telemetry?.getServiceName() + }) + + console.log( + 'GATHERED TELEMETRY FROM MIDDLEWARE', + totalReceivedInAssetScale4 + ) + + await next() + } +} diff --git a/packages/backend/src/payment-method/ilp/connector/core/rafiki.ts b/packages/backend/src/payment-method/ilp/connector/core/rafiki.ts index cf4f24190a..bcb385a23b 100644 --- a/packages/backend/src/payment-method/ilp/connector/core/rafiki.ts +++ b/packages/backend/src/payment-method/ilp/connector/core/rafiki.ts @@ -6,7 +6,6 @@ import { Redis } from 'ioredis' import Koa, { Middleware } from 'koa' import { Logger } from 'pino' //import { Router } from './services/router' -import { ValueType } from '@opentelemetry/api' import { CreateAccountError, TransferError @@ -187,25 +186,6 @@ export class Rafiki { const prepare = new ZeroCopyIlpPrepare(rawPrepare) const response = new IlpResponse() - if (!unfulfillable && Number(prepare.amount)) { - const scalingFactor = sourceAccount.asset.scale - ? Math.pow(10, 4 - sourceAccount.asset.scale) - : undefined - const totalReceivedInAssetScale4 = - Number(prepare.amount) * Number(scalingFactor) - - this.config.telemetry - ?.getOrCreate('transactions_amount', { - description: - 'Amount sent through the network. Asset Code & Asset Scale are sent as attributes', - valueType: ValueType.DOUBLE - }) - .add(totalReceivedInAssetScale4, { - asset_code: sourceAccount.asset.code, - source: this.config.telemetry?.getServiceName() - }) - } - await this.routes( { request: { prepare, rawPrepare }, diff --git a/packages/backend/src/payment-method/ilp/connector/index.ts b/packages/backend/src/payment-method/ilp/connector/index.ts index 11d496cfa9..de8b06f771 100644 --- a/packages/backend/src/payment-method/ilp/connector/index.ts +++ b/packages/backend/src/payment-method/ilp/connector/index.ts @@ -28,6 +28,7 @@ import { createStreamAddressMiddleware, createStreamController } from './core' +import { createTelemetryMiddleware } from './core/middleware/telemetry' interface ServiceDependencies extends BaseService { redis: Redis @@ -80,6 +81,8 @@ export async function createConnectorService({ // Local pay createBalanceMiddleware(), + + createTelemetryMiddleware(), // Outgoing Rules createStreamController(), createOutgoingThroughputMiddleware(), From 26d952640b9444db070d336119b465fbe359a0f9 Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Wed, 29 Nov 2023 13:06:59 +0200 Subject: [PATCH 09/67] feat(telemetry): add unit tests for ILP Connector telemetry middleware --- .../core/factories/rafiki-services.ts | 3 + .../connector/core/middleware/telemetry.ts | 6 -- .../core/test/middleware/telemetry.test.ts | 76 +++++++++++++++++++ .../connector/core/test/mocks/telemetry.ts | 16 ++++ 4 files changed, 95 insertions(+), 6 deletions(-) create mode 100644 packages/backend/src/payment-method/ilp/connector/core/test/middleware/telemetry.test.ts create mode 100644 packages/backend/src/payment-method/ilp/connector/core/test/mocks/telemetry.ts diff --git a/packages/backend/src/payment-method/ilp/connector/core/factories/rafiki-services.ts b/packages/backend/src/payment-method/ilp/connector/core/factories/rafiki-services.ts index 12343967a9..9634fbb162 100644 --- a/packages/backend/src/payment-method/ilp/connector/core/factories/rafiki-services.ts +++ b/packages/backend/src/payment-method/ilp/connector/core/factories/rafiki-services.ts @@ -5,9 +5,11 @@ import { StreamServer } from '@interledger/stream-receiver' import { RafikiServices } from '../rafiki' import { MockAccountingService } from '../test/mocks/accounting-service' import { TestLoggerFactory } from './test-logger' +import { MockTelemetryService } from '../test/mocks/telemetry' interface MockRafikiServices extends RafikiServices { accounting: MockAccountingService + telemetry: MockTelemetryService } export const RafikiServicesFactory = Factory.define( @@ -20,6 +22,7 @@ export const RafikiServicesFactory = Factory.define( .attr('accounting', () => { return new MockAccountingService() }) + .attr('telemetry', new MockTelemetryService()) .attr('logger', TestLoggerFactory.build()) .attr( 'walletAddresses', diff --git a/packages/backend/src/payment-method/ilp/connector/core/middleware/telemetry.ts b/packages/backend/src/payment-method/ilp/connector/core/middleware/telemetry.ts index 1497e52a39..3dc666860c 100644 --- a/packages/backend/src/payment-method/ilp/connector/core/middleware/telemetry.ts +++ b/packages/backend/src/payment-method/ilp/connector/core/middleware/telemetry.ts @@ -6,7 +6,6 @@ export function createTelemetryMiddleware(): ILPMiddleware { { request, services, accounts, state }: ILPContext, next: () => Promise ): Promise => { - console.log('IN TELEMETRY MIDDLEWARE') const { amount } = request.prepare if (state.unfulfillable || !Number(amount)) { await next() @@ -28,11 +27,6 @@ export function createTelemetryMiddleware(): ILPMiddleware { source: services.telemetry?.getServiceName() }) - console.log( - 'GATHERED TELEMETRY FROM MIDDLEWARE', - totalReceivedInAssetScale4 - ) - await next() } } diff --git a/packages/backend/src/payment-method/ilp/connector/core/test/middleware/telemetry.test.ts b/packages/backend/src/payment-method/ilp/connector/core/test/middleware/telemetry.test.ts new file mode 100644 index 0000000000..762d328019 --- /dev/null +++ b/packages/backend/src/payment-method/ilp/connector/core/test/middleware/telemetry.test.ts @@ -0,0 +1,76 @@ +import { ValueType } from '@opentelemetry/api' +import assert from 'assert' +import { OutgoingAccount, ZeroCopyIlpPrepare } from '../..' +import { IncomingAccountFactory, RafikiServicesFactory } from '../../factories' +import { createTelemetryMiddleware } from '../../middleware/telemetry' +import { createILPContext } from '../../utils' +import { mockCounter } from '../mocks/telemetry' + +const incomingAccount = IncomingAccountFactory.build({ id: 'alice' }) + +assert.ok(incomingAccount.id) +const services = RafikiServicesFactory.build({}) + +const ctx = createILPContext({ + services, + request: { + prepare: { + amount: 100n + } as unknown as ZeroCopyIlpPrepare, + rawPrepare: Buffer.from('') + }, + accounts: { incoming: incomingAccount, outgoing: {} as OutgoingAccount }, + state: { + unfulfillable: false + } +}) + +const middleware = createTelemetryMiddleware() +const next = jest.fn().mockImplementation(() => Promise.resolve()) + +beforeEach(async () => { + incomingAccount.balance = 100n + incomingAccount.asset.scale = 2 + incomingAccount.asset.code = 'USD' + ctx.state.unfulfillable = false +}) + +describe('Telemetry Middleware', function () { + it('should gather telemetry and call next', async () => { + const getOrCreateSpy = jest + .spyOn(ctx.services.telemetry!, 'getOrCreate') + .mockImplementation(() => mockCounter) + + console.log(ctx) + await middleware(ctx, next) + + expect(getOrCreateSpy).toHaveBeenCalledWith('transactions_amount', { + description: expect.any(String), + valueType: ValueType.DOUBLE + }) + + expect( + ctx.services.telemetry!.getOrCreate('transactions_amount').add + ).toHaveBeenCalledWith( + expect.any(Number), + expect.objectContaining({ + asset_code: 'USD', + source: 'serviceName' + }) + ) + + expect(next).toHaveBeenCalled() + }) + + it('should call next without gathering telemetry when state is unfulfillable', async () => { + ctx.state.unfulfillable = true + + const getOrCreateSpy = jest + .spyOn(ctx.services.telemetry!, 'getOrCreate') + .mockImplementation(() => mockCounter) + + await middleware(ctx, next) + + expect(getOrCreateSpy).not.toHaveBeenCalled() + }) +}) diff --git a/packages/backend/src/payment-method/ilp/connector/core/test/mocks/telemetry.ts b/packages/backend/src/payment-method/ilp/connector/core/test/mocks/telemetry.ts new file mode 100644 index 0000000000..e88f5f5451 --- /dev/null +++ b/packages/backend/src/payment-method/ilp/connector/core/test/mocks/telemetry.ts @@ -0,0 +1,16 @@ +import { Attributes, Counter, MetricOptions } from '@opentelemetry/api' +import { TelemetryService } from '../../../../../../telemetry/meter' + +export const mockCounter = { add: jest.fn() } as Counter + +export class MockTelemetryService implements TelemetryService { + getOrCreate( + _name: string, + _options?: MetricOptions | undefined + ): Counter { + return mockCounter + } + getServiceName(): string | undefined { + return 'serviceName' + } +} From 783d54894f82c24f421258fda559601ecb2bd290 Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Wed, 29 Nov 2023 13:09:16 +0200 Subject: [PATCH 10/67] feat(telemetry):remove clog --- .../ilp/connector/core/test/middleware/telemetry.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/backend/src/payment-method/ilp/connector/core/test/middleware/telemetry.test.ts b/packages/backend/src/payment-method/ilp/connector/core/test/middleware/telemetry.test.ts index 762d328019..cfeb15d260 100644 --- a/packages/backend/src/payment-method/ilp/connector/core/test/middleware/telemetry.test.ts +++ b/packages/backend/src/payment-method/ilp/connector/core/test/middleware/telemetry.test.ts @@ -41,7 +41,6 @@ describe('Telemetry Middleware', function () { .spyOn(ctx.services.telemetry!, 'getOrCreate') .mockImplementation(() => mockCounter) - console.log(ctx) await middleware(ctx, next) expect(getOrCreateSpy).toHaveBeenCalledWith('transactions_amount', { From 5a9920641d13258580eae6161b415d89718bb731 Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Mon, 4 Dec 2023 09:40:09 +0200 Subject: [PATCH 11/67] feat(telemetry): add telemetry middleware and service tests --- .../core/factories/rafiki-services.ts | 6 +++-- .../ilp/connector/core/rafiki.ts | 9 ------- .../core/test/middleware/telemetry.test.ts | 25 ++++++++++++++++--- packages/backend/src/telemetry/meter.test.ts | 21 ++++++++++++++++ packages/backend/src/telemetry/meter.ts | 3 --- .../mocks/telemetry.ts => tests/meter.ts} | 2 +- 6 files changed, 47 insertions(+), 19 deletions(-) create mode 100644 packages/backend/src/telemetry/meter.test.ts rename packages/backend/src/{payment-method/ilp/connector/core/test/mocks/telemetry.ts => tests/meter.ts} (85%) diff --git a/packages/backend/src/payment-method/ilp/connector/core/factories/rafiki-services.ts b/packages/backend/src/payment-method/ilp/connector/core/factories/rafiki-services.ts index 9634fbb162..a5b64ad737 100644 --- a/packages/backend/src/payment-method/ilp/connector/core/factories/rafiki-services.ts +++ b/packages/backend/src/payment-method/ilp/connector/core/factories/rafiki-services.ts @@ -5,7 +5,7 @@ import { StreamServer } from '@interledger/stream-receiver' import { RafikiServices } from '../rafiki' import { MockAccountingService } from '../test/mocks/accounting-service' import { TestLoggerFactory } from './test-logger' -import { MockTelemetryService } from '../test/mocks/telemetry' +import { MockTelemetryService } from '../../../../../tests/meter' interface MockRafikiServices extends RafikiServices { accounting: MockAccountingService @@ -22,7 +22,9 @@ export const RafikiServicesFactory = Factory.define( .attr('accounting', () => { return new MockAccountingService() }) - .attr('telemetry', new MockTelemetryService()) + .attr('telemetry', () => { + return new MockTelemetryService() + }) .attr('logger', TestLoggerFactory.build()) .attr( 'walletAddresses', diff --git a/packages/backend/src/payment-method/ilp/connector/core/rafiki.ts b/packages/backend/src/payment-method/ilp/connector/core/rafiki.ts index bcb385a23b..b02d12bc5a 100644 --- a/packages/backend/src/payment-method/ilp/connector/core/rafiki.ts +++ b/packages/backend/src/payment-method/ilp/connector/core/rafiki.ts @@ -114,15 +114,6 @@ export type ILPContext = { revertTotalReceived?: () => Promise state: T } -export type IlpObservabilityParameters = { - asset: LiquidityAccountAsset - amount: bigint - unfulfillable: boolean -} - -export type IlpObservabilityCallback = ( - params: IlpObservabilityParameters -) => void export class Rafiki { //private _router?: Router diff --git a/packages/backend/src/payment-method/ilp/connector/core/test/middleware/telemetry.test.ts b/packages/backend/src/payment-method/ilp/connector/core/test/middleware/telemetry.test.ts index cfeb15d260..1d1a36b0a7 100644 --- a/packages/backend/src/payment-method/ilp/connector/core/test/middleware/telemetry.test.ts +++ b/packages/backend/src/payment-method/ilp/connector/core/test/middleware/telemetry.test.ts @@ -1,10 +1,10 @@ import { ValueType } from '@opentelemetry/api' import assert from 'assert' import { OutgoingAccount, ZeroCopyIlpPrepare } from '../..' +import { mockCounter } from '../../../../../../tests/meter' import { IncomingAccountFactory, RafikiServicesFactory } from '../../factories' import { createTelemetryMiddleware } from '../../middleware/telemetry' import { createILPContext } from '../../utils' -import { mockCounter } from '../mocks/telemetry' const incomingAccount = IncomingAccountFactory.build({ id: 'alice' }) @@ -36,11 +36,15 @@ beforeEach(async () => { }) describe('Telemetry Middleware', function () { - it('should gather telemetry and call next', async () => { + test('should gather telemetry in correct asset scale and call next', async () => { const getOrCreateSpy = jest .spyOn(ctx.services.telemetry!, 'getOrCreate') .mockImplementation(() => mockCounter) + const expectedScaledValue = + Number(ctx.request.prepare.amount) * + Math.pow(10, 4 - incomingAccount.asset.scale) + await middleware(ctx, next) expect(getOrCreateSpy).toHaveBeenCalledWith('transactions_amount', { @@ -51,7 +55,7 @@ describe('Telemetry Middleware', function () { expect( ctx.services.telemetry!.getOrCreate('transactions_amount').add ).toHaveBeenCalledWith( - expect.any(Number), + expectedScaledValue, expect.objectContaining({ asset_code: 'USD', source: 'serviceName' @@ -61,7 +65,7 @@ describe('Telemetry Middleware', function () { expect(next).toHaveBeenCalled() }) - it('should call next without gathering telemetry when state is unfulfillable', async () => { + test('should call next without gathering telemetry when state is unfulfillable', async () => { ctx.state.unfulfillable = true const getOrCreateSpy = jest @@ -72,4 +76,17 @@ describe('Telemetry Middleware', function () { expect(getOrCreateSpy).not.toHaveBeenCalled() }) + + test('should handle invalid amount by calling next without gathering telemetry', async () => { + ctx.request.prepare.amount = '0' + + const getOrCreateSpy = jest + .spyOn(ctx.services.telemetry!, 'getOrCreate') + .mockImplementation(() => mockCounter) + + await middleware(ctx, next) + + expect(getOrCreateSpy).not.toHaveBeenCalled() + expect(next).toHaveBeenCalled() + }) }) diff --git a/packages/backend/src/telemetry/meter.test.ts b/packages/backend/src/telemetry/meter.test.ts new file mode 100644 index 0000000000..8b21c2a3c9 --- /dev/null +++ b/packages/backend/src/telemetry/meter.test.ts @@ -0,0 +1,21 @@ +import { MockTelemetryService, mockCounter } from '../tests/meter' + +const telemetryService = new MockTelemetryService() +describe('TelemetryServiceImpl', () => { + it('should create a counter when getOrCreate is called for a new metric', () => { + const counter = telemetryService.getOrCreate('testMetric') + expect(counter).toBe(mockCounter) + }) + + it('should return an existing counter when getOrCreate is called for an existing metric', () => { + const existingCounter = telemetryService.getOrCreate('existingMetric') + const retrievedCounter = telemetryService.getOrCreate('existingMetric') + expect(retrievedCounter).toBe(existingCounter) + }) + + it('should return the instance name when calling getServiceName', () => { + const serviceName = telemetryService.getServiceName() + + expect(serviceName).toBe('serviceName') + }) +}) diff --git a/packages/backend/src/telemetry/meter.ts b/packages/backend/src/telemetry/meter.ts index c8c46e8310..11cb3a69fe 100644 --- a/packages/backend/src/telemetry/meter.ts +++ b/packages/backend/src/telemetry/meter.ts @@ -39,9 +39,6 @@ class TelemetryServiceImpl implements TelemetryService { constructor(private deps: TelemetryServiceDependencies) { diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.DEBUG) this.serviceName = deps.serviceName - console.log( - `serviceName: ${deps.serviceName}, collectorUrl: ${deps.collectorUrl} }` - ) const meterProvider = new MeterProvider({ resource: new Resource({ 'service.name': 'RAFIKI_NETWORK' }) diff --git a/packages/backend/src/payment-method/ilp/connector/core/test/mocks/telemetry.ts b/packages/backend/src/tests/meter.ts similarity index 85% rename from packages/backend/src/payment-method/ilp/connector/core/test/mocks/telemetry.ts rename to packages/backend/src/tests/meter.ts index e88f5f5451..9ad2c9f243 100644 --- a/packages/backend/src/payment-method/ilp/connector/core/test/mocks/telemetry.ts +++ b/packages/backend/src/tests/meter.ts @@ -1,5 +1,5 @@ import { Attributes, Counter, MetricOptions } from '@opentelemetry/api' -import { TelemetryService } from '../../../../../../telemetry/meter' +import { TelemetryService } from '../telemetry/meter' export const mockCounter = { add: jest.fn() } as Counter From 29bcd07bc4af506174ecf9c84ca6bdd0a69371ed Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Mon, 4 Dec 2023 12:09:21 +0200 Subject: [PATCH 12/67] feat(telemetry): mock TelemetryService in createTestApp --- .../backend/src/payment-method/ilp/connector/core/rafiki.ts | 1 - packages/backend/src/tests/app.ts | 5 +++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/payment-method/ilp/connector/core/rafiki.ts b/packages/backend/src/payment-method/ilp/connector/core/rafiki.ts index b02d12bc5a..722036254a 100644 --- a/packages/backend/src/payment-method/ilp/connector/core/rafiki.ts +++ b/packages/backend/src/payment-method/ilp/connector/core/rafiki.ts @@ -12,7 +12,6 @@ import { } from '../../../../accounting/errors' import { LiquidityAccount, - LiquidityAccountAsset, LiquidityAccountType, Transaction } from '../../../../accounting/service' diff --git a/packages/backend/src/tests/app.ts b/packages/backend/src/tests/app.ts index 287aec89ab..9d8ab3b506 100644 --- a/packages/backend/src/tests/app.ts +++ b/packages/backend/src/tests/app.ts @@ -16,6 +16,7 @@ import { setContext } from '@apollo/client/link/context' import { start, gracefulShutdown } from '..' import { App, AppServices } from '../app' +import { MockTelemetryService } from './meter' export const testAccessToken = 'test-app-access' export interface TestContainer { @@ -53,6 +54,10 @@ export const createTestApp = async ( container.bind('logger', async () => logger) + container.bind('telemetry', async () => { + return new MockTelemetryService() + }) + const app = new App(container) await start(container, app) From 770573f677d9510fe73219ff96b6e178c4167522 Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Mon, 4 Dec 2023 15:56:55 +0200 Subject: [PATCH 13/67] feat(telemetry): fix DI of middleware. Add condition for early return in the case of receiving side --- .../connector/core/middleware/telemetry.ts | 6 ++++- .../ilp/connector/core/rafiki.ts | 3 +++ .../core/test/middleware/telemetry.test.ts | 23 +++++++++++++++---- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/packages/backend/src/payment-method/ilp/connector/core/middleware/telemetry.ts b/packages/backend/src/payment-method/ilp/connector/core/middleware/telemetry.ts index 3dc666860c..2765ddbd10 100644 --- a/packages/backend/src/payment-method/ilp/connector/core/middleware/telemetry.ts +++ b/packages/backend/src/payment-method/ilp/connector/core/middleware/telemetry.ts @@ -7,7 +7,11 @@ export function createTelemetryMiddleware(): ILPMiddleware { next: () => Promise ): Promise => { const { amount } = request.prepare - if (state.unfulfillable || !Number(amount)) { + if ( + state.unfulfillable || + !state.incomingAccount.quote || + !Number(amount) + ) { await next() return } diff --git a/packages/backend/src/payment-method/ilp/connector/core/rafiki.ts b/packages/backend/src/payment-method/ilp/connector/core/rafiki.ts index 722036254a..1e46fb834e 100644 --- a/packages/backend/src/payment-method/ilp/connector/core/rafiki.ts +++ b/packages/backend/src/payment-method/ilp/connector/core/rafiki.ts @@ -161,6 +161,9 @@ export class Rafiki { get walletAddresses(): WalletAddressService { return config.walletAddresses }, + get telemetry(): TelemetryService | undefined { + return config.telemetry + }, logger } diff --git a/packages/backend/src/payment-method/ilp/connector/core/test/middleware/telemetry.test.ts b/packages/backend/src/payment-method/ilp/connector/core/test/middleware/telemetry.test.ts index 1d1a36b0a7..b286e443a5 100644 --- a/packages/backend/src/payment-method/ilp/connector/core/test/middleware/telemetry.test.ts +++ b/packages/backend/src/payment-method/ilp/connector/core/test/middleware/telemetry.test.ts @@ -21,7 +21,10 @@ const ctx = createILPContext({ }, accounts: { incoming: incomingAccount, outgoing: {} as OutgoingAccount }, state: { - unfulfillable: false + unfulfillable: false, + incomingAccount: { + quote: 'exists' + } } }) @@ -36,7 +39,7 @@ beforeEach(async () => { }) describe('Telemetry Middleware', function () { - test('should gather telemetry in correct asset scale and call next', async () => { + it('should gather telemetry in correct asset scale and call next', async () => { const getOrCreateSpy = jest .spyOn(ctx.services.telemetry!, 'getOrCreate') .mockImplementation(() => mockCounter) @@ -65,7 +68,7 @@ describe('Telemetry Middleware', function () { expect(next).toHaveBeenCalled() }) - test('should call next without gathering telemetry when state is unfulfillable', async () => { + it('should call next without gathering telemetry when state is unfulfillable', async () => { ctx.state.unfulfillable = true const getOrCreateSpy = jest @@ -77,7 +80,19 @@ describe('Telemetry Middleware', function () { expect(getOrCreateSpy).not.toHaveBeenCalled() }) - test('should handle invalid amount by calling next without gathering telemetry', async () => { + it('should only gather amount data on the sending side of a transaction. It should call next when there is no quote on the incomingAccount.', async () => { + ctx.state.incomingAccount.quote = '' + + const getOrCreateSpy = jest + .spyOn(ctx.services.telemetry!, 'getOrCreate') + .mockImplementation(() => mockCounter) + + await middleware(ctx, next) + + expect(getOrCreateSpy).not.toHaveBeenCalled() + }) + + it('should handle invalid amount by calling next without gathering telemetry', async () => { ctx.request.prepare.amount = '0' const getOrCreateSpy = jest From 126f0dad3bf52e40929c48126e761849aaeb2c15 Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Tue, 5 Dec 2023 01:33:16 +0200 Subject: [PATCH 14/67] feat(telemetry): move telemetry middleware after createStreamController in the connector middleware chain. --- .../ilp/connector/core/middleware/telemetry.ts | 9 +++------ .../backend/src/payment-method/ilp/connector/index.ts | 2 +- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/backend/src/payment-method/ilp/connector/core/middleware/telemetry.ts b/packages/backend/src/payment-method/ilp/connector/core/middleware/telemetry.ts index 2765ddbd10..28a6e81352 100644 --- a/packages/backend/src/payment-method/ilp/connector/core/middleware/telemetry.ts +++ b/packages/backend/src/payment-method/ilp/connector/core/middleware/telemetry.ts @@ -7,15 +7,12 @@ export function createTelemetryMiddleware(): ILPMiddleware { next: () => Promise ): Promise => { const { amount } = request.prepare - if ( - state.unfulfillable || - !state.incomingAccount.quote || - !Number(amount) - ) { + if (state.unfulfillable || !Number(amount)) { await next() return } - const { code, scale } = accounts.incoming.asset + const { scale } = accounts.incoming.asset + const code: string = state.incomingAccount?.quote?.receiveAmountAssetCode const scalingFactor = scale ? Math.pow(10, 4 - scale) : undefined const totalReceivedInAssetScale4 = Number(amount) * Number(scalingFactor) diff --git a/packages/backend/src/payment-method/ilp/connector/index.ts b/packages/backend/src/payment-method/ilp/connector/index.ts index de8b06f771..73ddd1cccb 100644 --- a/packages/backend/src/payment-method/ilp/connector/index.ts +++ b/packages/backend/src/payment-method/ilp/connector/index.ts @@ -82,9 +82,9 @@ export async function createConnectorService({ // Local pay createBalanceMiddleware(), - createTelemetryMiddleware(), // Outgoing Rules createStreamController(), + createTelemetryMiddleware(), createOutgoingThroughputMiddleware(), createOutgoingReduceExpiryMiddleware({}), createOutgoingExpireMiddleware(), From 603fbe16720b901ae6d8d2bca6c66ecd8d2eae49 Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Mon, 11 Dec 2023 14:55:37 +0200 Subject: [PATCH 15/67] feat(telemetry): telemetry service now supports sending metrics to multiple OTEL collectors defined in env variable. --- localenv/cloud-nine-wallet/docker-compose.yml | 2 +- localenv/happy-life-bank/docker-compose.yml | 2 +- packages/backend/src/config/app.ts | 12 ++++--- packages/backend/src/index.ts | 2 +- packages/backend/src/telemetry/meter.ts | 36 +++++++++++++------ 5 files changed, 35 insertions(+), 19 deletions(-) diff --git a/localenv/cloud-nine-wallet/docker-compose.yml b/localenv/cloud-nine-wallet/docker-compose.yml index 118202d2b8..411aea6f24 100644 --- a/localenv/cloud-nine-wallet/docker-compose.yml +++ b/localenv/cloud-nine-wallet/docker-compose.yml @@ -61,7 +61,7 @@ services: WALLET_ADDRESS_URL: ${CLOUD_NINE_WALLET_ADDRESS_URL:-https://cloud-nine-wallet-backend/.well-known/pay} ILP_CONNECTOR_ADDRESS: ${CLOUD_NINE_CONNECTOR_URL} ENABLE_TELEMETRY: true - OPEN_TELEMETRY_COLLECTOR_URL: http://cloud-nine-otel-collector:4317 + OPEN_TELEMETRY_COLLECTOR_URL: http://cloud-nine-otel-collector:4317,http://otel-collector-NLB-e3172ff9d2f4bc8a.elb.eu-west-2.amazonaws.com:4317 OPEN_TELEMETRY_EXPORT_INTERVAL: 15000 depends_on: - shared-database diff --git a/localenv/happy-life-bank/docker-compose.yml b/localenv/happy-life-bank/docker-compose.yml index 5c7f4aa895..cf018c8526 100644 --- a/localenv/happy-life-bank/docker-compose.yml +++ b/localenv/happy-life-bank/docker-compose.yml @@ -53,7 +53,7 @@ services: REDIS_URL: redis://shared-redis:6379/1 WALLET_ADDRESS_URL: ${HAPPY_LIFE_BANK_WALLET_ADDRESS_URL:-https://happy-life-bank-backend/.well-known/pay} ENABLE_TELEMETRY: true - OPEN_TELEMETRY_COLLECTOR_URL: http://happy-life-otel-collector:4317 + OPEN_TELEMETRY_COLLECTOR_URL: http://happy-life-otel-collector:4317,http://otel-collector-NLB-e3172ff9d2f4bc8a.elb.eu-west-2.amazonaws.com:4317 OPEN_TELEMETRY_EXPORT_INTERVAL: 15000 depends_on: diff --git a/packages/backend/src/config/app.ts b/packages/backend/src/config/app.ts index eaaa6673c6..2967d3e5ea 100644 --- a/packages/backend/src/config/app.ts +++ b/packages/backend/src/config/app.ts @@ -9,6 +9,11 @@ function envString(name: string, value: string): string { return envValue == null ? value : envValue } +function envStringArray(name: string, value: string[]): string[] { + const envValue = process.env[name] + return envValue == null ? value : envValue.split(',').map((s) => s.trim()) +} + function envInt(name: string, value: number): number { const envValue = process.env[name] return envValue == null ? value : parseInt(envValue) @@ -33,11 +38,8 @@ dotenv.config({ export const Config = { logLevel: envString('LOG_LEVEL', 'info'), enableTelemetry: envBool('ENABLE_TELEMETRY', true), - openTelemetryCollectorUrl: envString( - 'OPEN_TELEMETRY_COLLECTOR_URL', - 'http://otel-collector:4317' - ), - openTelemetryExportInterval: envInt('OPEN_TELEMETRY_EXPORT_INTERVAL', 60000), + openTelemetryCollectors: envStringArray('OPEN_TELEMETRY_COLLECTOR_URL', []), + openTelemetryExportInterval: envInt('OPEN_TELEMETRY_EXPORT_INTERVAL', 15000), // publicHost is for open payments URLs. publicHost: envString('PUBLIC_HOST', 'http://127.0.0.1:3001'), adminPort: envInt('ADMIN_PORT', 3001), diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 63c36b7879..bf8479acc1 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -135,7 +135,7 @@ export function initIocContainer( return createTelemetryService({ logger: await deps.use('logger'), serviceName: config.instanceName, - collectorUrl: config.openTelemetryCollectorUrl, + collectorUrls: config.openTelemetryCollectors, exportIntervalMillis: config.openTelemetryExportInterval }) }) diff --git a/packages/backend/src/telemetry/meter.ts b/packages/backend/src/telemetry/meter.ts index 11cb3a69fe..9844504242 100644 --- a/packages/backend/src/telemetry/meter.ts +++ b/packages/backend/src/telemetry/meter.ts @@ -22,7 +22,7 @@ export interface TelemetryService { interface TelemetryServiceDependencies extends BaseService { serviceName: string - collectorUrl?: string + collectorUrls: string[] exportIntervalMillis?: number } @@ -34,28 +34,42 @@ export function createTelemetryService( class TelemetryServiceImpl implements TelemetryService { private serviceName: string + private meterProvider?: MeterProvider private counters = new Map() constructor(private deps: TelemetryServiceDependencies) { diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.DEBUG) this.serviceName = deps.serviceName - const meterProvider = new MeterProvider({ + if ( + deps.collectorUrls && + Array.isArray(deps.collectorUrls) && + deps.collectorUrls.length === 0 + ) { + deps.logger.info( + 'No collector URLs specified, metrics will not be exported' + ) + return + } + + this.meterProvider = new MeterProvider({ resource: new Resource({ 'service.name': 'RAFIKI_NETWORK' }) }) - const metricExporter = new OTLPMetricExporter({ - url: deps.collectorUrl ?? 'http://otel-collector:4317' - }) + deps.collectorUrls.forEach((url) => { + const metricExporter = new OTLPMetricExporter({ + url: url + }) - const metricReader = new PeriodicExportingMetricReader({ - exporter: metricExporter, - exportIntervalMillis: deps.exportIntervalMillis ?? 60000 - }) + const metricReader = new PeriodicExportingMetricReader({ + exporter: metricExporter, + exportIntervalMillis: deps.exportIntervalMillis ?? 15000 + }) - meterProvider.addMetricReader(metricReader) + this.meterProvider?.addMetricReader(metricReader) + }) - metrics.setGlobalMeterProvider(meterProvider) + metrics.setGlobalMeterProvider(this.meterProvider) } private createCounter( From 67f279be16fd5c6b0c0b043c921060e6f6436168 Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Mon, 11 Dec 2023 17:59:58 +0200 Subject: [PATCH 16/67] feat:(telemetry): remove asset_code information from transactions_amount and transactions_count metrics --- .../open_payments/payment/outgoing/lifecycle.ts | 3 +-- .../ilp/connector/core/middleware/telemetry.ts | 16 ++++++++-------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/backend/src/open_payments/payment/outgoing/lifecycle.ts b/packages/backend/src/open_payments/payment/outgoing/lifecycle.ts index 80464258b6..2c0dcd1076 100644 --- a/packages/backend/src/open_payments/payment/outgoing/lifecycle.ts +++ b/packages/backend/src/open_payments/payment/outgoing/lifecycle.ts @@ -86,8 +86,7 @@ export async function handleSending( description: 'Count of funded transactions' }) .add(1, { - source: deps.telemetry?.getServiceName(), - asset_code: payment.sentAmount.assetCode + source: deps.telemetry?.getServiceName() }) await handleCompleted(deps, payment) diff --git a/packages/backend/src/payment-method/ilp/connector/core/middleware/telemetry.ts b/packages/backend/src/payment-method/ilp/connector/core/middleware/telemetry.ts index 28a6e81352..ef1d0ddc67 100644 --- a/packages/backend/src/payment-method/ilp/connector/core/middleware/telemetry.ts +++ b/packages/backend/src/payment-method/ilp/connector/core/middleware/telemetry.ts @@ -11,20 +11,20 @@ export function createTelemetryMiddleware(): ILPMiddleware { await next() return } - const { scale } = accounts.incoming.asset - const code: string = state.incomingAccount?.quote?.receiveAmountAssetCode + const { scale: incomingScale } = accounts.incoming.asset - const scalingFactor = scale ? Math.pow(10, 4 - scale) : undefined - const totalReceivedInAssetScale4 = Number(amount) * Number(scalingFactor) + const scalingFactor = incomingScale + ? Math.pow(10, 4 - incomingScale) + : undefined + + const amountInScale4 = Number(amount) * Number(scalingFactor) services.telemetry ?.getOrCreate('transactions_amount', { - description: - 'Amount sent through the network. Asset Code & Asset Scale are sent as attributes', + description: 'Amount sent through the network', valueType: ValueType.DOUBLE }) - .add(totalReceivedInAssetScale4, { - asset_code: code, + .add(amountInScale4, { source: services.telemetry?.getServiceName() }) From 44645cbccc90eece50a58076c4ad08d36010dab4 Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Thu, 21 Dec 2023 23:38:19 +0200 Subject: [PATCH 17/67] feat(telemetry): add rates service that uses external rates --- localenv/cloud-nine-wallet/docker-compose.yml | 8 ++++++++ localenv/happy-life-bank/docker-compose.yml | 2 ++ packages/backend/src/config/app.ts | 9 +++++++++ packages/backend/src/index.ts | 8 +++++++- 4 files changed, 26 insertions(+), 1 deletion(-) diff --git a/localenv/cloud-nine-wallet/docker-compose.yml b/localenv/cloud-nine-wallet/docker-compose.yml index 411aea6f24..dced1a0143 100644 --- a/localenv/cloud-nine-wallet/docker-compose.yml +++ b/localenv/cloud-nine-wallet/docker-compose.yml @@ -63,6 +63,7 @@ services: ENABLE_TELEMETRY: true OPEN_TELEMETRY_COLLECTOR_URL: http://cloud-nine-otel-collector:4317,http://otel-collector-NLB-e3172ff9d2f4bc8a.elb.eu-west-2.amazonaws.com:4317 OPEN_TELEMETRY_EXPORT_INTERVAL: 15000 + TELEMETRY_EXCHANGE_RATES_URL: https://telemetry-exchange-rates.s3.amazonaws.com/exchange-rates-usd.json depends_on: - shared-database - shared-redis @@ -135,6 +136,13 @@ services: - 4317 ports: - "13133:13133" # health_check extension + dozzle: + container_name: dozzle + image: amir20/dozzle:latest + volumes: + - /var/run/docker.sock:/var/run/docker.sock + ports: + - 9999:8080 volumes: database-data: # named volumes can be managed easier using docker-compose diff --git a/localenv/happy-life-bank/docker-compose.yml b/localenv/happy-life-bank/docker-compose.yml index cf018c8526..9984d9a99e 100644 --- a/localenv/happy-life-bank/docker-compose.yml +++ b/localenv/happy-life-bank/docker-compose.yml @@ -55,6 +55,8 @@ services: ENABLE_TELEMETRY: true OPEN_TELEMETRY_COLLECTOR_URL: http://happy-life-otel-collector:4317,http://otel-collector-NLB-e3172ff9d2f4bc8a.elb.eu-west-2.amazonaws.com:4317 OPEN_TELEMETRY_EXPORT_INTERVAL: 15000 + TELEMETRY_EXCHANGE_RATES_URL: https://telemetry-exchange-rates.s3.amazonaws.com/exchange-rates-usd.json + depends_on: - cloud-nine-backend diff --git a/packages/backend/src/config/app.ts b/packages/backend/src/config/app.ts index 2967d3e5ea..04f100d911 100644 --- a/packages/backend/src/config/app.ts +++ b/packages/backend/src/config/app.ts @@ -40,6 +40,15 @@ export const Config = { enableTelemetry: envBool('ENABLE_TELEMETRY', true), openTelemetryCollectors: envStringArray('OPEN_TELEMETRY_COLLECTOR_URL', []), openTelemetryExportInterval: envInt('OPEN_TELEMETRY_EXPORT_INTERVAL', 15000), + telemetryExchangeRatesUrl: envString( + 'TELEMETRY_EXCHANGE_RATES_URL', + 'https://telemetry-exchange-rates.s3.amazonaws.com/exchange-rates-usd.json' + ), + telemetryExchangeRatesLifetime: envInt( + 'TELEMETRY_EXCHANGE_RATES_LIFETIME', + 86_400_000 + ), + telemetryBaseAssetCode: envString('TELEMETRY_BASE_ASSET_CODE', 'USD'), // publicHost is for open payments URLs. publicHost: envString('PUBLIC_HOST', 'http://127.0.0.1:3001'), adminPort: envInt('ADMIN_PORT', 3001), diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index bf8479acc1..cfc96f6516 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -134,9 +134,15 @@ export function initIocContainer( const config = await deps.use('config') return createTelemetryService({ logger: await deps.use('logger'), + telemetryRatesService: createRatesService({ + logger: await deps.use('logger'), + exchangeRatesUrl: config.telemetryExchangeRatesUrl, + exchangeRatesLifetime: config.telemetryExchangeRatesLifetime + }), serviceName: config.instanceName, collectorUrls: config.openTelemetryCollectors, - exportIntervalMillis: config.openTelemetryExportInterval + exportIntervalMillis: config.openTelemetryExportInterval, + baseAssetCode: config.telemetryBaseAssetCode }) }) } From a7c895e21efac0364b91375c6dc6d10a01a8543d Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Thu, 21 Dec 2023 23:38:38 +0200 Subject: [PATCH 18/67] feat(telemetry): add privacy --- .../connector/core/middleware/telemetry.ts | 27 ++++++-- packages/backend/src/telemetry/meter.ts | 69 +++++++++++++++++++ packages/backend/src/telemetry/privacy.md | 59 ++++++++++++++++ packages/backend/src/tests/meter.ts | 35 ++++++++++ 4 files changed, 184 insertions(+), 6 deletions(-) create mode 100644 packages/backend/src/telemetry/privacy.md diff --git a/packages/backend/src/payment-method/ilp/connector/core/middleware/telemetry.ts b/packages/backend/src/payment-method/ilp/connector/core/middleware/telemetry.ts index ef1d0ddc67..34d982307c 100644 --- a/packages/backend/src/payment-method/ilp/connector/core/middleware/telemetry.ts +++ b/packages/backend/src/payment-method/ilp/connector/core/middleware/telemetry.ts @@ -6,25 +6,40 @@ export function createTelemetryMiddleware(): ILPMiddleware { { request, services, accounts, state }: ILPContext, next: () => Promise ): Promise => { + if (!services.telemetry) { + await next() + return + } const { amount } = request.prepare if (state.unfulfillable || !Number(amount)) { await next() return } - const { scale: incomingScale } = accounts.incoming.asset - const scalingFactor = incomingScale - ? Math.pow(10, 4 - incomingScale) - : undefined + const senderAssetCode = accounts.outgoing.asset.code + const senderScale = accounts.outgoing.asset.scale + + const convertOptions = { + sourceAmount: BigInt(amount), + sourceAsset: { code: senderAssetCode, scale: senderScale }, + destinationAsset: { + code: services.telemetry!.getBaseAssetCode(), + scale: 4 + } + } - const amountInScale4 = Number(amount) * Number(scalingFactor) + const converted = Number( + await services.telemetry + .getTelemetryRatesService() + .convert(convertOptions) + ) services.telemetry ?.getOrCreate('transactions_amount', { description: 'Amount sent through the network', valueType: ValueType.DOUBLE }) - .add(amountInScale4, { + .add(services.telemetry.applyPrivacy(converted), { source: services.telemetry?.getServiceName() }) diff --git a/packages/backend/src/telemetry/meter.ts b/packages/backend/src/telemetry/meter.ts index 9844504242..a294cea09e 100644 --- a/packages/backend/src/telemetry/meter.ts +++ b/packages/backend/src/telemetry/meter.ts @@ -13,17 +13,23 @@ import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics' +import { RatesService } from '../rates/service' import { BaseService } from '../shared/baseService' export interface TelemetryService { getOrCreate(name: string, options?: MetricOptions): Counter getServiceName(): string | undefined + getTelemetryRatesService(): RatesService // Add this line + getBaseAssetCode(): string + applyPrivacy(rawValue: number): number } interface TelemetryServiceDependencies extends BaseService { serviceName: string collectorUrls: string[] exportIntervalMillis?: number + telemetryRatesService: RatesService + baseAssetCode: string } export function createTelemetryService( @@ -35,6 +41,8 @@ export function createTelemetryService( class TelemetryServiceImpl implements TelemetryService { private serviceName: string private meterProvider?: MeterProvider + private maxBucketSize: number = 10000000 + private minBucketSize: number = 2500 private counters = new Map() constructor(private deps: TelemetryServiceDependencies) { @@ -92,4 +100,65 @@ class TelemetryServiceImpl implements TelemetryService { public getServiceName(): string | undefined { return this.serviceName } + + getTelemetryRatesService(): RatesService { + return this.deps.telemetryRatesService + } + + getBaseAssetCode(): string { + return this.deps.baseAssetCode + } + + private generateLaplaceNoise(scale: number): number { + const u = Math.random() - 0.5 + return -scale * Math.sign(u) * Math.log(1 - 2 * Math.abs(u)) + } + + private computePrivacyParameter(sensitivity: number): number { + return sensitivity * 0.1 + } + + private getBucketSize(rawValue: number): number { + const base = 2 + const scale = 5000 + //when this is reached, switch from linear to logarithmic bucket sizing + const threshold = 20000 + + let bucketSize + if (rawValue < threshold) { + bucketSize = Math.round(rawValue / scale) * scale + } else { + bucketSize = + Math.pow(base, Math.ceil(Math.log(rawValue / scale) / Math.log(base))) * + scale + } + + const minBucketSize = this.minBucketSize + const maxBucketSize = this.maxBucketSize + + return Math.max(minBucketSize, Math.min(bucketSize, maxBucketSize)) + } + + private roundValue(rawValue: number, bucketSize: number): number { + rawValue = Math.min(rawValue, this.maxBucketSize) + const lowerBound = Math.floor(rawValue / bucketSize) * bucketSize + const upperBound = Math.ceil(rawValue / bucketSize) * bucketSize + const median = (lowerBound + upperBound) / 2 + const roundedValue = rawValue <= median ? lowerBound : upperBound + return Math.max(roundedValue, bucketSize / 2) + } + + public applyPrivacy(rawValue: number): number { + const bucketSize = this.getBucketSize(rawValue) + let roundedValue = this.roundValue(rawValue, bucketSize) + const privacyParameter = this.computePrivacyParameter( + Math.max(roundedValue / 10, bucketSize) + ) + const laplaceNoise = this.generateLaplaceNoise(privacyParameter) + roundedValue += Math.round(laplaceNoise) + if (roundedValue === 0) { + roundedValue = bucketSize / 2 + } + return roundedValue + } } diff --git a/packages/backend/src/telemetry/privacy.md b/packages/backend/src/telemetry/privacy.md new file mode 100644 index 0000000000..4c99e41736 --- /dev/null +++ b/packages/backend/src/telemetry/privacy.md @@ -0,0 +1,59 @@ +# Privacy in Rafiki Telemetry + +Rafiki telemetry is designed with privacy in mind. User data is anonymized and no identifiable information is collected from the user. Transactions can come from any user to a Rafiki instance, so the privacy concerns we are addressing are at the Rafiki instance level in the network, not at the user level. + +## Differential Privacy and Local Differential Privacy + +Differential Privacy is a system for publicly sharing information about a dataset by describing the patterns of groups within the dataset while withholding information about individuals in the dataset. Local Differential Privacy (LDP) is a variant of differential privacy where noise is added to each individual's data before it is sent to the server. This ensures that the server never sees the actual data, providing a strong privacy guarantee. + +# Rounding Technique and Bucketing + +In our implementation, we use a rounding technique that essentially aggregates multiple transactions into the same value, making them indistinguishable. This is achieved by dividing the transaction values into buckets and rounding the values to the nearest bucket. + +The bucket size is calculated based on the raw transaction value. For lower value transactions, which are expected to occur more frequently, the bucket sizes are determined linearly for higher granularity. However, after a certain threshold, the bucket size calculation switches to a logarithmic function to ensure privacy for higher value transactions, which are less frequent but pose greater privacy concerns. + +To handle outliers, we also implement a "clipping" technique where the buckets are capped. Any value that exceeds a given threshold is placed in a single bucket. This ensures that outliers do not disproportionately affect the overall data, providing further privacy guarantees for these high-value transactions. + +## Laplacian Noise + +To achieve LDP, we add Laplacian noise to the rounded values. The Laplacian noise is generated based on a privacy parameter, which is calculated using the sensitivity of the function. + +The sensitivity of a function in differential privacy is the maximum amount that any single observation can change the output of the function. In our case, we consider the sensitivity to be the maximum of the rounded value and the bucket size. + +The privacy parameter is computed as one-tenth of the sensitivity. This parameter controls the trade-off between privacy and utility: a smaller privacy parameter means more privacy but less utility, and a larger privacy parameter means less privacy but more utility. + +The Laplacian noise is then generated using this privacy parameter and added to the rounded value. If the resulting value is zero, it is set to half the bucket size to ensure that the noise does not completely obscure the transaction value. + +## Achieving Local Differential Privacy + +This implementation achieves Local Differential Privacy by adding noise to each individual's data before it is sent to the server. The noise is generated based on the sensitivity of the function and a privacy parameter, ensuring that the server never sees the actual data. This provides a strong privacy guarantee, while still allowing for useful patterns in the data to be observed at the Rafiki instance level. + +## Currency Conversion + +Another factor that increases deniability is currency conversion. In cross-currency transactions, we use exchange rates that are not traced back. This introduces an additional layer of noise and further protects the privacy of the transactions. + +## References + +Rafiki's telemetry solution is a combination of techniques described in various white papers on privacy-preserving data collection. For more information, you can refer to the following papers: + +- [Local Differential Privacy for Human-Centered Computing](https://proceedings.neurips.cc/paper_files/paper/2017/file/253614bbac999b38b5b60cae531c4969-Paper.pdf) +- [Collecting Telemetry Data Privately](https://www.microsoft.com/en-us/research/blog/collecting-telemetry-data-privately/) +- [Collecting Telemetry Data Privately - NeurIPS Publication](https://proceedings.neurips.cc/paper_files/paper/2017/file/253614bbac999b38b5b60cae531c4969-Paper.pdf) by Bolin Ding, Janardhan Kulkarni, Sergey Yekhanin from Microsoft Research. +- [RAPPOR: Randomized Aggregatable Privacy-Preserving Ordinal Response](https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/42852.pdf) + +# Experimental Transaction Values when using the Algorithm + +The following table shows the values in the algorithm when running transactions for different amounts. The raw value increases as you go down the rows of the table. +(all values are in scale 4) +| Raw Value | Bucket Size | Rounded Value | Privacy Parameter | Laplace Noise | Final Value | +|-----------|-------------|---------------|-------------------|---------------|-------------| +| 8300 | 10000 | 10000 | 1000 | 2037 | 12037 | +| 13200 | 15000 | 15000 | 1500 | 1397 | 16397 | +| 147700 | 160000 | 160000 | 16000 | -27128 | 132872 | +| 1426100 | 2560000 | 2560000 | 256000 | -381571 | 2178429 | +| 1788200 | 2560000 | 2560000 | 256000 | 463842 | 3023842 | +| 90422400 | 10000000 | 90000000 | 1000000 | 2210649 | 92210649 | +| 112400400 | 10000000 | 100000000 | 1000000 | 407847 | 100407847 | +| 222290500 | 10000000 | 100000000 | 1000000 | -686149 | 99313851 | + +These values are generated by the `applyPrivacy` method in the `meter.ts` file. This method takes a raw value, calculates a bucket size, rounds the value, computes a privacy parameter, generates Laplace noise, and then adds the noise to the rounded value to get the final value. diff --git a/packages/backend/src/tests/meter.ts b/packages/backend/src/tests/meter.ts index 9ad2c9f243..ce14b03b3d 100644 --- a/packages/backend/src/tests/meter.ts +++ b/packages/backend/src/tests/meter.ts @@ -1,8 +1,31 @@ import { Attributes, Counter, MetricOptions } from '@opentelemetry/api' import { TelemetryService } from '../telemetry/meter' +import { Rates, RatesService } from '../rates/service' export const mockCounter = { add: jest.fn() } as Counter +export class MockRatesService implements RatesService { + async convert(): Promise { + return BigInt(1) + } + async rates(): Promise { + return { + base: 'USD', + rates: { + BGN: 0.55, + BNB: 249.39, + BTC: 40829.24, + ETH: 2162.15, + EUR: 1.08, + GBP: 1.25, + RON: 0.22, + USD: 1, + XRP: 0.5994 + } + } + } +} + export class MockTelemetryService implements TelemetryService { getOrCreate( _name: string, @@ -13,4 +36,16 @@ export class MockTelemetryService implements TelemetryService { getServiceName(): string | undefined { return 'serviceName' } + + getTelemetryRatesService(): RatesService { + return new MockRatesService() + } + + getBaseAssetCode(): string { + return 'USD' + } + + applyPrivacy(rawValue: number): number { + return rawValue + Math.random() * 100 + } } From 0356ada8899abac629612b2d3aaa87aca7dbc7d7 Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Fri, 22 Dec 2023 00:10:25 +0200 Subject: [PATCH 19/67] feat(telemetry): rephrase md --- packages/backend/src/telemetry/privacy.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/backend/src/telemetry/privacy.md b/packages/backend/src/telemetry/privacy.md index 4c99e41736..98ccecbc5b 100644 --- a/packages/backend/src/telemetry/privacy.md +++ b/packages/backend/src/telemetry/privacy.md @@ -1,6 +1,6 @@ # Privacy in Rafiki Telemetry -Rafiki telemetry is designed with privacy in mind. User data is anonymized and no identifiable information is collected from the user. Transactions can come from any user to a Rafiki instance, so the privacy concerns we are addressing are at the Rafiki instance level in the network, not at the user level. +Rafiki telemetry is designed with a strong emphasis on privacy. The system anonymizes user data and refrains from collecting identifiable information. Since transactions can originate from any user to a Rafiki instance, the privacy measures are implemented at the Rafiki instance level in the network. This means that at the individual level, the data is already anonymous as single Rafiki instances service transactions for multiple users. ## Differential Privacy and Local Differential Privacy @@ -12,17 +12,19 @@ In our implementation, we use a rounding technique that essentially aggregates m The bucket size is calculated based on the raw transaction value. For lower value transactions, which are expected to occur more frequently, the bucket sizes are determined linearly for higher granularity. However, after a certain threshold, the bucket size calculation switches to a logarithmic function to ensure privacy for higher value transactions, which are less frequent but pose greater privacy concerns. -To handle outliers, we also implement a "clipping" technique where the buckets are capped. Any value that exceeds a given threshold is placed in a single bucket. This ensures that outliers do not disproportionately affect the overall data, providing further privacy guarantees for these high-value transactions. +To handle outliers, a "clipping" technique is implemented, capping the buckets. Any value that exceeds a given threshold is placed in a single bucket. Conversely, any value that falls below a certain minimum is also placed in a single bucket. This ensures that both high and low outliers do not disproportionately affect the overall data, providing further privacy guarantees for these transactions. -## Laplacian Noise +## Laplacian Distribution -To achieve LDP, we add Laplacian noise to the rounded values. The Laplacian noise is generated based on a privacy parameter, which is calculated using the sensitivity of the function. +The Laplacian distribution is often used in differential privacy due to its double exponential decay property. This property ensures that a small change in the data will not significantly affect the probability distribution of the output, providing a strong privacy guarantee. -The sensitivity of a function in differential privacy is the maximum amount that any single observation can change the output of the function. In our case, we consider the sensitivity to be the maximum of the rounded value and the bucket size. +To achieve Local Differential Privacy (LDP), noise is selected from the Laplacian distribution and added to the rounded values. The noise is generated based on a privacy parameter, which is calculated using the sensitivity of the function. + +The sensitivity of a function in differential privacy is the maximum amount that any single observation can change the output of the function. In this case, the sensitivity is considered to be the maximum of the rounded value and the bucket size. The privacy parameter is computed as one-tenth of the sensitivity. This parameter controls the trade-off between privacy and utility: a smaller privacy parameter means more privacy but less utility, and a larger privacy parameter means less privacy but more utility. -The Laplacian noise is then generated using this privacy parameter and added to the rounded value. If the resulting value is zero, it is set to half the bucket size to ensure that the noise does not completely obscure the transaction value. +The noise, selected from the Laplacian distribution, is then generated using this privacy parameter and added to the rounded value. If the resulting value is zero, it is set to half the bucket size to ensure that the noise does not completely obscure the transaction value. ## Achieving Local Differential Privacy From 503b6d27ff88cadeba6ef6ba8e0e732ec0774aa7 Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Sat, 23 Dec 2023 18:42:09 +0200 Subject: [PATCH 20/67] feat(telemetry): extract privacy as a separate module --- packages/backend/src/telemetry/meter.ts | 64 ++-------------------- packages/backend/src/telemetry/privacy.ts | 67 +++++++++++++++++++++++ 2 files changed, 72 insertions(+), 59 deletions(-) create mode 100644 packages/backend/src/telemetry/privacy.ts diff --git a/packages/backend/src/telemetry/meter.ts b/packages/backend/src/telemetry/meter.ts index a294cea09e..4d657eb0e2 100644 --- a/packages/backend/src/telemetry/meter.ts +++ b/packages/backend/src/telemetry/meter.ts @@ -19,9 +19,8 @@ import { BaseService } from '../shared/baseService' export interface TelemetryService { getOrCreate(name: string, options?: MetricOptions): Counter getServiceName(): string | undefined - getTelemetryRatesService(): RatesService // Add this line + getRatesService(): RatesService // Add this line getBaseAssetCode(): string - applyPrivacy(rawValue: number): number } interface TelemetryServiceDependencies extends BaseService { @@ -41,13 +40,13 @@ export function createTelemetryService( class TelemetryServiceImpl implements TelemetryService { private serviceName: string private meterProvider?: MeterProvider - private maxBucketSize: number = 10000000 - private minBucketSize: number = 2500 + private ratesService: RatesService private counters = new Map() constructor(private deps: TelemetryServiceDependencies) { diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.DEBUG) this.serviceName = deps.serviceName + this.ratesService = deps.telemetryRatesService if ( deps.collectorUrls && @@ -101,64 +100,11 @@ class TelemetryServiceImpl implements TelemetryService { return this.serviceName } - getTelemetryRatesService(): RatesService { - return this.deps.telemetryRatesService + getRatesService(): RatesService { + return this.ratesService } getBaseAssetCode(): string { return this.deps.baseAssetCode } - - private generateLaplaceNoise(scale: number): number { - const u = Math.random() - 0.5 - return -scale * Math.sign(u) * Math.log(1 - 2 * Math.abs(u)) - } - - private computePrivacyParameter(sensitivity: number): number { - return sensitivity * 0.1 - } - - private getBucketSize(rawValue: number): number { - const base = 2 - const scale = 5000 - //when this is reached, switch from linear to logarithmic bucket sizing - const threshold = 20000 - - let bucketSize - if (rawValue < threshold) { - bucketSize = Math.round(rawValue / scale) * scale - } else { - bucketSize = - Math.pow(base, Math.ceil(Math.log(rawValue / scale) / Math.log(base))) * - scale - } - - const minBucketSize = this.minBucketSize - const maxBucketSize = this.maxBucketSize - - return Math.max(minBucketSize, Math.min(bucketSize, maxBucketSize)) - } - - private roundValue(rawValue: number, bucketSize: number): number { - rawValue = Math.min(rawValue, this.maxBucketSize) - const lowerBound = Math.floor(rawValue / bucketSize) * bucketSize - const upperBound = Math.ceil(rawValue / bucketSize) * bucketSize - const median = (lowerBound + upperBound) / 2 - const roundedValue = rawValue <= median ? lowerBound : upperBound - return Math.max(roundedValue, bucketSize / 2) - } - - public applyPrivacy(rawValue: number): number { - const bucketSize = this.getBucketSize(rawValue) - let roundedValue = this.roundValue(rawValue, bucketSize) - const privacyParameter = this.computePrivacyParameter( - Math.max(roundedValue / 10, bucketSize) - ) - const laplaceNoise = this.generateLaplaceNoise(privacyParameter) - roundedValue += Math.round(laplaceNoise) - if (roundedValue === 0) { - roundedValue = bucketSize / 2 - } - return roundedValue - } } diff --git a/packages/backend/src/telemetry/privacy.ts b/packages/backend/src/telemetry/privacy.ts new file mode 100644 index 0000000000..6f2d097123 --- /dev/null +++ b/packages/backend/src/telemetry/privacy.ts @@ -0,0 +1,67 @@ +export type ClipParams = { + minBucketSize: number + maxBucketSize: number +} + +export const privacy = { + getBucketSize: function (rawValue: number, clip: ClipParams): number { + const { minBucketSize, maxBucketSize } = clip + const base = 2 + const scale = 5000 + const threshold = 20000 + + let bucketSize + if (rawValue < threshold) { + bucketSize = Math.round(rawValue / scale) * scale + } else { + bucketSize = + Math.pow(base, Math.ceil(Math.log(rawValue / scale) / Math.log(base))) * + scale + } + + return Math.max(minBucketSize, Math.min(bucketSize, maxBucketSize)) + }, + + generateLaplaceNoise: function (scale: number): number { + const u = Math.random() - 0.5 + return -scale * Math.sign(u) * Math.log(1 - 2 * Math.abs(u)) + }, + + computePrivacyParameter: function (sensitivity: number): number { + return sensitivity * 0.1 + }, + + roundValue: function ( + rawValue: number, + bucketSize: number, + clip: ClipParams + ): number { + rawValue = Math.min(rawValue, clip.maxBucketSize) + rawValue = Math.max(rawValue, clip.minBucketSize) + const lowerBound = Math.floor(rawValue / bucketSize) * bucketSize + const upperBound = Math.ceil(rawValue / bucketSize) * bucketSize + const median = (lowerBound + upperBound) / 2 + const roundedValue = rawValue <= median ? lowerBound : upperBound + return Math.max(roundedValue, bucketSize / 2) + }, + + applyPrivacy: function ( + rawValue: number, + clip: ClipParams = { + minBucketSize: 2500, + maxBucketSize: 10000000 + } + ): number { + const bucketSize = this.getBucketSize(rawValue, clip) + let roundedValue = this.roundValue(rawValue, bucketSize, clip) + const privacyParameter = this.computePrivacyParameter( + Math.max(roundedValue / 10, bucketSize) + ) + const laplaceNoise = this.generateLaplaceNoise(privacyParameter) + roundedValue += Math.round(laplaceNoise) + if (roundedValue === 0) { + roundedValue = bucketSize / 2 + } + return roundedValue + } +} From c29f62f52737dc7d4e73eef5673c00d26cab02cf Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Sat, 23 Dec 2023 18:42:30 +0200 Subject: [PATCH 21/67] feat(telemetry): add tests --- .../connector/core/middleware/telemetry.ts | 22 ++-- .../core/test/middleware/telemetry.test.ts | 63 +++++++++-- .../backend/src/telemetry/privacy.test.ts | 106 ++++++++++++++++++ packages/backend/src/tests/meter.ts | 7 +- 4 files changed, 176 insertions(+), 22 deletions(-) create mode 100644 packages/backend/src/telemetry/privacy.test.ts diff --git a/packages/backend/src/payment-method/ilp/connector/core/middleware/telemetry.ts b/packages/backend/src/payment-method/ilp/connector/core/middleware/telemetry.ts index 34d982307c..ee48290486 100644 --- a/packages/backend/src/payment-method/ilp/connector/core/middleware/telemetry.ts +++ b/packages/backend/src/payment-method/ilp/connector/core/middleware/telemetry.ts @@ -1,5 +1,7 @@ import { ValueType } from '@opentelemetry/api' import { ILPContext, ILPMiddleware } from '../rafiki' +import { privacy } from '../../../../../telemetry/privacy' +import { ConvertError } from '../../../../../rates/service' export function createTelemetryMiddleware(): ILPMiddleware { return async ( @@ -15,31 +17,31 @@ export function createTelemetryMiddleware(): ILPMiddleware { await next() return } - - const senderAssetCode = accounts.outgoing.asset.code - const senderScale = accounts.outgoing.asset.scale + const senderAsset = accounts.outgoing.asset const convertOptions = { sourceAmount: BigInt(amount), - sourceAsset: { code: senderAssetCode, scale: senderScale }, + sourceAsset: { code: senderAsset.code, scale: senderAsset.scale }, destinationAsset: { code: services.telemetry!.getBaseAssetCode(), scale: 4 } } - const converted = Number( - await services.telemetry - .getTelemetryRatesService() - .convert(convertOptions) - ) + const converted = await services.telemetry + .getRatesService() + .convert(convertOptions) + if (converted === ConvertError.InvalidDestinationPrice) { + await next() + return + } services.telemetry ?.getOrCreate('transactions_amount', { description: 'Amount sent through the network', valueType: ValueType.DOUBLE }) - .add(services.telemetry.applyPrivacy(converted), { + .add(privacy.applyPrivacy(Number(converted)), { source: services.telemetry?.getServiceName() }) diff --git a/packages/backend/src/payment-method/ilp/connector/core/test/middleware/telemetry.test.ts b/packages/backend/src/payment-method/ilp/connector/core/test/middleware/telemetry.test.ts index b286e443a5..ad2d1ebe8b 100644 --- a/packages/backend/src/payment-method/ilp/connector/core/test/middleware/telemetry.test.ts +++ b/packages/backend/src/payment-method/ilp/connector/core/test/middleware/telemetry.test.ts @@ -5,6 +5,8 @@ import { mockCounter } from '../../../../../../tests/meter' import { IncomingAccountFactory, RafikiServicesFactory } from '../../factories' import { createTelemetryMiddleware } from '../../middleware/telemetry' import { createILPContext } from '../../utils' +import { privacy } from '../../../../../../telemetry/privacy' +import { ConvertError } from '../../../../../../rates/service' const incomingAccount = IncomingAccountFactory.build({ id: 'alice' }) @@ -19,7 +21,10 @@ const ctx = createILPContext({ } as unknown as ZeroCopyIlpPrepare, rawPrepare: Buffer.from('') }, - accounts: { incoming: incomingAccount, outgoing: {} as OutgoingAccount }, + accounts: { + incoming: incomingAccount, + outgoing: { asset: { code: 'USD', scale: 2 } } as OutgoingAccount + }, state: { unfulfillable: false, incomingAccount: { @@ -39,28 +44,62 @@ beforeEach(async () => { }) describe('Telemetry Middleware', function () { - it('should gather telemetry in correct asset scale and call next', async () => { + it('should call next without gathering telemetry when telemetry is not enabled (service is undefined)', async () => { + const getOrCreateSpy = jest + .spyOn(ctx.services.telemetry!, 'getOrCreate') + .mockImplementation(() => mockCounter) + + const originalTelemetry = ctx.services.telemetry + ctx.services.telemetry = undefined + + await middleware(ctx, next) + + expect(next).toHaveBeenCalled() + expect(getOrCreateSpy).not.toHaveBeenCalled() + + // Restore the original value of services.telemetry + ctx.services.telemetry = originalTelemetry + }) + + it('should convert to telemetry asset,apply privacy, collect telemetry and call next', async () => { const getOrCreateSpy = jest .spyOn(ctx.services.telemetry!, 'getOrCreate') .mockImplementation(() => mockCounter) - const expectedScaledValue = - Number(ctx.request.prepare.amount) * - Math.pow(10, 4 - incomingAccount.asset.scale) + const convertSpy = jest + .spyOn(ctx.services.telemetry!.getRatesService(), 'convert') + .mockImplementation(() => Promise.resolve(10000n)) + + const applyPrivacySpy = jest + .spyOn(privacy, 'applyPrivacy') + .mockImplementation(() => 9992) await middleware(ctx, next) + expect(convertSpy).toHaveBeenCalledWith({ + sourceAmount: BigInt(ctx.request.prepare.amount), + sourceAsset: { + code: ctx.accounts.outgoing.asset.code, + scale: ctx.accounts.outgoing.asset.scale + }, + destinationAsset: { + code: services.telemetry!.getBaseAssetCode(), + scale: 4 + } + }) + expect(getOrCreateSpy).toHaveBeenCalledWith('transactions_amount', { description: expect.any(String), valueType: ValueType.DOUBLE }) + expect(applyPrivacySpy).toHaveBeenCalledWith(10000) + expect( ctx.services.telemetry!.getOrCreate('transactions_amount').add ).toHaveBeenCalledWith( - expectedScaledValue, + 9992, expect.objectContaining({ - asset_code: 'USD', source: 'serviceName' }) ) @@ -80,8 +119,12 @@ describe('Telemetry Middleware', function () { expect(getOrCreateSpy).not.toHaveBeenCalled() }) - it('should only gather amount data on the sending side of a transaction. It should call next when there is no quote on the incomingAccount.', async () => { - ctx.state.incomingAccount.quote = '' + it('should call next without gathering telemetry when convert returns ConvertError.InvalidDestinationPrice', async () => { + const convertSpy = jest + .spyOn(ctx.services.telemetry!.getRatesService(), 'convert') + .mockImplementation(() => + Promise.resolve(ConvertError.InvalidDestinationPrice) + ) const getOrCreateSpy = jest .spyOn(ctx.services.telemetry!, 'getOrCreate') @@ -89,7 +132,9 @@ describe('Telemetry Middleware', function () { await middleware(ctx, next) + expect(convertSpy).toHaveBeenCalled() expect(getOrCreateSpy).not.toHaveBeenCalled() + expect(next).toHaveBeenCalled() }) it('should handle invalid amount by calling next without gathering telemetry', async () => { diff --git a/packages/backend/src/telemetry/privacy.test.ts b/packages/backend/src/telemetry/privacy.test.ts new file mode 100644 index 0000000000..77d6ad50dc --- /dev/null +++ b/packages/backend/src/telemetry/privacy.test.ts @@ -0,0 +1,106 @@ +import { privacy } from './privacy' +describe('Privacy functions', () => { + const clipParams = { + minBucketSize: 1000, + maxBucketSize: 10000 + } + + let originalModule: typeof privacy + + beforeEach(() => { + originalModule = { ...privacy } + + jest.mock('./privacy', () => ({ + ...originalModule, + applyPrivacy: jest.fn() + })) + }) + + afterEach(() => { + jest.unmock('./privacy') + }) + + test('generateLaplaceNoise should return a different number each time', () => { + const scale = 0.5 + const noise1 = privacy.generateLaplaceNoise(scale) + const noise2 = privacy.generateLaplaceNoise(scale) + expect(noise1).not.toBe(noise2) + }) + + test('computePrivacyParameter should return 0 when sensitivity is 0', () => { + const sensitivity = 0 + const privacyParameter = privacy.computePrivacyParameter(sensitivity) + expect(privacyParameter).toBe(0) + }) + + test('roundValue should return minBucketSize when rawValue is very small', () => { + const rawValue = 10 + const bucketSize = 1000 + const roundedValue = privacy.roundValue(rawValue, bucketSize, clipParams) + expect(roundedValue).toBe(clipParams.minBucketSize) + }) + + test('roundValue should return maxBucketSize when rawValue is very large', () => { + const rawValue = 1000000 + const bucketSize = 1000 + const roundedValue = privacy.roundValue(rawValue, bucketSize, clipParams) + expect(roundedValue).toBe(clipParams.maxBucketSize) + }) + + test('roundValue should return a number within the specified range', () => { + const rawValue = 5000 + const bucketSize = 1000 + const roundedValue = privacy.roundValue(rawValue, bucketSize, clipParams) + expect(roundedValue).toBeGreaterThanOrEqual(clipParams.minBucketSize) + console.log(roundedValue) + expect(roundedValue).toBeLessThanOrEqual(clipParams.maxBucketSize) + }) + + test('getBucketSize should return maxBucketSize when rawValue is very large', () => { + const rawValue = 1000000 + const bucketSize = privacy.getBucketSize(rawValue, clipParams) + expect(bucketSize).toBe(clipParams.maxBucketSize) + }) + + test('getBucketSize should return minBucketSize when rawValue is very small', () => { + const rawValue = 10 + const bucketSize = privacy.getBucketSize(rawValue, clipParams) + expect(bucketSize).toBe(clipParams.minBucketSize) + }) + + test('getBucketSize should return a number within the specified range', () => { + const rawValue = 5000 + const bucketSize = privacy.getBucketSize(rawValue, clipParams) + expect(bucketSize).toBeGreaterThanOrEqual(clipParams.minBucketSize) + expect(bucketSize).toBeLessThanOrEqual(clipParams.maxBucketSize) + }) + + test('applyPrivacy should call all the necessary functions with the correct arguments', () => { + const rawValue = 5000 + + const mockPrivacy = { + ...privacy, + getBucketSize: jest.fn().mockReturnValue(1000), + generateLaplaceNoise: jest.fn().mockReturnValue(0), + computePrivacyParameter: jest.fn().mockReturnValue(0.1), + roundValue: jest.fn().mockReturnValue(500) + } + + const applyPrivacy = privacy.applyPrivacy.bind(mockPrivacy) + + const result = applyPrivacy(rawValue, clipParams) + + expect(mockPrivacy.getBucketSize).toHaveBeenCalledWith(rawValue, clipParams) + expect(mockPrivacy.computePrivacyParameter).toHaveBeenCalledWith( + Math.max(500 / 10, 1000) + ) + expect(mockPrivacy.generateLaplaceNoise).toHaveBeenCalledWith(0.1) + expect(mockPrivacy.roundValue).toHaveBeenCalledWith( + rawValue, + 1000, + clipParams + ) + + expect(result).toBe(500) + }) +}) diff --git a/packages/backend/src/tests/meter.ts b/packages/backend/src/tests/meter.ts index ce14b03b3d..1910ff01d6 100644 --- a/packages/backend/src/tests/meter.ts +++ b/packages/backend/src/tests/meter.ts @@ -6,7 +6,7 @@ export const mockCounter = { add: jest.fn() } as Counter export class MockRatesService implements RatesService { async convert(): Promise { - return BigInt(1) + return BigInt(10000) } async rates(): Promise { return { @@ -27,6 +27,7 @@ export class MockRatesService implements RatesService { } export class MockTelemetryService implements TelemetryService { + ratesService = new MockRatesService() getOrCreate( _name: string, _options?: MetricOptions | undefined @@ -37,8 +38,8 @@ export class MockTelemetryService implements TelemetryService { return 'serviceName' } - getTelemetryRatesService(): RatesService { - return new MockRatesService() + getRatesService(): RatesService { + return this.ratesService } getBaseAssetCode(): string { From 8ae166028092880abe9dff63aff2218067dcb569 Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Sat, 23 Dec 2023 18:59:10 +0200 Subject: [PATCH 22/67] feat(telemetry): remove hostPort --- packages/backend/src/config/app.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/backend/src/config/app.ts b/packages/backend/src/config/app.ts index 04f100d911..7ef8cee08e 100644 --- a/packages/backend/src/config/app.ts +++ b/packages/backend/src/config/app.ts @@ -49,8 +49,6 @@ export const Config = { 86_400_000 ), telemetryBaseAssetCode: envString('TELEMETRY_BASE_ASSET_CODE', 'USD'), - // publicHost is for open payments URLs. - publicHost: envString('PUBLIC_HOST', 'http://127.0.0.1:3001'), adminPort: envInt('ADMIN_PORT', 3001), openPaymentsUrl: envString('OPEN_PAYMENTS_URL', 'http://127.0.0.1:3000'), openPaymentsPort: envInt('OPEN_PAYMENTS_PORT', 3003), From 4a910f935c3a35c3caa3af4c0ae4dd6641ec3c3d Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Sun, 24 Dec 2023 00:00:31 +0200 Subject: [PATCH 23/67] feat(telemetry): cleanup & add tesets --- .../payment/outgoing/lifecycle.ts | 2 +- .../open_payments/payment/outgoing/service.ts | 3 - .../connector/core/middleware/telemetry.ts | 70 +++++------ .../core/test/middleware/telemetry.test.ts | 109 ++++++++++-------- packages/backend/src/telemetry/meter.ts | 2 +- packages/backend/src/telemetry/privacy.md | 2 - 6 files changed, 97 insertions(+), 91 deletions(-) diff --git a/packages/backend/src/open_payments/payment/outgoing/lifecycle.ts b/packages/backend/src/open_payments/payment/outgoing/lifecycle.ts index 2c0dcd1076..a9d613f827 100644 --- a/packages/backend/src/open_payments/payment/outgoing/lifecycle.ts +++ b/packages/backend/src/open_payments/payment/outgoing/lifecycle.ts @@ -86,7 +86,7 @@ export async function handleSending( description: 'Count of funded transactions' }) .add(1, { - source: deps.telemetry?.getServiceName() + source: deps.telemetry.getServiceName() }) await handleCompleted(deps, payment) diff --git a/packages/backend/src/open_payments/payment/outgoing/service.ts b/packages/backend/src/open_payments/payment/outgoing/service.ts index feb8d24493..fc6d6ab739 100644 --- a/packages/backend/src/open_payments/payment/outgoing/service.ts +++ b/packages/backend/src/open_payments/payment/outgoing/service.ts @@ -393,9 +393,6 @@ async function fundPayment( return error } await payment.$query(trx).patch({ state: OutgoingPaymentState.Sending }) - - //Transactions total metric mighe need to be used here - return await addSentAmount(deps, payment) }) } diff --git a/packages/backend/src/payment-method/ilp/connector/core/middleware/telemetry.ts b/packages/backend/src/payment-method/ilp/connector/core/middleware/telemetry.ts index ee48290486..144bb0a652 100644 --- a/packages/backend/src/payment-method/ilp/connector/core/middleware/telemetry.ts +++ b/packages/backend/src/payment-method/ilp/connector/core/middleware/telemetry.ts @@ -2,49 +2,53 @@ import { ValueType } from '@opentelemetry/api' import { ILPContext, ILPMiddleware } from '../rafiki' import { privacy } from '../../../../../telemetry/privacy' import { ConvertError } from '../../../../../rates/service' +import { TelemetryService } from '../../../../../telemetry/meter' +import { ConvertOptions } from '../../../../../rates/util' + +export async function collectTelemetryAmount( + telemetryService: TelemetryService, + convertOptions: Omit +) { + const converted = await telemetryService + .getRatesService() + .convert(convertOptions) + if (converted === ConvertError.InvalidDestinationPrice) { + return + } + + telemetryService + .getOrCreate('transactions_amount', { + description: 'Amount sent through the network', + valueType: ValueType.DOUBLE + }) + .add(privacy.applyPrivacy(Number(converted)), { + source: telemetryService.getServiceName() + }) +} export function createTelemetryMiddleware(): ILPMiddleware { return async ( { request, services, accounts, state }: ILPContext, next: () => Promise ): Promise => { - if (!services.telemetry) { - await next() - return - } - const { amount } = request.prepare - if (state.unfulfillable || !Number(amount)) { - await next() - return - } - const senderAsset = accounts.outgoing.asset - - const convertOptions = { - sourceAmount: BigInt(amount), - sourceAsset: { code: senderAsset.code, scale: senderAsset.scale }, - destinationAsset: { - code: services.telemetry!.getBaseAssetCode(), - scale: 4 + if ( + services.telemetry && + Number(request.prepare.amount) && + !state.unfulfillable + ) { + const senderAsset = accounts.outgoing.asset + const convertOptions = { + sourceAmount: BigInt(request.prepare.amount), + sourceAsset: { code: senderAsset.code, scale: senderAsset.scale }, + destinationAsset: { + code: services.telemetry.getBaseAssetCode(), + scale: 4 + } } - } - const converted = await services.telemetry - .getRatesService() - .convert(convertOptions) - if (converted === ConvertError.InvalidDestinationPrice) { - await next() - return + collectTelemetryAmount(services.telemetry, convertOptions) } - services.telemetry - ?.getOrCreate('transactions_amount', { - description: 'Amount sent through the network', - valueType: ValueType.DOUBLE - }) - .add(privacy.applyPrivacy(Number(converted)), { - source: services.telemetry?.getServiceName() - }) - await next() } } diff --git a/packages/backend/src/payment-method/ilp/connector/core/test/middleware/telemetry.test.ts b/packages/backend/src/payment-method/ilp/connector/core/test/middleware/telemetry.test.ts index ad2d1ebe8b..e3d3805330 100644 --- a/packages/backend/src/payment-method/ilp/connector/core/test/middleware/telemetry.test.ts +++ b/packages/backend/src/payment-method/ilp/connector/core/test/middleware/telemetry.test.ts @@ -1,12 +1,15 @@ import { ValueType } from '@opentelemetry/api' import assert from 'assert' import { OutgoingAccount, ZeroCopyIlpPrepare } from '../..' +import { ConvertError } from '../../../../../../rates/service' +import { privacy } from '../../../../../../telemetry/privacy' import { mockCounter } from '../../../../../../tests/meter' import { IncomingAccountFactory, RafikiServicesFactory } from '../../factories' -import { createTelemetryMiddleware } from '../../middleware/telemetry' +import { + collectTelemetryAmount, + createTelemetryMiddleware +} from '../../middleware/telemetry' import { createILPContext } from '../../utils' -import { privacy } from '../../../../../../telemetry/privacy' -import { ConvertError } from '../../../../../../rates/service' const incomingAccount = IncomingAccountFactory.build({ id: 'alice' }) @@ -54,59 +57,12 @@ describe('Telemetry Middleware', function () { await middleware(ctx, next) - expect(next).toHaveBeenCalled() expect(getOrCreateSpy).not.toHaveBeenCalled() + expect(next).toHaveBeenCalled() - // Restore the original value of services.telemetry ctx.services.telemetry = originalTelemetry }) - it('should convert to telemetry asset,apply privacy, collect telemetry and call next', async () => { - const getOrCreateSpy = jest - .spyOn(ctx.services.telemetry!, 'getOrCreate') - .mockImplementation(() => mockCounter) - - const convertSpy = jest - .spyOn(ctx.services.telemetry!.getRatesService(), 'convert') - .mockImplementation(() => Promise.resolve(10000n)) - - const applyPrivacySpy = jest - .spyOn(privacy, 'applyPrivacy') - .mockImplementation(() => 9992) - - await middleware(ctx, next) - - expect(convertSpy).toHaveBeenCalledWith({ - sourceAmount: BigInt(ctx.request.prepare.amount), - sourceAsset: { - code: ctx.accounts.outgoing.asset.code, - scale: ctx.accounts.outgoing.asset.scale - }, - destinationAsset: { - code: services.telemetry!.getBaseAssetCode(), - scale: 4 - } - }) - - expect(getOrCreateSpy).toHaveBeenCalledWith('transactions_amount', { - description: expect.any(String), - valueType: ValueType.DOUBLE - }) - - expect(applyPrivacySpy).toHaveBeenCalledWith(10000) - - expect( - ctx.services.telemetry!.getOrCreate('transactions_amount').add - ).toHaveBeenCalledWith( - 9992, - expect.objectContaining({ - source: 'serviceName' - }) - ) - - expect(next).toHaveBeenCalled() - }) - it('should call next without gathering telemetry when state is unfulfillable', async () => { ctx.state.unfulfillable = true @@ -117,6 +73,7 @@ describe('Telemetry Middleware', function () { await middleware(ctx, next) expect(getOrCreateSpy).not.toHaveBeenCalled() + expect(next).toHaveBeenCalled() }) it('should call next without gathering telemetry when convert returns ConvertError.InvalidDestinationPrice', async () => { @@ -149,4 +106,54 @@ describe('Telemetry Middleware', function () { expect(getOrCreateSpy).not.toHaveBeenCalled() expect(next).toHaveBeenCalled() }) + + describe('collectTelemetry', () => { + it('should convert to telemetry asset,apply privacy, collect telemetry', async () => { + const ratesService = ctx.services.telemetry!.getRatesService() + const convertSpy = jest + .spyOn(ratesService, 'convert') + .mockResolvedValue(10000n) + + const addSpy = jest.spyOn( + ctx.services.telemetry!.getOrCreate('transactions_amount', { + description: 'Amount sent through the network', + valueType: ValueType.DOUBLE + }), + 'add' + ) + + const applyPrivacySpy = jest + .spyOn(privacy, 'applyPrivacy') + .mockReturnValue(10000) + + await collectTelemetryAmount(ctx.services.telemetry!, { + sourceAmount: BigInt(ctx.request.prepare.amount), + sourceAsset: { + code: ctx.accounts.outgoing.asset.code, + scale: ctx.accounts.outgoing.asset.scale + }, + destinationAsset: { + code: services.telemetry!.getBaseAssetCode(), + scale: 4 + } + }) + + expect(convertSpy).toHaveBeenCalledWith({ + sourceAmount: BigInt(ctx.request.prepare.amount), + sourceAsset: { + code: ctx.accounts.outgoing.asset.code, + scale: ctx.accounts.outgoing.asset.scale + }, + destinationAsset: { + code: services.telemetry!.getBaseAssetCode(), + scale: 4 + } + }) + + expect(applyPrivacySpy).toHaveBeenCalledWith(10000) + expect(addSpy).toHaveBeenCalledWith(10000, { + source: ctx.services.telemetry!.getServiceName() + }) + }) + }) }) diff --git a/packages/backend/src/telemetry/meter.ts b/packages/backend/src/telemetry/meter.ts index 4d657eb0e2..eabe8b03d3 100644 --- a/packages/backend/src/telemetry/meter.ts +++ b/packages/backend/src/telemetry/meter.ts @@ -19,7 +19,7 @@ import { BaseService } from '../shared/baseService' export interface TelemetryService { getOrCreate(name: string, options?: MetricOptions): Counter getServiceName(): string | undefined - getRatesService(): RatesService // Add this line + getRatesService(): RatesService getBaseAssetCode(): string } diff --git a/packages/backend/src/telemetry/privacy.md b/packages/backend/src/telemetry/privacy.md index 98ccecbc5b..e19dab7c77 100644 --- a/packages/backend/src/telemetry/privacy.md +++ b/packages/backend/src/telemetry/privacy.md @@ -57,5 +57,3 @@ The following table shows the values in the algorithm when running transactions | 90422400 | 10000000 | 90000000 | 1000000 | 2210649 | 92210649 | | 112400400 | 10000000 | 100000000 | 1000000 | 407847 | 100407847 | | 222290500 | 10000000 | 100000000 | 1000000 | -686149 | 99313851 | - -These values are generated by the `applyPrivacy` method in the `meter.ts` file. This method takes a raw value, calculates a bucket size, rounds the value, computes a privacy parameter, generates Laplace noise, and then adds the noise to the rounded value to get the final value. From 87161b8049ce542ae175e00a14673a9d967a7989 Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Tue, 2 Jan 2024 11:10:46 +0200 Subject: [PATCH 24/67] feat(telemetry): change privacy doc wording --- packages/backend/src/telemetry/privacy.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/backend/src/telemetry/privacy.md b/packages/backend/src/telemetry/privacy.md index e19dab7c77..2ff1585854 100644 --- a/packages/backend/src/telemetry/privacy.md +++ b/packages/backend/src/telemetry/privacy.md @@ -26,13 +26,9 @@ The privacy parameter is computed as one-tenth of the sensitivity. This paramete The noise, selected from the Laplacian distribution, is then generated using this privacy parameter and added to the rounded value. If the resulting value is zero, it is set to half the bucket size to ensure that the noise does not completely obscure the transaction value. -## Achieving Local Differential Privacy - -This implementation achieves Local Differential Privacy by adding noise to each individual's data before it is sent to the server. The noise is generated based on the sensitivity of the function and a privacy parameter, ensuring that the server never sees the actual data. This provides a strong privacy guarantee, while still allowing for useful patterns in the data to be observed at the Rafiki instance level. - ## Currency Conversion -Another factor that increases deniability is currency conversion. In cross-currency transactions, we use exchange rates that are not traced back. This introduces an additional layer of noise and further protects the privacy of the transactions. +Another factor that obscures sensitive data is currency conversion. In cross-currency transactions, we use exchange rates that are not traced back. This introduces an additional layer of noise and further protects the privacy of the transactions. ## References From 01ba59a526bca4f9c3dacf3a7ed81ce78ca9e207 Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Sat, 6 Jan 2024 14:41:51 +0200 Subject: [PATCH 25/67] feat(telemetry): Remove local otel collector setup from cloud-nine and happy-life. Keeping it just as an example for ASE integrators --- localenv/cloud-nine-wallet/.env.example | 2 - localenv/cloud-nine-wallet/docker-compose.yml | 40 +++++++++---------- localenv/collector/otel-collector-config.yaml | 4 +- localenv/happy-life-bank/.env.example | 2 - localenv/happy-life-bank/docker-compose.yml | 33 ++++++++------- 5 files changed, 39 insertions(+), 42 deletions(-) delete mode 100644 localenv/cloud-nine-wallet/.env.example delete mode 100644 localenv/happy-life-bank/.env.example diff --git a/localenv/cloud-nine-wallet/.env.example b/localenv/cloud-nine-wallet/.env.example deleted file mode 100644 index 6b6d9295fa..0000000000 --- a/localenv/cloud-nine-wallet/.env.example +++ /dev/null @@ -1,2 +0,0 @@ -AWS_ACCESS_KEY_ID=OPENTELEMETRY_COLLECTOR_ACCESS_KEY -AWS_SECRET_ACCESS_KEY=OPENTELEMETRY_COLLECTOR_SCCESS_KEY diff --git a/localenv/cloud-nine-wallet/docker-compose.yml b/localenv/cloud-nine-wallet/docker-compose.yml index dced1a0143..8188985253 100644 --- a/localenv/cloud-nine-wallet/docker-compose.yml +++ b/localenv/cloud-nine-wallet/docker-compose.yml @@ -61,7 +61,8 @@ services: WALLET_ADDRESS_URL: ${CLOUD_NINE_WALLET_ADDRESS_URL:-https://cloud-nine-wallet-backend/.well-known/pay} ILP_CONNECTOR_ADDRESS: ${CLOUD_NINE_CONNECTOR_URL} ENABLE_TELEMETRY: true - OPEN_TELEMETRY_COLLECTOR_URL: http://cloud-nine-otel-collector:4317,http://otel-collector-NLB-e3172ff9d2f4bc8a.elb.eu-west-2.amazonaws.com:4317 + # url is a list of csv. Example for including local collector: http://cloud-nine-otel-collector:4317,http://otel-collector-NLB-e3172ff9d2f4bc8a.elb.eu-west-2.amazonaws.com:4317 + OPEN_TELEMETRY_COLLECTOR_URL: http://otel-collector-NLB-e3172ff9d2f4bc8a.elb.eu-west-2.amazonaws.com:4317 OPEN_TELEMETRY_EXPORT_INTERVAL: 15000 TELEMETRY_EXCHANGE_RATES_URL: https://telemetry-exchange-rates.s3.amazonaws.com/exchange-rates-usd.json depends_on: @@ -122,27 +123,22 @@ services: ENABLE_INSECURE_MESSAGE_COOKIE: true depends_on: - cloud-nine-backend - cloud-nine-otel-collector: - image: otel/opentelemetry-collector-contrib:latest - command: ["--config=/etc/otel-collector-config.yaml", ""] - environment: - - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID-''} - - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY-''} - volumes: - - ../collector/otel-collector-config.yaml:/etc/otel-collector-config.yaml - networks: - - rafiki - expose: - - 4317 - ports: - - "13133:13133" # health_check extension - dozzle: - container_name: dozzle - image: amir20/dozzle:latest - volumes: - - /var/run/docker.sock:/var/run/docker.sock - ports: - - 9999:8080 + + #Serves as example for optional local collector + # cloud-nine-otel-collector: + # image: otel/opentelemetry-collector-contrib:latest + # command: ["--config=/etc/otel-collector-config.yaml", ""] + # environment: + # - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID-''} + # - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY-''} + # volumes: + # - ../collector/otel-collector-config.yaml:/etc/otel-collector-config.yaml + # networks: + # - rafiki + # expose: + # - 4317 + # ports: + # - "13133:13133" # health_check extension volumes: database-data: # named volumes can be managed easier using docker-compose diff --git a/localenv/collector/otel-collector-config.yaml b/localenv/collector/otel-collector-config.yaml index dd0c35cfd2..97853f7238 100644 --- a/localenv/collector/otel-collector-config.yaml +++ b/localenv/collector/otel-collector-config.yaml @@ -1,3 +1,5 @@ +#Serves as example for the configuration of a local OpenTelemetry Collector +#Sigv4auth required for AWS Prometheus Remote Write access (USER with accesss keys needed) extensions: sigv4auth: assume_role: @@ -21,7 +23,7 @@ exporters: logging: verbosity: 'normal' prometheusremotewrite: - endpoint: 'https://aps-workspaces.eu-west-2.amazonaws.com/workspaces/ws-7c8fd050-cb3b-451f-9971-1e6099db2e8f/api/v1/remote_write' + endpoint: 'https://aps-workspaces.YOUR-REGION.amazonaws.com/workspaces/ws-YOUR-WORKSPACE-IDENTIFIER/api/v1/remote_write' auth: authenticator: sigv4auth diff --git a/localenv/happy-life-bank/.env.example b/localenv/happy-life-bank/.env.example deleted file mode 100644 index 6b6d9295fa..0000000000 --- a/localenv/happy-life-bank/.env.example +++ /dev/null @@ -1,2 +0,0 @@ -AWS_ACCESS_KEY_ID=OPENTELEMETRY_COLLECTOR_ACCESS_KEY -AWS_SECRET_ACCESS_KEY=OPENTELEMETRY_COLLECTOR_SCCESS_KEY diff --git a/localenv/happy-life-bank/docker-compose.yml b/localenv/happy-life-bank/docker-compose.yml index 9984d9a99e..2c25be00f1 100644 --- a/localenv/happy-life-bank/docker-compose.yml +++ b/localenv/happy-life-bank/docker-compose.yml @@ -53,7 +53,8 @@ services: REDIS_URL: redis://shared-redis:6379/1 WALLET_ADDRESS_URL: ${HAPPY_LIFE_BANK_WALLET_ADDRESS_URL:-https://happy-life-bank-backend/.well-known/pay} ENABLE_TELEMETRY: true - OPEN_TELEMETRY_COLLECTOR_URL: http://happy-life-otel-collector:4317,http://otel-collector-NLB-e3172ff9d2f4bc8a.elb.eu-west-2.amazonaws.com:4317 + # url is a list of csv. Example for including local collector: http://happy-life-otel-collector:4317,http://otel-collector-NLB-e3172ff9d2f4bc8a.elb.eu-west-2.amazonaws.com:4317 + OPEN_TELEMETRY_COLLECTOR_URL: http://otel-collector-NLB-e3172ff9d2f4bc8a.elb.eu-west-2.amazonaws.com:4317 OPEN_TELEMETRY_EXPORT_INTERVAL: 15000 TELEMETRY_EXCHANGE_RATES_URL: https://telemetry-exchange-rates.s3.amazonaws.com/exchange-rates-usd.json @@ -93,20 +94,22 @@ services: depends_on: - cloud-nine-admin - happy-life-backend - happy-life-otel-collector: - image: otel/opentelemetry-collector-contrib:latest - command: ["--config=/etc/otel-collector-config.yaml", ""] - environment: - - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID-''} - - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY-''} - volumes: - - ../collector/otel-collector-config.yaml:/etc/otel-collector-config.yaml - networks: - - rafiki - expose: - - 4317 - ports: - - "13132:13133" # health_check extension + + #Serves as example for optional local collector configuration + # happy-life-otel-collector: + # image: otel/opentelemetry-collector-contrib:latest + # command: ["--config=/etc/otel-collector-config.yaml", ""] + # environment: + # - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID-''} + # - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY-''} + # volumes: + # - ../collector/otel-collector-config.yaml:/etc/otel-collector-config.yaml + # networks: + # - rafiki + # expose: + # - 4317 + # ports: + # - "13132:13133" # health_check extension networks: From 7770f8f91c560e164991e8b1cef6ca70c47538d9 Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Mon, 8 Jan 2024 23:17:31 +0200 Subject: [PATCH 26/67] feat(telemetry): prioritize ase provided exchange rates before external rates --- localenv/collector/otel-collector-config.yaml | 6 +- .../connector/core/middleware/telemetry.ts | 63 ++++++++++++------- .../core/test/middleware/telemetry.test.ts | 37 ++++++++++- 3 files changed, 77 insertions(+), 29 deletions(-) diff --git a/localenv/collector/otel-collector-config.yaml b/localenv/collector/otel-collector-config.yaml index 97853f7238..3f969a44b3 100644 --- a/localenv/collector/otel-collector-config.yaml +++ b/localenv/collector/otel-collector-config.yaml @@ -1,10 +1,10 @@ -#Serves as example for the configuration of a local OpenTelemetry Collector +#Serves as example for the configuration of a local OpenTelemetry Collector that sends metrics to an AWS Managed Prometheus Workspace #Sigv4auth required for AWS Prometheus Remote Write access (USER with accesss keys needed) extensions: sigv4auth: assume_role: - arn: 'arn:aws:iam::145795521759:role/PrometheusRemoteWrite' - sts_region: 'eu-west-2' + arn: 'arn:aws:iam::YOUR-ROLE:role/PrometheusRemoteWrite' + sts_region: 'YOUR-REGION' receivers: otlp: diff --git a/packages/backend/src/payment-method/ilp/connector/core/middleware/telemetry.ts b/packages/backend/src/payment-method/ilp/connector/core/middleware/telemetry.ts index 144bb0a652..803aa15616 100644 --- a/packages/backend/src/payment-method/ilp/connector/core/middleware/telemetry.ts +++ b/packages/backend/src/payment-method/ilp/connector/core/middleware/telemetry.ts @@ -1,31 +1,10 @@ import { ValueType } from '@opentelemetry/api' import { ILPContext, ILPMiddleware } from '../rafiki' import { privacy } from '../../../../../telemetry/privacy' -import { ConvertError } from '../../../../../rates/service' +import { ConvertError, RatesService } from '../../../../../rates/service' import { TelemetryService } from '../../../../../telemetry/meter' import { ConvertOptions } from '../../../../../rates/util' -export async function collectTelemetryAmount( - telemetryService: TelemetryService, - convertOptions: Omit -) { - const converted = await telemetryService - .getRatesService() - .convert(convertOptions) - if (converted === ConvertError.InvalidDestinationPrice) { - return - } - - telemetryService - .getOrCreate('transactions_amount', { - description: 'Amount sent through the network', - valueType: ValueType.DOUBLE - }) - .add(privacy.applyPrivacy(Number(converted)), { - source: telemetryService.getServiceName() - }) -} - export function createTelemetryMiddleware(): ILPMiddleware { return async ( { request, services, accounts, state }: ILPContext, @@ -46,9 +25,47 @@ export function createTelemetryMiddleware(): ILPMiddleware { } } - collectTelemetryAmount(services.telemetry, convertOptions) + collectTelemetryAmount(services.telemetry, services.rates, convertOptions) } await next() } } + +export async function collectTelemetryAmount( + telemetryService: TelemetryService, + aseRates: RatesService, + convertOptions: Omit +) { + const converted = await convertAmount( + aseRates, + telemetryService.getRatesService(), + convertOptions + ) + if (converted === ConvertError.InvalidDestinationPrice) { + return + } + + telemetryService + .getOrCreate('transactions_amount', { + description: 'Amount sent through the network', + valueType: ValueType.DOUBLE + }) + .add(privacy.applyPrivacy(Number(converted)), { + source: telemetryService.getServiceName() + }) +} + +export async function convertAmount( + aseRates: RatesService, + telemetryRates: RatesService, + convertOptions: Omit +) { + try { + const aseConvert = await aseRates.convert(convertOptions) + return aseConvert + } catch (error) { + const telemetryRatesConverted = await telemetryRates.convert(convertOptions) + return telemetryRatesConverted + } +} diff --git a/packages/backend/src/payment-method/ilp/connector/core/test/middleware/telemetry.test.ts b/packages/backend/src/payment-method/ilp/connector/core/test/middleware/telemetry.test.ts index e3d3805330..06f055d2f9 100644 --- a/packages/backend/src/payment-method/ilp/connector/core/test/middleware/telemetry.test.ts +++ b/packages/backend/src/payment-method/ilp/connector/core/test/middleware/telemetry.test.ts @@ -78,7 +78,7 @@ describe('Telemetry Middleware', function () { it('should call next without gathering telemetry when convert returns ConvertError.InvalidDestinationPrice', async () => { const convertSpy = jest - .spyOn(ctx.services.telemetry!.getRatesService(), 'convert') + .spyOn(ctx.services.rates!, 'convert') .mockImplementation(() => Promise.resolve(ConvertError.InvalidDestinationPrice) ) @@ -108,8 +108,39 @@ describe('Telemetry Middleware', function () { }) describe('collectTelemetry', () => { + it(`should first try to convert to telemetry base currency using the ASE provided rates through the aseRatesService, + and fallback to convert using external rates from telemetryRatesService`, async () => { + const aseRatesService = ctx.services.rates + const telemetryRatesService = ctx.services.telemetry!.getRatesService() + + const aseConvertSpy = jest + .spyOn(aseRatesService, 'convert') + .mockImplementation(() => + Promise.reject(ConvertError.InvalidDestinationPrice) + ) + + const telemetryConvertSpy = jest + .spyOn(telemetryRatesService, 'convert') + .mockImplementation(() => Promise.resolve(10000n)) + + await collectTelemetryAmount(ctx.services.telemetry!, aseRatesService, { + sourceAmount: BigInt(ctx.request.prepare.amount), + sourceAsset: { + code: ctx.accounts.outgoing.asset.code, + scale: ctx.accounts.outgoing.asset.scale + }, + destinationAsset: { + code: services.telemetry!.getBaseAssetCode(), + scale: 4 + } + }) + + expect(aseConvertSpy).toHaveBeenCalled() + expect(telemetryConvertSpy).toHaveBeenCalled() + }) it('should convert to telemetry asset,apply privacy, collect telemetry', async () => { - const ratesService = ctx.services.telemetry!.getRatesService() + const ratesService = ctx.services.rates + const convertSpy = jest .spyOn(ratesService, 'convert') .mockResolvedValue(10000n) @@ -126,7 +157,7 @@ describe('Telemetry Middleware', function () { .spyOn(privacy, 'applyPrivacy') .mockReturnValue(10000) - await collectTelemetryAmount(ctx.services.telemetry!, { + await collectTelemetryAmount(ctx.services.telemetry!, ratesService, { sourceAmount: BigInt(ctx.request.prepare.amount), sourceAsset: { code: ctx.accounts.outgoing.asset.code, From de49bf397f5d767c7f1878c81f9c397f5c61bf4a Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Mon, 8 Jan 2024 23:19:57 +0200 Subject: [PATCH 27/67] feat(telemetry): openTelemetryCollectors env var now has a default --- packages/backend/src/config/app.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/config/app.ts b/packages/backend/src/config/app.ts index 7ef8cee08e..fe93099ee9 100644 --- a/packages/backend/src/config/app.ts +++ b/packages/backend/src/config/app.ts @@ -38,7 +38,9 @@ dotenv.config({ export const Config = { logLevel: envString('LOG_LEVEL', 'info'), enableTelemetry: envBool('ENABLE_TELEMETRY', true), - openTelemetryCollectors: envStringArray('OPEN_TELEMETRY_COLLECTOR_URL', []), + openTelemetryCollectors: envStringArray('OPEN_TELEMETRY_COLLECTOR_URL', [ + 'http://otel-collector-NLB-e3172ff9d2f4bc8a.elb.eu-west-2.amazonaws.com:4317' + ]), openTelemetryExportInterval: envInt('OPEN_TELEMETRY_EXPORT_INTERVAL', 15000), telemetryExchangeRatesUrl: envString( 'TELEMETRY_EXCHANGE_RATES_URL', From 2357e01a19921bed78e4229d3854fc89989e2220 Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Mon, 8 Jan 2024 23:22:50 +0200 Subject: [PATCH 28/67] feat(telemetry): remove source from amount metrics for privacy concerns --- .../payment-method/ilp/connector/core/middleware/telemetry.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/backend/src/payment-method/ilp/connector/core/middleware/telemetry.ts b/packages/backend/src/payment-method/ilp/connector/core/middleware/telemetry.ts index 803aa15616..1633dd6894 100644 --- a/packages/backend/src/payment-method/ilp/connector/core/middleware/telemetry.ts +++ b/packages/backend/src/payment-method/ilp/connector/core/middleware/telemetry.ts @@ -51,9 +51,7 @@ export async function collectTelemetryAmount( description: 'Amount sent through the network', valueType: ValueType.DOUBLE }) - .add(privacy.applyPrivacy(Number(converted)), { - source: telemetryService.getServiceName() - }) + .add(privacy.applyPrivacy(Number(converted))) } export async function convertAmount( From b1b1489013fc9658fc274479c5c33fb557f18009 Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Tue, 9 Jan 2024 11:02:58 +0200 Subject: [PATCH 29/67] feat(telemetry): add exchange rates retrieval lambda to codebase --- aws/lambdas/exchange-rates/go.mod | 9 +++ aws/lambdas/exchange-rates/go.sum | 12 +++ aws/lambdas/exchange-rates/main.go | 119 +++++++++++++++++++++++++++++ 3 files changed, 140 insertions(+) create mode 100644 aws/lambdas/exchange-rates/go.mod create mode 100644 aws/lambdas/exchange-rates/go.sum create mode 100644 aws/lambdas/exchange-rates/main.go diff --git a/aws/lambdas/exchange-rates/go.mod b/aws/lambdas/exchange-rates/go.mod new file mode 100644 index 0000000000..fa1167230f --- /dev/null +++ b/aws/lambdas/exchange-rates/go.mod @@ -0,0 +1,9 @@ +module exchange-rates-lambda + +go 1.21.0 + +require ( + github.com/aws/aws-lambda-go v1.42.0 // indirect + github.com/aws/aws-sdk-go v1.49.1 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect +) diff --git a/aws/lambdas/exchange-rates/go.sum b/aws/lambdas/exchange-rates/go.sum new file mode 100644 index 0000000000..bd6d4c9469 --- /dev/null +++ b/aws/lambdas/exchange-rates/go.sum @@ -0,0 +1,12 @@ +github.com/aws/aws-lambda-go v1.42.0 h1:U4QKkxLp/il15RJGAANxiT9VumQzimsUER7gokqA0+c= +github.com/aws/aws-lambda-go v1.42.0/go.mod h1:dpMpZgvWx5vuQJfBt0zqBha60q7Dd7RfgJv23DymV8A= +github.com/aws/aws-sdk-go v1.49.1 h1:Dsamcd8d/nNb3A+bZ0ucfGl0vGZsW5wlRW0vhoYGoeQ= +github.com/aws/aws-sdk-go v1.49.1/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/aws/lambdas/exchange-rates/main.go b/aws/lambdas/exchange-rates/main.go new file mode 100644 index 0000000000..7d0d5bef90 --- /dev/null +++ b/aws/lambdas/exchange-rates/main.go @@ -0,0 +1,119 @@ +package main + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "net/http" + "os" + "strconv" + "time" + + "github.com/aws/aws-lambda-go/lambda" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/s3/s3manager" +) + +type RatesResponse struct { + Base string `json:"base"` + Rates map[string]float64 `json:"rates"` +} + +type MerchantRates struct { + Merchant map[string]map[string]string `json:"merchant"` +} + +func getExchangeRates(apiURL string) (map[string]map[string]string, error) { + client := &http.Client{Timeout: 15 * time.Second} + response, err := client.Get(apiURL) + if err != nil { + return nil, fmt.Errorf("failed to get exchange rates from %s: %w", apiURL, err) + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + return nil, fmt.Errorf("received non-200 response code: %d", response.StatusCode) + } + + var ratesResponse MerchantRates + err = json.NewDecoder(response.Body).Decode(&ratesResponse) + if err != nil { + return nil, fmt.Errorf("failed to decode response body: %w", err) + } + + return ratesResponse.Merchant, nil +} + +func transformToBaseCurrency(baseCurrency string, merchantRates map[string]map[string]string) (*RatesResponse, error) { + + usdRates := make(map[string]float64) + + for currency, rateMap := range merchantRates { + rateStr, ok := rateMap[baseCurrency] + if !ok { + return nil, fmt.Errorf("failed to find %s rate for currency %s in merchant rates", baseCurrency, currency) + } + + rate, err := strconv.ParseFloat(rateStr, 64) + if err != nil { + return nil, fmt.Errorf("failed to convert rate to float64 for currency %s: %v", currency, err) + } + + usdRates[currency] = rate + } + + return &RatesResponse{ + Base: baseCurrency, + Rates: usdRates, + }, nil +} + +func Handler() (string, error) { + apiUrl := os.Getenv("API_URL") + bucketName := os.Getenv("BUCKET_NAME") + keyName := os.Getenv("KEY_NAME") + region := os.Getenv("REGION") + baseCurrency := os.Getenv("BASE_CURRENCY") + + if apiUrl == "" || bucketName == "" || keyName == "" || region == "" { + return "", errors.New("API_URL, BUCKET_NAME, or KEY_NAME environment variable is not set") + } + + merchantRates, err := getExchangeRates(apiUrl) + if err != nil { + return "", err + } + + data, err := transformToBaseCurrency(baseCurrency, merchantRates) + if err != nil { + return "", err + } + + dataBytes, err := json.Marshal(data) + if err != nil { + return "", err + } + + sess := session.Must(session.NewSession(&aws.Config{ + Region: aws.String(region)}, + )) + + uploader := s3manager.NewUploader(sess) + + _, err = uploader.Upload(&s3manager.UploadInput{ + Bucket: aws.String(bucketName), + Key: aws.String(keyName), + Body: bytes.NewReader(dataBytes), + }) + if err != nil { + return "", err + } + + return "Successfully uploaded data to " + bucketName + "/" + keyName, nil +} + +func main() { + lambda.Start(Handler) +} From b50cfcfbda5f5f549edaf9ee18147240e3270c70 Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Tue, 9 Jan 2024 12:46:55 +0200 Subject: [PATCH 30/67] feat(telemetry): docs --- .../src/content/docs}/telemetry/privacy.md | 0 .../src/content/docs/telemetry/telemetry.md | 74 ++++++++++++++++++ .../docs/telemetry/telemetry_architecture.png | Bin 0 -> 215253 bytes 3 files changed, 74 insertions(+) rename packages/{backend/src => documentation/src/content/docs}/telemetry/privacy.md (100%) create mode 100644 packages/documentation/src/content/docs/telemetry/telemetry.md create mode 100644 packages/documentation/src/content/docs/telemetry/telemetry_architecture.png diff --git a/packages/backend/src/telemetry/privacy.md b/packages/documentation/src/content/docs/telemetry/privacy.md similarity index 100% rename from packages/backend/src/telemetry/privacy.md rename to packages/documentation/src/content/docs/telemetry/privacy.md diff --git a/packages/documentation/src/content/docs/telemetry/telemetry.md b/packages/documentation/src/content/docs/telemetry/telemetry.md new file mode 100644 index 0000000000..3ed57587d0 --- /dev/null +++ b/packages/documentation/src/content/docs/telemetry/telemetry.md @@ -0,0 +1,74 @@ +# Telemetry + +## Purpose + +The objective of the telemetry feature is to gather metrics and establish an infrastructure for visualizing valuable network insights. The metrics we collect include: + +- The total amount of money transferred via packet data within a specified time frame (daily, weekly, monthly). +- The number of transactions from outgoing payments that have been at least partially successful. +- The average amount of money held within the network per transaction. + +Our goal is to use these data for our own insights and to enable Account Servicing Entities (ASEs) to gain their own insights. We aim to track the growth of the network in terms of transaction sizes and the number of transactions processed. + +## Privacy and Optionality + +Privacy is a paramount concern for us. Rafiki's telemetry feature is designed to provide valuable network insights without violating privacy or aiding malicious ASEs. For more information, please [Read the privacy docs](./privacy.md). + +The telemetry feature is optional for ASEs. + +## Architecture + +The architecture of the telemetry feature is illustrated below: + +![Telemetry Architecture](telemetry_architecture.png) + +## Opentelemetry + +We have adopted Opentelemetry to ensure compliance with a standardized framework that is compatible with a variety of tool suites. This allows clients to use their preferred tools for data analysis, while Rafiki is instrumented and observable through a standardized metrics format. + +## Telemetry ECS Cluster + +The Telemetry Replica service is hosted on AWS ECS Fargate and is configured for availability and load balancing of custom ADOT collector ECS tasks. + +When ASEs opt for telemetry, metrics are sent to our Telemetry Service. To enable ASEs to build their own telemetry solutions, instrumented Rafiki can send data to multiple endpoints. This allows the integration of a local Otel collector container that can support custom requirements. Metrics communication is facilitated through GRPC. + +## Otel SDK - Rafiki Instrumentation + +The Opentelemetry SDK is integrated into Rafiki to create, collect, and export metrics. The SDK integrates seamlessly with the OTEL Collector. + +## Prometheus - AMP + +We use Amazon's managed service for Prometheus to collect data from the Telemetry cluster. + +**Note**: AMP offers limited configuration options and cannot crawl data outside of AWS. This limitation led us to adopt a push model, using prometheusRemoteWrite, instead of a pull model. For future development, we may consider hosting our own Prometheus. + +## Grafana - Grafana Cloud + +Grafana Cloud is used for data visualization dashboards. It offers multiple tools that extend Prometheus Promql. + +**Note**: We initially used Amazon hosted Grafana, but it did not meet our needs for embedding dashboards. Grafana Cloud offers a feature called “Public dashboards”, which allows us to share dashboards. However, embedding may still pose a challenge. + +## Exchange Rates + +For telemetry purposes, all amounts collected by instrumented Rafiki should be converted to a base currency. + +**Privacy Reasoning**: If only two ASEs are peered over a non-USD currency and we collect data in that currency, it would be easy to determine the volumes moved between those two ASEs. To maintain privacy, we convert all amounts to a base currency. + +If an ASE does not provide the necessary exchange rate for a transaction, the telemetry solution will still convert the amount to the base currency using external exchange rates. A Lambda function on AWS retrieves and stores these external exchange rates. It is triggered by a daily CloudWatch event and stores the rates in a public S3 Bucket. The S3 Bucket does not have versioning, and the data is overwritten daily to further ensure privacy. + +## Instrumentation + +Rafiki currently has two counter metrics. All data points (counter increases) are exported to collection endpoints at a configurable interval (default recommended to 15s). + +Currently collected metrics: + +- `transactions_total` (Open-payments) - Counter metric + - Description: “Count of funded transactions” + - This counter metric increases by 1 for each successfully sent transaction. + - It is collected in the open-payments module, outgoing payment service. +- `transactions_amount` (ILP Connector) - Counter metric + - Description: “Amount sent through the network”. + - This amount metric increases by the amount sent in each ILP packet. + - It is collected inside the ILP connector core, by a new telemetry middleware. + +**Note**: The current implementation only collects metrics on the SENDING side of a transaction. Metrics for external open-payments transactions RECEIVED by a Rafiki instance in the network are not collected. diff --git a/packages/documentation/src/content/docs/telemetry/telemetry_architecture.png b/packages/documentation/src/content/docs/telemetry/telemetry_architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..f47ceb40bda28ad6970f7169f2c4e7a7b739adbb GIT binary patch literal 215253 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

1PxBT)h;+Je;EiS5i_9elEB3m6gs;IPJ(4+ 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

3DG*@%e28bC!3P#=y$#^R(Mei3D87j$4PVs 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 Date: Tue, 9 Jan 2024 19:33:59 +0800 Subject: [PATCH 31/67] Resolve documentation issues --- packages/documentation/astro.config.mjs | 7 +++++++ .../public/img/telemetry-architecture.png | Bin 0 -> 50798 bytes .../telemetry/{telemetry.md => overview.md} | 6 ++++-- .../src/content/docs/telemetry/privacy.md | 6 ++++-- .../docs/telemetry/telemetry_architecture.png | Bin 215253 -> 0 bytes 5 files changed, 15 insertions(+), 4 deletions(-) create mode 100644 packages/documentation/public/img/telemetry-architecture.png rename packages/documentation/src/content/docs/telemetry/{telemetry.md => overview.md} (98%) delete mode 100644 packages/documentation/src/content/docs/telemetry/telemetry_architecture.png diff --git a/packages/documentation/astro.config.mjs b/packages/documentation/astro.config.mjs index df6772f3f0..84ab7bc79c 100644 --- a/packages/documentation/astro.config.mjs +++ b/packages/documentation/astro.config.mjs @@ -143,6 +143,13 @@ export default defineConfig({ } ] }, + { + label: 'Telemetry', + collapsed: true, + autogenerate: { + directory: 'telemetry' + } + }, { label: 'Local Playground', collapsed: true, diff --git a/packages/documentation/public/img/telemetry-architecture.png b/packages/documentation/public/img/telemetry-architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..57958d8865b9d9e4cb236595d3d339a8c04bc665 GIT binary patch literal 50798 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%0DqFQC*584u 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

1PxBT)h;+Je;EiS5i_9elEB3m6gs;IPJ(4+ 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

3DG*@%e28bC!3P#=y$#^R(Mei3D87j$4PVs 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 Date: Sat, 13 Jan 2024 13:33:27 +0200 Subject: [PATCH 32/67] feat(telemetry): move back from ILP connector to accounting service --- .../backend/src/accounting/psql/service.ts | 6 + packages/backend/src/accounting/service.ts | 24 ++- .../src/accounting/tigerbeetle/service.ts | 6 + packages/backend/src/app.ts | 2 +- packages/backend/src/index.ts | 20 +- .../open_payments/payment/outgoing/service.ts | 2 +- .../core/factories/rafiki-services.ts | 5 - .../connector/core/middleware/telemetry.ts | 69 ------- .../ilp/connector/core/rafiki.ts | 6 +- .../core/test/middleware/telemetry.test.ts | 190 ------------------ .../src/payment-method/ilp/connector/index.ts | 6 - .../{tests/meter.ts => telemetry/mocks.ts} | 6 +- .../{meter.test.ts => service.test.ts} | 2 +- .../src/telemetry/{meter.ts => service.ts} | 0 .../src/telemetry/transaction-amount.test.ts | 120 +++++++++++ .../src/telemetry/transaction-amount.ts | 53 +++++ packages/backend/src/tests/app.ts | 2 +- 17 files changed, 230 insertions(+), 289 deletions(-) delete mode 100644 packages/backend/src/payment-method/ilp/connector/core/middleware/telemetry.ts delete mode 100644 packages/backend/src/payment-method/ilp/connector/core/test/middleware/telemetry.test.ts rename packages/backend/src/{tests/meter.ts => telemetry/mocks.ts} (85%) rename packages/backend/src/telemetry/{meter.test.ts => service.test.ts} (92%) rename packages/backend/src/telemetry/{meter.ts => service.ts} (100%) create mode 100644 packages/backend/src/telemetry/transaction-amount.test.ts create mode 100644 packages/backend/src/telemetry/transaction-amount.ts diff --git a/packages/backend/src/accounting/psql/service.ts b/packages/backend/src/accounting/psql/service.ts index c2952d477c..1515140c86 100644 --- a/packages/backend/src/accounting/psql/service.ts +++ b/packages/backend/src/accounting/psql/service.ts @@ -33,8 +33,12 @@ import { voidTransfers } from './ledger-transfer' import { LedgerTransfer, LedgerTransferType } from './ledger-transfer/model' +import { TelemetryService } from '../../telemetry/service' +import { RatesService } from '../../rates/service' export interface ServiceDependencies extends BaseService { + telemetry?: TelemetryService + aseRatesService?: RatesService knex: TransactionOrKnex withdrawalThrottleDelay?: number } @@ -201,6 +205,8 @@ export async function createTransfer( args: TransferOptions ): Promise { return createAccountToAccountTransfer({ + telemetry: deps.telemetry, + aseRatesService: deps.aseRatesService, transferArgs: args, withdrawalThrottleDelay: deps.withdrawalThrottleDelay, voidTransfers: async (transferRefs) => voidTransfers(deps, transferRefs), diff --git a/packages/backend/src/accounting/service.ts b/packages/backend/src/accounting/service.ts index 30491a5eb1..48e91a580b 100644 --- a/packages/backend/src/accounting/service.ts +++ b/packages/backend/src/accounting/service.ts @@ -1,5 +1,8 @@ import { TransactionOrKnex } from 'objection' import { TransferError, isTransferError } from './errors' +import { TelemetryService } from '../telemetry/service' +import { RatesService } from '../rates/service' +import { collectTelemetryAmount } from '../telemetry/transaction-amount' export enum LiquidityAccountType { ASSET = 'ASSET', @@ -91,6 +94,8 @@ export interface TransferToCreate { } interface CreateAccountToAccountTransferArgs { + telemetry?: TelemetryService + aseRatesService?: RatesService transferArgs: TransferOptions voidTransfers(transferIds: string[]): Promise postTransfers(transferIds: string[]): Promise @@ -112,7 +117,9 @@ export async function createAccountToAccountTransfer( getAccountReceived, getAccountBalance, withdrawalThrottleDelay, - transferArgs + transferArgs, + telemetry, + aseRatesService } = args const { sourceAccount, destinationAccount, sourceAmount, destinationAmount } = @@ -188,6 +195,21 @@ export async function createAccountToAccountTransfer( withdrawalThrottleDelay }) } + if ( + destinationAccount.onDebit && + telemetry && + aseRatesService && + sourceAccount.asset.code && + sourceAccount.asset.scale + ) { + collectTelemetryAmount(telemetry, aseRatesService, { + amount: sourceAmount, + asset: { + code: sourceAccount.asset.code, + scale: sourceAccount.asset.scale + } + }) + } }, void: async (): Promise => { const error = await voidTransfers(pendingTransferIdsOrError) diff --git a/packages/backend/src/accounting/tigerbeetle/service.ts b/packages/backend/src/accounting/tigerbeetle/service.ts index 9d03024628..fa1985b2e5 100644 --- a/packages/backend/src/accounting/tigerbeetle/service.ts +++ b/packages/backend/src/accounting/tigerbeetle/service.ts @@ -26,6 +26,8 @@ import { } from './errors' import { NewTransferOptions, createTransfers } from './transfers' import { toTigerbeetleId } from './utils' +import { TelemetryService } from '../../telemetry/service' +import { RatesService } from '../../rates/service' export enum TigerbeetleAccountCode { LIQUIDITY_WEB_MONETIZATION = 1, @@ -48,6 +50,8 @@ export const convertToTigerbeetleAccountCode: { } export interface ServiceDependencies extends BaseService { + telemetry?: TelemetryService + aseRatesService?: RatesService tigerbeetle: Client withdrawalThrottleDelay?: number } @@ -216,6 +220,8 @@ export async function createTransfer( args: TransferOptions ): Promise { return createAccountToAccountTransfer({ + telemetry: deps.telemetry, + aseRatesService: deps.aseRatesService, transferArgs: args, withdrawalThrottleDelay: deps.withdrawalThrottleDelay, voidTransfers: async (transferIds) => { diff --git a/packages/backend/src/app.ts b/packages/backend/src/app.ts index 429aa6a828..8dfc47d325 100644 --- a/packages/backend/src/app.ts +++ b/packages/backend/src/app.ts @@ -83,7 +83,7 @@ import { Rafiki as ConnectorApp } from './payment-method/ilp/connector/core' import { AxiosInstance } from 'axios' import { PaymentMethodHandlerService } from './payment-method/handler/service' import { IlpPaymentService } from './payment-method/ilp/service' -import { TelemetryService } from './telemetry/meter' +import { TelemetryService } from './telemetry/service' export interface AppContextData { logger: Logger container: AppContainer diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index cfc96f6516..d0d8fe273e 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -46,8 +46,8 @@ import { createPeerService } from './payment-method/ilp/peer/service' import { createIlpPaymentService } from './payment-method/ilp/service' import { createSPSPRoutes } from './payment-method/ilp/spsp/routes' import { createStreamCredentialsService } from './payment-method/ilp/stream-credentials/service' -import { createRatesService } from './rates/service' -import { createTelemetryService } from './telemetry/meter' +import { RatesService, createRatesService } from './rates/service' +import { TelemetryService, createTelemetryService } from './telemetry/service' import { createWebhookService } from './webhook/service' BigInt.prototype.toJSON = function () { @@ -204,11 +204,20 @@ export function initIocContainer( const knex = await deps.use('knex') const config = await deps.use('config') + let telemetry: TelemetryService | undefined + let aseRatesService: RatesService | undefined + if (config.enableTelemetry) { + telemetry = await deps.use('telemetry') + aseRatesService = await deps.use('ratesService') + } + if (config.useTigerbeetle) { const tigerbeetle = await deps.use('tigerbeetle') return createTigerbeetleAccountingService({ logger, + telemetry, + aseRatesService, knex, tigerbeetle, withdrawalThrottleDelay: config.withdrawalThrottleDelay @@ -217,6 +226,8 @@ export function initIocContainer( return createPsqlAccountingService({ logger, + telemetry, + aseRatesService, knex, withdrawalThrottleDelay: config.withdrawalThrottleDelay }) @@ -361,10 +372,7 @@ export function initIocContainer( peerService: await deps.use('peerService'), ratesService: await deps.use('ratesService'), streamServer: await deps.use('streamServer'), - ilpAddress: config.ilpAddress, - telemetry: config.enableTelemetry - ? await deps.use('telemetry') - : undefined + ilpAddress: config.ilpAddress }) }) diff --git a/packages/backend/src/open_payments/payment/outgoing/service.ts b/packages/backend/src/open_payments/payment/outgoing/service.ts index fc6d6ab739..5449507e52 100644 --- a/packages/backend/src/open_payments/payment/outgoing/service.ts +++ b/packages/backend/src/open_payments/payment/outgoing/service.ts @@ -35,7 +35,7 @@ import { Interval } from 'luxon' import { knex } from 'knex' import { AccountAlreadyExistsError } from '../../../accounting/errors' import { PaymentMethodHandlerService } from '../../../payment-method/handler/service' -import { TelemetryService } from '../../../telemetry/meter' +import { TelemetryService } from '../../../telemetry/service' export interface OutgoingPaymentService extends WalletAddressSubresourceService { diff --git a/packages/backend/src/payment-method/ilp/connector/core/factories/rafiki-services.ts b/packages/backend/src/payment-method/ilp/connector/core/factories/rafiki-services.ts index a5b64ad737..12343967a9 100644 --- a/packages/backend/src/payment-method/ilp/connector/core/factories/rafiki-services.ts +++ b/packages/backend/src/payment-method/ilp/connector/core/factories/rafiki-services.ts @@ -5,11 +5,9 @@ import { StreamServer } from '@interledger/stream-receiver' import { RafikiServices } from '../rafiki' import { MockAccountingService } from '../test/mocks/accounting-service' import { TestLoggerFactory } from './test-logger' -import { MockTelemetryService } from '../../../../../tests/meter' interface MockRafikiServices extends RafikiServices { accounting: MockAccountingService - telemetry: MockTelemetryService } export const RafikiServicesFactory = Factory.define( @@ -22,9 +20,6 @@ export const RafikiServicesFactory = Factory.define( .attr('accounting', () => { return new MockAccountingService() }) - .attr('telemetry', () => { - return new MockTelemetryService() - }) .attr('logger', TestLoggerFactory.build()) .attr( 'walletAddresses', diff --git a/packages/backend/src/payment-method/ilp/connector/core/middleware/telemetry.ts b/packages/backend/src/payment-method/ilp/connector/core/middleware/telemetry.ts deleted file mode 100644 index 1633dd6894..0000000000 --- a/packages/backend/src/payment-method/ilp/connector/core/middleware/telemetry.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { ValueType } from '@opentelemetry/api' -import { ILPContext, ILPMiddleware } from '../rafiki' -import { privacy } from '../../../../../telemetry/privacy' -import { ConvertError, RatesService } from '../../../../../rates/service' -import { TelemetryService } from '../../../../../telemetry/meter' -import { ConvertOptions } from '../../../../../rates/util' - -export function createTelemetryMiddleware(): ILPMiddleware { - return async ( - { request, services, accounts, state }: ILPContext, - next: () => Promise - ): Promise => { - if ( - services.telemetry && - Number(request.prepare.amount) && - !state.unfulfillable - ) { - const senderAsset = accounts.outgoing.asset - const convertOptions = { - sourceAmount: BigInt(request.prepare.amount), - sourceAsset: { code: senderAsset.code, scale: senderAsset.scale }, - destinationAsset: { - code: services.telemetry.getBaseAssetCode(), - scale: 4 - } - } - - collectTelemetryAmount(services.telemetry, services.rates, convertOptions) - } - - await next() - } -} - -export async function collectTelemetryAmount( - telemetryService: TelemetryService, - aseRates: RatesService, - convertOptions: Omit -) { - const converted = await convertAmount( - aseRates, - telemetryService.getRatesService(), - convertOptions - ) - if (converted === ConvertError.InvalidDestinationPrice) { - return - } - - telemetryService - .getOrCreate('transactions_amount', { - description: 'Amount sent through the network', - valueType: ValueType.DOUBLE - }) - .add(privacy.applyPrivacy(Number(converted))) -} - -export async function convertAmount( - aseRates: RatesService, - telemetryRates: RatesService, - convertOptions: Omit -) { - try { - const aseConvert = await aseRates.convert(convertOptions) - return aseConvert - } catch (error) { - const telemetryRatesConverted = await telemetryRates.convert(convertOptions) - return telemetryRatesConverted - } -} diff --git a/packages/backend/src/payment-method/ilp/connector/core/rafiki.ts b/packages/backend/src/payment-method/ilp/connector/core/rafiki.ts index 1e46fb834e..f5ed36ba9b 100644 --- a/packages/backend/src/payment-method/ilp/connector/core/rafiki.ts +++ b/packages/backend/src/payment-method/ilp/connector/core/rafiki.ts @@ -19,7 +19,6 @@ import { AssetOptions } from '../../../../asset/service' import { IncomingPaymentService } from '../../../../open_payments/payment/incoming/service' import { WalletAddressService } from '../../../../open_payments/wallet_address/service' import { RatesService } from '../../../../rates/service' -import { TelemetryService } from '../../../../telemetry/meter' import { PeerService } from '../../peer/service' import { createTokenAuthMiddleware } from './middleware' import { @@ -72,7 +71,6 @@ export interface AccountingService { export interface RafikiServices { //router: Router accounting: AccountingService - telemetry?: TelemetryService walletAddresses: WalletAddressService logger: Logger incomingPayments: IncomingPaymentService @@ -161,9 +159,7 @@ export class Rafiki { get walletAddresses(): WalletAddressService { return config.walletAddresses }, - get telemetry(): TelemetryService | undefined { - return config.telemetry - }, + logger } diff --git a/packages/backend/src/payment-method/ilp/connector/core/test/middleware/telemetry.test.ts b/packages/backend/src/payment-method/ilp/connector/core/test/middleware/telemetry.test.ts deleted file mode 100644 index 06f055d2f9..0000000000 --- a/packages/backend/src/payment-method/ilp/connector/core/test/middleware/telemetry.test.ts +++ /dev/null @@ -1,190 +0,0 @@ -import { ValueType } from '@opentelemetry/api' -import assert from 'assert' -import { OutgoingAccount, ZeroCopyIlpPrepare } from '../..' -import { ConvertError } from '../../../../../../rates/service' -import { privacy } from '../../../../../../telemetry/privacy' -import { mockCounter } from '../../../../../../tests/meter' -import { IncomingAccountFactory, RafikiServicesFactory } from '../../factories' -import { - collectTelemetryAmount, - createTelemetryMiddleware -} from '../../middleware/telemetry' -import { createILPContext } from '../../utils' - -const incomingAccount = IncomingAccountFactory.build({ id: 'alice' }) - -assert.ok(incomingAccount.id) -const services = RafikiServicesFactory.build({}) - -const ctx = createILPContext({ - services, - request: { - prepare: { - amount: 100n - } as unknown as ZeroCopyIlpPrepare, - rawPrepare: Buffer.from('') - }, - accounts: { - incoming: incomingAccount, - outgoing: { asset: { code: 'USD', scale: 2 } } as OutgoingAccount - }, - state: { - unfulfillable: false, - incomingAccount: { - quote: 'exists' - } - } -}) - -const middleware = createTelemetryMiddleware() -const next = jest.fn().mockImplementation(() => Promise.resolve()) - -beforeEach(async () => { - incomingAccount.balance = 100n - incomingAccount.asset.scale = 2 - incomingAccount.asset.code = 'USD' - ctx.state.unfulfillable = false -}) - -describe('Telemetry Middleware', function () { - it('should call next without gathering telemetry when telemetry is not enabled (service is undefined)', async () => { - const getOrCreateSpy = jest - .spyOn(ctx.services.telemetry!, 'getOrCreate') - .mockImplementation(() => mockCounter) - - const originalTelemetry = ctx.services.telemetry - ctx.services.telemetry = undefined - - await middleware(ctx, next) - - expect(getOrCreateSpy).not.toHaveBeenCalled() - expect(next).toHaveBeenCalled() - - ctx.services.telemetry = originalTelemetry - }) - - it('should call next without gathering telemetry when state is unfulfillable', async () => { - ctx.state.unfulfillable = true - - const getOrCreateSpy = jest - .spyOn(ctx.services.telemetry!, 'getOrCreate') - .mockImplementation(() => mockCounter) - - await middleware(ctx, next) - - expect(getOrCreateSpy).not.toHaveBeenCalled() - expect(next).toHaveBeenCalled() - }) - - it('should call next without gathering telemetry when convert returns ConvertError.InvalidDestinationPrice', async () => { - const convertSpy = jest - .spyOn(ctx.services.rates!, 'convert') - .mockImplementation(() => - Promise.resolve(ConvertError.InvalidDestinationPrice) - ) - - const getOrCreateSpy = jest - .spyOn(ctx.services.telemetry!, 'getOrCreate') - .mockImplementation(() => mockCounter) - - await middleware(ctx, next) - - expect(convertSpy).toHaveBeenCalled() - expect(getOrCreateSpy).not.toHaveBeenCalled() - expect(next).toHaveBeenCalled() - }) - - it('should handle invalid amount by calling next without gathering telemetry', async () => { - ctx.request.prepare.amount = '0' - - const getOrCreateSpy = jest - .spyOn(ctx.services.telemetry!, 'getOrCreate') - .mockImplementation(() => mockCounter) - - await middleware(ctx, next) - - expect(getOrCreateSpy).not.toHaveBeenCalled() - expect(next).toHaveBeenCalled() - }) - - describe('collectTelemetry', () => { - it(`should first try to convert to telemetry base currency using the ASE provided rates through the aseRatesService, - and fallback to convert using external rates from telemetryRatesService`, async () => { - const aseRatesService = ctx.services.rates - const telemetryRatesService = ctx.services.telemetry!.getRatesService() - - const aseConvertSpy = jest - .spyOn(aseRatesService, 'convert') - .mockImplementation(() => - Promise.reject(ConvertError.InvalidDestinationPrice) - ) - - const telemetryConvertSpy = jest - .spyOn(telemetryRatesService, 'convert') - .mockImplementation(() => Promise.resolve(10000n)) - - await collectTelemetryAmount(ctx.services.telemetry!, aseRatesService, { - sourceAmount: BigInt(ctx.request.prepare.amount), - sourceAsset: { - code: ctx.accounts.outgoing.asset.code, - scale: ctx.accounts.outgoing.asset.scale - }, - destinationAsset: { - code: services.telemetry!.getBaseAssetCode(), - scale: 4 - } - }) - - expect(aseConvertSpy).toHaveBeenCalled() - expect(telemetryConvertSpy).toHaveBeenCalled() - }) - it('should convert to telemetry asset,apply privacy, collect telemetry', async () => { - const ratesService = ctx.services.rates - - const convertSpy = jest - .spyOn(ratesService, 'convert') - .mockResolvedValue(10000n) - - const addSpy = jest.spyOn( - ctx.services.telemetry!.getOrCreate('transactions_amount', { - description: 'Amount sent through the network', - valueType: ValueType.DOUBLE - }), - 'add' - ) - - const applyPrivacySpy = jest - .spyOn(privacy, 'applyPrivacy') - .mockReturnValue(10000) - - await collectTelemetryAmount(ctx.services.telemetry!, ratesService, { - sourceAmount: BigInt(ctx.request.prepare.amount), - sourceAsset: { - code: ctx.accounts.outgoing.asset.code, - scale: ctx.accounts.outgoing.asset.scale - }, - destinationAsset: { - code: services.telemetry!.getBaseAssetCode(), - scale: 4 - } - }) - - expect(convertSpy).toHaveBeenCalledWith({ - sourceAmount: BigInt(ctx.request.prepare.amount), - sourceAsset: { - code: ctx.accounts.outgoing.asset.code, - scale: ctx.accounts.outgoing.asset.scale - }, - destinationAsset: { - code: services.telemetry!.getBaseAssetCode(), - scale: 4 - } - }) - - expect(applyPrivacySpy).toHaveBeenCalledWith(10000) - expect(addSpy).toHaveBeenCalledWith(10000, { - source: ctx.services.telemetry!.getServiceName() - }) - }) - }) -}) diff --git a/packages/backend/src/payment-method/ilp/connector/index.ts b/packages/backend/src/payment-method/ilp/connector/index.ts index 73ddd1cccb..c46f91552f 100644 --- a/packages/backend/src/payment-method/ilp/connector/index.ts +++ b/packages/backend/src/payment-method/ilp/connector/index.ts @@ -6,7 +6,6 @@ import { IncomingPaymentService } from '../../../open_payments/payment/incoming/ import { WalletAddressService } from '../../../open_payments/wallet_address/service' import { RatesService } from '../../../rates/service' import { BaseService } from '../../../shared/baseService' -import { TelemetryService } from '../../../telemetry/meter' import { PeerService } from '../peer/service' import { ILPContext, @@ -28,13 +27,11 @@ import { createStreamAddressMiddleware, createStreamController } from './core' -import { createTelemetryMiddleware } from './core/middleware/telemetry' interface ServiceDependencies extends BaseService { redis: Redis ratesService: RatesService accountingService: AccountingService - telemetry?: TelemetryService walletAddressService: WalletAddressService incomingPaymentService: IncomingPaymentService peerService: PeerService @@ -47,7 +44,6 @@ export async function createConnectorService({ redis, ratesService, accountingService, - telemetry, walletAddressService, incomingPaymentService, peerService, @@ -61,7 +57,6 @@ export async function createConnectorService({ service: 'ConnectorService' }), accounting: accountingService, - telemetry, walletAddresses: walletAddressService, incomingPayments: incomingPaymentService, peers: peerService, @@ -84,7 +79,6 @@ export async function createConnectorService({ // Outgoing Rules createStreamController(), - createTelemetryMiddleware(), createOutgoingThroughputMiddleware(), createOutgoingReduceExpiryMiddleware({}), createOutgoingExpireMiddleware(), diff --git a/packages/backend/src/tests/meter.ts b/packages/backend/src/telemetry/mocks.ts similarity index 85% rename from packages/backend/src/tests/meter.ts rename to packages/backend/src/telemetry/mocks.ts index 1910ff01d6..63a6980237 100644 --- a/packages/backend/src/tests/meter.ts +++ b/packages/backend/src/telemetry/mocks.ts @@ -1,11 +1,11 @@ import { Attributes, Counter, MetricOptions } from '@opentelemetry/api' -import { TelemetryService } from '../telemetry/meter' -import { Rates, RatesService } from '../rates/service' +import { TelemetryService } from './service' +import { ConvertError, Rates, RatesService } from '../rates/service' export const mockCounter = { add: jest.fn() } as Counter export class MockRatesService implements RatesService { - async convert(): Promise { + async convert(): Promise { return BigInt(10000) } async rates(): Promise { diff --git a/packages/backend/src/telemetry/meter.test.ts b/packages/backend/src/telemetry/service.test.ts similarity index 92% rename from packages/backend/src/telemetry/meter.test.ts rename to packages/backend/src/telemetry/service.test.ts index 8b21c2a3c9..fe2c59a193 100644 --- a/packages/backend/src/telemetry/meter.test.ts +++ b/packages/backend/src/telemetry/service.test.ts @@ -1,4 +1,4 @@ -import { MockTelemetryService, mockCounter } from '../tests/meter' +import { MockTelemetryService, mockCounter } from './mocks' const telemetryService = new MockTelemetryService() describe('TelemetryServiceImpl', () => { diff --git a/packages/backend/src/telemetry/meter.ts b/packages/backend/src/telemetry/service.ts similarity index 100% rename from packages/backend/src/telemetry/meter.ts rename to packages/backend/src/telemetry/service.ts diff --git a/packages/backend/src/telemetry/transaction-amount.test.ts b/packages/backend/src/telemetry/transaction-amount.test.ts new file mode 100644 index 0000000000..534b11ad9d --- /dev/null +++ b/packages/backend/src/telemetry/transaction-amount.test.ts @@ -0,0 +1,120 @@ +import { ValueType } from '@opentelemetry/api' +import { ConvertError } from '../rates/service' +import { Asset } from '../rates/util' +import { privacy } from './privacy' +import { MockRatesService, MockTelemetryService } from './mocks' +import { collectTelemetryAmount, convertAmount } from './transaction-amount' + +const telemetryService = new MockTelemetryService() +const aseRates = new MockRatesService() +const telemetryRates = new MockRatesService() + +const asset: Asset = { code: 'USD', scale: 2 } + +describe('Telemetry Amount Collection', function () { + it('should not collect telemetry when conversion returns InvalidDestinationPrice', async () => { + const convertSpy = jest + .spyOn(aseRates, 'convert') + .mockImplementation(() => + Promise.resolve(ConvertError.InvalidDestinationPrice) + ) + + const addSpy = jest.spyOn( + telemetryService.getOrCreate('transactions_amount', { + description: 'Amount sent through the network', + valueType: ValueType.DOUBLE + }), + 'add' + ) + + await collectTelemetryAmount(telemetryService, aseRates, { + amount: 100n, + asset + }) + + expect(convertSpy).toHaveBeenCalled() + expect(addSpy).not.toHaveBeenCalled() + }) + it('should handle invalid amount by not collecting telemetry', async () => { + const convertSpy = jest + .spyOn(aseRates, 'convert') + .mockImplementation(() => + Promise.resolve(ConvertError.InvalidDestinationPrice) + ) + + await collectTelemetryAmount(telemetryService, aseRates, { + amount: 0n, + asset + }) + + expect(convertSpy).not.toHaveBeenCalled() + }) + + it('should collect telemetry when conversion is successful', async () => { + const convertSpy = jest + .spyOn(aseRates, 'convert') + .mockImplementation(() => Promise.resolve(10000n)) + const addSpy = jest.spyOn( + telemetryService.getOrCreate('transactions_amount', { + description: 'Amount sent through the network', + valueType: ValueType.DOUBLE + }), + 'add' + ) + jest.spyOn(privacy, 'applyPrivacy').mockReturnValue(12000) + + await collectTelemetryAmount(telemetryService, aseRates, { + amount: 100n, + asset + }) + + expect(convertSpy).toHaveBeenCalled() + expect(addSpy).toHaveBeenCalledWith(12000) + }) + + it('should try to convert using external rates from telemetryRatesService when aseRatesService fails', async () => { + const aseConvertSpy = jest + .spyOn(aseRates, 'convert') + .mockImplementation(() => + Promise.reject(ConvertError.InvalidDestinationPrice) + ) + const telemetryConvertSpy = jest + .spyOn(telemetryRates, 'convert') + .mockImplementation(() => Promise.resolve(10000n)) + + const converted = await convertAmount(aseRates, telemetryRates, { + sourceAmount: 100n, + sourceAsset: asset, + destinationAsset: { code: 'USD', scale: 2 } + }) + + expect(aseConvertSpy).toHaveBeenCalled() + expect(telemetryConvertSpy).toHaveBeenCalled() + expect(converted).toBe(10000n) + }) + + it('should apply privacy to the collected telemetry', async () => { + const convertSpy = jest + .spyOn(aseRates, 'convert') + .mockImplementation(() => Promise.resolve(10000n)) + const privacySpy = jest + .spyOn(privacy, 'applyPrivacy') + .mockReturnValue(12000) + const addSpy = jest.spyOn( + telemetryService.getOrCreate('transactions_amount', { + description: 'Amount sent through the network', + valueType: ValueType.DOUBLE + }), + 'add' + ) + + await collectTelemetryAmount(telemetryService, aseRates, { + amount: 100n, + asset + }) + + expect(convertSpy).toHaveBeenCalled() + expect(privacySpy).toHaveBeenCalledWith(Number(10000n)) + expect(addSpy).toHaveBeenCalledWith(12000) + }) +}) diff --git a/packages/backend/src/telemetry/transaction-amount.ts b/packages/backend/src/telemetry/transaction-amount.ts new file mode 100644 index 0000000000..a4ac81ce3b --- /dev/null +++ b/packages/backend/src/telemetry/transaction-amount.ts @@ -0,0 +1,53 @@ +import { ValueType } from '@opentelemetry/api' +import { ConvertError, RatesService } from '../rates/service' +import { ConvertOptions } from '../rates/util' +import { TelemetryService } from './service' +import { privacy } from './privacy' +import { Asset } from '../rates/util' + +export async function collectTelemetryAmount( + telemetryService: TelemetryService, + aseRates: RatesService, + { amount, asset }: { amount: bigint; asset: Asset } +) { + if (!amount) { + return + } + const convertOptions = { + sourceAmount: amount, + sourceAsset: { code: asset.code, scale: asset.scale }, + destinationAsset: { + code: telemetryService.getBaseAssetCode(), + scale: 4 + } + } + const converted = await convertAmount( + aseRates, + telemetryService.getRatesService(), + convertOptions + ) + if (converted === ConvertError.InvalidDestinationPrice) { + return + } + + telemetryService + .getOrCreate('transactions_amount', { + description: 'Amount sent through the network', + valueType: ValueType.DOUBLE + }) + .add(privacy.applyPrivacy(Number(converted))) +} + +export async function convertAmount( + aseRates: RatesService, + telemetryRates: RatesService, + convertOptions: Omit +) { + try { + const aseConvert = await aseRates.convert(convertOptions) + return aseConvert + } catch (error) { + const telemetryRatesConverted = await telemetryRates.convert(convertOptions) + return telemetryRatesConverted + } +} diff --git a/packages/backend/src/tests/app.ts b/packages/backend/src/tests/app.ts index 9d8ab3b506..973bd30889 100644 --- a/packages/backend/src/tests/app.ts +++ b/packages/backend/src/tests/app.ts @@ -16,7 +16,7 @@ import { setContext } from '@apollo/client/link/context' import { start, gracefulShutdown } from '..' import { App, AppServices } from '../app' -import { MockTelemetryService } from './meter' +import { MockTelemetryService } from '../telemetry/mocks' export const testAccessToken = 'test-app-access' export interface TestContainer { From ff63f80afe3ba075ce75656f28c35c77167ee10d Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Sat, 13 Jan 2024 13:41:13 +0200 Subject: [PATCH 33/67] feat(telemetry): base asset code no longer externally configurable base asset scale added --- packages/backend/src/config/app.ts | 1 - packages/backend/src/index.ts | 3 ++- packages/backend/src/telemetry/mocks.ts | 4 ++++ packages/backend/src/telemetry/service.ts | 6 ++++++ 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/config/app.ts b/packages/backend/src/config/app.ts index fe93099ee9..7a12804077 100644 --- a/packages/backend/src/config/app.ts +++ b/packages/backend/src/config/app.ts @@ -50,7 +50,6 @@ export const Config = { 'TELEMETRY_EXCHANGE_RATES_LIFETIME', 86_400_000 ), - telemetryBaseAssetCode: envString('TELEMETRY_BASE_ASSET_CODE', 'USD'), adminPort: envInt('ADMIN_PORT', 3001), openPaymentsUrl: envString('OPEN_PAYMENTS_URL', 'http://127.0.0.1:3000'), openPaymentsPort: envInt('OPEN_PAYMENTS_PORT', 3003), diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index d0d8fe273e..4ca1392ac0 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -142,7 +142,8 @@ export function initIocContainer( serviceName: config.instanceName, collectorUrls: config.openTelemetryCollectors, exportIntervalMillis: config.openTelemetryExportInterval, - baseAssetCode: config.telemetryBaseAssetCode + baseAssetCode: 'USD', + baseScale: 4 }) }) } diff --git a/packages/backend/src/telemetry/mocks.ts b/packages/backend/src/telemetry/mocks.ts index 63a6980237..d9c253a503 100644 --- a/packages/backend/src/telemetry/mocks.ts +++ b/packages/backend/src/telemetry/mocks.ts @@ -46,6 +46,10 @@ export class MockTelemetryService implements TelemetryService { return 'USD' } + getBaseScale(): number { + return 4 + } + applyPrivacy(rawValue: number): number { return rawValue + Math.random() * 100 } diff --git a/packages/backend/src/telemetry/service.ts b/packages/backend/src/telemetry/service.ts index eabe8b03d3..66bb81a08e 100644 --- a/packages/backend/src/telemetry/service.ts +++ b/packages/backend/src/telemetry/service.ts @@ -21,6 +21,7 @@ export interface TelemetryService { getServiceName(): string | undefined getRatesService(): RatesService getBaseAssetCode(): string + getBaseScale(): number } interface TelemetryServiceDependencies extends BaseService { @@ -29,6 +30,7 @@ interface TelemetryServiceDependencies extends BaseService { exportIntervalMillis?: number telemetryRatesService: RatesService baseAssetCode: string + baseScale: number } export function createTelemetryService( @@ -107,4 +109,8 @@ class TelemetryServiceImpl implements TelemetryService { getBaseAssetCode(): string { return this.deps.baseAssetCode } + + getBaseScale(): number { + return this.deps.baseScale + } } From 19183c574343a191112c6c8f969e2850abf6c07a Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Sat, 13 Jan 2024 13:46:01 +0200 Subject: [PATCH 34/67] feat(telemetry): leftover of last commit --- packages/backend/src/telemetry/transaction-amount.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/telemetry/transaction-amount.ts b/packages/backend/src/telemetry/transaction-amount.ts index a4ac81ce3b..eb44d3939a 100644 --- a/packages/backend/src/telemetry/transaction-amount.ts +++ b/packages/backend/src/telemetry/transaction-amount.ts @@ -18,7 +18,7 @@ export async function collectTelemetryAmount( sourceAsset: { code: asset.code, scale: asset.scale }, destinationAsset: { code: telemetryService.getBaseAssetCode(), - scale: 4 + scale: telemetryService.getBaseScale() } } const converted = await convertAmount( From 86a569afb1968e167fc8575b3dfe4e192f862e9f Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Mon, 29 Jan 2024 11:15:20 +0200 Subject: [PATCH 35/67] feat(telemetry): some docs changes --- .../src/content/docs/telemetry/overview.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/documentation/src/content/docs/telemetry/overview.md b/packages/documentation/src/content/docs/telemetry/overview.md index d3dcd37166..5f6eabc0c7 100644 --- a/packages/documentation/src/content/docs/telemetry/overview.md +++ b/packages/documentation/src/content/docs/telemetry/overview.md @@ -10,11 +10,11 @@ The objective of the telemetry feature is to gather metrics and establish an inf - The number of transactions from outgoing payments that have been at least partially successful. - The average amount of money held within the network per transaction. -Our goal is to use these data for our own insights and to enable Account Servicing Entities (ASEs) to gain their own insights. We aim to track the growth of the network in terms of transaction sizes and the number of transactions processed. +Our goal is to use these data for our own insights and to enable [Account Servicing Entities](/reference/glossary#account-servicing-entity) (ASEs) to gain their own insights. We aim to track the growth of the network in terms of transaction sizes and the number of transactions processed. ## Privacy and Optionality -Privacy is a paramount concern for us. Rafiki's telemetry feature is designed to provide valuable network insights without violating privacy or aiding malicious ASEs. For more information, please [Read the privacy docs](./privacy.md). +Privacy is a paramount concern for us. Rafiki's telemetry feature is designed to provide valuable network insights without violating privacy or aiding malicious ASEs. For more information, please [Read the privacy docs](../privacy). The telemetry feature is optional for ASEs. @@ -26,11 +26,11 @@ The architecture of the telemetry feature is illustrated below: ## Opentelemetry -We have adopted Opentelemetry to ensure compliance with a standardized framework that is compatible with a variety of tool suites. This allows clients to use their preferred tools for data analysis, while Rafiki is instrumented and observable through a standardized metrics format. +We have adopted [Opentelemetry](https://opentelemetry.io/) to ensure compliance with a standardized framework that is compatible with a variety of tool suites. This allows clients to use their preferred tools for data analysis, while Rafiki is instrumented and observable through a standardized metrics format. ## Telemetry ECS Cluster -The Telemetry Replica service is hosted on AWS ECS Fargate and is configured for availability and load balancing of custom ADOT collector ECS tasks. +The Telemetry Replica service is hosted on AWS ECS Fargate and is configured for availability and load balancing of custom AWS Distro for Opentelemetry Collector (ADOT) ECS tasks. When ASEs opt for telemetry, metrics are sent to our Telemetry Service. To enable ASEs to build their own telemetry solutions, instrumented Rafiki can send data to multiple endpoints. This allows the integration of a local Otel collector container that can support custom requirements. Metrics communication is facilitated through GRPC. @@ -64,11 +64,11 @@ Rafiki currently has two counter metrics. All data points (counter increases) ar Currently collected metrics: -- `transactions_total` (Open-payments) - Counter metric +- `transactions_total` - Counter metric - Description: “Count of funded transactions” - This counter metric increases by 1 for each successfully sent transaction. - It is collected in the open-payments module, outgoing payment service. -- `transactions_amount` (ILP Connector) - Counter metric +- `transactions_amount` - Counter metric - Description: “Amount sent through the network”. - This amount metric increases by the amount sent in each ILP packet. - It is collected inside the ILP connector core, by a new telemetry middleware. From c528ab8b11dbc6af25f0a6deaf026726c2e3cefe Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Mon, 29 Jan 2024 11:15:40 +0200 Subject: [PATCH 36/67] feat(telemetry):docs changes --- packages/documentation/src/content/docs/telemetry/overview.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/documentation/src/content/docs/telemetry/overview.md b/packages/documentation/src/content/docs/telemetry/overview.md index 5f6eabc0c7..565bfdc0c5 100644 --- a/packages/documentation/src/content/docs/telemetry/overview.md +++ b/packages/documentation/src/content/docs/telemetry/overview.md @@ -30,7 +30,7 @@ We have adopted [Opentelemetry](https://opentelemetry.io/) to ensure compliance ## Telemetry ECS Cluster -The Telemetry Replica service is hosted on AWS ECS Fargate and is configured for availability and load balancing of custom AWS Distro for Opentelemetry Collector (ADOT) ECS tasks. +The Telemetry Replica service is hosted on AWS ECS Fargate and is configured for availability and load balancing of custom ADOT Collectors (AWS Distro for Opentelemetry) ECS tasks. When ASEs opt for telemetry, metrics are sent to our Telemetry Service. To enable ASEs to build their own telemetry solutions, instrumented Rafiki can send data to multiple endpoints. This allows the integration of a local Otel collector container that can support custom requirements. Metrics communication is facilitated through GRPC. From f171026e2bd711e0eaf80a377bb77550aba2b38f Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Mon, 29 Jan 2024 11:17:26 +0200 Subject: [PATCH 37/67] feat(telemetry):docs --- packages/documentation/src/content/docs/telemetry/overview.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/documentation/src/content/docs/telemetry/overview.md b/packages/documentation/src/content/docs/telemetry/overview.md index 565bfdc0c5..35456d6932 100644 --- a/packages/documentation/src/content/docs/telemetry/overview.md +++ b/packages/documentation/src/content/docs/telemetry/overview.md @@ -30,7 +30,7 @@ We have adopted [Opentelemetry](https://opentelemetry.io/) to ensure compliance ## Telemetry ECS Cluster -The Telemetry Replica service is hosted on AWS ECS Fargate and is configured for availability and load balancing of custom ADOT Collectors (AWS Distro for Opentelemetry) ECS tasks. +The Telemetry Replica service is hosted on AWS ECS Fargate and is configured for availability and load balancing of custom ADOT (AWS Distro for Opentelemetry) Collector ECS tasks. When ASEs opt for telemetry, metrics are sent to our Telemetry Service. To enable ASEs to build their own telemetry solutions, instrumented Rafiki can send data to multiple endpoints. This allows the integration of a local Otel collector container that can support custom requirements. Metrics communication is facilitated through GRPC. From 223e89f6091247989ee218af1730a1d7ca48253a Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Mon, 29 Jan 2024 11:27:27 +0200 Subject: [PATCH 38/67] feat(telemetry):more docs changes --- packages/documentation/src/content/docs/telemetry/overview.md | 4 ++-- packages/documentation/src/content/docs/telemetry/privacy.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/documentation/src/content/docs/telemetry/overview.md b/packages/documentation/src/content/docs/telemetry/overview.md index 35456d6932..9942208332 100644 --- a/packages/documentation/src/content/docs/telemetry/overview.md +++ b/packages/documentation/src/content/docs/telemetry/overview.md @@ -14,7 +14,7 @@ Our goal is to use these data for our own insights and to enable [Account Servic ## Privacy and Optionality -Privacy is a paramount concern for us. Rafiki's telemetry feature is designed to provide valuable network insights without violating privacy or aiding malicious ASEs. For more information, please [Read the privacy docs](../privacy). +Privacy is a paramount concern for us. Rafiki's telemetry feature is designed to provide valuable network insights without violating privacy or aiding malicious ASEs. For more information, please [read the privacy docs](../privacy). The telemetry feature is optional for ASEs. @@ -26,7 +26,7 @@ The architecture of the telemetry feature is illustrated below: ## Opentelemetry -We have adopted [Opentelemetry](https://opentelemetry.io/) to ensure compliance with a standardized framework that is compatible with a variety of tool suites. This allows clients to use their preferred tools for data analysis, while Rafiki is instrumented and observable through a standardized metrics format. +We have adopted [OpenTelemetry](https://opentelemetry.io/) to ensure compliance with a standardized framework that is compatible with a variety of tool suites. This allows clients to use their preferred tools for data analysis, while Rafiki is instrumented and observable through a standardized metrics format. ## Telemetry ECS Cluster diff --git a/packages/documentation/src/content/docs/telemetry/privacy.md b/packages/documentation/src/content/docs/telemetry/privacy.md index e15db5a5eb..e04e9fb4f7 100644 --- a/packages/documentation/src/content/docs/telemetry/privacy.md +++ b/packages/documentation/src/content/docs/telemetry/privacy.md @@ -1,5 +1,5 @@ --- -title: Privacy in Rafiki Telemetry +title: Privacy --- Rafiki telemetry is designed with a strong emphasis on privacy. The system anonymizes user data and refrains from collecting identifiable information. Since transactions can originate from any user to a Rafiki instance, the privacy measures are implemented at the Rafiki instance level in the network. This means that at the individual level, the data is already anonymous as single Rafiki instances service transactions for multiple users. @@ -30,7 +30,7 @@ The noise, selected from the Laplacian distribution, is then generated using thi ## Currency Conversion -Another factor that obscures sensitive data is currency conversion. In cross-currency transactions, we use exchange rates that are not traced back. This introduces an additional layer of noise and further protects the privacy of the transactions. +Another factor that obscures sensitive data is currency conversion. In cross-currency transactions, we use exchange rates that are not traced back. The rates are provided by the integrating [ASE](/reference/glossary#account-servicing-entity) at runtime and are not persisted. If the needed rates are not provided, external API's for exchange rates are used. The obtained exchange rates are overwritten frequently in this case, with no versioning or history access. This introduces an additional layer of noise and further protects the privacy of the transactions. ## References From ff70532c2a7d45808cac17bf483fa0bc5bd7a278 Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Mon, 29 Jan 2024 11:53:38 +0200 Subject: [PATCH 39/67] feat(telemetry): remove leftover mock --- packages/backend/src/telemetry/privacy.test.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/packages/backend/src/telemetry/privacy.test.ts b/packages/backend/src/telemetry/privacy.test.ts index 77d6ad50dc..31b8416ebe 100644 --- a/packages/backend/src/telemetry/privacy.test.ts +++ b/packages/backend/src/telemetry/privacy.test.ts @@ -5,17 +5,6 @@ describe('Privacy functions', () => { maxBucketSize: 10000 } - let originalModule: typeof privacy - - beforeEach(() => { - originalModule = { ...privacy } - - jest.mock('./privacy', () => ({ - ...originalModule, - applyPrivacy: jest.fn() - })) - }) - afterEach(() => { jest.unmock('./privacy') }) From 48631ff112f7409a1f6a4a8d5992d2e8af39b73f Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Mon, 29 Jan 2024 16:59:09 +0200 Subject: [PATCH 40/67] feat(telemetry): Move conversion in telemetry & update DI accordingly. Moved & updated tests --- .../backend/src/accounting/psql/service.ts | 5 +- packages/backend/src/accounting/service.ts | 10 ++-- .../src/accounting/tigerbeetle/service.ts | 5 +- packages/backend/src/index.ts | 27 +++++------ .../backend/src/telemetry/service.test.ts | 27 ++++++++++- packages/backend/src/telemetry/service.ts | 33 +++++++++---- .../src/telemetry/transaction-amount.test.ts | 43 ++++------------- .../src/telemetry/transaction-amount.ts | 48 +++++++------------ packages/backend/src/tests/app.ts | 2 +- .../mocks.ts => tests/telemetry.ts} | 30 +++++++----- 10 files changed, 115 insertions(+), 115 deletions(-) rename packages/backend/src/{telemetry/mocks.ts => tests/telemetry.ts} (51%) diff --git a/packages/backend/src/accounting/psql/service.ts b/packages/backend/src/accounting/psql/service.ts index 1515140c86..f67ce7e810 100644 --- a/packages/backend/src/accounting/psql/service.ts +++ b/packages/backend/src/accounting/psql/service.ts @@ -2,7 +2,9 @@ import { TransactionOrKnex } from 'objection' import { v4 as uuid } from 'uuid' import { Asset } from '../../asset/model' +import { RatesService } from '../../rates/service' import { BaseService } from '../../shared/baseService' +import { TelemetryService } from '../../telemetry/service' import { isTransferError, TransferError } from '../errors' import { AccountingService, @@ -33,8 +35,6 @@ import { voidTransfers } from './ledger-transfer' import { LedgerTransfer, LedgerTransferType } from './ledger-transfer/model' -import { TelemetryService } from '../../telemetry/service' -import { RatesService } from '../../rates/service' export interface ServiceDependencies extends BaseService { telemetry?: TelemetryService @@ -206,7 +206,6 @@ export async function createTransfer( ): Promise { return createAccountToAccountTransfer({ telemetry: deps.telemetry, - aseRatesService: deps.aseRatesService, transferArgs: args, withdrawalThrottleDelay: deps.withdrawalThrottleDelay, voidTransfers: async (transferRefs) => voidTransfers(deps, transferRefs), diff --git a/packages/backend/src/accounting/service.ts b/packages/backend/src/accounting/service.ts index 48e91a580b..028a9a13bf 100644 --- a/packages/backend/src/accounting/service.ts +++ b/packages/backend/src/accounting/service.ts @@ -1,8 +1,7 @@ import { TransactionOrKnex } from 'objection' -import { TransferError, isTransferError } from './errors' import { TelemetryService } from '../telemetry/service' -import { RatesService } from '../rates/service' import { collectTelemetryAmount } from '../telemetry/transaction-amount' +import { TransferError, isTransferError } from './errors' export enum LiquidityAccountType { ASSET = 'ASSET', @@ -95,7 +94,6 @@ export interface TransferToCreate { interface CreateAccountToAccountTransferArgs { telemetry?: TelemetryService - aseRatesService?: RatesService transferArgs: TransferOptions voidTransfers(transferIds: string[]): Promise postTransfers(transferIds: string[]): Promise @@ -118,8 +116,7 @@ export async function createAccountToAccountTransfer( getAccountBalance, withdrawalThrottleDelay, transferArgs, - telemetry, - aseRatesService + telemetry } = args const { sourceAccount, destinationAccount, sourceAmount, destinationAmount } = @@ -198,11 +195,10 @@ export async function createAccountToAccountTransfer( if ( destinationAccount.onDebit && telemetry && - aseRatesService && sourceAccount.asset.code && sourceAccount.asset.scale ) { - collectTelemetryAmount(telemetry, aseRatesService, { + collectTelemetryAmount(telemetry, { amount: sourceAmount, asset: { code: sourceAccount.asset.code, diff --git a/packages/backend/src/accounting/tigerbeetle/service.ts b/packages/backend/src/accounting/tigerbeetle/service.ts index fa1985b2e5..603e82bee2 100644 --- a/packages/backend/src/accounting/tigerbeetle/service.ts +++ b/packages/backend/src/accounting/tigerbeetle/service.ts @@ -3,6 +3,7 @@ import { v4 as uuid } from 'uuid' import { BaseService } from '../../shared/baseService' import { validateId } from '../../shared/utils' +import { TelemetryService } from '../../telemetry/service' import { AccountAlreadyExistsError, BalanceTransferError, @@ -26,8 +27,6 @@ import { } from './errors' import { NewTransferOptions, createTransfers } from './transfers' import { toTigerbeetleId } from './utils' -import { TelemetryService } from '../../telemetry/service' -import { RatesService } from '../../rates/service' export enum TigerbeetleAccountCode { LIQUIDITY_WEB_MONETIZATION = 1, @@ -51,7 +50,6 @@ export const convertToTigerbeetleAccountCode: { export interface ServiceDependencies extends BaseService { telemetry?: TelemetryService - aseRatesService?: RatesService tigerbeetle: Client withdrawalThrottleDelay?: number } @@ -221,7 +219,6 @@ export async function createTransfer( ): Promise { return createAccountToAccountTransfer({ telemetry: deps.telemetry, - aseRatesService: deps.aseRatesService, transferArgs: args, withdrawalThrottleDelay: deps.withdrawalThrottleDelay, voidTransfers: async (transferIds) => { diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 4ca1392ac0..aaff2d07cc 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -46,7 +46,7 @@ import { createPeerService } from './payment-method/ilp/peer/service' import { createIlpPaymentService } from './payment-method/ilp/service' import { createSPSPRoutes } from './payment-method/ilp/spsp/routes' import { createStreamCredentialsService } from './payment-method/ilp/stream-credentials/service' -import { RatesService, createRatesService } from './rates/service' +import { createRatesService } from './rates/service' import { TelemetryService, createTelemetryService } from './telemetry/service' import { createWebhookService } from './webhook/service' @@ -129,12 +129,22 @@ export function initIocContainer( }) }) + container.singleton('ratesService', async (deps) => { + const config = await deps.use('config') + return createRatesService({ + logger: await deps.use('logger'), + exchangeRatesUrl: config.exchangeRatesUrl, + exchangeRatesLifetime: config.exchangeRatesLifetime + }) + }) + if (config.enableTelemetry) { container.singleton('telemetry', async (deps) => { const config = await deps.use('config') return createTelemetryService({ logger: await deps.use('logger'), - telemetryRatesService: createRatesService({ + aseRatesService: await deps.use('ratesService'), + fallbackRatesService: createRatesService({ logger: await deps.use('logger'), exchangeRatesUrl: config.telemetryExchangeRatesUrl, exchangeRatesLifetime: config.telemetryExchangeRatesLifetime @@ -206,10 +216,8 @@ export function initIocContainer( const config = await deps.use('config') let telemetry: TelemetryService | undefined - let aseRatesService: RatesService | undefined if (config.enableTelemetry) { telemetry = await deps.use('telemetry') - aseRatesService = await deps.use('ratesService') } if (config.useTigerbeetle) { @@ -218,7 +226,6 @@ export function initIocContainer( return createTigerbeetleAccountingService({ logger, telemetry, - aseRatesService, knex, tigerbeetle, withdrawalThrottleDelay: config.withdrawalThrottleDelay @@ -228,7 +235,6 @@ export function initIocContainer( return createPsqlAccountingService({ logger, telemetry, - aseRatesService, knex, withdrawalThrottleDelay: config.withdrawalThrottleDelay }) @@ -346,15 +352,6 @@ export function initIocContainer( }) }) - container.singleton('ratesService', async (deps) => { - const config = await deps.use('config') - return createRatesService({ - logger: await deps.use('logger'), - exchangeRatesUrl: config.exchangeRatesUrl, - exchangeRatesLifetime: config.exchangeRatesLifetime - }) - }) - container.singleton('walletAddressKeyService', async (deps) => { return createWalletAddressKeyService({ logger: await deps.use('logger'), diff --git a/packages/backend/src/telemetry/service.test.ts b/packages/backend/src/telemetry/service.test.ts index fe2c59a193..5427f9e807 100644 --- a/packages/backend/src/telemetry/service.test.ts +++ b/packages/backend/src/telemetry/service.test.ts @@ -1,4 +1,5 @@ -import { MockTelemetryService, mockCounter } from './mocks' +import { ConvertError } from '../rates/service' +import { MockTelemetryService, mockCounter } from '../tests/telemetry' const telemetryService = new MockTelemetryService() describe('TelemetryServiceImpl', () => { @@ -18,4 +19,28 @@ describe('TelemetryServiceImpl', () => { expect(serviceName).toBe('serviceName') }) + + describe('conversion', () => { + it('should try to convert using aseRatesService and fallback to fallbackRatesService', async () => { + const aseConvertSpy = jest + .spyOn(telemetryService.aseRatesService, 'convert') + .mockImplementation(() => + Promise.resolve(ConvertError.InvalidDestinationPrice) + ) + + const fallbackConvertSpy = jest + .spyOn(telemetryService.fallbackRatesService, 'convert') + .mockImplementation(() => Promise.resolve(10000n)) + + const converted = await telemetryService.convertAmount({ + sourceAmount: 100n, + sourceAsset: { code: 'USD', scale: 2 }, + destinationAsset: { code: 'USD', scale: 2 } + }) + + expect(aseConvertSpy).toHaveBeenCalled() + expect(fallbackConvertSpy).toHaveBeenCalled() + expect(converted).toBe(10000n) + }) + }) }) diff --git a/packages/backend/src/telemetry/service.ts b/packages/backend/src/telemetry/service.ts index 66bb81a08e..5b5803c8b2 100644 --- a/packages/backend/src/telemetry/service.ts +++ b/packages/backend/src/telemetry/service.ts @@ -13,22 +13,26 @@ import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics' -import { RatesService } from '../rates/service' +import { ConvertError, RatesService } from '../rates/service' +import { ConvertOptions } from '../rates/util' import { BaseService } from '../shared/baseService' export interface TelemetryService { getOrCreate(name: string, options?: MetricOptions): Counter getServiceName(): string | undefined - getRatesService(): RatesService getBaseAssetCode(): string getBaseScale(): number + convertAmount( + convertOptions: Omit + ): Promise } interface TelemetryServiceDependencies extends BaseService { serviceName: string collectorUrls: string[] exportIntervalMillis?: number - telemetryRatesService: RatesService + aseRatesService: RatesService + fallbackRatesService: RatesService baseAssetCode: string baseScale: number } @@ -42,13 +46,15 @@ export function createTelemetryService( class TelemetryServiceImpl implements TelemetryService { private serviceName: string private meterProvider?: MeterProvider - private ratesService: RatesService + private fallbackRatesService: RatesService + private aseRatesService: RatesService private counters = new Map() constructor(private deps: TelemetryServiceDependencies) { diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.DEBUG) this.serviceName = deps.serviceName - this.ratesService = deps.telemetryRatesService + this.fallbackRatesService = deps.fallbackRatesService + this.aseRatesService = deps.aseRatesService if ( deps.collectorUrls && @@ -98,12 +104,21 @@ class TelemetryServiceImpl implements TelemetryService { return this.createCounter(name, options) } - public getServiceName(): string | undefined { - return this.serviceName + public async convertAmount( + convertOptions: Omit + ) { + let converted = await this.aseRatesService.convert(convertOptions) + if (typeof converted !== 'bigint' && converted in ConvertError) { + converted = await this.fallbackRatesService.convert(convertOptions) + if (typeof converted !== 'bigint' && converted in ConvertError) { + this.deps.logger.error(`Unable to convert amount: ${converted}`) + } + } + return converted } - getRatesService(): RatesService { - return this.ratesService + public getServiceName(): string | undefined { + return this.serviceName } getBaseAssetCode(): string { diff --git a/packages/backend/src/telemetry/transaction-amount.test.ts b/packages/backend/src/telemetry/transaction-amount.test.ts index 534b11ad9d..cfbdd830e9 100644 --- a/packages/backend/src/telemetry/transaction-amount.test.ts +++ b/packages/backend/src/telemetry/transaction-amount.test.ts @@ -1,20 +1,18 @@ import { ValueType } from '@opentelemetry/api' import { ConvertError } from '../rates/service' import { Asset } from '../rates/util' +import { MockTelemetryService } from '../tests/telemetry' import { privacy } from './privacy' -import { MockRatesService, MockTelemetryService } from './mocks' -import { collectTelemetryAmount, convertAmount } from './transaction-amount' +import { collectTelemetryAmount } from './transaction-amount' const telemetryService = new MockTelemetryService() -const aseRates = new MockRatesService() -const telemetryRates = new MockRatesService() const asset: Asset = { code: 'USD', scale: 2 } describe('Telemetry Amount Collection', function () { it('should not collect telemetry when conversion returns InvalidDestinationPrice', async () => { const convertSpy = jest - .spyOn(aseRates, 'convert') + .spyOn(telemetryService, 'convertAmount') .mockImplementation(() => Promise.resolve(ConvertError.InvalidDestinationPrice) ) @@ -27,7 +25,7 @@ describe('Telemetry Amount Collection', function () { 'add' ) - await collectTelemetryAmount(telemetryService, aseRates, { + await collectTelemetryAmount(telemetryService, { amount: 100n, asset }) @@ -37,12 +35,12 @@ describe('Telemetry Amount Collection', function () { }) it('should handle invalid amount by not collecting telemetry', async () => { const convertSpy = jest - .spyOn(aseRates, 'convert') + .spyOn(telemetryService, 'convertAmount') .mockImplementation(() => Promise.resolve(ConvertError.InvalidDestinationPrice) ) - await collectTelemetryAmount(telemetryService, aseRates, { + await collectTelemetryAmount(telemetryService, { amount: 0n, asset }) @@ -52,7 +50,7 @@ describe('Telemetry Amount Collection', function () { it('should collect telemetry when conversion is successful', async () => { const convertSpy = jest - .spyOn(aseRates, 'convert') + .spyOn(telemetryService, 'convertAmount') .mockImplementation(() => Promise.resolve(10000n)) const addSpy = jest.spyOn( telemetryService.getOrCreate('transactions_amount', { @@ -63,7 +61,7 @@ describe('Telemetry Amount Collection', function () { ) jest.spyOn(privacy, 'applyPrivacy').mockReturnValue(12000) - await collectTelemetryAmount(telemetryService, aseRates, { + await collectTelemetryAmount(telemetryService, { amount: 100n, asset }) @@ -72,30 +70,9 @@ describe('Telemetry Amount Collection', function () { expect(addSpy).toHaveBeenCalledWith(12000) }) - it('should try to convert using external rates from telemetryRatesService when aseRatesService fails', async () => { - const aseConvertSpy = jest - .spyOn(aseRates, 'convert') - .mockImplementation(() => - Promise.reject(ConvertError.InvalidDestinationPrice) - ) - const telemetryConvertSpy = jest - .spyOn(telemetryRates, 'convert') - .mockImplementation(() => Promise.resolve(10000n)) - - const converted = await convertAmount(aseRates, telemetryRates, { - sourceAmount: 100n, - sourceAsset: asset, - destinationAsset: { code: 'USD', scale: 2 } - }) - - expect(aseConvertSpy).toHaveBeenCalled() - expect(telemetryConvertSpy).toHaveBeenCalled() - expect(converted).toBe(10000n) - }) - it('should apply privacy to the collected telemetry', async () => { const convertSpy = jest - .spyOn(aseRates, 'convert') + .spyOn(telemetryService, 'convertAmount') .mockImplementation(() => Promise.resolve(10000n)) const privacySpy = jest .spyOn(privacy, 'applyPrivacy') @@ -108,7 +85,7 @@ describe('Telemetry Amount Collection', function () { 'add' ) - await collectTelemetryAmount(telemetryService, aseRates, { + await collectTelemetryAmount(telemetryService, { amount: 100n, asset }) diff --git a/packages/backend/src/telemetry/transaction-amount.ts b/packages/backend/src/telemetry/transaction-amount.ts index eb44d3939a..c46561d5d0 100644 --- a/packages/backend/src/telemetry/transaction-amount.ts +++ b/packages/backend/src/telemetry/transaction-amount.ts @@ -1,18 +1,17 @@ import { ValueType } from '@opentelemetry/api' -import { ConvertError, RatesService } from '../rates/service' -import { ConvertOptions } from '../rates/util' -import { TelemetryService } from './service' -import { privacy } from './privacy' +import { ConvertError } from '../rates/service' import { Asset } from '../rates/util' +import { privacy } from './privacy' +import { TelemetryService } from './service' export async function collectTelemetryAmount( telemetryService: TelemetryService, - aseRates: RatesService, { amount, asset }: { amount: bigint; asset: Asset } ) { if (!amount) { return } + const convertOptions = { sourceAmount: amount, sourceAsset: { code: asset.code, scale: asset.scale }, @@ -21,33 +20,20 @@ export async function collectTelemetryAmount( scale: telemetryService.getBaseScale() } } - const converted = await convertAmount( - aseRates, - telemetryService.getRatesService(), - convertOptions - ) - if (converted === ConvertError.InvalidDestinationPrice) { - return - } - - telemetryService - .getOrCreate('transactions_amount', { - description: 'Amount sent through the network', - valueType: ValueType.DOUBLE - }) - .add(privacy.applyPrivacy(Number(converted))) -} -export async function convertAmount( - aseRates: RatesService, - telemetryRates: RatesService, - convertOptions: Omit -) { try { - const aseConvert = await aseRates.convert(convertOptions) - return aseConvert - } catch (error) { - const telemetryRatesConverted = await telemetryRates.convert(convertOptions) - return telemetryRatesConverted + const converted = await telemetryService.convertAmount(convertOptions) + if (converted === ConvertError.InvalidDestinationPrice) { + return + } + + telemetryService + .getOrCreate('transactions_amount', { + description: 'Amount sent through the network', + valueType: ValueType.DOUBLE + }) + .add(privacy.applyPrivacy(Number(converted))) + } catch (e) { + console.error(`Unable to collect telemetry`, e) } } diff --git a/packages/backend/src/tests/app.ts b/packages/backend/src/tests/app.ts index 973bd30889..1c38dd451d 100644 --- a/packages/backend/src/tests/app.ts +++ b/packages/backend/src/tests/app.ts @@ -16,7 +16,7 @@ import { setContext } from '@apollo/client/link/context' import { start, gracefulShutdown } from '..' import { App, AppServices } from '../app' -import { MockTelemetryService } from '../telemetry/mocks' +import { MockTelemetryService } from './telemetry' export const testAccessToken = 'test-app-access' export interface TestContainer { diff --git a/packages/backend/src/telemetry/mocks.ts b/packages/backend/src/tests/telemetry.ts similarity index 51% rename from packages/backend/src/telemetry/mocks.ts rename to packages/backend/src/tests/telemetry.ts index d9c253a503..450542c71f 100644 --- a/packages/backend/src/telemetry/mocks.ts +++ b/packages/backend/src/tests/telemetry.ts @@ -1,6 +1,7 @@ import { Attributes, Counter, MetricOptions } from '@opentelemetry/api' -import { TelemetryService } from './service' +import { TelemetryService } from '../telemetry/service' import { ConvertError, Rates, RatesService } from '../rates/service' +import { ConvertOptions } from '../rates/util' export const mockCounter = { add: jest.fn() } as Counter @@ -27,30 +28,37 @@ export class MockRatesService implements RatesService { } export class MockTelemetryService implements TelemetryService { - ratesService = new MockRatesService() - getOrCreate( + public aseRatesService = new MockRatesService() + public fallbackRatesService = new MockRatesService() + public getOrCreate( _name: string, _options?: MetricOptions | undefined ): Counter { return mockCounter } - getServiceName(): string | undefined { + public getServiceName(): string | undefined { return 'serviceName' } - getRatesService(): RatesService { - return this.ratesService + public async convertAmount( + _convertOptions: Omit + ): Promise { + let converted = await this.aseRatesService.convert() + if (typeof converted !== 'bigint' && converted in ConvertError) { + converted = await this.fallbackRatesService.convert() + } + return Promise.resolve(converted) } - getBaseAssetCode(): string { + public getBaseAssetCode(): string { return 'USD' } - getBaseScale(): number { + public getBaseScale(): number { return 4 } - applyPrivacy(rawValue: number): number { - return rawValue + Math.random() * 100 - } + // public applyPrivacy(rawValue: number): number { + // return rawValue + Math.random() * 100 + // } } From 73f3f8312174b44d51ed0f45aa25eef84dde5972 Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Mon, 29 Jan 2024 18:11:32 +0200 Subject: [PATCH 41/67] feat(telemetry): docs - create new integration section --- localenv/cloud-nine-wallet/docker-compose.yml | 15 ---- localenv/collector/otel-collector-config.yaml | 42 --------- localenv/happy-life-bank/docker-compose.yml | 17 ---- .../src/content/docs/telemetry/integrating.md | 88 +++++++++++++++++++ 4 files changed, 88 insertions(+), 74 deletions(-) delete mode 100644 localenv/collector/otel-collector-config.yaml create mode 100644 packages/documentation/src/content/docs/telemetry/integrating.md diff --git a/localenv/cloud-nine-wallet/docker-compose.yml b/localenv/cloud-nine-wallet/docker-compose.yml index 8188985253..f6ceeb6883 100644 --- a/localenv/cloud-nine-wallet/docker-compose.yml +++ b/localenv/cloud-nine-wallet/docker-compose.yml @@ -124,21 +124,6 @@ services: depends_on: - cloud-nine-backend - #Serves as example for optional local collector - # cloud-nine-otel-collector: - # image: otel/opentelemetry-collector-contrib:latest - # command: ["--config=/etc/otel-collector-config.yaml", ""] - # environment: - # - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID-''} - # - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY-''} - # volumes: - # - ../collector/otel-collector-config.yaml:/etc/otel-collector-config.yaml - # networks: - # - rafiki - # expose: - # - 4317 - # ports: - # - "13133:13133" # health_check extension volumes: database-data: # named volumes can be managed easier using docker-compose diff --git a/localenv/collector/otel-collector-config.yaml b/localenv/collector/otel-collector-config.yaml deleted file mode 100644 index 3f969a44b3..0000000000 --- a/localenv/collector/otel-collector-config.yaml +++ /dev/null @@ -1,42 +0,0 @@ -#Serves as example for the configuration of a local OpenTelemetry Collector that sends metrics to an AWS Managed Prometheus Workspace -#Sigv4auth required for AWS Prometheus Remote Write access (USER with accesss keys needed) -extensions: - sigv4auth: - assume_role: - arn: 'arn:aws:iam::YOUR-ROLE:role/PrometheusRemoteWrite' - sts_region: 'YOUR-REGION' - -receivers: - otlp: - protocols: - grpc: - http: - cors: - allowed_origins: - - http://* - - https://* - -processors: - batch: - -exporters: - logging: - verbosity: 'normal' - prometheusremotewrite: - endpoint: 'https://aps-workspaces.YOUR-REGION.amazonaws.com/workspaces/ws-YOUR-WORKSPACE-IDENTIFIER/api/v1/remote_write' - auth: - authenticator: sigv4auth - -service: - telemetry: - logs: - level: 'debug' - metrics: - level: 'detailed' - address: 0.0.0.0:8888 - extensions: [sigv4auth] - pipelines: - metrics: - receivers: [otlp] - processors: [batch] - exporters: [logging, prometheusremotewrite] diff --git a/localenv/happy-life-bank/docker-compose.yml b/localenv/happy-life-bank/docker-compose.yml index 2c25be00f1..72bd4d3148 100644 --- a/localenv/happy-life-bank/docker-compose.yml +++ b/localenv/happy-life-bank/docker-compose.yml @@ -95,23 +95,6 @@ services: - cloud-nine-admin - happy-life-backend - #Serves as example for optional local collector configuration - # happy-life-otel-collector: - # image: otel/opentelemetry-collector-contrib:latest - # command: ["--config=/etc/otel-collector-config.yaml", ""] - # environment: - # - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID-''} - # - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY-''} - # volumes: - # - ../collector/otel-collector-config.yaml:/etc/otel-collector-config.yaml - # networks: - # - rafiki - # expose: - # - 4317 - # ports: - # - "13132:13133" # health_check extension - - networks: rafiki: external: true diff --git a/packages/documentation/src/content/docs/telemetry/integrating.md b/packages/documentation/src/content/docs/telemetry/integrating.md new file mode 100644 index 0000000000..1e150ebafe --- /dev/null +++ b/packages/documentation/src/content/docs/telemetry/integrating.md @@ -0,0 +1,88 @@ +--- +title: Building on Top of Rafiki Metrics +--- + +Rafiki allows for integrating [Account Servicing Entities](/reference/glossary#account-servicing-entity) (ASE) to build their own telemetry solution based on the [OpenTelemetry](https://opentelemetry.io/) standardized metrics format that Rafiki exposes. + +In order to do so, the integrating ASE must deploy its own OpenTelemetry collector that should act as a sidecar container to Rafiki. It needs to provide the OpenTelemetry collector's ingest endpoint so that Rafiki can start sending metrics to it. + +## Rafiki Telemetry Environment Variables + +- `ENABLE_TELEMETRY`: boolean, defaults to `true`. +- `OPEN_TELEMETRY_COLLECTOR_URL`: CSV of URLs (e.g., `http://otel-collector-NLB-e3172ff9d2f4bc8a.elb.eu-west-2.amazonaws.com:4317,http://happy-life-otel-collector:4317`). +- `OPEN_TELEMETRY_EXPORT_INTERVAL`: number in milliseconds, defaults to `15000`. +- `TELEMETRY_EXCHANGE_RATES_URL`: string URL, defaults to `https://telemetry-exchange-rates.s3.amazonaws.com/exchange-rates-usd.json`. If set, the response format of the external exchange rates API should be of type Rates, as the rates service expects. + +## Example Docker OpentTelemetry Collector Image and Configuration + +Example of Docker OpenTelemetry Collector image and configuration that integrates with Rafiki and sends data to a Prometheus remote write endpoint: + +(it can be tested in our [Local Playground](/playground/overview) setup, by also providing the environment variables listed above to happy-life-backend in the [docker-compose](https://github.com/interledger/rafiki/blob/main/localenv/happy-life-bank/docker-compose.yml)) + +#### Docker-compose config: + +```yaml +#Serves as example for optional local collector configuration +happy-life-otel-collector: + image: otel/opentelemetry-collector-contrib:latest + command: ['--config=/etc/otel-collector-config.yaml', ''] + environment: + - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID-''} + - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY-''} + volumes: + - ../collector/otel-collector-config.yaml:/etc/otel-collector-config.yaml + networks: + - rafiki + expose: + - 4317 + ports: + - '13132:13133' # health_check extension +``` + +#### OpenTelemetry OTEL collector config: + +```yaml +# Serves as example for the configuration of a local OpenTelemetry Collector that sends metrics to an AWS Managed Prometheus Workspace +# Sigv4auth required for AWS Prometheus Remote Write access (USER with access keys needed) + +extensions: + sigv4auth: + assume_role: + arn: 'arn:aws:iam::YOUR-ROLE:role/PrometheusRemoteWrite' + sts_region: 'YOUR-REGION' + +receivers: + otlp: + protocols: + grpc: + http: + cors: + allowed*origins: + - http://* + - https://\_ + +processors: + batch: + +exporters: + logging: + verbosity: 'normal' + prometheusremotewrite: + endpoint: 'https://aps-workspaces.YOUR-REGION.amazonaws.com/workspaces/ws-YOUR-WORKSPACE-IDENTIFIER/api/v1/remote_write' + auth: + authenticator: sigv4auth + +service: + telemetry: + logs: + level: 'debug' + metrics: + level: 'detailed' + address: 0.0.0.0:8888 + extensions: [sigv4auth] + pipelines: + metrics: + receivers: [otlp] + processors: [batch] + exporters: [logging, prometheusremotewrite] +``` From eb7e189021f09460b5cb8c4d16be9aac5ed7f868 Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Thu, 1 Feb 2024 17:41:28 +0200 Subject: [PATCH 42/67] feat(telemetry): logger DI - add BaseAccountingServiceDependencies interface test updates doc updates --- .../backend/src/accounting/psql/service.ts | 4 +-- packages/backend/src/accounting/service.ts | 18 ++++++++---- .../src/accounting/tigerbeetle/service.ts | 4 +-- .../payment/outgoing/lifecycle.ts | 3 +- .../backend/src/telemetry/privacy.test.ts | 21 +++++++++++++- .../backend/src/telemetry/service.test.ts | 7 +++-- packages/backend/src/telemetry/service.ts | 28 +++++++++++++++---- .../src/telemetry/transaction-amount.test.ts | 16 ++++++----- .../src/telemetry/transaction-amount.ts | 21 ++++++++------ packages/backend/src/tests/telemetry.ts | 6 +--- .../src/content/docs/telemetry/integrating.md | 15 ++++++---- .../src/content/docs/telemetry/overview.md | 2 +- 12 files changed, 93 insertions(+), 52 deletions(-) diff --git a/packages/backend/src/accounting/psql/service.ts b/packages/backend/src/accounting/psql/service.ts index f67ce7e810..1ce9371787 100644 --- a/packages/backend/src/accounting/psql/service.ts +++ b/packages/backend/src/accounting/psql/service.ts @@ -204,10 +204,8 @@ export async function createTransfer( deps: ServiceDependencies, args: TransferOptions ): Promise { - return createAccountToAccountTransfer({ - telemetry: deps.telemetry, + return createAccountToAccountTransfer(deps, { transferArgs: args, - withdrawalThrottleDelay: deps.withdrawalThrottleDelay, voidTransfers: async (transferRefs) => voidTransfers(deps, transferRefs), postTransfers: async (transferRefs) => postTransfers(deps, transferRefs), getAccountReceived: async (accountRef) => diff --git a/packages/backend/src/accounting/service.ts b/packages/backend/src/accounting/service.ts index 028a9a13bf..6be1a82bd9 100644 --- a/packages/backend/src/accounting/service.ts +++ b/packages/backend/src/accounting/service.ts @@ -1,4 +1,5 @@ import { TransactionOrKnex } from 'objection' +import { BaseService } from '../shared/baseService' import { TelemetryService } from '../telemetry/service' import { collectTelemetryAmount } from '../telemetry/transaction-amount' import { TransferError, isTransferError } from './errors' @@ -92,8 +93,12 @@ export interface TransferToCreate { ledger: number } -interface CreateAccountToAccountTransferArgs { +export interface BaseAccountingServiceDependencies extends BaseService { telemetry?: TelemetryService + withdrawalThrottleDelay?: number +} + +interface CreateAccountToAccountTransferArgs { transferArgs: TransferOptions voidTransfers(transferIds: string[]): Promise postTransfers(transferIds: string[]): Promise @@ -102,10 +107,10 @@ interface CreateAccountToAccountTransferArgs { createPendingTransfers( transfers: TransferToCreate[] ): Promise - withdrawalThrottleDelay?: number } export async function createAccountToAccountTransfer( + deps: BaseAccountingServiceDependencies, args: CreateAccountToAccountTransferArgs ): Promise { const { @@ -114,11 +119,11 @@ export async function createAccountToAccountTransfer( createPendingTransfers, getAccountReceived, getAccountBalance, - withdrawalThrottleDelay, - transferArgs, - telemetry + transferArgs } = args + const { withdrawalThrottleDelay, telemetry, logger } = deps + const { sourceAccount, destinationAccount, sourceAmount, destinationAmount } = transferArgs @@ -192,13 +197,14 @@ export async function createAccountToAccountTransfer( withdrawalThrottleDelay }) } + if ( destinationAccount.onDebit && telemetry && sourceAccount.asset.code && sourceAccount.asset.scale ) { - collectTelemetryAmount(telemetry, { + collectTelemetryAmount(telemetry, logger, { amount: sourceAmount, asset: { code: sourceAccount.asset.code, diff --git a/packages/backend/src/accounting/tigerbeetle/service.ts b/packages/backend/src/accounting/tigerbeetle/service.ts index 603e82bee2..d06ead22bb 100644 --- a/packages/backend/src/accounting/tigerbeetle/service.ts +++ b/packages/backend/src/accounting/tigerbeetle/service.ts @@ -217,10 +217,8 @@ export async function createTransfer( deps: ServiceDependencies, args: TransferOptions ): Promise { - return createAccountToAccountTransfer({ - telemetry: deps.telemetry, + return createAccountToAccountTransfer(deps, { transferArgs: args, - withdrawalThrottleDelay: deps.withdrawalThrottleDelay, voidTransfers: async (transferIds) => { const error = await createTransfers( deps, diff --git a/packages/backend/src/open_payments/payment/outgoing/lifecycle.ts b/packages/backend/src/open_payments/payment/outgoing/lifecycle.ts index a9d613f827..f3e5d83475 100644 --- a/packages/backend/src/open_payments/payment/outgoing/lifecycle.ts +++ b/packages/backend/src/open_payments/payment/outgoing/lifecycle.ts @@ -80,9 +80,8 @@ export async function handleSending( finalDebitAmount: maxDebitAmount, finalReceiveAmount: maxReceiveAmount }) - deps.telemetry - ?.getOrCreate('transactions_total', { + ?.getOrCreateMetric('transactions_total', { description: 'Count of funded transactions' }) .add(1, { diff --git a/packages/backend/src/telemetry/privacy.test.ts b/packages/backend/src/telemetry/privacy.test.ts index 31b8416ebe..a1b9bea3dc 100644 --- a/packages/backend/src/telemetry/privacy.test.ts +++ b/packages/backend/src/telemetry/privacy.test.ts @@ -16,12 +16,32 @@ describe('Privacy functions', () => { expect(noise1).not.toBe(noise2) }) + test('test laplace distributuin math', () => { + const scale = 0.5 + jest.spyOn(Math, 'random').mockReturnValueOnce(0.25) + const noise1 = privacy.generateLaplaceNoise(scale) + + jest.spyOn(Math, 'random').mockReturnValueOnce(0.75) + const noise2 = privacy.generateLaplaceNoise(scale) + + expect(noise1).toBe(-0.34657359027997264) + expect(noise2).toBe(0.34657359027997264) + + jest.spyOn(Math, 'random').mockRestore() + }) + test('computePrivacyParameter should return 0 when sensitivity is 0', () => { const sensitivity = 0 const privacyParameter = privacy.computePrivacyParameter(sensitivity) expect(privacyParameter).toBe(0) }) + test('computePrivacyParameter should return a non-zero value when sensitivity is non-zero', () => { + const sensitivity = 1 + const privacyParameter = privacy.computePrivacyParameter(sensitivity) + expect(privacyParameter).not.toBe(0) + }) + test('roundValue should return minBucketSize when rawValue is very small', () => { const rawValue = 10 const bucketSize = 1000 @@ -41,7 +61,6 @@ describe('Privacy functions', () => { const bucketSize = 1000 const roundedValue = privacy.roundValue(rawValue, bucketSize, clipParams) expect(roundedValue).toBeGreaterThanOrEqual(clipParams.minBucketSize) - console.log(roundedValue) expect(roundedValue).toBeLessThanOrEqual(clipParams.maxBucketSize) }) diff --git a/packages/backend/src/telemetry/service.test.ts b/packages/backend/src/telemetry/service.test.ts index 5427f9e807..1adf38b843 100644 --- a/packages/backend/src/telemetry/service.test.ts +++ b/packages/backend/src/telemetry/service.test.ts @@ -4,13 +4,14 @@ import { MockTelemetryService, mockCounter } from '../tests/telemetry' const telemetryService = new MockTelemetryService() describe('TelemetryServiceImpl', () => { it('should create a counter when getOrCreate is called for a new metric', () => { - const counter = telemetryService.getOrCreate('testMetric') + const counter = telemetryService.getOrCreateMetric('testMetric') expect(counter).toBe(mockCounter) }) it('should return an existing counter when getOrCreate is called for an existing metric', () => { - const existingCounter = telemetryService.getOrCreate('existingMetric') - const retrievedCounter = telemetryService.getOrCreate('existingMetric') + const existingCounter = telemetryService.getOrCreateMetric('existingMetric') + const retrievedCounter = + telemetryService.getOrCreateMetric('existingMetric') expect(retrievedCounter).toBe(existingCounter) }) diff --git a/packages/backend/src/telemetry/service.ts b/packages/backend/src/telemetry/service.ts index 5b5803c8b2..8085471d8d 100644 --- a/packages/backend/src/telemetry/service.ts +++ b/packages/backend/src/telemetry/service.ts @@ -18,12 +18,12 @@ import { ConvertOptions } from '../rates/util' import { BaseService } from '../shared/baseService' export interface TelemetryService { - getOrCreate(name: string, options?: MetricOptions): Counter + getOrCreateMetric(name: string, options?: MetricOptions): Counter getServiceName(): string | undefined getBaseAssetCode(): string getBaseScale(): number convertAmount( - convertOptions: Omit + convertOptions: Omit ): Promise } @@ -96,7 +96,7 @@ class TelemetryServiceImpl implements TelemetryService { return counter } - public getOrCreate(name: string, options?: MetricOptions): Counter { + public getOrCreateMetric(name: string, options?: MetricOptions): Counter { const existing = this.counters.get(name) if (existing) { return existing @@ -107,11 +107,27 @@ class TelemetryServiceImpl implements TelemetryService { public async convertAmount( convertOptions: Omit ) { - let converted = await this.aseRatesService.convert(convertOptions) + const destinationAsset = { + code: this.deps.baseAssetCode, + scale: this.deps.baseScale + } + + let converted = await this.aseRatesService.convert({ + ...convertOptions, + destinationAsset + }) if (typeof converted !== 'bigint' && converted in ConvertError) { - converted = await this.fallbackRatesService.convert(convertOptions) + this.deps.logger.error( + `Unable to convert amount from provided rates: ${converted}` + ) + converted = await this.fallbackRatesService.convert({ + ...convertOptions, + destinationAsset + }) if (typeof converted !== 'bigint' && converted in ConvertError) { - this.deps.logger.error(`Unable to convert amount: ${converted}`) + this.deps.logger.error( + `Unable to convert amount from fallback rates: ${converted}` + ) } } return converted diff --git a/packages/backend/src/telemetry/transaction-amount.test.ts b/packages/backend/src/telemetry/transaction-amount.test.ts index cfbdd830e9..cb4d73e357 100644 --- a/packages/backend/src/telemetry/transaction-amount.test.ts +++ b/packages/backend/src/telemetry/transaction-amount.test.ts @@ -4,8 +4,10 @@ import { Asset } from '../rates/util' import { MockTelemetryService } from '../tests/telemetry' import { privacy } from './privacy' import { collectTelemetryAmount } from './transaction-amount' +import { Logger } from 'pino' const telemetryService = new MockTelemetryService() +const mockLogger = { error: jest.fn() } as unknown as Logger const asset: Asset = { code: 'USD', scale: 2 } @@ -18,14 +20,14 @@ describe('Telemetry Amount Collection', function () { ) const addSpy = jest.spyOn( - telemetryService.getOrCreate('transactions_amount', { + telemetryService.getOrCreateMetric('transactions_amount', { description: 'Amount sent through the network', valueType: ValueType.DOUBLE }), 'add' ) - await collectTelemetryAmount(telemetryService, { + await collectTelemetryAmount(telemetryService, mockLogger, { amount: 100n, asset }) @@ -40,7 +42,7 @@ describe('Telemetry Amount Collection', function () { Promise.resolve(ConvertError.InvalidDestinationPrice) ) - await collectTelemetryAmount(telemetryService, { + await collectTelemetryAmount(telemetryService, mockLogger, { amount: 0n, asset }) @@ -53,7 +55,7 @@ describe('Telemetry Amount Collection', function () { .spyOn(telemetryService, 'convertAmount') .mockImplementation(() => Promise.resolve(10000n)) const addSpy = jest.spyOn( - telemetryService.getOrCreate('transactions_amount', { + telemetryService.getOrCreateMetric('transactions_amount', { description: 'Amount sent through the network', valueType: ValueType.DOUBLE }), @@ -61,7 +63,7 @@ describe('Telemetry Amount Collection', function () { ) jest.spyOn(privacy, 'applyPrivacy').mockReturnValue(12000) - await collectTelemetryAmount(telemetryService, { + await collectTelemetryAmount(telemetryService, mockLogger, { amount: 100n, asset }) @@ -78,14 +80,14 @@ describe('Telemetry Amount Collection', function () { .spyOn(privacy, 'applyPrivacy') .mockReturnValue(12000) const addSpy = jest.spyOn( - telemetryService.getOrCreate('transactions_amount', { + telemetryService.getOrCreateMetric('transactions_amount', { description: 'Amount sent through the network', valueType: ValueType.DOUBLE }), 'add' ) - await collectTelemetryAmount(telemetryService, { + await collectTelemetryAmount(telemetryService, mockLogger, { amount: 100n, asset }) diff --git a/packages/backend/src/telemetry/transaction-amount.ts b/packages/backend/src/telemetry/transaction-amount.ts index c46561d5d0..bbf53ef945 100644 --- a/packages/backend/src/telemetry/transaction-amount.ts +++ b/packages/backend/src/telemetry/transaction-amount.ts @@ -1,24 +1,25 @@ import { ValueType } from '@opentelemetry/api' import { ConvertError } from '../rates/service' -import { Asset } from '../rates/util' +import { Asset, ConvertOptions } from '../rates/util' import { privacy } from './privacy' import { TelemetryService } from './service' +import { Logger } from 'pino' export async function collectTelemetryAmount( telemetryService: TelemetryService, + logger: Logger, { amount, asset }: { amount: bigint; asset: Asset } ) { if (!amount) { return } - const convertOptions = { + const convertOptions: Omit< + ConvertOptions, + 'exchangeRate' | 'destinationAsset' + > = { sourceAmount: amount, - sourceAsset: { code: asset.code, scale: asset.scale }, - destinationAsset: { - code: telemetryService.getBaseAssetCode(), - scale: telemetryService.getBaseScale() - } + sourceAsset: { code: asset.code, scale: asset.scale } } try { @@ -28,12 +29,14 @@ export async function collectTelemetryAmount( } telemetryService - .getOrCreate('transactions_amount', { + .getOrCreateMetric('transactions_amount', { description: 'Amount sent through the network', valueType: ValueType.DOUBLE }) .add(privacy.applyPrivacy(Number(converted))) + + console.log('AFTER TELEMEGTRY AMOUNT', converted) } catch (e) { - console.error(`Unable to collect telemetry`, e) + logger.error(e, `Unable to collect telemetry`) } } diff --git a/packages/backend/src/tests/telemetry.ts b/packages/backend/src/tests/telemetry.ts index 450542c71f..536db8bd12 100644 --- a/packages/backend/src/tests/telemetry.ts +++ b/packages/backend/src/tests/telemetry.ts @@ -30,7 +30,7 @@ export class MockRatesService implements RatesService { export class MockTelemetryService implements TelemetryService { public aseRatesService = new MockRatesService() public fallbackRatesService = new MockRatesService() - public getOrCreate( + public getOrCreateMetric( _name: string, _options?: MetricOptions | undefined ): Counter { @@ -57,8 +57,4 @@ export class MockTelemetryService implements TelemetryService { public getBaseScale(): number { return 4 } - - // public applyPrivacy(rawValue: number): number { - // return rawValue + Math.random() * 100 - // } } diff --git a/packages/documentation/src/content/docs/telemetry/integrating.md b/packages/documentation/src/content/docs/telemetry/integrating.md index 1e150ebafe..a4cfd512b0 100644 --- a/packages/documentation/src/content/docs/telemetry/integrating.md +++ b/packages/documentation/src/content/docs/telemetry/integrating.md @@ -1,5 +1,5 @@ --- -title: Building on Top of Rafiki Metrics +title: Deploying Custom Telemetry Service --- Rafiki allows for integrating [Account Servicing Entities](/reference/glossary#account-servicing-entity) (ASE) to build their own telemetry solution based on the [OpenTelemetry](https://opentelemetry.io/) standardized metrics format that Rafiki exposes. @@ -8,12 +8,13 @@ In order to do so, the integrating ASE must deploy its own OpenTelemetry collect ## Rafiki Telemetry Environment Variables -- `ENABLE_TELEMETRY`: boolean, defaults to `true`. -- `OPEN_TELEMETRY_COLLECTOR_URL`: CSV of URLs (e.g., `http://otel-collector-NLB-e3172ff9d2f4bc8a.elb.eu-west-2.amazonaws.com:4317,http://happy-life-otel-collector:4317`). -- `OPEN_TELEMETRY_EXPORT_INTERVAL`: number in milliseconds, defaults to `15000`. -- `TELEMETRY_EXCHANGE_RATES_URL`: string URL, defaults to `https://telemetry-exchange-rates.s3.amazonaws.com/exchange-rates-usd.json`. If set, the response format of the external exchange rates API should be of type Rates, as the rates service expects. +- `ENABLE_TELEMETRY`: boolean, defaults to `true`. Enables the telemetry service on Rafiki. +- `OPEN_TELEMETRY_COLLECTOR_URL`: CSV of URLs for Open Telemetry collectors (e.g., `http://otel-collector-NLB-e3172ff9d2f4bc8a.elb.eu-west-2.amazonaws.com:4317,http://happy-life-otel-collector:4317`). +- `OPEN_TELEMETRY_EXPORT_INTERVAL`: number in milliseconds, defaults to `15000`. Defines how often the instrumented Rafiki instance should send metrics. +- `TELEMETRY_EXCHANGE_RATES_URL`: string URL, defaults to `https://telemetry-exchange-rates.s3.amazonaws.com/exchange-rates-usd.json`. It defines the endpoint that Rafiki will query for exchange rates, as a fallback when ASE does not provide them. If set, the response format of the external exchange rates API should be of type Rates, as the rates service expects. + The default endpoint set here points to a public S3 that has the previously mentioned required format, updated daily. -## Example Docker OpentTelemetry Collector Image and Configuration +## Example Docker OpenTelemetry Collector Image and Configuration Example of Docker OpenTelemetry Collector image and configuration that integrates with Rafiki and sends data to a Prometheus remote write endpoint: @@ -41,6 +42,8 @@ happy-life-otel-collector: #### OpenTelemetry OTEL collector config: +[OTEL Collector config docs](https://opentelemetry.io/docs/collector/configuration/) + ```yaml # Serves as example for the configuration of a local OpenTelemetry Collector that sends metrics to an AWS Managed Prometheus Workspace # Sigv4auth required for AWS Prometheus Remote Write access (USER with access keys needed) diff --git a/packages/documentation/src/content/docs/telemetry/overview.md b/packages/documentation/src/content/docs/telemetry/overview.md index 9942208332..a57999b044 100644 --- a/packages/documentation/src/content/docs/telemetry/overview.md +++ b/packages/documentation/src/content/docs/telemetry/overview.md @@ -24,7 +24,7 @@ The architecture of the telemetry feature is illustrated below: ![Telemetry architecture](/img/telemetry-architecture.png) -## Opentelemetry +## OpenTelemetry We have adopted [OpenTelemetry](https://opentelemetry.io/) to ensure compliance with a standardized framework that is compatible with a variety of tool suites. This allows clients to use their preferred tools for data analysis, while Rafiki is instrumented and observable through a standardized metrics format. From 8f9ec363edfadcea238aed1561b790e524638aa2 Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Thu, 1 Feb 2024 17:49:34 +0200 Subject: [PATCH 43/67] feat(telemetry): telemetry optional in the AppServices interface --- packages/backend/src/app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/app.ts b/packages/backend/src/app.ts index 8dfc47d325..95f83a7e37 100644 --- a/packages/backend/src/app.ts +++ b/packages/backend/src/app.ts @@ -204,7 +204,7 @@ const WALLET_ADDRESS_PATH = '/:walletAddressPath+' export interface AppServices { logger: Promise - telemetry: Promise + telemetry?: Promise knex: Promise axios: Promise config: Promise From 08967174e77a8239121529abb8cb6685cafa2fce Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Thu, 1 Feb 2024 17:53:52 +0200 Subject: [PATCH 44/67] docs(telemetry): remove collection implementation details --- packages/documentation/src/content/docs/telemetry/overview.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/documentation/src/content/docs/telemetry/overview.md b/packages/documentation/src/content/docs/telemetry/overview.md index a57999b044..c6784ff2a8 100644 --- a/packages/documentation/src/content/docs/telemetry/overview.md +++ b/packages/documentation/src/content/docs/telemetry/overview.md @@ -67,10 +67,8 @@ Currently collected metrics: - `transactions_total` - Counter metric - Description: “Count of funded transactions” - This counter metric increases by 1 for each successfully sent transaction. - - It is collected in the open-payments module, outgoing payment service. - `transactions_amount` - Counter metric - Description: “Amount sent through the network”. - This amount metric increases by the amount sent in each ILP packet. - - It is collected inside the ILP connector core, by a new telemetry middleware. **Note**: The current implementation only collects metrics on the SENDING side of a transaction. Metrics for external open-payments transactions RECEIVED by a Rafiki instance in the network are not collected. From 7232209f09034b4875e65c31472f3b4f0bbebe2c Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Fri, 2 Feb 2024 11:47:41 +0200 Subject: [PATCH 45/67] feat(telemetry): cleanup --- packages/backend/src/accounting/service.ts | 7 ++--- .../payment/outgoing/lifecycle.ts | 2 +- packages/backend/src/rates/service.ts | 4 +++ .../backend/src/telemetry/privacy.test.ts | 11 +------ .../backend/src/telemetry/service.test.ts | 2 +- packages/backend/src/telemetry/service.ts | 31 +++++++++---------- .../src/telemetry/transaction-amount.ts | 2 -- packages/backend/src/tests/telemetry.ts | 2 +- 8 files changed, 26 insertions(+), 35 deletions(-) diff --git a/packages/backend/src/accounting/service.ts b/packages/backend/src/accounting/service.ts index 6be1a82bd9..a7c0e18400 100644 --- a/packages/backend/src/accounting/service.ts +++ b/packages/backend/src/accounting/service.ts @@ -12,18 +12,17 @@ export enum LiquidityAccountType { WEB_MONETIZATION = 'WEB_MONETIZATION' } -export type LiquidityAccountAsset = { +export interface LiquidityAccountAsset { id: string code?: string scale?: number ledger: number + onDebit?: (options: OnDebitOptions) => Promise } export interface LiquidityAccount { id: string - asset: LiquidityAccountAsset & { - onDebit?: (options: OnDebitOptions) => Promise - } + asset: LiquidityAccountAsset onCredit?: (options: OnCreditOptions) => Promise onDebit?: (options: OnDebitOptions) => Promise } diff --git a/packages/backend/src/open_payments/payment/outgoing/lifecycle.ts b/packages/backend/src/open_payments/payment/outgoing/lifecycle.ts index f3e5d83475..2109731139 100644 --- a/packages/backend/src/open_payments/payment/outgoing/lifecycle.ts +++ b/packages/backend/src/open_payments/payment/outgoing/lifecycle.ts @@ -85,7 +85,7 @@ export async function handleSending( description: 'Count of funded transactions' }) .add(1, { - source: deps.telemetry.getServiceName() + source: deps.telemetry.getInstanceName() }) await handleCompleted(deps, payment) diff --git a/packages/backend/src/rates/service.ts b/packages/backend/src/rates/service.ts index 150b246376..5a6ed7322e 100644 --- a/packages/backend/src/rates/service.ts +++ b/packages/backend/src/rates/service.ts @@ -29,6 +29,10 @@ export enum ConvertError { InvalidDestinationPrice = 'InvalidDestinationPrice' } +// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types +export const isConvertError = (o: any): o is ConvertError => + Object.values(ConvertError).includes(o) + export function createRatesService(deps: ServiceDependencies): RatesService { return new RatesServiceImpl(deps) } diff --git a/packages/backend/src/telemetry/privacy.test.ts b/packages/backend/src/telemetry/privacy.test.ts index a1b9bea3dc..cd1e531b60 100644 --- a/packages/backend/src/telemetry/privacy.test.ts +++ b/packages/backend/src/telemetry/privacy.test.ts @@ -9,13 +9,6 @@ describe('Privacy functions', () => { jest.unmock('./privacy') }) - test('generateLaplaceNoise should return a different number each time', () => { - const scale = 0.5 - const noise1 = privacy.generateLaplaceNoise(scale) - const noise2 = privacy.generateLaplaceNoise(scale) - expect(noise1).not.toBe(noise2) - }) - test('test laplace distributuin math', () => { const scale = 0.5 jest.spyOn(Math, 'random').mockReturnValueOnce(0.25) @@ -26,8 +19,6 @@ describe('Privacy functions', () => { expect(noise1).toBe(-0.34657359027997264) expect(noise2).toBe(0.34657359027997264) - - jest.spyOn(Math, 'random').mockRestore() }) test('computePrivacyParameter should return 0 when sensitivity is 0', () => { @@ -39,7 +30,7 @@ describe('Privacy functions', () => { test('computePrivacyParameter should return a non-zero value when sensitivity is non-zero', () => { const sensitivity = 1 const privacyParameter = privacy.computePrivacyParameter(sensitivity) - expect(privacyParameter).not.toBe(0) + expect(privacyParameter).toBe(0.1) }) test('roundValue should return minBucketSize when rawValue is very small', () => { diff --git a/packages/backend/src/telemetry/service.test.ts b/packages/backend/src/telemetry/service.test.ts index 1adf38b843..1c520719be 100644 --- a/packages/backend/src/telemetry/service.test.ts +++ b/packages/backend/src/telemetry/service.test.ts @@ -16,7 +16,7 @@ describe('TelemetryServiceImpl', () => { }) it('should return the instance name when calling getServiceName', () => { - const serviceName = telemetryService.getServiceName() + const serviceName = telemetryService.getInstanceName() expect(serviceName).toBe('serviceName') }) diff --git a/packages/backend/src/telemetry/service.ts b/packages/backend/src/telemetry/service.ts index 8085471d8d..53bed30210 100644 --- a/packages/backend/src/telemetry/service.ts +++ b/packages/backend/src/telemetry/service.ts @@ -13,13 +13,13 @@ import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics' -import { ConvertError, RatesService } from '../rates/service' +import { ConvertError, RatesService, isConvertError } from '../rates/service' import { ConvertOptions } from '../rates/util' import { BaseService } from '../shared/baseService' export interface TelemetryService { getOrCreateMetric(name: string, options?: MetricOptions): Counter - getServiceName(): string | undefined + getInstanceName(): string | undefined getBaseAssetCode(): string getBaseScale(): number convertAmount( @@ -28,7 +28,7 @@ export interface TelemetryService { } interface TelemetryServiceDependencies extends BaseService { - serviceName: string + instanceName: string collectorUrls: string[] exportIntervalMillis?: number aseRatesService: RatesService @@ -37,6 +37,9 @@ interface TelemetryServiceDependencies extends BaseService { baseScale: number } +const METER_NAME = 'Rafiki' +const SERVICE_NAME = 'RAFIKI_NETWORK' + export function createTelemetryService( deps: TelemetryServiceDependencies ): TelemetryService { @@ -44,7 +47,7 @@ export function createTelemetryService( } class TelemetryServiceImpl implements TelemetryService { - private serviceName: string + private instanceName: string private meterProvider?: MeterProvider private fallbackRatesService: RatesService private aseRatesService: RatesService @@ -52,15 +55,11 @@ class TelemetryServiceImpl implements TelemetryService { private counters = new Map() constructor(private deps: TelemetryServiceDependencies) { diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.DEBUG) - this.serviceName = deps.serviceName + this.instanceName = deps.instanceName this.fallbackRatesService = deps.fallbackRatesService this.aseRatesService = deps.aseRatesService - if ( - deps.collectorUrls && - Array.isArray(deps.collectorUrls) && - deps.collectorUrls.length === 0 - ) { + if (deps.collectorUrls.length === 0) { deps.logger.info( 'No collector URLs specified, metrics will not be exported' ) @@ -68,7 +67,7 @@ class TelemetryServiceImpl implements TelemetryService { } this.meterProvider = new MeterProvider({ - resource: new Resource({ 'service.name': 'RAFIKI_NETWORK' }) + resource: new Resource({ 'service.name': SERVICE_NAME }) }) deps.collectorUrls.forEach((url) => { @@ -91,7 +90,7 @@ class TelemetryServiceImpl implements TelemetryService { name: string, options: MetricOptions | undefined ): Counter { - const counter = metrics.getMeter('Rafiki').createCounter(name, options) + const counter = metrics.getMeter(METER_NAME).createCounter(name, options) this.counters.set(name, counter) return counter } @@ -116,7 +115,7 @@ class TelemetryServiceImpl implements TelemetryService { ...convertOptions, destinationAsset }) - if (typeof converted !== 'bigint' && converted in ConvertError) { + if (isConvertError(converted)) { this.deps.logger.error( `Unable to convert amount from provided rates: ${converted}` ) @@ -124,7 +123,7 @@ class TelemetryServiceImpl implements TelemetryService { ...convertOptions, destinationAsset }) - if (typeof converted !== 'bigint' && converted in ConvertError) { + if (isConvertError(converted)) { this.deps.logger.error( `Unable to convert amount from fallback rates: ${converted}` ) @@ -133,8 +132,8 @@ class TelemetryServiceImpl implements TelemetryService { return converted } - public getServiceName(): string | undefined { - return this.serviceName + public getInstanceName(): string | undefined { + return this.instanceName } getBaseAssetCode(): string { diff --git a/packages/backend/src/telemetry/transaction-amount.ts b/packages/backend/src/telemetry/transaction-amount.ts index bbf53ef945..c07700ca8f 100644 --- a/packages/backend/src/telemetry/transaction-amount.ts +++ b/packages/backend/src/telemetry/transaction-amount.ts @@ -34,8 +34,6 @@ export async function collectTelemetryAmount( valueType: ValueType.DOUBLE }) .add(privacy.applyPrivacy(Number(converted))) - - console.log('AFTER TELEMEGTRY AMOUNT', converted) } catch (e) { logger.error(e, `Unable to collect telemetry`) } diff --git a/packages/backend/src/tests/telemetry.ts b/packages/backend/src/tests/telemetry.ts index 536db8bd12..af8e5853c4 100644 --- a/packages/backend/src/tests/telemetry.ts +++ b/packages/backend/src/tests/telemetry.ts @@ -36,7 +36,7 @@ export class MockTelemetryService implements TelemetryService { ): Counter { return mockCounter } - public getServiceName(): string | undefined { + public getInstanceName(): string | undefined { return 'serviceName' } From bf092bb692792f9125ea1c82c7d1107bb57549a1 Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Fri, 2 Feb 2024 14:46:21 +0200 Subject: [PATCH 46/67] feat(telemetry):testing- telemetry service tests now properly mock otel dependencies and tests the actual service. This meant moving the fallbackRatesService as an AppService, no longer proprietary to telemetry, and just having it injected in telemetry. --- packages/backend/src/app.ts | 1 + packages/backend/src/index.ts | 16 +++-- .../backend/src/telemetry/service.test.ts | 68 ++++++++++++++++--- packages/backend/src/tests/app.ts | 23 +++---- 4 files changed, 79 insertions(+), 29 deletions(-) diff --git a/packages/backend/src/app.ts b/packages/backend/src/app.ts index 95f83a7e37..eb1601baa5 100644 --- a/packages/backend/src/app.ts +++ b/packages/backend/src/app.ts @@ -205,6 +205,7 @@ const WALLET_ADDRESS_PATH = '/:walletAddressPath+' export interface AppServices { logger: Promise telemetry?: Promise + fallbackRatesService?: Promise knex: Promise axios: Promise config: Promise diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index aaff2d07cc..b0cee01660 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -139,17 +139,21 @@ export function initIocContainer( }) if (config.enableTelemetry) { + container.singleton('fallbackRatesService', async (deps) => { + return createRatesService({ + logger: await deps.use('logger'), + exchangeRatesUrl: config.telemetryExchangeRatesUrl, + exchangeRatesLifetime: config.telemetryExchangeRatesLifetime + }) + }) + container.singleton('telemetry', async (deps) => { const config = await deps.use('config') return createTelemetryService({ logger: await deps.use('logger'), aseRatesService: await deps.use('ratesService'), - fallbackRatesService: createRatesService({ - logger: await deps.use('logger'), - exchangeRatesUrl: config.telemetryExchangeRatesUrl, - exchangeRatesLifetime: config.telemetryExchangeRatesLifetime - }), - serviceName: config.instanceName, + fallbackRatesService: await deps.use('fallbackRatesService')!, + instanceName: config.instanceName, collectorUrls: config.openTelemetryCollectors, exportIntervalMillis: config.openTelemetryExportInterval, baseAssetCode: 'USD', diff --git a/packages/backend/src/telemetry/service.test.ts b/packages/backend/src/telemetry/service.test.ts index 1c520719be..8576faf3a8 100644 --- a/packages/backend/src/telemetry/service.test.ts +++ b/packages/backend/src/telemetry/service.test.ts @@ -1,8 +1,59 @@ -import { ConvertError } from '../rates/service' -import { MockTelemetryService, mockCounter } from '../tests/telemetry' +import { IocContract } from '@adonisjs/fold' +import { initIocContainer } from '..' +import { AppServices } from '../app' +import { Config } from '../config/app' +import { ConvertError, RatesService } from '../rates/service' +import { TestContainer, createTestApp } from '../tests/app' +import { mockCounter } from '../tests/telemetry' +import { TelemetryService } from './service' + +jest.mock('@opentelemetry/api', () => ({ + ...jest.requireActual('@opentelemetry/api'), + metrics: { + setGlobalMeterProvider: jest.fn(), + getMeter: jest.fn().mockReturnValue({ + createCounter: jest.fn().mockImplementation(() => mockCounter) + }) + } +})) + +jest.mock('@opentelemetry/exporter-metrics-otlp-grpc', () => ({ + OTLPMetricExporter: jest.fn().mockImplementation(() => ({})) +})) + +jest.mock('@opentelemetry/sdk-metrics', () => ({ + MeterProvider: jest.fn().mockImplementation(() => ({ + addMetricReader: jest.fn() + })), + PeriodicExportingMetricReader: jest.fn().mockImplementation(() => ({})) +})) -const telemetryService = new MockTelemetryService() describe('TelemetryServiceImpl', () => { + let deps: IocContract + let appContainer: TestContainer + let telemetryService: TelemetryService + let aseRatesService: RatesService + let fallbackRatesService: RatesService + + beforeAll(async (): Promise => { + deps = initIocContainer({ + ...Config, + enableTelemetry: true, + telemetryExchangeRatesUrl: 'http://example-rates.com', + telemetryExchangeRatesLifetime: 100, + openTelemetryCollectors: ['http://example-collector.com'] + }) + + appContainer = await createTestApp(deps) + telemetryService = await deps.use('telemetry')! + aseRatesService = await deps.use('ratesService')! + fallbackRatesService = await deps.use('fallbackRatesService')! + }) + + afterAll(async (): Promise => { + await appContainer.shutdown() + }) + it('should create a counter when getOrCreate is called for a new metric', () => { const counter = telemetryService.getOrCreateMetric('testMetric') expect(counter).toBe(mockCounter) @@ -15,28 +66,27 @@ describe('TelemetryServiceImpl', () => { expect(retrievedCounter).toBe(existingCounter) }) - it('should return the instance name when calling getServiceName', () => { + it('should return the instance name when calling getInstanceName', () => { const serviceName = telemetryService.getInstanceName() - expect(serviceName).toBe('serviceName') + expect(serviceName).toBe('Rafiki') }) describe('conversion', () => { it('should try to convert using aseRatesService and fallback to fallbackRatesService', async () => { const aseConvertSpy = jest - .spyOn(telemetryService.aseRatesService, 'convert') + .spyOn(aseRatesService, 'convert') .mockImplementation(() => Promise.resolve(ConvertError.InvalidDestinationPrice) ) const fallbackConvertSpy = jest - .spyOn(telemetryService.fallbackRatesService, 'convert') + .spyOn(fallbackRatesService, 'convert') .mockImplementation(() => Promise.resolve(10000n)) const converted = await telemetryService.convertAmount({ sourceAmount: 100n, - sourceAsset: { code: 'USD', scale: 2 }, - destinationAsset: { code: 'USD', scale: 2 } + sourceAsset: { code: 'USD', scale: 2 } }) expect(aseConvertSpy).toHaveBeenCalled() diff --git a/packages/backend/src/tests/app.ts b/packages/backend/src/tests/app.ts index 1c38dd451d..11a4e38a1b 100644 --- a/packages/backend/src/tests/app.ts +++ b/packages/backend/src/tests/app.ts @@ -1,22 +1,21 @@ -import Axios from 'axios' -import createLogger from 'pino' -import { Knex } from 'knex' -import nock from 'nock' -import fetch from 'cross-fetch' import { IocContract } from '@adonisjs/fold' import { ApolloClient, + ApolloLink, InMemoryCache, NormalizedCacheObject, - createHttpLink, - ApolloLink + createHttpLink } from '@apollo/client' -import { onError } from '@apollo/client/link/error' import { setContext } from '@apollo/client/link/context' +import { onError } from '@apollo/client/link/error' +import Axios from 'axios' +import fetch from 'cross-fetch' +import { Knex } from 'knex' +import nock from 'nock' +import createLogger from 'pino' -import { start, gracefulShutdown } from '..' +import { gracefulShutdown, start } from '..' import { App, AppServices } from '../app' -import { MockTelemetryService } from './telemetry' export const testAccessToken = 'test-app-access' export interface TestContainer { @@ -54,10 +53,6 @@ export const createTestApp = async ( container.bind('logger', async () => logger) - container.bind('telemetry', async () => { - return new MockTelemetryService() - }) - const app = new App(container) await start(container, app) From 8d3872659eaa356afa3bc661efa933351adb8f15 Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Fri, 2 Feb 2024 15:15:54 +0200 Subject: [PATCH 47/67] feat(telemetry): opt out of otel sdk periodic logging on periodic exports --- packages/backend/src/telemetry/service.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/backend/src/telemetry/service.ts b/packages/backend/src/telemetry/service.ts index 53bed30210..5f37c9762f 100644 --- a/packages/backend/src/telemetry/service.ts +++ b/packages/backend/src/telemetry/service.ts @@ -1,11 +1,4 @@ -import { - Counter, - DiagConsoleLogger, - DiagLogLevel, - MetricOptions, - diag, - metrics -} from '@opentelemetry/api' +import { Counter, MetricOptions, metrics } from '@opentelemetry/api' import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-grpc' import { Resource } from '@opentelemetry/resources' import { @@ -54,7 +47,8 @@ class TelemetryServiceImpl implements TelemetryService { private counters = new Map() constructor(private deps: TelemetryServiceDependencies) { - diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.DEBUG) + // debug logger: + // diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.DEBUG) this.instanceName = deps.instanceName this.fallbackRatesService = deps.fallbackRatesService this.aseRatesService = deps.aseRatesService From b4dec155046074622d97690860fb9bd84abd5bde Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Fri, 2 Feb 2024 15:41:21 +0200 Subject: [PATCH 48/67] feat(telemetry): meter provider shutdown exposed in telemetryService interface, and called in the gracefull shutdown as to avoid leaks in tests --- packages/backend/src/index.ts | 5 +++++ packages/backend/src/telemetry/service.test.ts | 3 +++ packages/backend/src/telemetry/service.ts | 5 +++++ packages/backend/src/tests/telemetry.ts | 3 +++ 4 files changed, 16 insertions(+) diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index b0cee01660..168f681c39 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -508,6 +508,11 @@ export const gracefulShutdown = async ( tigerbeetle.destroy() const redis = await container.use('redis') redis.disconnect() + + const telemetry = await container.use('telemetry') + if (telemetry) { + telemetry.shutdown() + } } export const start = async ( diff --git a/packages/backend/src/telemetry/service.test.ts b/packages/backend/src/telemetry/service.test.ts index 8576faf3a8..776e479458 100644 --- a/packages/backend/src/telemetry/service.test.ts +++ b/packages/backend/src/telemetry/service.test.ts @@ -21,8 +21,11 @@ jest.mock('@opentelemetry/exporter-metrics-otlp-grpc', () => ({ OTLPMetricExporter: jest.fn().mockImplementation(() => ({})) })) +jest.mock('@opentelemetry/resources', () => ({ Resource: jest.fn() })) + jest.mock('@opentelemetry/sdk-metrics', () => ({ MeterProvider: jest.fn().mockImplementation(() => ({ + shutdown: jest.fn(), addMetricReader: jest.fn() })), PeriodicExportingMetricReader: jest.fn().mockImplementation(() => ({})) diff --git a/packages/backend/src/telemetry/service.ts b/packages/backend/src/telemetry/service.ts index 5f37c9762f..78fde244c6 100644 --- a/packages/backend/src/telemetry/service.ts +++ b/packages/backend/src/telemetry/service.ts @@ -11,6 +11,7 @@ import { ConvertOptions } from '../rates/util' import { BaseService } from '../shared/baseService' export interface TelemetryService { + shutdown(): void getOrCreateMetric(name: string, options?: MetricOptions): Counter getInstanceName(): string | undefined getBaseAssetCode(): string @@ -80,6 +81,10 @@ class TelemetryServiceImpl implements TelemetryService { metrics.setGlobalMeterProvider(this.meterProvider) } + public shutdown(): void { + this.meterProvider?.shutdown() + } + private createCounter( name: string, options: MetricOptions | undefined diff --git a/packages/backend/src/tests/telemetry.ts b/packages/backend/src/tests/telemetry.ts index af8e5853c4..11d535ae66 100644 --- a/packages/backend/src/tests/telemetry.ts +++ b/packages/backend/src/tests/telemetry.ts @@ -39,6 +39,9 @@ export class MockTelemetryService implements TelemetryService { public getInstanceName(): string | undefined { return 'serviceName' } + public shutdown(): void { + console.log('telemetry service shutdown') + } public async convertAmount( _convertOptions: Omit From 1e3e678405122cc1136cb236023446e9f1d872cf Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Fri, 2 Feb 2024 18:14:27 +0200 Subject: [PATCH 49/67] feat(telemetry): tests now provide empty list of collector url's, so that no metric exporters are actually instantiated --- packages/backend/src/telemetry/service.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/telemetry/service.test.ts b/packages/backend/src/telemetry/service.test.ts index 776e479458..8d0c344be6 100644 --- a/packages/backend/src/telemetry/service.test.ts +++ b/packages/backend/src/telemetry/service.test.ts @@ -44,7 +44,7 @@ describe('TelemetryServiceImpl', () => { enableTelemetry: true, telemetryExchangeRatesUrl: 'http://example-rates.com', telemetryExchangeRatesLifetime: 100, - openTelemetryCollectors: ['http://example-collector.com'] + openTelemetryCollectors: [] }) appContainer = await createTestApp(deps) From 8b90d53c2298663b6a94b99638a3baeef09165e7 Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Fri, 2 Feb 2024 18:19:32 +0200 Subject: [PATCH 50/67] feat(telemetry): tests now no longer need to mock exporters, as they are not created when collectorUrl list is empty --- packages/backend/src/telemetry/service.test.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/backend/src/telemetry/service.test.ts b/packages/backend/src/telemetry/service.test.ts index 8d0c344be6..eb44168ff4 100644 --- a/packages/backend/src/telemetry/service.test.ts +++ b/packages/backend/src/telemetry/service.test.ts @@ -17,18 +17,13 @@ jest.mock('@opentelemetry/api', () => ({ } })) -jest.mock('@opentelemetry/exporter-metrics-otlp-grpc', () => ({ - OTLPMetricExporter: jest.fn().mockImplementation(() => ({})) -})) - jest.mock('@opentelemetry/resources', () => ({ Resource: jest.fn() })) jest.mock('@opentelemetry/sdk-metrics', () => ({ MeterProvider: jest.fn().mockImplementation(() => ({ shutdown: jest.fn(), addMetricReader: jest.fn() - })), - PeriodicExportingMetricReader: jest.fn().mockImplementation(() => ({})) + })) })) describe('TelemetryServiceImpl', () => { From ed1f86b5256cd79628b872da7f6bd290c847d02f Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Sun, 4 Feb 2024 18:06:09 +0200 Subject: [PATCH 51/67] feat(telemetry): code review cleanup --- packages/backend/src/index.ts | 2 +- packages/backend/src/telemetry/service.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 168f681c39..966153fb7e 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -511,7 +511,7 @@ export const gracefulShutdown = async ( const telemetry = await container.use('telemetry') if (telemetry) { - telemetry.shutdown() + await telemetry.shutdown() } } diff --git a/packages/backend/src/telemetry/service.ts b/packages/backend/src/telemetry/service.ts index 78fde244c6..e3b10e0c8d 100644 --- a/packages/backend/src/telemetry/service.ts +++ b/packages/backend/src/telemetry/service.ts @@ -81,8 +81,8 @@ class TelemetryServiceImpl implements TelemetryService { metrics.setGlobalMeterProvider(this.meterProvider) } - public shutdown(): void { - this.meterProvider?.shutdown() + public async shutdown(): Promise { + await this.meterProvider?.shutdown() } private createCounter( From ac83b255c8e76fb7c1c1695645457c47bd2faa42 Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Tue, 6 Feb 2024 16:15:42 +0200 Subject: [PATCH 52/67] feat(telemetry): docs updates --- packages/documentation/astro.config.mjs | 11 ++++++++--- .../src/content/docs/telemetry/integrating.md | 2 +- .../src/content/docs/telemetry/overview.md | 2 +- .../src/content/docs/telemetry/privacy.md | 6 +++--- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/packages/documentation/astro.config.mjs b/packages/documentation/astro.config.mjs index 84ab7bc79c..164b7f9343 100644 --- a/packages/documentation/astro.config.mjs +++ b/packages/documentation/astro.config.mjs @@ -146,9 +146,14 @@ export default defineConfig({ { label: 'Telemetry', collapsed: true, - autogenerate: { - directory: 'telemetry' - } + items: [ + { label: 'Overview', link: 'telemetry/overview' }, + { label: 'Privacy', link: 'telemetry/privacy' }, + { + label: 'Deploying Custom Telemetry', + link: 'telemetry/integrating' + } + ] }, { label: 'Local Playground', diff --git a/packages/documentation/src/content/docs/telemetry/integrating.md b/packages/documentation/src/content/docs/telemetry/integrating.md index a4cfd512b0..a9a0db2798 100644 --- a/packages/documentation/src/content/docs/telemetry/integrating.md +++ b/packages/documentation/src/content/docs/telemetry/integrating.md @@ -11,7 +11,7 @@ In order to do so, the integrating ASE must deploy its own OpenTelemetry collect - `ENABLE_TELEMETRY`: boolean, defaults to `true`. Enables the telemetry service on Rafiki. - `OPEN_TELEMETRY_COLLECTOR_URL`: CSV of URLs for Open Telemetry collectors (e.g., `http://otel-collector-NLB-e3172ff9d2f4bc8a.elb.eu-west-2.amazonaws.com:4317,http://happy-life-otel-collector:4317`). - `OPEN_TELEMETRY_EXPORT_INTERVAL`: number in milliseconds, defaults to `15000`. Defines how often the instrumented Rafiki instance should send metrics. -- `TELEMETRY_EXCHANGE_RATES_URL`: string URL, defaults to `https://telemetry-exchange-rates.s3.amazonaws.com/exchange-rates-usd.json`. It defines the endpoint that Rafiki will query for exchange rates, as a fallback when ASE does not provide them. If set, the response format of the external exchange rates API should be of type Rates, as the rates service expects. +- `TELEMETRY_EXCHANGE_RATES_URL`: string URL, defaults to `https://telemetry-exchange-rates.s3.amazonaws.com/exchange-rates-usd.json`. It defines the endpoint that Rafiki will query for exchange rates, as a fallback when ASE does not [provide them](../integration/getting-started.md#exchange-rates). If set, the response format of the external exchange rates API should be of type Rates, as the rates service expects. The default endpoint set here points to a public S3 that has the previously mentioned required format, updated daily. ## Example Docker OpenTelemetry Collector Image and Configuration diff --git a/packages/documentation/src/content/docs/telemetry/overview.md b/packages/documentation/src/content/docs/telemetry/overview.md index c6784ff2a8..852ab45529 100644 --- a/packages/documentation/src/content/docs/telemetry/overview.md +++ b/packages/documentation/src/content/docs/telemetry/overview.md @@ -10,7 +10,7 @@ The objective of the telemetry feature is to gather metrics and establish an inf - The number of transactions from outgoing payments that have been at least partially successful. - The average amount of money held within the network per transaction. -Our goal is to use these data for our own insights and to enable [Account Servicing Entities](/reference/glossary#account-servicing-entity) (ASEs) to gain their own insights. We aim to track the growth of the network in terms of transaction sizes and the number of transactions processed. +We aim to track the growth of the network in terms of transaction sizes and the number of transactions processed. Our goal is to use these data for our own insights and to enable [Account Servicing Entities](/reference/glossary#account-servicing-entity) (ASEs) to gain their own insights. ## Privacy and Optionality diff --git a/packages/documentation/src/content/docs/telemetry/privacy.md b/packages/documentation/src/content/docs/telemetry/privacy.md index e04e9fb4f7..9ed42c6c82 100644 --- a/packages/documentation/src/content/docs/telemetry/privacy.md +++ b/packages/documentation/src/content/docs/telemetry/privacy.md @@ -2,11 +2,11 @@ title: Privacy --- -Rafiki telemetry is designed with a strong emphasis on privacy. The system anonymizes user data and refrains from collecting identifiable information. Since transactions can originate from any user to a Rafiki instance, the privacy measures are implemented at the Rafiki instance level in the network. This means that at the individual level, the data is already anonymous as single Rafiki instances service transactions for multiple users. +Rafiki telemetry is designed with a strong emphasis on privacy. The system anonymizes user data and refrains from collecting identifiable information. Since transactions can originate from any user to a Rafiki instance, the privacy measures are implemented directly at the source (each Rafiki instance). This means that at the individual level, the data is already anonymous as single Rafiki instances service transactions for multiple users. ## Differential Privacy and Local Differential Privacy -Differential Privacy is a system for publicly sharing information about a dataset by describing the patterns of groups within the dataset while withholding information about individuals in the dataset. Local Differential Privacy (LDP) is a variant of differential privacy where noise is added to each individual's data before it is sent to the server. This ensures that the server never sees the actual data, providing a strong privacy guarantee. +Differential Privacy is a system for publicly sharing information about a dataset by describing the patterns of groups within the dataset while withholding information about individuals in the dataset. Local Differential Privacy (LDP) is a variant of differential privacy where noise is added to each individual's data point before it is sent to the server. This ensures that the server never sees the actual data, providing a strong privacy guarantee. ## Rounding Technique and Bucketing @@ -30,7 +30,7 @@ The noise, selected from the Laplacian distribution, is then generated using thi ## Currency Conversion -Another factor that obscures sensitive data is currency conversion. In cross-currency transactions, we use exchange rates that are not traced back. The rates are provided by the integrating [ASE](/reference/glossary#account-servicing-entity) at runtime and are not persisted. If the needed rates are not provided, external API's for exchange rates are used. The obtained exchange rates are overwritten frequently in this case, with no versioning or history access. This introduces an additional layer of noise and further protects the privacy of the transactions. +Another factor that obscures sensitive data is currency conversion. In cross-currency transactions, exchange rates are provided by [ASE](/reference/glossary#account-servicing-entity) internally. As such, they cannot be correlated to an individual transaction. If the necessary rates are not provided or not available from the ASE, an external API for exchange rates is used. The obtained exchange rates are overwritten frequently in this case, with no versioning or history access. This introduces an additional layer of noise and further protects the privacy of the transactions. ## References From 7e09fb3c68bf35280ede95e1fbae620a23b7adff Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Thu, 8 Feb 2024 12:59:56 +0200 Subject: [PATCH 53/67] feat(telemetry): docs improvements --- .../documentation/src/content/docs/telemetry/integrating.md | 2 +- .../documentation/src/content/docs/telemetry/overview.md | 6 +++--- .../documentation/src/content/docs/telemetry/privacy.md | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/documentation/src/content/docs/telemetry/integrating.md b/packages/documentation/src/content/docs/telemetry/integrating.md index a9a0db2798..a8a9c74301 100644 --- a/packages/documentation/src/content/docs/telemetry/integrating.md +++ b/packages/documentation/src/content/docs/telemetry/integrating.md @@ -11,7 +11,7 @@ In order to do so, the integrating ASE must deploy its own OpenTelemetry collect - `ENABLE_TELEMETRY`: boolean, defaults to `true`. Enables the telemetry service on Rafiki. - `OPEN_TELEMETRY_COLLECTOR_URL`: CSV of URLs for Open Telemetry collectors (e.g., `http://otel-collector-NLB-e3172ff9d2f4bc8a.elb.eu-west-2.amazonaws.com:4317,http://happy-life-otel-collector:4317`). - `OPEN_TELEMETRY_EXPORT_INTERVAL`: number in milliseconds, defaults to `15000`. Defines how often the instrumented Rafiki instance should send metrics. -- `TELEMETRY_EXCHANGE_RATES_URL`: string URL, defaults to `https://telemetry-exchange-rates.s3.amazonaws.com/exchange-rates-usd.json`. It defines the endpoint that Rafiki will query for exchange rates, as a fallback when ASE does not [provide them](../integration/getting-started.md#exchange-rates). If set, the response format of the external exchange rates API should be of type Rates, as the rates service expects. +- `TELEMETRY_EXCHANGE_RATES_URL`: string URL, defaults to `https://telemetry-exchange-rates.s3.amazonaws.com/exchange-rates-usd.json`. It defines the endpoint that Rafiki will query for exchange rates, as a fallback when ASE does not [provide them](../integration/getting-started#exchange-rates). If set, the response format of the external exchange rates API should be of type Rates, as the rates service expects. The default endpoint set here points to a public S3 that has the previously mentioned required format, updated daily. ## Example Docker OpenTelemetry Collector Image and Configuration diff --git a/packages/documentation/src/content/docs/telemetry/overview.md b/packages/documentation/src/content/docs/telemetry/overview.md index 852ab45529..ee7cdc844e 100644 --- a/packages/documentation/src/content/docs/telemetry/overview.md +++ b/packages/documentation/src/content/docs/telemetry/overview.md @@ -4,7 +4,7 @@ title: Overview ## Purpose -The objective of the telemetry feature is to gather metrics and establish an infrastructure for visualizing valuable network insights. The metrics we collect include: +The objective of the telemetry feature is to gather metrics and establish an infrastructure for visualizing valuable network insights. The metrics we at the Interledger Foundation (interledger.org) collect include: - The total amount of money transferred via packet data within a specified time frame (daily, weekly, monthly). - The number of transactions from outgoing payments that have been at least partially successful. @@ -32,7 +32,7 @@ We have adopted [OpenTelemetry](https://opentelemetry.io/) to ensure compliance The Telemetry Replica service is hosted on AWS ECS Fargate and is configured for availability and load balancing of custom ADOT (AWS Distro for Opentelemetry) Collector ECS tasks. -When ASEs opt for telemetry, metrics are sent to our Telemetry Service. To enable ASEs to build their own telemetry solutions, instrumented Rafiki can send data to multiple endpoints. This allows the integration of a local Otel collector container that can support custom requirements. Metrics communication is facilitated through GRPC. +When ASEs opt for telemetry, metrics are sent to our Telemetry Service. To enable ASEs to build their own telemetry solutions, instrumented Rafiki can send data to multiple endpoints. This allows the integration of a local [Otel collector](https://opentelemetry.io/docs/collector/) container that can support custom requirements. Metrics communication is facilitated through [gRPC](https://grpc.io/). ## Otel SDK - Rafiki Instrumentation @@ -40,7 +40,7 @@ The Opentelemetry SDK is integrated into Rafiki to create, collect, and export m ## Prometheus - AMP -We use Amazon's managed service for Prometheus to collect data from the Telemetry cluster. +We use Amazon's managed service for Prometheus (AMP) to collect data from the Telemetry cluster. **Note**: AMP offers limited configuration options and cannot crawl data outside of AWS. This limitation led us to adopt a push model, using prometheusRemoteWrite, instead of a pull model. For future development, we may consider hosting our own Prometheus. diff --git a/packages/documentation/src/content/docs/telemetry/privacy.md b/packages/documentation/src/content/docs/telemetry/privacy.md index 9ed42c6c82..403f55fc28 100644 --- a/packages/documentation/src/content/docs/telemetry/privacy.md +++ b/packages/documentation/src/content/docs/telemetry/privacy.md @@ -30,13 +30,13 @@ The noise, selected from the Laplacian distribution, is then generated using thi ## Currency Conversion -Another factor that obscures sensitive data is currency conversion. In cross-currency transactions, exchange rates are provided by [ASE](/reference/glossary#account-servicing-entity) internally. As such, they cannot be correlated to an individual transaction. If the necessary rates are not provided or not available from the ASE, an external API for exchange rates is used. The obtained exchange rates are overwritten frequently in this case, with no versioning or history access. This introduces an additional layer of noise and further protects the privacy of the transactions. +Another factor that obscures sensitive data is currency conversion. In cross-currency transactions, exchange rates are provided by [ASEs](/reference/glossary#account-servicing-entity) internally. As such, they cannot be correlated to an individual transaction. If the necessary rates are not provided or not available from the ASE, an external API for exchange rates is used. The obtained exchange rates are overwritten frequently in this case, with no versioning or history access. This introduces an additional layer of noise and further protects the privacy of the transactions. ## References Rafiki's telemetry solution is a combination of techniques described in various white papers on privacy-preserving data collection. For more information, you can refer to the following papers: -- [Local Differential Privacy for Human-Centered Computing](https://proceedings.neurips.cc/paper_files/paper/2017/file/253614bbac999b38b5b60cae531c4969-Paper.pdf) +- [Local Differential Privacy for Human-Centered Computing](https://jwcn-eurasipjournals.springeropen.com/articles/10.1186/s13638-020-01675-8) - [Collecting Telemetry Data Privately](https://www.microsoft.com/en-us/research/blog/collecting-telemetry-data-privately/) - [Collecting Telemetry Data Privately - NeurIPS Publication](https://proceedings.neurips.cc/paper_files/paper/2017/file/253614bbac999b38b5b60cae531c4969-Paper.pdf) by Bolin Ding, Janardhan Kulkarni, Sergey Yekhanin from Microsoft Research. - [RAPPOR: Randomized Aggregatable Privacy-Preserving Ordinal Response](https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/42852.pdf) From 05e308bda1bf61e0f4efaf4f6907f8180f4aacae Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Thu, 8 Feb 2024 13:12:55 +0200 Subject: [PATCH 54/67] fix(telemetry):docs link --- .../documentation/src/content/docs/telemetry/integrating.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/documentation/src/content/docs/telemetry/integrating.md b/packages/documentation/src/content/docs/telemetry/integrating.md index a8a9c74301..e690f5edd6 100644 --- a/packages/documentation/src/content/docs/telemetry/integrating.md +++ b/packages/documentation/src/content/docs/telemetry/integrating.md @@ -11,7 +11,7 @@ In order to do so, the integrating ASE must deploy its own OpenTelemetry collect - `ENABLE_TELEMETRY`: boolean, defaults to `true`. Enables the telemetry service on Rafiki. - `OPEN_TELEMETRY_COLLECTOR_URL`: CSV of URLs for Open Telemetry collectors (e.g., `http://otel-collector-NLB-e3172ff9d2f4bc8a.elb.eu-west-2.amazonaws.com:4317,http://happy-life-otel-collector:4317`). - `OPEN_TELEMETRY_EXPORT_INTERVAL`: number in milliseconds, defaults to `15000`. Defines how often the instrumented Rafiki instance should send metrics. -- `TELEMETRY_EXCHANGE_RATES_URL`: string URL, defaults to `https://telemetry-exchange-rates.s3.amazonaws.com/exchange-rates-usd.json`. It defines the endpoint that Rafiki will query for exchange rates, as a fallback when ASE does not [provide them](../integration/getting-started#exchange-rates). If set, the response format of the external exchange rates API should be of type Rates, as the rates service expects. +- `TELEMETRY_EXCHANGE_RATES_URL`: string URL, defaults to `https://telemetry-exchange-rates.s3.amazonaws.com/exchange-rates-usd.json`. It defines the endpoint that Rafiki will query for exchange rates, as a fallback when ASE does not [provide them](../integration/getting-started/#exchange-rates). If set, the response format of the external exchange rates API should be of type Rates, as the rates service expects. The default endpoint set here points to a public S3 that has the previously mentioned required format, updated daily. ## Example Docker OpenTelemetry Collector Image and Configuration From a2d007bafadee83ec61988b6f8886c4208a85a0e Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Thu, 8 Feb 2024 13:16:41 +0200 Subject: [PATCH 55/67] feat(telemetry): docs change --- packages/documentation/src/content/docs/telemetry/overview.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/documentation/src/content/docs/telemetry/overview.md b/packages/documentation/src/content/docs/telemetry/overview.md index ee7cdc844e..d96cbdfe28 100644 --- a/packages/documentation/src/content/docs/telemetry/overview.md +++ b/packages/documentation/src/content/docs/telemetry/overview.md @@ -4,7 +4,7 @@ title: Overview ## Purpose -The objective of the telemetry feature is to gather metrics and establish an infrastructure for visualizing valuable network insights. The metrics we at the Interledger Foundation (interledger.org) collect include: +The objective of the telemetry feature is to gather metrics and establish an infrastructure for visualizing valuable network insights. The metrics we at the Interledger Foundation collect include: - The total amount of money transferred via packet data within a specified time frame (daily, weekly, monthly). - The number of transactions from outgoing payments that have been at least partially successful. From e6b06e569fd7bbb6160db523716bacf37834fe67 Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Mon, 12 Feb 2024 22:48:15 +0200 Subject: [PATCH 56/67] feat(telemetry): fix docs link --- .../documentation/src/content/docs/telemetry/integrating.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/documentation/src/content/docs/telemetry/integrating.md b/packages/documentation/src/content/docs/telemetry/integrating.md index e690f5edd6..870e25a726 100644 --- a/packages/documentation/src/content/docs/telemetry/integrating.md +++ b/packages/documentation/src/content/docs/telemetry/integrating.md @@ -11,7 +11,7 @@ In order to do so, the integrating ASE must deploy its own OpenTelemetry collect - `ENABLE_TELEMETRY`: boolean, defaults to `true`. Enables the telemetry service on Rafiki. - `OPEN_TELEMETRY_COLLECTOR_URL`: CSV of URLs for Open Telemetry collectors (e.g., `http://otel-collector-NLB-e3172ff9d2f4bc8a.elb.eu-west-2.amazonaws.com:4317,http://happy-life-otel-collector:4317`). - `OPEN_TELEMETRY_EXPORT_INTERVAL`: number in milliseconds, defaults to `15000`. Defines how often the instrumented Rafiki instance should send metrics. -- `TELEMETRY_EXCHANGE_RATES_URL`: string URL, defaults to `https://telemetry-exchange-rates.s3.amazonaws.com/exchange-rates-usd.json`. It defines the endpoint that Rafiki will query for exchange rates, as a fallback when ASE does not [provide them](../integration/getting-started/#exchange-rates). If set, the response format of the external exchange rates API should be of type Rates, as the rates service expects. +- `TELEMETRY_EXCHANGE_RATES_URL`: string URL, defaults to `https://telemetry-exchange-rates.s3.amazonaws.com/exchange-rates-usd.json`. It defines the endpoint that Rafiki will query for exchange rates, as a fallback when ASE does not [provide them](/integration/getting-started/#exchange-rates). If set, the response format of the external exchange rates API should be of type Rates, as the rates service expects. The default endpoint set here points to a public S3 that has the previously mentioned required format, updated daily. ## Example Docker OpenTelemetry Collector Image and Configuration From 08d8efad329e3e2e2ed443f8824941baaecf1de8 Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Tue, 13 Feb 2024 15:43:08 +0200 Subject: [PATCH 57/67] feat(telemetry): fix seed not having peeringAsset --- localenv/cloud-nine-wallet/seed.yml | 1 + localenv/happy-life-bank/seed.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/localenv/cloud-nine-wallet/seed.yml b/localenv/cloud-nine-wallet/seed.yml index c30eeb475f..763398e96c 100644 --- a/localenv/cloud-nine-wallet/seed.yml +++ b/localenv/cloud-nine-wallet/seed.yml @@ -19,6 +19,7 @@ assets: scale: 0 liquidity: 1000000 liquidityThreshold: 100000 +peeringAsset: 'USD' peers: - initialLiquidity: '10000000' peerUrl: http://happy-life-bank-backend:3002 diff --git a/localenv/happy-life-bank/seed.yml b/localenv/happy-life-bank/seed.yml index c3e147f2d1..1289d7cac8 100644 --- a/localenv/happy-life-bank/seed.yml +++ b/localenv/happy-life-bank/seed.yml @@ -19,6 +19,7 @@ assets: scale: 0 liquidity: 1000000000 liquidityThreshold: 1000000 +peeringAsset: 'USD' peers: - initialLiquidity: '1000000000000' peerUrl: http://cloud-nine-wallet-backend:3002 From 482aa6764e91fef8b8d29d248febcdfc0ae60653 Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Wed, 14 Feb 2024 11:59:53 +0200 Subject: [PATCH 58/67] feat(telemetry): remove unused aseRatesService --- packages/backend/src/accounting/psql/service.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/backend/src/accounting/psql/service.ts b/packages/backend/src/accounting/psql/service.ts index 1ce9371787..3a64820ea6 100644 --- a/packages/backend/src/accounting/psql/service.ts +++ b/packages/backend/src/accounting/psql/service.ts @@ -2,7 +2,6 @@ import { TransactionOrKnex } from 'objection' import { v4 as uuid } from 'uuid' import { Asset } from '../../asset/model' -import { RatesService } from '../../rates/service' import { BaseService } from '../../shared/baseService' import { TelemetryService } from '../../telemetry/service' import { isTransferError, TransferError } from '../errors' @@ -38,7 +37,6 @@ import { LedgerTransfer, LedgerTransferType } from './ledger-transfer/model' export interface ServiceDependencies extends BaseService { telemetry?: TelemetryService - aseRatesService?: RatesService knex: TransactionOrKnex withdrawalThrottleDelay?: number } From a91c5d43ecd165838c42c2cbf2e67792e9ed68a3 Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Wed, 14 Feb 2024 12:03:25 +0200 Subject: [PATCH 59/67] feat(telemetry): rename fallbackRates to internalRates --- packages/backend/src/app.ts | 2 +- packages/backend/src/index.ts | 4 ++-- packages/backend/src/telemetry/service.test.ts | 12 ++++++------ packages/backend/src/telemetry/service.ts | 10 +++++----- packages/backend/src/tests/telemetry.ts | 4 ++-- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/backend/src/app.ts b/packages/backend/src/app.ts index eb1601baa5..ed646ec5e8 100644 --- a/packages/backend/src/app.ts +++ b/packages/backend/src/app.ts @@ -205,7 +205,7 @@ const WALLET_ADDRESS_PATH = '/:walletAddressPath+' export interface AppServices { logger: Promise telemetry?: Promise - fallbackRatesService?: Promise + internalRatesService?: Promise knex: Promise axios: Promise config: Promise diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 966153fb7e..9bc108fe9b 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -139,7 +139,7 @@ export function initIocContainer( }) if (config.enableTelemetry) { - container.singleton('fallbackRatesService', async (deps) => { + container.singleton('internalRatesService', async (deps) => { return createRatesService({ logger: await deps.use('logger'), exchangeRatesUrl: config.telemetryExchangeRatesUrl, @@ -152,7 +152,7 @@ export function initIocContainer( return createTelemetryService({ logger: await deps.use('logger'), aseRatesService: await deps.use('ratesService'), - fallbackRatesService: await deps.use('fallbackRatesService')!, + internalRatesService: await deps.use('internalRatesService')!, instanceName: config.instanceName, collectorUrls: config.openTelemetryCollectors, exportIntervalMillis: config.openTelemetryExportInterval, diff --git a/packages/backend/src/telemetry/service.test.ts b/packages/backend/src/telemetry/service.test.ts index eb44168ff4..97c41b98d3 100644 --- a/packages/backend/src/telemetry/service.test.ts +++ b/packages/backend/src/telemetry/service.test.ts @@ -31,7 +31,7 @@ describe('TelemetryServiceImpl', () => { let appContainer: TestContainer let telemetryService: TelemetryService let aseRatesService: RatesService - let fallbackRatesService: RatesService + let internalRatesService: RatesService beforeAll(async (): Promise => { deps = initIocContainer({ @@ -45,7 +45,7 @@ describe('TelemetryServiceImpl', () => { appContainer = await createTestApp(deps) telemetryService = await deps.use('telemetry')! aseRatesService = await deps.use('ratesService')! - fallbackRatesService = await deps.use('fallbackRatesService')! + internalRatesService = await deps.use('internalRatesService')! }) afterAll(async (): Promise => { @@ -71,15 +71,15 @@ describe('TelemetryServiceImpl', () => { }) describe('conversion', () => { - it('should try to convert using aseRatesService and fallback to fallbackRatesService', async () => { + it('should try to convert using aseRatesService and fallback to internalRatesService', async () => { const aseConvertSpy = jest .spyOn(aseRatesService, 'convert') .mockImplementation(() => Promise.resolve(ConvertError.InvalidDestinationPrice) ) - const fallbackConvertSpy = jest - .spyOn(fallbackRatesService, 'convert') + const internalConvertSpy = jest + .spyOn(internalRatesService, 'convert') .mockImplementation(() => Promise.resolve(10000n)) const converted = await telemetryService.convertAmount({ @@ -88,7 +88,7 @@ describe('TelemetryServiceImpl', () => { }) expect(aseConvertSpy).toHaveBeenCalled() - expect(fallbackConvertSpy).toHaveBeenCalled() + expect(internalConvertSpy).toHaveBeenCalled() expect(converted).toBe(10000n) }) }) diff --git a/packages/backend/src/telemetry/service.ts b/packages/backend/src/telemetry/service.ts index e3b10e0c8d..386b8628c8 100644 --- a/packages/backend/src/telemetry/service.ts +++ b/packages/backend/src/telemetry/service.ts @@ -26,7 +26,7 @@ interface TelemetryServiceDependencies extends BaseService { collectorUrls: string[] exportIntervalMillis?: number aseRatesService: RatesService - fallbackRatesService: RatesService + internalRatesService: RatesService baseAssetCode: string baseScale: number } @@ -43,7 +43,7 @@ export function createTelemetryService( class TelemetryServiceImpl implements TelemetryService { private instanceName: string private meterProvider?: MeterProvider - private fallbackRatesService: RatesService + private internalRatesService: RatesService private aseRatesService: RatesService private counters = new Map() @@ -51,7 +51,7 @@ class TelemetryServiceImpl implements TelemetryService { // debug logger: // diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.DEBUG) this.instanceName = deps.instanceName - this.fallbackRatesService = deps.fallbackRatesService + this.internalRatesService = deps.internalRatesService this.aseRatesService = deps.aseRatesService if (deps.collectorUrls.length === 0) { @@ -118,13 +118,13 @@ class TelemetryServiceImpl implements TelemetryService { this.deps.logger.error( `Unable to convert amount from provided rates: ${converted}` ) - converted = await this.fallbackRatesService.convert({ + converted = await this.internalRatesService.convert({ ...convertOptions, destinationAsset }) if (isConvertError(converted)) { this.deps.logger.error( - `Unable to convert amount from fallback rates: ${converted}` + `Unable to convert amount from internal rates: ${converted}` ) } } diff --git a/packages/backend/src/tests/telemetry.ts b/packages/backend/src/tests/telemetry.ts index 11d535ae66..1cf89e0676 100644 --- a/packages/backend/src/tests/telemetry.ts +++ b/packages/backend/src/tests/telemetry.ts @@ -29,7 +29,7 @@ export class MockRatesService implements RatesService { export class MockTelemetryService implements TelemetryService { public aseRatesService = new MockRatesService() - public fallbackRatesService = new MockRatesService() + public internalRatesService = new MockRatesService() public getOrCreateMetric( _name: string, _options?: MetricOptions | undefined @@ -48,7 +48,7 @@ export class MockTelemetryService implements TelemetryService { ): Promise { let converted = await this.aseRatesService.convert() if (typeof converted !== 'bigint' && converted in ConvertError) { - converted = await this.fallbackRatesService.convert() + converted = await this.internalRatesService.convert() } return Promise.resolve(converted) } From 409f2e1c59564049e63c8d6b541b2e5f98cbb14c Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Wed, 14 Feb 2024 17:38:40 +0200 Subject: [PATCH 60/67] feat(telemetry): added comments for privacy implementation + added a test --- packages/backend/src/telemetry/privacy.ts | 7 +++++++ packages/backend/src/telemetry/service.test.ts | 17 +++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/packages/backend/src/telemetry/privacy.ts b/packages/backend/src/telemetry/privacy.ts index 6f2d097123..14ccafe942 100644 --- a/packages/backend/src/telemetry/privacy.ts +++ b/packages/backend/src/telemetry/privacy.ts @@ -6,8 +6,15 @@ export type ClipParams = { export const privacy = { getBucketSize: function (rawValue: number, clip: ClipParams): number { const { minBucketSize, maxBucketSize } = clip + + // Base parameter is used in the logarithimc function for calculating the bucket size when the rawValue exceeds the threshold. + // Increasing the base would result in larger bucket sizes. const base = 2 + // The scale parameter is used in both the linear and logarithmic functions for calculating the bucket size. + // Increasing the scale would result in larger bucket sizes. const scale = 5000 + // Used to determine when to switch from linear to logarithmic function + // Increasing the threshold would result in smaller bucket sizes. const threshold = 20000 let bucketSize diff --git a/packages/backend/src/telemetry/service.test.ts b/packages/backend/src/telemetry/service.test.ts index 97c41b98d3..08616ee218 100644 --- a/packages/backend/src/telemetry/service.test.ts +++ b/packages/backend/src/telemetry/service.test.ts @@ -91,5 +91,22 @@ describe('TelemetryServiceImpl', () => { expect(internalConvertSpy).toHaveBeenCalled() expect(converted).toBe(10000n) }) + + it('should not call the fallback internalRatesService if aseRatesService call is successful', async () => { + const aseConvertSpy = jest + .spyOn(aseRatesService, 'convert') + .mockImplementation(() => Promise.resolve(500n)) + + const internalConvertSpy = jest.spyOn(internalRatesService, 'convert') + + const converted = await telemetryService.convertAmount({ + sourceAmount: 100n, + sourceAsset: { code: 'USD', scale: 2 } + }) + + expect(aseConvertSpy).toHaveBeenCalled() + expect(internalConvertSpy).not.toHaveBeenCalled() + expect(converted).toBe(500n) + }) }) }) From a7f0a3f3ae82e97524596c33e8c5206393c64eca Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Thu, 15 Feb 2024 17:18:49 +0200 Subject: [PATCH 61/67] feat(telemetry): remove testing clog --- packages/backend/src/tests/telemetry.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/backend/src/tests/telemetry.ts b/packages/backend/src/tests/telemetry.ts index 1cf89e0676..de7b204290 100644 --- a/packages/backend/src/tests/telemetry.ts +++ b/packages/backend/src/tests/telemetry.ts @@ -39,9 +39,7 @@ export class MockTelemetryService implements TelemetryService { public getInstanceName(): string | undefined { return 'serviceName' } - public shutdown(): void { - console.log('telemetry service shutdown') - } + public shutdown(): void {} public async convertAmount( _convertOptions: Omit From b59814ae15fd64a7be70dd46e21c689d270cee6e Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Mon, 19 Feb 2024 16:13:28 +0200 Subject: [PATCH 62/67] feaet(telemetry): remove telemetry from localenv --- localenv/cloud-nine-wallet/docker-compose.yml | 5 ----- localenv/happy-life-bank/docker-compose.yml | 7 ------- 2 files changed, 12 deletions(-) diff --git a/localenv/cloud-nine-wallet/docker-compose.yml b/localenv/cloud-nine-wallet/docker-compose.yml index f6ceeb6883..6602abe78f 100644 --- a/localenv/cloud-nine-wallet/docker-compose.yml +++ b/localenv/cloud-nine-wallet/docker-compose.yml @@ -60,11 +60,6 @@ services: REDIS_URL: redis://shared-redis:6379/0 WALLET_ADDRESS_URL: ${CLOUD_NINE_WALLET_ADDRESS_URL:-https://cloud-nine-wallet-backend/.well-known/pay} ILP_CONNECTOR_ADDRESS: ${CLOUD_NINE_CONNECTOR_URL} - ENABLE_TELEMETRY: true - # url is a list of csv. Example for including local collector: http://cloud-nine-otel-collector:4317,http://otel-collector-NLB-e3172ff9d2f4bc8a.elb.eu-west-2.amazonaws.com:4317 - OPEN_TELEMETRY_COLLECTOR_URL: http://otel-collector-NLB-e3172ff9d2f4bc8a.elb.eu-west-2.amazonaws.com:4317 - OPEN_TELEMETRY_EXPORT_INTERVAL: 15000 - TELEMETRY_EXCHANGE_RATES_URL: https://telemetry-exchange-rates.s3.amazonaws.com/exchange-rates-usd.json depends_on: - shared-database - shared-redis diff --git a/localenv/happy-life-bank/docker-compose.yml b/localenv/happy-life-bank/docker-compose.yml index 72bd4d3148..8c48266401 100644 --- a/localenv/happy-life-bank/docker-compose.yml +++ b/localenv/happy-life-bank/docker-compose.yml @@ -52,13 +52,6 @@ services: EXCHANGE_RATES_URL: http://happy-life-bank/rates REDIS_URL: redis://shared-redis:6379/1 WALLET_ADDRESS_URL: ${HAPPY_LIFE_BANK_WALLET_ADDRESS_URL:-https://happy-life-bank-backend/.well-known/pay} - ENABLE_TELEMETRY: true - # url is a list of csv. Example for including local collector: http://happy-life-otel-collector:4317,http://otel-collector-NLB-e3172ff9d2f4bc8a.elb.eu-west-2.amazonaws.com:4317 - OPEN_TELEMETRY_COLLECTOR_URL: http://otel-collector-NLB-e3172ff9d2f4bc8a.elb.eu-west-2.amazonaws.com:4317 - OPEN_TELEMETRY_EXPORT_INTERVAL: 15000 - TELEMETRY_EXCHANGE_RATES_URL: https://telemetry-exchange-rates.s3.amazonaws.com/exchange-rates-usd.json - - depends_on: - cloud-nine-backend happy-life-auth: From 3a9b8ffd9c154130f2cf4def91a816edd863742f Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Mon, 19 Feb 2024 16:14:38 +0200 Subject: [PATCH 63/67] feat(telemetry): enableTelemetry defaults to false --- packages/backend/src/config/app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/config/app.ts b/packages/backend/src/config/app.ts index 7a12804077..ce3f75bd8f 100644 --- a/packages/backend/src/config/app.ts +++ b/packages/backend/src/config/app.ts @@ -37,7 +37,7 @@ dotenv.config({ export const Config = { logLevel: envString('LOG_LEVEL', 'info'), - enableTelemetry: envBool('ENABLE_TELEMETRY', true), + enableTelemetry: envBool('ENABLE_TELEMETRY', false), openTelemetryCollectors: envStringArray('OPEN_TELEMETRY_COLLECTOR_URL', [ 'http://otel-collector-NLB-e3172ff9d2f4bc8a.elb.eu-west-2.amazonaws.com:4317' ]), From efe4c525a7a1ffa8050681cf4e9a4474a01306f5 Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Mon, 19 Feb 2024 16:21:39 +0200 Subject: [PATCH 64/67] feat(telemetry): turn default telemetry back on, but off on localenv --- localenv/cloud-nine-wallet/docker-compose.yml | 1 + localenv/happy-life-bank/docker-compose.yml | 1 + packages/backend/src/config/app.ts | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/localenv/cloud-nine-wallet/docker-compose.yml b/localenv/cloud-nine-wallet/docker-compose.yml index 6602abe78f..243fe66bca 100644 --- a/localenv/cloud-nine-wallet/docker-compose.yml +++ b/localenv/cloud-nine-wallet/docker-compose.yml @@ -60,6 +60,7 @@ services: REDIS_URL: redis://shared-redis:6379/0 WALLET_ADDRESS_URL: ${CLOUD_NINE_WALLET_ADDRESS_URL:-https://cloud-nine-wallet-backend/.well-known/pay} ILP_CONNECTOR_ADDRESS: ${CLOUD_NINE_CONNECTOR_URL} + ENABLE_TELEMETRY: false depends_on: - shared-database - shared-redis diff --git a/localenv/happy-life-bank/docker-compose.yml b/localenv/happy-life-bank/docker-compose.yml index 8c48266401..f5ebc53777 100644 --- a/localenv/happy-life-bank/docker-compose.yml +++ b/localenv/happy-life-bank/docker-compose.yml @@ -52,6 +52,7 @@ services: EXCHANGE_RATES_URL: http://happy-life-bank/rates REDIS_URL: redis://shared-redis:6379/1 WALLET_ADDRESS_URL: ${HAPPY_LIFE_BANK_WALLET_ADDRESS_URL:-https://happy-life-bank-backend/.well-known/pay} + ENABLE_TELEMETRY: false depends_on: - cloud-nine-backend happy-life-auth: diff --git a/packages/backend/src/config/app.ts b/packages/backend/src/config/app.ts index ce3f75bd8f..7a12804077 100644 --- a/packages/backend/src/config/app.ts +++ b/packages/backend/src/config/app.ts @@ -37,7 +37,7 @@ dotenv.config({ export const Config = { logLevel: envString('LOG_LEVEL', 'info'), - enableTelemetry: envBool('ENABLE_TELEMETRY', false), + enableTelemetry: envBool('ENABLE_TELEMETRY', true), openTelemetryCollectors: envStringArray('OPEN_TELEMETRY_COLLECTOR_URL', [ 'http://otel-collector-NLB-e3172ff9d2f4bc8a.elb.eu-west-2.amazonaws.com:4317' ]), From 395c86ae58d6a3791fe2da873c8de6b6728f18b3 Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Wed, 21 Feb 2024 15:37:14 +0200 Subject: [PATCH 65/67] feat(telemetry): add distinction between livenet and testnet --- packages/backend/src/config/app.ts | 12 +++++++++--- packages/backend/src/index.ts | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/backend/src/config/app.ts b/packages/backend/src/config/app.ts index 7a12804077..bb4a6dbe1b 100644 --- a/packages/backend/src/config/app.ts +++ b/packages/backend/src/config/app.ts @@ -38,9 +38,15 @@ dotenv.config({ export const Config = { logLevel: envString('LOG_LEVEL', 'info'), enableTelemetry: envBool('ENABLE_TELEMETRY', true), - openTelemetryCollectors: envStringArray('OPEN_TELEMETRY_COLLECTOR_URL', [ - 'http://otel-collector-NLB-e3172ff9d2f4bc8a.elb.eu-west-2.amazonaws.com:4317' - ]), + livenet: envBool('LIVENET', false), + openTelemetryCollectors: envStringArray( + 'OPEN_TELEMETRY_COLLECTOR_URL', + process.env.LIVENET + ? [] + : [ + 'http://otel-collector-NLB-e3172ff9d2f4bc8a.elb.eu-west-2.amazonaws.com:4317' + ] + ), openTelemetryExportInterval: envInt('OPEN_TELEMETRY_EXPORT_INTERVAL', 15000), telemetryExchangeRatesUrl: envString( 'TELEMETRY_EXCHANGE_RATES_URL', diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 9bc108fe9b..08a82a94d8 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -220,7 +220,7 @@ export function initIocContainer( const config = await deps.use('config') let telemetry: TelemetryService | undefined - if (config.enableTelemetry) { + if (config.enableTelemetry && config.openTelemetryCollectors.length > 0) { telemetry = await deps.use('telemetry') } From 1c759e3129395013217f80984ca51c54263ae4b3 Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Wed, 21 Feb 2024 15:41:30 +0200 Subject: [PATCH 66/67] feat(telemetry): update docs to new LIVENET env variable --- packages/documentation/src/content/docs/telemetry/integrating.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/documentation/src/content/docs/telemetry/integrating.md b/packages/documentation/src/content/docs/telemetry/integrating.md index 870e25a726..0752df25cb 100644 --- a/packages/documentation/src/content/docs/telemetry/integrating.md +++ b/packages/documentation/src/content/docs/telemetry/integrating.md @@ -9,6 +9,7 @@ In order to do so, the integrating ASE must deploy its own OpenTelemetry collect ## Rafiki Telemetry Environment Variables - `ENABLE_TELEMETRY`: boolean, defaults to `true`. Enables the telemetry service on Rafiki. +- `LIVENET`: boolean. Should be set to `true` on production environments. If it is not set, it will default to `false`, and metrics will get sent to the testnet otel-collector - `OPEN_TELEMETRY_COLLECTOR_URL`: CSV of URLs for Open Telemetry collectors (e.g., `http://otel-collector-NLB-e3172ff9d2f4bc8a.elb.eu-west-2.amazonaws.com:4317,http://happy-life-otel-collector:4317`). - `OPEN_TELEMETRY_EXPORT_INTERVAL`: number in milliseconds, defaults to `15000`. Defines how often the instrumented Rafiki instance should send metrics. - `TELEMETRY_EXCHANGE_RATES_URL`: string URL, defaults to `https://telemetry-exchange-rates.s3.amazonaws.com/exchange-rates-usd.json`. It defines the endpoint that Rafiki will query for exchange rates, as a fallback when ASE does not [provide them](/integration/getting-started/#exchange-rates). If set, the response format of the external exchange rates API should be of type Rates, as the rates service expects. From 8135aac9e479a36c15660b8b7683f621b6bd2446 Mon Sep 17 00:00:00 2001 From: beniaminmunteanu Date: Thu, 22 Feb 2024 13:33:32 +0200 Subject: [PATCH 67/67] feat(telemetry): plural on OPEN_TELEMETRY_COLLECTOR_URLS & docs changes --- packages/backend/src/config/app.ts | 2 +- .../documentation/src/content/docs/telemetry/integrating.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/config/app.ts b/packages/backend/src/config/app.ts index bb4a6dbe1b..8005cea73f 100644 --- a/packages/backend/src/config/app.ts +++ b/packages/backend/src/config/app.ts @@ -40,7 +40,7 @@ export const Config = { enableTelemetry: envBool('ENABLE_TELEMETRY', true), livenet: envBool('LIVENET', false), openTelemetryCollectors: envStringArray( - 'OPEN_TELEMETRY_COLLECTOR_URL', + 'OPEN_TELEMETRY_COLLECTOR_URLS', process.env.LIVENET ? [] : [ diff --git a/packages/documentation/src/content/docs/telemetry/integrating.md b/packages/documentation/src/content/docs/telemetry/integrating.md index 0752df25cb..8bbd89b38d 100644 --- a/packages/documentation/src/content/docs/telemetry/integrating.md +++ b/packages/documentation/src/content/docs/telemetry/integrating.md @@ -9,8 +9,8 @@ In order to do so, the integrating ASE must deploy its own OpenTelemetry collect ## Rafiki Telemetry Environment Variables - `ENABLE_TELEMETRY`: boolean, defaults to `true`. Enables the telemetry service on Rafiki. -- `LIVENET`: boolean. Should be set to `true` on production environments. If it is not set, it will default to `false`, and metrics will get sent to the testnet otel-collector -- `OPEN_TELEMETRY_COLLECTOR_URL`: CSV of URLs for Open Telemetry collectors (e.g., `http://otel-collector-NLB-e3172ff9d2f4bc8a.elb.eu-west-2.amazonaws.com:4317,http://happy-life-otel-collector:4317`). +- `LIVENET`: boolean. Should be set to `true` on production environments dealing with real money. If it is not set, it will default to `false`, and metrics will get sent to the testnet otel-collector +- `OPEN_TELEMETRY_COLLECTOR_URLS`: CSV of URLs for Open Telemetry collectors (e.g., `http://otel-collector-NLB-e3172ff9d2f4bc8a.elb.eu-west-2.amazonaws.com:4317,http://happy-life-otel-collector:4317`). - `OPEN_TELEMETRY_EXPORT_INTERVAL`: number in milliseconds, defaults to `15000`. Defines how often the instrumented Rafiki instance should send metrics. - `TELEMETRY_EXCHANGE_RATES_URL`: string URL, defaults to `https://telemetry-exchange-rates.s3.amazonaws.com/exchange-rates-usd.json`. It defines the endpoint that Rafiki will query for exchange rates, as a fallback when ASE does not [provide them](/integration/getting-started/#exchange-rates). If set, the response format of the external exchange rates API should be of type Rates, as the rates service expects. The default endpoint set here points to a public S3 that has the previously mentioned required format, updated daily.