diff --git a/api/deno.json b/api/deno.json index b4790ff..e902987 100644 --- a/api/deno.json +++ b/api/deno.json @@ -1,10 +1,11 @@ { "imports": { "@std/fmt": "jsr:@std/fmt@^1.0.9", - "@std/http": "jsr:@std/http@^1.0.25" + "@std/http": "jsr:@std/http@^1.0.25", + "@std/streams": "jsr:@std/streams@^1.1.1" }, "name": "@01edu/api", - "version": "0.2.7", + "version": "0.2.8", "license": "MIT", "exports": { "./context": "./context.ts", @@ -13,6 +14,7 @@ "./response": "./response.ts", "./router": "./router.ts", "./server": "./server.ts", - "./validator": "./validator.ts" + "./validator": "./validator.ts", + "./local_ipc_client": "./local_ipc_client.ts" } } diff --git a/api/env.ts b/api/env.ts index 85cda06..ffa9ca9 100644 --- a/api/env.ts +++ b/api/env.ts @@ -55,6 +55,19 @@ if (APP_ENV !== 'dev' && APP_ENV !== 'prod' && APP_ENV !== 'test') { throw Error(`APP_ENV: "${APP_ENV}" must be "dev", "test" or "prod"`) } +/** + * The port number the application should listen on, determined by the `PORT` environment variable. + * Defaults to '8080' if not set. + * + * @example + * ```ts + * import { PORT } from '@01edu/api/env'; + * + * console.log(`Server will listen on port: ${PORT}`); + * ``` + */ +export const PORT: string = ENV('PORT', '8080') + /** * The git commit SHA of the current build, typically provided by a CI/CD system. * @@ -78,7 +91,10 @@ export const CI_COMMIT_SHA: string = ENV('CI_COMMIT_SHA', '') * }; * ``` */ -export const DEVTOOL_REPORT_TOKEN: string = ENV('DEVTOOL_REPORT_TOKEN', '') +export const DEVTOOL_REPORT_TOKEN: string = ENV( + 'DEVTOOL_REPORT_TOKEN', + `localhost:${PORT}`, +) /** * The URL for a developer tool service. * diff --git a/api/local_ipc_client.ts b/api/local_ipc_client.ts new file mode 100644 index 0000000..6bf51c1 --- /dev/null +++ b/api/local_ipc_client.ts @@ -0,0 +1,49 @@ +import { TextLineStream } from '@std/streams/text-line-stream' + +export const defaultSocketPath: string = Deno.build.os === 'windows' + ? '\\\\.\\pipe\\01-devtools' + : `${Deno.env.get('XDG_RUNTIME_DIR') || '/tmp'}/01-devtools/01-devtools.sock` + +const encoder = new TextEncoder() + +async function sendCommand( + socketPath: string, + command: string, +): Promise | null> { + try { + const conn = await Deno.connect({ transport: 'unix', path: socketPath }) + await conn.write(encoder.encode(`${command}\n`)) + const reader = conn.readable + .pipeThrough(new TextDecoderStream()) + .pipeThrough(new TextLineStream()) + .getReader() + const { value } = await reader.read() + reader.releaseLock() + conn.close() + return value ? JSON.parse(value) : null + } catch { + return null + } +} + +export let devtoolsPort: number | null = null + +export interface RegisterPayload { + projectId: string + name?: string + url: string + sqlEndpoint?: string | null +} + +export async function register( + payload: RegisterPayload, + socketPath = defaultSocketPath, +): Promise { + const res = await sendCommand( + socketPath, + `register/${JSON.stringify(payload)}`, + ) + if (!res) return null + + devtoolsPort = res.port as number +} diff --git a/api/log.ts b/api/log.ts index f31be9b..4fbfbbc 100644 --- a/api/log.ts +++ b/api/log.ts @@ -28,6 +28,7 @@ import { DEVTOOL_REPORT_TOKEN, DEVTOOL_URL, } from './env.ts' +import { devtoolsPort } from './local_ipc_client.ts' // Types type LogLevel = 'info' | 'error' | 'warn' | 'debug' @@ -146,7 +147,7 @@ const bind = (log: LogFunction) => */ export const logger = async ({ filters, - batchInterval = 5000, + batchInterval = APP_ENV === 'prod' ? 5000 : 500, maxBatchSize = 50, logUrl = DEVTOOL_URL, logToken = DEVTOOL_REPORT_TOKEN, @@ -176,6 +177,27 @@ export const logger = async ({ } } + function forwardLogsToDevtool( + level: LogLevel, + event: string, + props?: Record, + ) { + const { trace, span } = getContext() + const logData = { + severity_number: levels[level].level, + trace_id: trace, + span_id: span, + event_name: event, + attributes: props, + timestamp: now() * 1000, + service_version: version, + service_instance_id: startTime.toString(), + } + + logBatch.push(logData) + logBatch.length >= maxBatchSize && flushLogs() + } + // DEVTOOLS Batch Logic async function flushLogs() { if (logBatch.length === 0) return @@ -184,7 +206,11 @@ export const logger = async ({ logBatch = [] try { - const response = await fetch(logUrl!, { + const logUrlWithPort = APP_ENV === 'dev' && devtoolsPort + ? `http://localhost:${devtoolsPort}/api/logs` + : logUrl + + const response = await fetch(logUrlWithPort, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -206,8 +232,7 @@ export const logger = async ({ const rootDir = import.meta.dirname?.slice(0, -'/lib'.length).replaceAll('\\', '/') || '' - const f = filters || new Set() - if (APP_ENV === 'prod') { + if (APP_ENV === 'prod' || APP_ENV === 'dev') { // Initialize batch interval const interval = setInterval(flushLogs, batchInterval) @@ -220,24 +245,14 @@ export const logger = async ({ Deno.addSignalListener('SIGINT', cleanup) Deno.addSignalListener('SIGTERM', cleanup) + } + + const f = filters || new Set() + if (APP_ENV === 'prod') { return bind((level, event, props) => { if (f.has(event)) return - const { trace, span } = getContext() - const logData = { - severity_number: levels[level].level, - trace_id: trace, - span_id: span, - event_name: event, - attributes: props, - timestamp: now() * 1000, - service_version: version, - service_instance_id: startTime.toString(), - } - // Local logging - console.log(event, props) - - logBatch.push(logData) - logBatch.length >= maxBatchSize && flushLogs() + forwardLogsToDevtool(level, event, props) + console[level](event, props) }) } @@ -252,6 +267,7 @@ export const logger = async ({ if (APP_ENV === 'dev') { return bind((level, event, props) => { if (f.has(event)) return + forwardLogsToDevtool(level, event, props) let callChain = '' for (const s of Error('').stack!.split('\n').slice(2).reverse()) { if (!s.includes(rootDir)) continue @@ -264,7 +280,6 @@ export const logger = async ({ )) callChain = callChain ? `${callChain}/${coloredName}` : coloredName } - const ev = `${makePrettyTimestamp(level, event)} ${callChain}`.trim() props ? console[level](ev, props) : console[level](ev) })