diff --git a/src/common/factory.ts b/src/common/factory.ts index f8e0cb94..2522a942 100644 --- a/src/common/factory.ts +++ b/src/common/factory.ts @@ -15,20 +15,33 @@ * along with this program. If not, see . */ import { TurboUnauthenticatedConfiguration } from '../types.js'; +import { TurboWinstonLogger } from './logger.js'; import { TurboUnauthenticatedPaymentService } from './payment.js'; import { TurboUnauthenticatedClient } from './turbo.js'; import { TurboUnauthenticatedUploadService } from './upload.js'; export class TurboBaseFactory { + protected static logger = new TurboWinstonLogger(); + + static setLogLevel(level: string) { + this.logger.setLogLevel(level); + } + + static setLogFormat(format: string) { + this.logger.setLogFormat(format); + } + static unauthenticated({ paymentServiceConfig = {}, uploadServiceConfig = {}, }: TurboUnauthenticatedConfiguration = {}) { const paymentService = new TurboUnauthenticatedPaymentService({ ...paymentServiceConfig, + logger: this.logger, }); const uploadService = new TurboUnauthenticatedUploadService({ ...uploadServiceConfig, + logger: this.logger, }); return new TurboUnauthenticatedClient({ uploadService, diff --git a/src/common/http.ts b/src/common/http.ts index 8e1242e8..360770e6 100644 --- a/src/common/http.ts +++ b/src/common/http.ts @@ -21,6 +21,7 @@ import { ReadableStream } from 'stream/web'; import { TurboHTTPServiceInterface, + TurboLogger, TurboSignedRequestHeaders, } from '../types.js'; import { createAxiosInstance } from '../utils/axiosClient.js'; @@ -28,18 +29,34 @@ import { FailedRequestError } from '../utils/errors.js'; export class TurboHTTPService implements TurboHTTPServiceInterface { protected axios: AxiosInstance; + protected logger: TurboLogger; + constructor({ url, retryConfig, + logger, }: { url: string; retryConfig?: IAxiosRetryConfig; + logger: TurboLogger; }) { + this.logger = logger; this.axios = createAxiosInstance({ axiosConfig: { baseURL: url, + onUploadProgress: (progressEvent) => { + this.logger.debug(`Uploading...`, { + percent: Math.floor((progressEvent.progress ?? 0) * 100), + loaded: `${progressEvent.loaded} bytes`, + total: `${progressEvent.total} bytes`, + }); + if (progressEvent.progress === 1) { + this.logger.debug(`Upload complete!`); + } + }, }, retryConfig, + logger: this.logger, }); } async get({ diff --git a/src/common/logger.ts b/src/common/logger.ts new file mode 100644 index 00000000..ad6c07dc --- /dev/null +++ b/src/common/logger.ts @@ -0,0 +1,77 @@ +/** + * Copyright (C) 2022-2023 Permanent Data Solutions, Inc. All Rights Reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import winston, { createLogger, format, transports } from 'winston'; + +import { TurboLogger } from '../types.js'; +import { version } from '../version.js'; + +export class TurboWinstonLogger implements TurboLogger { + protected logger: winston.Logger; + constructor({ + level = 'info', + logFormat = 'simple', + }: { + level?: 'info' | 'debug' | 'error' | 'none' | undefined; + logFormat?: 'simple' | 'json' | undefined; + } = {}) { + this.logger = createLogger({ + level, + defaultMeta: { client: 'turbo-sdk', version }, + silent: level === 'none', + format: getLogFormat(logFormat), + transports: [new transports.Console()], + }); + } + + info(message: string, ...args: any[]) { + this.logger.info(message, ...args); + } + + warn(message: string, ...args: any[]) { + this.logger.warn(message, ...args); + } + + error(message: string, ...args: any[]) { + this.logger.error(message, ...args); + } + + debug(message: string, ...args: any[]) { + this.logger.debug(message, ...args); + } + + setLogLevel(level: string) { + this.logger.level = level; + } + + setLogFormat(logFormat: string) { + this.logger.format = getLogFormat(logFormat); + } +} + +function getLogFormat(logFormat: string) { + return format.combine( + format((info) => { + if (info.stack && info.level !== 'error') { + delete info.stack; + } + return info; + })(), + format.errors({ stack: true }), // Ensure errors show a stack trace + format.timestamp(), + logFormat === 'json' ? format.json() : format.simple(), + ); +} diff --git a/src/common/payment.ts b/src/common/payment.ts index 91acdfeb..4bb51623 100644 --- a/src/common/payment.ts +++ b/src/common/payment.ts @@ -25,6 +25,7 @@ import { TurboCountriesResponse, TurboCurrenciesResponse, TurboFiatToArResponse, + TurboLogger, TurboPriceResponse, TurboRatesResponse, TurboSignedRequestHeaders, @@ -35,6 +36,7 @@ import { TurboWincForFiatResponse, } from '../types.js'; import { TurboHTTPService } from './http.js'; +import { TurboWinstonLogger } from './logger.js'; export const developmentPaymentServiceURL = 'https://payment.ardrive.dev'; export const defaultPaymentServiceURL = 'https://payment.ardrive.io'; @@ -43,14 +45,18 @@ export class TurboUnauthenticatedPaymentService implements TurboUnauthenticatedPaymentServiceInterface { protected readonly httpService: TurboHTTPService; + protected logger: TurboLogger; constructor({ url = defaultPaymentServiceURL, retryConfig, + logger = new TurboWinstonLogger(), }: TurboUnauthenticatedPaymentServiceConfiguration) { + this.logger = logger; this.httpService = new TurboHTTPService({ url: `${url}/v1`, retryConfig, + logger: this.logger, }); } @@ -154,8 +160,9 @@ export class TurboAuthenticatedPaymentService url = defaultPaymentServiceURL, retryConfig, signer, + logger, }: TurboAuthenticatedPaymentServiceConfiguration) { - super({ url, retryConfig }); + super({ url, retryConfig, logger }); this.signer = signer; } diff --git a/src/common/upload.ts b/src/common/upload.ts index c92c1630..ee42bd78 100644 --- a/src/common/upload.ts +++ b/src/common/upload.ts @@ -19,6 +19,7 @@ import { TurboAuthenticatedUploadServiceConfiguration, TurboAuthenticatedUploadServiceInterface, TurboFileFactory, + TurboLogger, TurboSignedDataItemFactory, TurboUnauthenticatedUploadServiceConfiguration, TurboUnauthenticatedUploadServiceInterface, @@ -26,6 +27,7 @@ import { TurboWalletSigner, } from '../types.js'; import { TurboHTTPService } from './http.js'; +import { TurboWinstonLogger } from './logger.js'; export const developmentUploadServiceURL = 'https://upload.ardrive.dev'; export const defaultUploadServiceURL = 'https://upload.ardrive.io'; @@ -34,14 +36,18 @@ export class TurboUnauthenticatedUploadService implements TurboUnauthenticatedUploadServiceInterface { protected httpService: TurboHTTPService; + protected logger: TurboLogger; constructor({ url = defaultUploadServiceURL, retryConfig, + logger = new TurboWinstonLogger(), }: TurboUnauthenticatedUploadServiceConfiguration) { + this.logger = logger; this.httpService = new TurboHTTPService({ url: `${url}/v1`, retryConfig, + logger: this.logger, }); } @@ -52,6 +58,7 @@ export class TurboUnauthenticatedUploadService }: TurboSignedDataItemFactory & TurboAbortSignal): Promise { const fileSize = dataItemSizeFactory(); + this.logger.debug('Uploading signed data item...'); // TODO: add p-limit constraint or replace with separate upload class return this.httpService.post({ endpoint: `/tx`, @@ -76,8 +83,9 @@ export class TurboAuthenticatedUploadService url = defaultUploadServiceURL, retryConfig, signer, + logger, }: TurboAuthenticatedUploadServiceConfiguration) { - super({ url, retryConfig }); + super({ url, retryConfig, logger }); this.signer = signer; } @@ -94,6 +102,7 @@ export class TurboAuthenticatedUploadService }); const signedDataItem = dataItemStreamFactory(); const fileSize = dataItemSizeFactory(); + this.logger.debug('Uploading signed data item...'); // TODO: add p-limit constraint or replace with separate upload class return this.httpService.post({ endpoint: `/tx`, diff --git a/src/node/factory.ts b/src/node/factory.ts index aad6dd14..ef6e7c1a 100644 --- a/src/node/factory.ts +++ b/src/node/factory.ts @@ -29,14 +29,19 @@ export class TurboFactory extends TurboBaseFactory { paymentServiceConfig = {}, uploadServiceConfig = {}, }: TurboAuthenticatedConfiguration) { - const signer = new TurboNodeArweaveSigner({ privateKey }); + const signer = new TurboNodeArweaveSigner({ + privateKey, + logger: this.logger, + }); const paymentService = new TurboAuthenticatedPaymentService({ ...paymentServiceConfig, signer, + logger: this.logger, }); const uploadService = new TurboAuthenticatedUploadService({ ...uploadServiceConfig, signer, + logger: this.logger, }); return new TurboAuthenticatedClient({ uploadService, diff --git a/src/node/signer.ts b/src/node/signer.ts index 03c75c6b..32ec078c 100644 --- a/src/node/signer.ts +++ b/src/node/signer.ts @@ -20,16 +20,23 @@ import { randomBytes } from 'node:crypto'; import { Readable } from 'node:stream'; import { JWKInterface } from '../common/jwk.js'; -import { StreamSizeFactory, TurboWalletSigner } from '../types.js'; +import { StreamSizeFactory, TurboLogger, TurboWalletSigner } from '../types.js'; import { toB64Url } from '../utils/base64.js'; export class TurboNodeArweaveSigner implements TurboWalletSigner { protected privateKey: JWKInterface; protected signer: ArweaveSigner; // TODO: replace with internal signer class - + protected logger: TurboLogger; // TODO: replace with internal signer class - constructor({ privateKey }: { privateKey: JWKInterface }) { + constructor({ + privateKey, + logger, + }: { + privateKey: JWKInterface; + logger: TurboLogger; + }) { this.privateKey = privateKey; + this.logger = logger; this.signer = new ArweaveSigner(this.privateKey); } @@ -44,8 +51,11 @@ export class TurboNodeArweaveSigner implements TurboWalletSigner { dataItemSizeFactory: StreamSizeFactory; }> { // TODO: replace with our own signer implementation + this.logger.debug('Signing data item...'); const [stream1, stream2] = [fileStreamFactory(), fileStreamFactory()]; const signedDataItem = await streamSigner(stream1, stream2, this.signer); + this.logger.debug('Successfully signed data item...'); + // TODO: support target, anchor, and tags const signedDataItemSize = this.calculateSignedDataHeadersSize({ dataSize: fileSizeFactory(), diff --git a/src/types.ts b/src/types.ts index 0d85aa01..f43796dc 100644 --- a/src/types.ts +++ b/src/types.ts @@ -17,7 +17,6 @@ import { IAxiosRetryConfig } from 'axios-retry'; import { Readable } from 'node:stream'; import { ReadableStream } from 'node:stream/web'; -import winston from 'winston'; import { CurrencyMap } from './common/currency.js'; import { JWKInterface } from './common/jwk.js'; @@ -121,7 +120,7 @@ type TurboAuthConfiguration = { type TurboServiceConfiguration = { url?: string; retryConfig?: IAxiosRetryConfig; - logger?: winston.Logger; + logger?: TurboLogger; }; export type TurboUnauthenticatedUploadServiceConfiguration = @@ -139,6 +138,15 @@ export type TurboUnauthenticatedConfiguration = { uploadServiceConfig?: TurboUnauthenticatedUploadServiceConfiguration; }; +export interface TurboLogger { + setLogLevel: (level: string) => void; + setLogFormat: (logFormat: string) => void; + info: (message: string, ...args: any[]) => void; + warn: (message: string, ...args: any[]) => void; + error: (message: string, ...args: any[]) => void; + debug: (message: string, ...args: any[]) => void; +} + export type TurboAuthenticatedConfiguration = TurboUnauthenticatedConfiguration & { privateKey: TurboWallet; diff --git a/src/utils/axiosClient.ts b/src/utils/axiosClient.ts index 1ffa6be2..a610789e 100644 --- a/src/utils/axiosClient.ts +++ b/src/utils/axiosClient.ts @@ -17,6 +17,8 @@ import axios, { AxiosInstance, AxiosRequestConfig, CanceledError } from 'axios'; import axiosRetry, { IAxiosRetryConfig } from 'axios-retry'; +import { TurboWinstonLogger } from '../common/logger.js'; +import { TurboLogger } from '../types.js'; import { version } from '../version.js'; export const defaultRequestHeaders = { @@ -27,9 +29,11 @@ export const defaultRequestHeaders = { export interface AxiosInstanceParameters { axiosConfig?: Omit; retryConfig?: IAxiosRetryConfig; + logger?: TurboLogger; } export const createAxiosInstance = ({ + logger = new TurboWinstonLogger(), axiosConfig = {}, retryConfig = { retryDelay: axiosRetry.exponentialDelay, @@ -41,15 +45,16 @@ export const createAxiosInstance = ({ ); }, onRetry: (retryCount, error) => { - console.debug( - `Request failed, ${error}. Retry attempt #${retryCount}...`, - ); + logger.debug(`Request failed, ${error}. Retry attempt #${retryCount}...`); }, }, -}: AxiosInstanceParameters): AxiosInstance => { +}: AxiosInstanceParameters = {}): AxiosInstance => { const axiosInstance = axios.create({ ...axiosConfig, - headers: defaultRequestHeaders, + headers: { + ...axiosConfig.headers, + ...defaultRequestHeaders, + }, validateStatus: () => true, // don't throw on non-200 status codes }); diff --git a/src/web/factory.ts b/src/web/factory.ts index 6dbd3a16..f199b8d7 100644 --- a/src/web/factory.ts +++ b/src/web/factory.ts @@ -29,14 +29,19 @@ export class TurboFactory extends TurboBaseFactory { paymentServiceConfig = {}, uploadServiceConfig = {}, }: TurboAuthenticatedConfiguration) { - const signer = new TurboWebArweaveSigner({ privateKey }); + const signer = new TurboWebArweaveSigner({ + privateKey, + logger: this.logger, + }); const paymentService = new TurboAuthenticatedPaymentService({ ...paymentServiceConfig, signer, + logger: this.logger, }); const uploadService = new TurboAuthenticatedUploadService({ ...uploadServiceConfig, signer, + logger: this.logger, }); return new TurboAuthenticatedClient({ uploadService, diff --git a/src/web/signer.ts b/src/web/signer.ts index f46b1f5d..5529a83f 100644 --- a/src/web/signer.ts +++ b/src/web/signer.ts @@ -20,16 +20,23 @@ import { randomBytes } from 'node:crypto'; import { ReadableStream } from 'node:stream/web'; import { JWKInterface } from '../common/jwk.js'; -import { StreamSizeFactory, TurboWalletSigner } from '../types.js'; +import { StreamSizeFactory, TurboLogger, TurboWalletSigner } from '../types.js'; import { toB64Url } from '../utils/base64.js'; import { readableStreamToBuffer } from '../utils/readableStream.js'; export class TurboWebArweaveSigner implements TurboWalletSigner { protected privateKey: JWKInterface; protected signer: ArweaveSigner; // TODO: replace with internal signer class - - constructor({ privateKey }: { privateKey: JWKInterface }) { + protected logger: TurboLogger; + constructor({ + privateKey, + logger, + }: { + privateKey: JWKInterface; + logger: TurboLogger; + }) { this.privateKey = privateKey; + this.logger = logger; this.signer = new ArweaveSigner(this.privateKey); } @@ -49,9 +56,11 @@ export class TurboWebArweaveSigner implements TurboWalletSigner { stream: fileStreamFactory(), size: fileSizeFactory(), }); + this.logger.debug('Signing data item...'); // TODO: support target, anchor and tags for upload const signedDataItem = createData(buffer, this.signer, {}); await signedDataItem.sign(this.signer); + this.logger.debug('Successfully signed data item...'); return { // while this returns a Buffer - it needs to match our return type for uploading dataItemStreamFactory: () => signedDataItem.getRaw(),