mirror of
https://github.com/HeyPuter/puter.git
synced 2026-05-29 12:50:59 +00:00
perf: improve tel + decrease logs (#2309)
Docker Image CI / build-and-push-image (push) Has been cancelled
Maintain Release Merge PR / update-release-pr (push) Has been cancelled
release-please / release-please (push) Has been cancelled
test / test-backend (24.x) (push) Has been cancelled
test / API tests (node env, api-test) (24.x) (push) Has been cancelled
test / puterjs (node env, vitest) (24.x) (push) Has been cancelled
Docker Image CI / build-and-push-image (push) Has been cancelled
Maintain Release Merge PR / update-release-pr (push) Has been cancelled
release-please / release-please (push) Has been cancelled
test / test-backend (24.x) (push) Has been cancelled
test / API tests (node env, api-test) (24.x) (push) Has been cancelled
test / puterjs (node env, vitest) (24.x) (push) Has been cancelled
* perfmon: lower healthcheck status cache, add kv health signal * perf: improve tel + decrease logs * logs
This commit is contained in:
@@ -96,8 +96,10 @@ export class HttpError extends Error {
|
||||
|
||||
// Registers all routes from a decorated controller instance to an Express router
|
||||
export class ExtensionController {
|
||||
logger?: Console;
|
||||
// TODO DS: make this work with other express-like routers
|
||||
registerRoutes () {
|
||||
const logger = this.logger || console;
|
||||
const prefix = Object.getPrototypeOf(this).__controllerPrefix || '';
|
||||
const adminsForController = Object.getPrototypeOf(this).__adminUsernames as
|
||||
| string[]
|
||||
@@ -126,7 +128,7 @@ export class ExtensionController {
|
||||
if ( ! extension[route.method] ) {
|
||||
throw new Error(`Unsupported HTTP method: ${route.method}`);
|
||||
} else {
|
||||
console.log(`Registering route: [${route.method.toUpperCase()}] ${fullPath}`);
|
||||
logger.log(`Registering route: [${route.method.toUpperCase()}] ${fullPath}`);
|
||||
|
||||
(extension[route.method] as RouterMethods[HttpMethod])(
|
||||
fullPath,
|
||||
@@ -154,20 +156,16 @@ export class ExtensionController {
|
||||
} catch ( error ) {
|
||||
if ( error instanceof HttpError ) {
|
||||
res.status(error.statusCode).send({ error: error.message });
|
||||
console.error('httpError:', error);
|
||||
logger.warn('httpError:', error);
|
||||
return;
|
||||
}
|
||||
if ( error instanceof Error ) {
|
||||
res
|
||||
.status(StatusCodes.INTERNAL_SERVER_ERROR)
|
||||
.send({ error: error.message });
|
||||
console.error('Non-http error:', error);
|
||||
res.status(StatusCodes.INTERNAL_SERVER_ERROR).send({ error: error.message });
|
||||
logger.error('Non-http error:', error);
|
||||
return;
|
||||
}
|
||||
res
|
||||
.status(StatusCodes.INTERNAL_SERVER_ERROR)
|
||||
.send({ error: 'An unknown error occurred' });
|
||||
console.error('An unknown error occurred:', error);
|
||||
res.status(StatusCodes.INTERNAL_SERVER_ERROR).send({ error: 'An unknown error occurred' });
|
||||
logger.error('An unknown error occurred:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -35,7 +35,6 @@ import { EntityStoreModule } from './src/modules/entitystore/EntityStoreModule.j
|
||||
import { HostOSModule } from './src/modules/hostos/HostOSModule.js';
|
||||
import { InternetModule } from './src/modules/internet/InternetModule.js';
|
||||
import { KVStoreModule } from './src/modules/kvstore/KVStoreModule.js';
|
||||
import { PerfMonModule } from './src/modules/perfmon/PerfMonModule.js';
|
||||
import { PuterFSModule } from './src/modules/puterfs/PuterFSModule.js';
|
||||
import SelfHostedModule from './src/modules/selfhosted/SelfHostedModule.js';
|
||||
import { TestConfigModule } from './src/modules/test-config/TestConfigModule.js';
|
||||
@@ -89,6 +88,5 @@ export default {
|
||||
DataAccessModule,
|
||||
|
||||
// Development modules
|
||||
PerfMonModule,
|
||||
DevelopmentModule,
|
||||
};
|
||||
|
||||
@@ -35,6 +35,10 @@ const query = require('./om/query/query');
|
||||
*/
|
||||
const install = async ({ context, services, app, useapi, modapi }) => {
|
||||
const config = require('./config');
|
||||
const { TelemetryService } = require('./modules/perfmon/TelemetryService');
|
||||
if ( ! services.has('telemetry') ) {
|
||||
services.registerService('telemetry', TelemetryService);
|
||||
}
|
||||
|
||||
// === LIBRARIES ===
|
||||
|
||||
|
||||
@@ -276,9 +276,7 @@ async function refresh_apps_cache (options, override) {
|
||||
async function refresh_associations_cache () {
|
||||
/** @type BaseDatabaseAccessService */
|
||||
const db = _servicesHolder.services.get('database').get(DB_READ, 'apps');
|
||||
|
||||
const log = _servicesHolder.services.get('log-service').create('helpers.js');
|
||||
log.tick('refresh file associations');
|
||||
console.debug('refresh file associations');
|
||||
const associations = await db.read('SELECT * FROM app_filetype_association');
|
||||
const lists = {};
|
||||
for ( const association of associations ) {
|
||||
@@ -356,6 +354,7 @@ async function get_app (options) {
|
||||
const pendingKey = `pending_app:${queryKey}`;
|
||||
const pending = kv.get(pendingKey);
|
||||
if ( pending ) {
|
||||
// Reuse the existing pending query
|
||||
const result = await pending;
|
||||
// shallow clone the result
|
||||
return result ? { ...result } : null;
|
||||
|
||||
@@ -90,7 +90,7 @@ class BroadcastService extends BaseService {
|
||||
}
|
||||
|
||||
if ( key === 'test' ) {
|
||||
this.log.noticeme(`test message: ${
|
||||
console.debug(`test message: ${
|
||||
JSON.stringify(data)}`);
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ class BroadcastService extends BaseService {
|
||||
{
|
||||
id: 'test',
|
||||
description: 'send a test message',
|
||||
handler: async (args, ctx) => {
|
||||
handler: async () => {
|
||||
this.on_event('test', {
|
||||
contents: 'I am a test message',
|
||||
}, {});
|
||||
|
||||
@@ -404,16 +404,15 @@ class CaptchaService extends BaseService {
|
||||
* @returns {boolean} Whether the answer is valid
|
||||
*/
|
||||
verifyCaptcha (token, userAnswer) {
|
||||
console.log('====== CAPTCHA SERVICE VERIFICATION DIAGNOSTIC ======');
|
||||
console.log('TOKENS_TRACKING: verifyCaptcha called. Service ID:', this.serviceId);
|
||||
console.log('TOKENS_TRACKING: Request counter during verification:', this.requestCounter);
|
||||
console.log('TOKENS_TRACKING: Static test token exists:', this.captchaTokens.has('test-static-token'));
|
||||
console.log('TOKENS_TRACKING: Trying to verify token:', token ? `${token.substring(0, 8) }...` : 'undefined');
|
||||
|
||||
console.log('verifyCaptcha called with token:', token ? `${token.substring(0, 8) }...` : 'undefined');
|
||||
console.log('userAnswer:', userAnswer);
|
||||
console.log('Service enabled:', this.enabled);
|
||||
console.log('Number of tokens in captchaTokens:', this.captchaTokens.size);
|
||||
console.debug('====== CAPTCHA SERVICE VERIFICATION DIAGNOSTIC ======');
|
||||
console.debug('TOKENS_TRACKING: verifyCaptcha called. Service ID:', this.serviceId);
|
||||
console.debug('TOKENS_TRACKING: Request counter during verification:', this.requestCounter);
|
||||
console.debug('TOKENS_TRACKING: Static test token exists:', this.captchaTokens.has('test-static-token'));
|
||||
console.debug('TOKENS_TRACKING: Trying to verify token:', token ? `${token.substring(0, 8) }...` : 'undefined');
|
||||
console.debug('verifyCaptcha called with token:', token ? `${token.substring(0, 8) }...` : 'undefined');
|
||||
console.debug('userAnswer:', userAnswer);
|
||||
console.debug('Service enabled:', this.enabled);
|
||||
console.debug('Number of tokens in captchaTokens:', this.captchaTokens.size);
|
||||
|
||||
// Service health check
|
||||
this._checkServiceHealth();
|
||||
|
||||
@@ -255,7 +255,7 @@ class WinstonLogger {
|
||||
constructor (winst) {
|
||||
this.winst = winst;
|
||||
}
|
||||
onLogMessage (log_lvl, crumbs, message, fields, objects) {
|
||||
onLogMessage (log_lvl, crumbs, message, fields) {
|
||||
this.winst.log({
|
||||
...fields,
|
||||
label: crumbs.join('.'),
|
||||
@@ -336,7 +336,7 @@ class CustomLogger {
|
||||
args: a,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('error?', e);
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
if ( ret && ret.skip ) return;
|
||||
@@ -404,7 +404,7 @@ class LogService extends BaseService {
|
||||
{
|
||||
id: 'show',
|
||||
description: 'toggle log output',
|
||||
handler: async (args, log) => {
|
||||
handler: async () => {
|
||||
this.devlogger && (this.devlogger.off = !this.devlogger.off);
|
||||
},
|
||||
},
|
||||
@@ -423,7 +423,7 @@ class LogService extends BaseService {
|
||||
{
|
||||
id: 'stop',
|
||||
description: 'stop recording to a file via dev logger',
|
||||
handler: async ([name], log) => {
|
||||
handler: async ([_name], log) => {
|
||||
if ( ! this.devlogger ) {
|
||||
log('no dev logger; what are you doing?');
|
||||
}
|
||||
@@ -433,7 +433,7 @@ class LogService extends BaseService {
|
||||
{
|
||||
id: 'indent',
|
||||
description: 'toggle log indentation',
|
||||
handler: async (args, log) => {
|
||||
handler: async () => {
|
||||
globalThis.dev_console_indent_on =
|
||||
!globalThis.dev_console_indent_on;
|
||||
},
|
||||
@@ -481,21 +481,21 @@ class LogService extends BaseService {
|
||||
filename: `${this.log_directory}/%DATE%.log`,
|
||||
datePattern: 'YYYY-MM-DD',
|
||||
maxSize: '20m',
|
||||
maxFiles: '14d',
|
||||
maxFiles: '2d',
|
||||
}),
|
||||
new winston.transports.DailyRotateFile({
|
||||
level: 'error',
|
||||
filename: `${this.log_directory}/error-%DATE%.log`,
|
||||
datePattern: 'YYYY-MM-DD',
|
||||
maxSize: '20m',
|
||||
maxFiles: '14d',
|
||||
maxFiles: '2d',
|
||||
}),
|
||||
new winston.transports.DailyRotateFile({
|
||||
level: 'system',
|
||||
filename: `${this.log_directory}/system-%DATE%.log`,
|
||||
datePattern: 'YYYY-MM-DD',
|
||||
maxSize: '20m',
|
||||
maxFiles: '14d',
|
||||
maxFiles: '2d',
|
||||
}),
|
||||
],
|
||||
}));
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
const BaseService = require('../../services/BaseService');
|
||||
const { kv } = require('../../util/kvSingleton');
|
||||
const { promise } = require('@heyputer/putility').libs;
|
||||
const SECOND = 1000;
|
||||
|
||||
@@ -202,10 +203,10 @@ class ServerHealthService extends BaseService {
|
||||
add_check (name, fn) {
|
||||
const chainable = {
|
||||
on_fail_: [],
|
||||
};
|
||||
chainable.on_fail = (fn) => {
|
||||
chainable.on_fail_.push(fn);
|
||||
return chainable;
|
||||
on_fail: (fn) => {
|
||||
chainable.on_fail_.push(fn);
|
||||
return chainable;
|
||||
},
|
||||
};
|
||||
this.checks_.push({ name, fn, chainable });
|
||||
return chainable;
|
||||
@@ -221,27 +222,27 @@ class ServerHealthService extends BaseService {
|
||||
*/
|
||||
async get_status () {
|
||||
const cache_key = 'server-health:status';
|
||||
|
||||
|
||||
// Check cache first
|
||||
if ( globalThis.kv ) {
|
||||
const cached = globalThis.kv.get(cache_key);
|
||||
if ( kv ) {
|
||||
const cached = kv.get(cache_key);
|
||||
if ( cached ) {
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Compute status
|
||||
const failures = this.failures_.map(v => v.name);
|
||||
const status = {
|
||||
ok: failures.length === 0,
|
||||
...(failures.length ? { failed: failures } : {}),
|
||||
};
|
||||
|
||||
|
||||
// Cache with 30 second TTL
|
||||
if ( globalThis.kv ) {
|
||||
globalThis.kv.set(cache_key, status, { EX: 30 });
|
||||
if ( kv ) {
|
||||
kv.set(cache_key, status, { EX: 5 });
|
||||
}
|
||||
|
||||
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ class LocalTerminalService extends BaseService {
|
||||
const svc_socketio = req.services.get('socketio');
|
||||
proc.stdout.on('data', data => {
|
||||
const base64 = data.toString('base64');
|
||||
console.log('---------------------- CHUNK?', base64);
|
||||
console.debug('---------------------- CHUNK?', base64);
|
||||
svc_socketio.send({ room: req.user.id },
|
||||
'local-terminal.stdout',
|
||||
{
|
||||
@@ -111,7 +111,7 @@ class LocalTerminalService extends BaseService {
|
||||
});
|
||||
proc.stderr.on('data', data => {
|
||||
const base64 = data.toString('base64');
|
||||
console.log('---------------------- CHUNK?', base64);
|
||||
console.debug('---------------------- CHUNK?', base64);
|
||||
svc_socketio.send({ room: req.user.id },
|
||||
'local-terminal.stderr',
|
||||
{
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2024-present Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Puter.
|
||||
*
|
||||
* Puter 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
const { AdvancedBase } = require('@heyputer/putility');
|
||||
|
||||
/**
|
||||
* Enable this module when you want performance monitoring.
|
||||
*
|
||||
* Performance monitoring requires additional setup. Jaegar should be installed
|
||||
* and running.
|
||||
*/
|
||||
class PerfMonModule extends AdvancedBase {
|
||||
async install (context) {
|
||||
const services = context.get('services');
|
||||
|
||||
const { TelemetryService } = require('./TelemetryService');
|
||||
services.registerService('telemetry', TelemetryService);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
PerfMonModule,
|
||||
};
|
||||
@@ -24,51 +24,36 @@ import { Resource } from '@opentelemetry/resources';
|
||||
import { ConsoleMetricExporter, PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
|
||||
import { NodeSDK } from '@opentelemetry/sdk-node';
|
||||
import { ConsoleSpanExporter } from '@opentelemetry/sdk-trace-base';
|
||||
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
|
||||
import { SemanticAttributes, SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
|
||||
import config from '../../config.js';
|
||||
import BaseService from '../../services/BaseService.js';
|
||||
|
||||
export class TelemetryService extends BaseService {
|
||||
static TRACER_NAME = 'puter-tracer';
|
||||
static #sharedSdk = null;
|
||||
static #sharedTracer = null;
|
||||
static #telemetryStarted = false;
|
||||
|
||||
/** @type {import('@opentelemetry/api').Tracer} */
|
||||
#tracer = null;
|
||||
_construct () {
|
||||
|
||||
const traceExporter = this.#getConfiguredExporter();
|
||||
const metricExporter = this.#getMetricExporter();
|
||||
|
||||
if ( !traceExporter && !metricExporter ) {
|
||||
console.log('TelemetryService not configured, skipping initialization.');
|
||||
return;
|
||||
}
|
||||
|
||||
const resource = Resource.default().merge(
|
||||
new Resource({
|
||||
[SemanticResourceAttributes.SERVICE_NAME]: 'puter-backend',
|
||||
[SemanticResourceAttributes.SERVICE_VERSION]: '0.1.0',
|
||||
}));
|
||||
|
||||
const sdk = new NodeSDK({
|
||||
resource,
|
||||
traceExporter: traceExporter,
|
||||
metricReader: new PeriodicExportingMetricReader({
|
||||
exporter: metricExporter,
|
||||
}),
|
||||
instrumentations: [getNodeAutoInstrumentations()],
|
||||
constructor (service_resources, ...args) {
|
||||
super(service_resources, ...args);
|
||||
const { sdk, tracer } = TelemetryService.#startTelemetry({
|
||||
serviceConfig: this.config,
|
||||
});
|
||||
|
||||
this.sdk = sdk;
|
||||
|
||||
this.sdk.start();
|
||||
|
||||
this.#tracer = trace.getTracer('puter-tracer');
|
||||
|
||||
this.#tracer = tracer;
|
||||
}
|
||||
|
||||
_init () {
|
||||
if ( ! this.#tracer ) {
|
||||
return;
|
||||
}
|
||||
const svc_context = this.services.get('context');
|
||||
const svc_context = this.services.get('context', { optional: true });
|
||||
if ( ! svc_context ) {
|
||||
return;
|
||||
}
|
||||
svc_context.register_context_hook('pre_arun', ({ hints, trace_name, callback, replace_callback }) => {
|
||||
if ( ! trace_name ) return;
|
||||
if ( ! hints.trace ) return;
|
||||
@@ -87,21 +72,138 @@ export class TelemetryService extends BaseService {
|
||||
});
|
||||
}
|
||||
|
||||
#getConfiguredExporter () {
|
||||
if ( config.jaeger ?? this.config.jaeger ) {
|
||||
return new OTLPTraceExporter(config.jaeger ?? this.config.jaeger);
|
||||
static #normalizeRoute (route) {
|
||||
if ( Array.isArray(route) ) {
|
||||
for ( const entry of route ) {
|
||||
if ( typeof entry === 'string' ) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
if ( this.config.console ) {
|
||||
if ( typeof route === 'string' ) {
|
||||
return route;
|
||||
}
|
||||
if ( route instanceof RegExp ) {
|
||||
return route.toString();
|
||||
}
|
||||
}
|
||||
|
||||
static #buildRoute (req, route) {
|
||||
const normalized = TelemetryService.#normalizeRoute(route);
|
||||
if ( ! normalized ) {
|
||||
return undefined;
|
||||
}
|
||||
const baseUrl = typeof req?.baseUrl === 'string' ? req.baseUrl : '';
|
||||
const combined = `${baseUrl}${normalized}`;
|
||||
return combined || normalized;
|
||||
}
|
||||
|
||||
static #applyRouteToSpan (span, req, route) {
|
||||
if ( ! route ) {
|
||||
return;
|
||||
}
|
||||
span.setAttribute(SemanticAttributes.HTTP_ROUTE, route);
|
||||
if ( typeof span.updateName === 'function' && req?.method ) {
|
||||
span.updateName(`HTTP ${req.method} ${route}`);
|
||||
}
|
||||
}
|
||||
|
||||
static #buildInstrumentationConfig () {
|
||||
return {
|
||||
'@opentelemetry/instrumentation-http': {
|
||||
responseHook: (span, response) => {
|
||||
const req = response?.req;
|
||||
const route = TelemetryService.#buildRoute(req, req?.route?.path);
|
||||
TelemetryService.#applyRouteToSpan(span, req, route);
|
||||
},
|
||||
},
|
||||
'@opentelemetry/instrumentation-express': {
|
||||
spanNameHook: (info, defaultName) => {
|
||||
if ( info.layerType !== 'request_handler' ) {
|
||||
return defaultName;
|
||||
}
|
||||
const route = TelemetryService.#buildRoute(info.request, info.route);
|
||||
if ( !route || !info.request?.method ) {
|
||||
return defaultName;
|
||||
}
|
||||
return `HTTP ${info.request.method} ${route}`;
|
||||
},
|
||||
requestHook: (span, info) => {
|
||||
const route = TelemetryService.#buildRoute(info.request, info.route);
|
||||
if ( route ) {
|
||||
span.setAttribute(SemanticAttributes.HTTP_ROUTE, route);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
static #resolveExporterConfig (serviceConfig) {
|
||||
return config.jaeger ?? serviceConfig?.jaeger;
|
||||
}
|
||||
|
||||
static #getConfiguredExporter (serviceConfig) {
|
||||
const exporterConfig = TelemetryService.#resolveExporterConfig(serviceConfig);
|
||||
if ( exporterConfig ) {
|
||||
return new OTLPTraceExporter(exporterConfig);
|
||||
}
|
||||
if ( serviceConfig?.console ) {
|
||||
return new ConsoleSpanExporter();
|
||||
}
|
||||
}
|
||||
|
||||
#getMetricExporter () {
|
||||
if ( config.jaeger ?? this.config.jaeger ) {
|
||||
return new OTLPMetricExporter(config.jaeger ?? this.config.jaeger);
|
||||
static #getMetricExporter (serviceConfig) {
|
||||
const exporterConfig = TelemetryService.#resolveExporterConfig(serviceConfig);
|
||||
if ( exporterConfig ) {
|
||||
return new OTLPMetricExporter(exporterConfig);
|
||||
}
|
||||
if ( this.config.console ) {
|
||||
if ( serviceConfig?.console ) {
|
||||
return new ConsoleMetricExporter();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static #startTelemetry ({ serviceConfig } = {}) {
|
||||
if ( TelemetryService.#telemetryStarted ) {
|
||||
return { sdk: TelemetryService.#sharedSdk, tracer: TelemetryService.#sharedTracer };
|
||||
}
|
||||
TelemetryService.#telemetryStarted = true;
|
||||
|
||||
const effectiveConfig = serviceConfig ?? config.services?.telemetry ?? {};
|
||||
const traceExporter = TelemetryService.#getConfiguredExporter(effectiveConfig);
|
||||
const metricExporter = TelemetryService.#getMetricExporter(effectiveConfig);
|
||||
|
||||
if ( !traceExporter && !metricExporter ) {
|
||||
console.log('TelemetryService not configured, skipping initialization.');
|
||||
return { sdk: null, tracer: null };
|
||||
}
|
||||
|
||||
const resource = Resource.default().merge(
|
||||
new Resource({
|
||||
[SemanticResourceAttributes.SERVICE_NAME]: 'puter-backend',
|
||||
[SemanticResourceAttributes.SERVICE_VERSION]: '0.1.0',
|
||||
}));
|
||||
|
||||
const sdkConfig = {
|
||||
resource,
|
||||
instrumentations: [
|
||||
getNodeAutoInstrumentations(TelemetryService.#buildInstrumentationConfig()),
|
||||
],
|
||||
};
|
||||
|
||||
if ( traceExporter ) {
|
||||
sdkConfig.traceExporter = traceExporter;
|
||||
}
|
||||
if ( metricExporter ) {
|
||||
sdkConfig.metricReader = new PeriodicExportingMetricReader({
|
||||
exporter: metricExporter,
|
||||
});
|
||||
}
|
||||
|
||||
TelemetryService.#sharedSdk = new NodeSDK(sdkConfig);
|
||||
TelemetryService.#sharedSdk.start();
|
||||
TelemetryService.#sharedTracer = trace.getTracer(TelemetryService.TRACER_NAME);
|
||||
|
||||
return { sdk: TelemetryService.#sharedSdk, tracer: TelemetryService.#sharedTracer };
|
||||
}
|
||||
}
|
||||
|
||||
+8
-3
@@ -1,14 +1,19 @@
|
||||
import type { ServerHealthService } from '../modules/core/ServerHealthService';
|
||||
import { SqliteDatabaseAccessService } from './database/SqliteDatabaseAccessService';
|
||||
import type { MeteringService } from './MeteringService/MeteringService';
|
||||
import { MeteringServiceWrapper } from './MeteringService/MeteringServiceWrapper.mjs';
|
||||
import { DDBClient } from './repositories/DDBClient';
|
||||
import { DynamoKVStore } from './repositories/DynamoKVStore/DynamoKVStore';
|
||||
import type { SUService } from './SUService';
|
||||
|
||||
export interface ServiceResources {
|
||||
services: {
|
||||
get (name: 'meteringService'): MeteringServiceWrapper;
|
||||
get (name: 'puter-kvstore'): DynamoKVStore;
|
||||
get (name: 'database'): SqliteDatabaseAccessService
|
||||
get (name: string): any
|
||||
get (name: 'database'): SqliteDatabaseAccessService;
|
||||
get (name: 'server-health'): ServerHealthService;
|
||||
get (name: 'su'): SUService;
|
||||
get (name: 'dynamo'): DDBClient;
|
||||
get (name: string): any;
|
||||
};
|
||||
config: Record<string, any> & { services?: Record<string, any>; server_id?: string };
|
||||
name?: string;
|
||||
|
||||
@@ -20,7 +20,11 @@ const { AdvancedBase } = require('@heyputer/putility');
|
||||
const BaseService = require('../BaseService');
|
||||
const { Context } = require('../../util/context');
|
||||
const config = require('../../config');
|
||||
|
||||
const isBot = require('isbot');
|
||||
const IGNORED_BOT_UAS = new Set([
|
||||
'Amazon-Route53-Health-Check-Service (ref 7599f3a9-c2af-43ac-a4b6-299da2e3861c; report http://amzn.to/1vsZADi)',
|
||||
'Better Stack Better Uptime Bot Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36',
|
||||
]);
|
||||
/**
|
||||
* @class Requester
|
||||
* @classdesc This class represents a requester in the system. It encapsulates
|
||||
@@ -127,9 +131,6 @@ class Requester {
|
||||
* The class uses the 'isbot' module to determine if the requester is a bot.
|
||||
*/
|
||||
class RequesterIdentificationExpressMiddleware extends AdvancedBase {
|
||||
static MODULES = {
|
||||
isbot: require('isbot'),
|
||||
};
|
||||
register_initializer (initializer) {
|
||||
this.value_initializers_.push(initializer);
|
||||
}
|
||||
@@ -140,14 +141,18 @@ class RequesterIdentificationExpressMiddleware extends AdvancedBase {
|
||||
const x = Context.get();
|
||||
|
||||
const requester = Requester.from_request(req);
|
||||
const is_bot = this.modules.isbot(requester.ua);
|
||||
const is_bot = isBot(requester.ua);
|
||||
requester.is_bot = is_bot;
|
||||
|
||||
x.set('requester', requester);
|
||||
req.requester = requester;
|
||||
|
||||
if ( requester.is_bot ) {
|
||||
this.log.info('bot detected', requester.serialize());
|
||||
if ( IGNORED_BOT_UAS.has(requester.ua) ) {
|
||||
this.log.info('healthcheck bot:', requester.serialize());
|
||||
} else {
|
||||
this.log.info('bot detected', requester.serialize());
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
|
||||
@@ -16,14 +16,14 @@ interface DBClientConfig {
|
||||
}
|
||||
|
||||
export class DDBClient {
|
||||
ddbClient: Promise<DynamoDBClient>;
|
||||
ddbClientPromise: Promise<DynamoDBClient>;
|
||||
#documentClient!: DynamoDBDocumentClient;
|
||||
config?: DBClientConfig;
|
||||
|
||||
constructor (config?: DBClientConfig) {
|
||||
this.config = config;
|
||||
this.ddbClient = this.#getClient();
|
||||
this.ddbClient.then(client => {
|
||||
this.ddbClientPromise = this.#getClient();
|
||||
this.ddbClientPromise.then(client => {
|
||||
this.#documentClient = DynamoDBDocumentClient.from(client, {
|
||||
marshallOptions: {
|
||||
removeUndefinedValues: true,
|
||||
@@ -31,6 +31,14 @@ export class DDBClient {
|
||||
});
|
||||
}
|
||||
|
||||
async recreateClient () {
|
||||
this.ddbClientPromise = this.#getClient();
|
||||
this.#documentClient = DynamoDBDocumentClient.from(await this.ddbClientPromise, {
|
||||
marshallOptions: {
|
||||
removeUndefinedValues: true,
|
||||
} });
|
||||
}
|
||||
|
||||
async #getClient () {
|
||||
if ( ! this.config?.aws ) {
|
||||
console.warn('No config for DynamoDB, will fall back on local dynalite');
|
||||
|
||||
@@ -7,7 +7,7 @@ class DDBClientServiceWrapper extends BaseService {
|
||||
async _construct () {
|
||||
this.ddbClient = new DDBClient(this.config as unknown as ConstructorParameters<typeof DDBClient>[0]);
|
||||
|
||||
await this.ddbClient.ddbClient; // ensure client is ready
|
||||
await this.ddbClient.ddbClientPromise; // ensure client is ready
|
||||
|
||||
Object.getOwnPropertyNames(DDBClient.prototype).forEach(fn => {
|
||||
if ( fn === 'constructor' ) return;
|
||||
|
||||
@@ -20,6 +20,30 @@ class DynamoKVStoreServiceWrapper extends BaseService {
|
||||
this[fn] = (...args: unknown[]) => this.kvStore[fn](...args);
|
||||
});
|
||||
}
|
||||
|
||||
async registerHealthcheck () {
|
||||
const healthcheckService = this.services.get('server-health');
|
||||
|
||||
healthcheckService.add_check('kv-store', async () => {
|
||||
try {
|
||||
const passed = await this.services.get('su').sudo(async () => {
|
||||
const rand = Math.floor(Math.random() * 1000000);
|
||||
await this.kvStore.set({ key: 'healthTestKey', value: rand });
|
||||
const setRight = await this.kvStore.get({ key: 'healthTestKey' }) === rand;
|
||||
await this.kvStore.del({ key: 'healthTestKey' });
|
||||
return setRight;
|
||||
});
|
||||
if ( ! passed ) {
|
||||
throw new Error('KV Store healthcheck failed: set/get mismatch');
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error(`KV Store healthcheck failed: ${(e as Error).message}`);
|
||||
}
|
||||
}).on_fail(async () => {
|
||||
await this.services.get('dynamo').recreateClient();
|
||||
});
|
||||
}
|
||||
|
||||
static IMPLEMENTS = {
|
||||
['puter-kvstore']: Object.getOwnPropertyNames(DynamoKVStore.prototype)
|
||||
.filter(n => n !== 'constructor')
|
||||
|
||||
@@ -349,9 +349,6 @@ class HTTPThumbnailService extends BaseService {
|
||||
|
||||
const results = resp.data;
|
||||
|
||||
console.debug('response?', { resp });
|
||||
console.debug('data?', { data: resp.data });
|
||||
|
||||
if ( results.length !== queue.length ) {
|
||||
this.log.error('Thumbnail service returned wrong number of results');
|
||||
throw new Error('Thumbnail service returned wrong number of results');
|
||||
@@ -393,9 +390,7 @@ class HTTPThumbnailService extends BaseService {
|
||||
}
|
||||
|
||||
const form = new FormData();
|
||||
let expected = 0;
|
||||
for ( const job of queue ) {
|
||||
expected++;
|
||||
|
||||
/**
|
||||
* Prepares and sends a request to the thumbnail service for processing multiple files.
|
||||
|
||||
@@ -98,7 +98,6 @@ const main = async () => {
|
||||
InternetModule,
|
||||
DevelopmentModule,
|
||||
DNSModule,
|
||||
PerfMonModule,
|
||||
DataAccessModule,
|
||||
} = (await import('@heyputer/backend')).default;
|
||||
|
||||
@@ -118,7 +117,6 @@ const main = async () => {
|
||||
k.add_module(new PuterAIModule());
|
||||
k.add_module(new InternetModule());
|
||||
k.add_module(new DNSModule());
|
||||
k.add_module(new PerfMonModule());
|
||||
if ( process.env.UNSAFE_PUTER_DEV ) {
|
||||
k.add_module(new DevelopmentModule());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user