Skip to content

Commit

Permalink
expand object serialization
Browse files Browse the repository at this point in the history
  • Loading branch information
maddsua committed Jan 21, 2025
1 parent 87b1443 commit 150c4fd
Show file tree
Hide file tree
Showing 3 changed files with 133 additions and 38 deletions.
126 changes: 90 additions & 36 deletions client/lib/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,7 @@ type Metadata = Record<string, string>;
type MetadataInitValue = string | number | boolean | null | undefined;
type MetadataInit = Record<string, MetadataInitValue>;

const unwrapMetadata = (init?: MetadataInit): Metadata | null => {

if (!init) {
return null;
}

const transform = (val: MetadataInitValue): string | null => {
switch (typeof val) {
case 'string':
return val.trim();
case 'number':
return `${val}`;
case 'boolean':
return `${val}`;
default:
return null;
}
};

return Object.fromEntries(Object.entries(init)
.map(([key, value]) => ([key, transform(value)]))
.filter(([_, val]) => !!val)) as Metadata;
};

export interface LogEntry {
interface LogEntry {
date: number;
level: LogLevel;
message: string;
Expand All @@ -37,6 +13,11 @@ export interface LogEntry {

type LoggerPushFn = (message: string, meta?: MetadataInit) => void;

/**
* Logger implements a similar to go slog interface.
*
* Use it to push new log entries.
*/
export interface Logger {
log: LoggerPushFn;
info: LoggerPushFn;
Expand All @@ -45,6 +26,19 @@ export interface Logger {
error: LoggerPushFn;
};

/**
* Console is a compatibility interface for eventdb and loki-serverless clients.
*
* It implements the most frequently used methods of the standard ES console,
* however not all objects and classes can be properly serialized by it.
*
* To ensure proper serialization consider preferring to pass only objects
* that can be JSON-serialized. Some widely used classes as FormData, Date, Set, Map and RegExp
* are fully supported, while classes like Request and Response will not be fully serialized to
* avoid any side effects of calling their async methods.
*
* For new projects you should use the Logger interface instead.
*/
export interface LogpushConsole {
info: (...args: any[]) => void;
log: (...args: any[]) => void;
Expand All @@ -53,6 +47,12 @@ export interface LogpushConsole {
debug: (...args: any[]) => void;
};

/**
* Logpush agent is a class that holds instance/context level metadata, log queue and a connection to Logpush service.
*
* All metadata fields added to the agent will be copied to every log entry that it pushes,
* so put stuff like app environment name and other static options here.
*/
export class Agent {

readonly url: string;
Expand Down Expand Up @@ -109,7 +109,7 @@ export class Agent {
message: args.map(item => stringifyArg(item)).join(' '),
});

const logFn = console[level];
const logFn = console[level] || console.log;
if (typeof logFn === 'function') {
logFn(...args);
}
Expand Down Expand Up @@ -144,6 +144,30 @@ export class Agent {
};
};

const unwrapMetadata = (init?: MetadataInit): Metadata | null => {

if (!init) {
return null;
}

const transform = (val: MetadataInitValue): string | null => {
switch (typeof val) {
case 'string':
return val.trim();
case 'number':
return `${val}`;
case 'boolean':
return `${val}`;
default:
return null;
}
};

return Object.fromEntries(Object.entries(init)
.map(([key, value]) => ([key, transform(value)]))
.filter(([_, val]) => !!val)) as Metadata;
};

const slogDate = (date: Date): string => {

const year = date.getFullYear();
Expand All @@ -156,38 +180,57 @@ const slogDate = (date: Date): string => {
return `${year}/${month}/${day} ${hour}:${min}:${sec}`;
};

const stringifyArg = (item: any): string => {
const stringifyArg = (item: any, nested?: boolean): string => {
switch (typeof item) {
case 'string': return item;
case 'string': return nested ? `'${item}'` : item;
case 'number': return item.toString();
case 'bigint': return item.toString();
case 'boolean': return `${item}`;
case 'object': return stringifyObjectArg(item);
case 'function': return '[fn()]';
case 'symbol': return item.toString();
default: return '[undefined]';
default: return '{}';
}
};

const stringifyObjectArg = (item: object): string => {
const stringifyObjectArg = (value: object): string => {

try {
return JSON.stringify(item, objectArgReplacerFn);

if (value instanceof Error) {
return value.stack ? `${value.stack}\n` : `${value.name || 'Error'}: '${value.message}'`;
}

if (value instanceof Date) {
return `'${value.toUTCString()}'`;
}

if (value instanceof RegExp) {
return `'${value}'`;
}

if (value instanceof URL) {
return `'${value.href}'`;
}

return JSON.stringify(value, stringifyObjectReplacer);

} catch (_) {
return '{}';
}
};

const objectArgReplacerFn = (_: string, value: any): any => {
const stringifyObjectReplacer = (_: string, value: any): any => {

if (typeof value !== 'object') {
return value;
}

if (value instanceof Error) {
return { message: value.message };
return { message: value.message, stack: value.stack, type: value.name };
}

if (value instanceof FormData) {
if (value instanceof FormData || value instanceof Map || value instanceof Headers) {
return Object.fromEntries(value);
}

Expand All @@ -203,8 +246,19 @@ const objectArgReplacerFn = (_: string, value: any): any => {
return Array.from(value.keys());
}

if (value instanceof Map) {
return Object.fromEntries(value);
if (value instanceof Request) {
return {
url: value.url,
method: value.method,
headers: value.headers,
referrer: value.referrer,
credentials: value.credentials,
mode: value.mode,
};
}

if (value instanceof Response) {
return { status: value.status, headers: value.headers, type: value.type };
}

return value;
Expand Down
2 changes: 1 addition & 1 deletion client/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@maddsua/logpush-client",
"version": "0.2.0",
"version": "0.3.0",
"author": "maddsua",
"license": "MIT",
"type": "module",
Expand Down
43 changes: 42 additions & 1 deletion client/tests/console.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,47 @@ formData.set('phone', '+100000000000');

agent.console.debug('Dumping form data:', formData);
agent.console.debug('Dumping trpc:', { name: 'Jane Doe', ig_username: 42 });
agent.console.log('Just writing multiple values', true, 42, new Date(), /heeey/ig, new Map([['uhm', 'secret']]), new Error('Task failed successfullly'));

agent.console.debug(
'Just writing multiple values',
true,
42,
new Date(),
/heeey/ig,
new Map([['uhm', 'secret']]),
new Set(['aaa', { key: 'bbb' }]),
);

agent.console.debug(new Error('Task failed successfullly'));

agent.console.debug([
1,
'two',
{ value: 'three'},
{ value: 4, alt_value: 5 },
new Map([['key', 'value']]),
]);

agent.console.debug({
type: 'lead data',
title: 'miata shop',
name: 'maddsua',
phone: '+380960000000',
bid_price: 35_000,
nested: {
type: 'lead data',
title: 'miata shop',
name: 'maddsua',
phone: '+380960000000',
bid_price: 35_000,
}
});

agent.console.debug(new URL('https://localhost:8080/path?query=goth'));

agent.console.debug(new Headers({ 'content-type': 'application/json' }));

agent.console.debug(new Response('ok status', { status: 200, headers: new Headers({ 'content-type': 'application/json' }) }));
agent.console.debug(new Request('https://localhost:8080/path?query=goth', { method: 'GET', headers: new Headers({ 'content-type': 'application/json' }) }));

await agent.flush();

0 comments on commit 150c4fd

Please sign in to comment.