@sincpro/mobile 0.3.1 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +234 -2
- package/dist/entrypoints/db/migrations.d.ts +3 -1
- package/dist/entrypoints/db/migrations.js +65 -0
- package/dist/entrypoints/queue/QueueProcessor.js +1 -25
- package/dist/entrypoints/queue/subscriber_filter.d.ts +9 -0
- package/dist/entrypoints/queue/subscriber_filter.js +25 -0
- package/dist/framework/base_module.d.ts +1 -0
- package/dist/framework/base_module.js +6 -1
- package/dist/framework/createApp.d.ts +6 -0
- package/dist/framework/createApp.js +5 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.js +3 -0
- package/dist/infrastructure/context_manager/context.d.ts +16 -0
- package/dist/infrastructure/context_manager/context.js +37 -0
- package/dist/infrastructure/context_manager/context_api.d.ts +28 -0
- package/dist/infrastructure/context_manager/context_api.js +55 -0
- package/dist/infrastructure/context_manager/context_key.d.ts +15 -0
- package/dist/infrastructure/context_manager/context_key.js +3 -0
- package/dist/infrastructure/context_manager/context_manager_facade.d.ts +107 -0
- package/dist/infrastructure/context_manager/context_manager_facade.js +113 -0
- package/dist/infrastructure/context_manager/decorators/propagate_context.d.ts +24 -0
- package/dist/infrastructure/context_manager/decorators/propagate_context.js +52 -0
- package/dist/infrastructure/context_manager/decorators/with_context.d.ts +34 -0
- package/dist/infrastructure/context_manager/decorators/with_context.js +44 -0
- package/dist/infrastructure/context_manager/index.d.ts +11 -0
- package/dist/infrastructure/context_manager/index.js +12 -0
- package/dist/infrastructure/context_manager/managed_context.d.ts +87 -0
- package/dist/infrastructure/context_manager/managed_context.js +144 -0
- package/dist/infrastructure/context_manager/stack_context_manager.d.ts +39 -0
- package/dist/infrastructure/context_manager/stack_context_manager.js +56 -0
- package/dist/infrastructure/database/connector.js +10 -1
- package/dist/infrastructure/interceptor/index.d.ts +4 -0
- package/dist/infrastructure/interceptor/index.js +2 -0
- package/dist/infrastructure/interceptor/intercept.d.ts +57 -0
- package/dist/infrastructure/interceptor/intercept.js +112 -0
- package/dist/infrastructure/interceptor/logging_hooks.d.ts +22 -0
- package/dist/infrastructure/interceptor/logging_hooks.js +27 -0
- package/dist/infrastructure/logger.d.ts +11 -16
- package/dist/infrastructure/logger.js +61 -18
- package/dist/infrastructure/telemetry/buffer_registry.d.ts +15 -0
- package/dist/infrastructure/telemetry/buffer_registry.js +26 -0
- package/dist/infrastructure/telemetry/connectivity_state.d.ts +20 -0
- package/dist/infrastructure/telemetry/connectivity_state.js +28 -0
- package/dist/infrastructure/telemetry/context_keys.d.ts +7 -0
- package/dist/infrastructure/telemetry/context_keys.js +7 -0
- package/dist/infrastructure/telemetry/fetch_with_timeout.d.ts +8 -0
- package/dist/infrastructure/telemetry/fetch_with_timeout.js +17 -0
- package/dist/infrastructure/telemetry/flush_telemetry.d.ts +40 -0
- package/dist/infrastructure/telemetry/flush_telemetry.js +72 -0
- package/dist/infrastructure/telemetry/index.d.ts +17 -0
- package/dist/infrastructure/telemetry/index.js +17 -0
- package/dist/infrastructure/telemetry/init_telemetry.d.ts +21 -0
- package/dist/infrastructure/telemetry/init_telemetry.js +85 -0
- package/dist/infrastructure/telemetry/logging/flush_job.d.ts +44 -0
- package/dist/infrastructure/telemetry/logging/flush_job.js +69 -0
- package/dist/infrastructure/telemetry/logging/log_queue_repository.d.ts +76 -0
- package/dist/infrastructure/telemetry/logging/log_queue_repository.js +147 -0
- package/dist/infrastructure/telemetry/logging/loki_client.d.ts +32 -0
- package/dist/infrastructure/telemetry/logging/loki_client.js +118 -0
- package/dist/infrastructure/telemetry/logging/loki_registry.d.ts +12 -0
- package/dist/infrastructure/telemetry/logging/loki_registry.js +17 -0
- package/dist/infrastructure/telemetry/telemetry_flush_worker.d.ts +35 -0
- package/dist/infrastructure/telemetry/telemetry_flush_worker.js +65 -0
- package/dist/infrastructure/telemetry/telemetry_signal.d.ts +18 -0
- package/dist/infrastructure/telemetry/telemetry_signal.js +33 -0
- package/dist/infrastructure/telemetry/tracing/active_span.d.ts +22 -0
- package/dist/infrastructure/telemetry/tracing/active_span.js +40 -0
- package/dist/infrastructure/telemetry/tracing/otlp_client.d.ts +13 -0
- package/dist/infrastructure/telemetry/tracing/otlp_client.js +34 -0
- package/dist/infrastructure/telemetry/tracing/otlp_serializer.d.ts +67 -0
- package/dist/infrastructure/telemetry/tracing/otlp_serializer.js +101 -0
- package/dist/infrastructure/telemetry/tracing/span_flush_job.d.ts +25 -0
- package/dist/infrastructure/telemetry/tracing/span_flush_job.js +42 -0
- package/dist/infrastructure/telemetry/tracing/span_queue_repository.d.ts +82 -0
- package/dist/infrastructure/telemetry/tracing/span_queue_repository.js +134 -0
- package/dist/infrastructure/telemetry/tracing/span_sampler.d.ts +40 -0
- package/dist/infrastructure/telemetry/tracing/span_sampler.js +75 -0
- package/dist/infrastructure/telemetry/tracing/span_serializer.d.ts +48 -0
- package/dist/infrastructure/telemetry/tracing/span_serializer.js +36 -0
- package/dist/infrastructure/telemetry/tracing/sqlite_span_exporter.d.ts +26 -0
- package/dist/infrastructure/telemetry/tracing/sqlite_span_exporter.js +47 -0
- package/dist/infrastructure/telemetry/tracing/tracer.d.ts +22 -0
- package/dist/infrastructure/telemetry/tracing/tracer.js +45 -0
- package/dist/infrastructure/telemetry/tracing/tracing_hooks.d.ts +10 -0
- package/dist/infrastructure/telemetry/tracing/tracing_hooks.js +59 -0
- package/dist/infrastructure/telemetry/tracing_facade.d.ts +50 -0
- package/dist/infrastructure/telemetry/tracing_facade.js +139 -0
- package/dist/infrastructure/telemetry/types.d.ts +120 -0
- package/dist/infrastructure/telemetry/types.js +1 -0
- package/dist/ui/components/molecules/TimeZoneSelectorModal.js +7 -1
- package/package.json +17 -11
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Context } from "./context";
|
|
2
|
+
/**
|
|
3
|
+
* Hermes-compatible context manager.
|
|
4
|
+
*
|
|
5
|
+
* Hermes (React Native JS engine) has no AsyncLocalStorage and no native
|
|
6
|
+
* Promise hooks, so automatic async context propagation is not possible.
|
|
7
|
+
* This manager uses an explicit stack instead:
|
|
8
|
+
*
|
|
9
|
+
* - with(ctx, fn) — pushes ctx, calls fn synchronously, pops on return.
|
|
10
|
+
* Works correctly for synchronous code and for sequential
|
|
11
|
+
* await chains (where each await resolves before the next
|
|
12
|
+
* call starts).
|
|
13
|
+
*
|
|
14
|
+
* - push(ctx) / pop() — manual variant used by before/after interceptor hooks
|
|
15
|
+
* so the context stays active across async boundaries
|
|
16
|
+
* (the push in before() and the pop in after() bracket the
|
|
17
|
+
* entire async method execution, not just the sync part).
|
|
18
|
+
*
|
|
19
|
+
* Known limitation: concurrent branches (Promise.all with independent contexts)
|
|
20
|
+
* share the top of stack. For mobile single-user sequential flows this is
|
|
21
|
+
* acceptable — document it, never hide it.
|
|
22
|
+
*/
|
|
23
|
+
export declare class StackContextManager {
|
|
24
|
+
private _stack;
|
|
25
|
+
active(): Context;
|
|
26
|
+
with<T>(ctx: Context, fn: () => T): T;
|
|
27
|
+
/**
|
|
28
|
+
* Push a context explicitly. Must be paired with pop().
|
|
29
|
+
* Use when you need the context to survive past a synchronous return,
|
|
30
|
+
* i.e. inside before/after interceptor hooks for async methods.
|
|
31
|
+
*/
|
|
32
|
+
push(ctx: Context): void;
|
|
33
|
+
/**
|
|
34
|
+
* Pop the top context. Never pops the root — safe to call on empty stacks.
|
|
35
|
+
*/
|
|
36
|
+
pop(): void;
|
|
37
|
+
/** @internal — test and framework init use only */
|
|
38
|
+
reset(): void;
|
|
39
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Context } from "./context";
|
|
2
|
+
/**
|
|
3
|
+
* Hermes-compatible context manager.
|
|
4
|
+
*
|
|
5
|
+
* Hermes (React Native JS engine) has no AsyncLocalStorage and no native
|
|
6
|
+
* Promise hooks, so automatic async context propagation is not possible.
|
|
7
|
+
* This manager uses an explicit stack instead:
|
|
8
|
+
*
|
|
9
|
+
* - with(ctx, fn) — pushes ctx, calls fn synchronously, pops on return.
|
|
10
|
+
* Works correctly for synchronous code and for sequential
|
|
11
|
+
* await chains (where each await resolves before the next
|
|
12
|
+
* call starts).
|
|
13
|
+
*
|
|
14
|
+
* - push(ctx) / pop() — manual variant used by before/after interceptor hooks
|
|
15
|
+
* so the context stays active across async boundaries
|
|
16
|
+
* (the push in before() and the pop in after() bracket the
|
|
17
|
+
* entire async method execution, not just the sync part).
|
|
18
|
+
*
|
|
19
|
+
* Known limitation: concurrent branches (Promise.all with independent contexts)
|
|
20
|
+
* share the top of stack. For mobile single-user sequential flows this is
|
|
21
|
+
* acceptable — document it, never hide it.
|
|
22
|
+
*/
|
|
23
|
+
export class StackContextManager {
|
|
24
|
+
_stack = [Context.ROOT];
|
|
25
|
+
active() {
|
|
26
|
+
return this._stack[this._stack.length - 1] ?? Context.ROOT;
|
|
27
|
+
}
|
|
28
|
+
with(ctx, fn) {
|
|
29
|
+
this._stack.push(ctx);
|
|
30
|
+
try {
|
|
31
|
+
return fn();
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
this._stack.pop();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Push a context explicitly. Must be paired with pop().
|
|
39
|
+
* Use when you need the context to survive past a synchronous return,
|
|
40
|
+
* i.e. inside before/after interceptor hooks for async methods.
|
|
41
|
+
*/
|
|
42
|
+
push(ctx) {
|
|
43
|
+
this._stack.push(ctx);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Pop the top context. Never pops the root — safe to call on empty stacks.
|
|
47
|
+
*/
|
|
48
|
+
pop() {
|
|
49
|
+
if (this._stack.length > 1)
|
|
50
|
+
this._stack.pop();
|
|
51
|
+
}
|
|
52
|
+
/** @internal — test and framework init use only */
|
|
53
|
+
reset() {
|
|
54
|
+
this._stack = [Context.ROOT];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -4,7 +4,16 @@ import { openDatabaseAsync, openDatabaseSync, } from "expo-sqlite";
|
|
|
4
4
|
export const DB_NAME = "distribution.db";
|
|
5
5
|
let dbInstance = null;
|
|
6
6
|
const dbLock = new Mutex();
|
|
7
|
-
|
|
7
|
+
// Telemetry/queue tables are excluded from query logging: logging their
|
|
8
|
+
// mutations would feed the log pipeline its own writes — and a failing
|
|
9
|
+
// telemetry insert logs at ERROR (always remote), which tries to insert again,
|
|
10
|
+
// amplifying under exactly the offline/full conditions where inserts fail.
|
|
11
|
+
const SKIP_LOG_TABLES = [
|
|
12
|
+
"event_queue",
|
|
13
|
+
"dead_letter_queue",
|
|
14
|
+
"telemetry_queue",
|
|
15
|
+
"spans_queue",
|
|
16
|
+
];
|
|
8
17
|
function shouldLogQuery(query) {
|
|
9
18
|
const lowerQuery = query.toLowerCase();
|
|
10
19
|
return !SKIP_LOG_TABLES.some((table) => lowerQuery.includes(table));
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export interface MethodCall {
|
|
2
|
+
className: string;
|
|
3
|
+
methodName: string;
|
|
4
|
+
args: unknown[];
|
|
5
|
+
}
|
|
6
|
+
export interface InterceptorHooks {
|
|
7
|
+
before?: (call: MethodCall) => void;
|
|
8
|
+
after?: (call: MethodCall, result: unknown) => void;
|
|
9
|
+
onError?: (call: MethodCall, error: unknown) => void;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Mutates the class prototype so every instance is automatically traced.
|
|
13
|
+
* Use when the class is instantiated multiple times, or as the implementation
|
|
14
|
+
* behind a class decorator.
|
|
15
|
+
*
|
|
16
|
+
* ```ts
|
|
17
|
+
* interceptClass(CustomerService, otelHooks());
|
|
18
|
+
* // all CustomerService instances are now traced
|
|
19
|
+
*
|
|
20
|
+
* // as a decorator:
|
|
21
|
+
* function Traced(hooks: InterceptorHooks) {
|
|
22
|
+
* return <T extends new (...args: never[]) => object>(target: T) =>
|
|
23
|
+
* interceptClass(target, hooks);
|
|
24
|
+
* }
|
|
25
|
+
* @Traced(otelHooks())
|
|
26
|
+
* class OrderService { ... }
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* Throws if called twice on the same class — hooks stack silently otherwise.
|
|
30
|
+
* Getters, setters, and the constructor are never wrapped.
|
|
31
|
+
*/
|
|
32
|
+
export declare function interceptClass<T extends new (...args: never[]) => object>(target: T, hooks: InterceptorHooks): T;
|
|
33
|
+
/**
|
|
34
|
+
* Wraps a standalone function. Use for module-level functions or factory outputs.
|
|
35
|
+
*
|
|
36
|
+
* ```ts
|
|
37
|
+
* export const fetchOrders = interceptFunction(rawFetchOrders, otelHooks());
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* `MethodCall.className` is empty for standalone functions; `methodName` is the
|
|
41
|
+
* function's `.name` (or `"anonymous"` if unnamed).
|
|
42
|
+
*/
|
|
43
|
+
export declare function interceptFunction<T extends (...args: never[]) => unknown>(fn: T, hooks: InterceptorHooks): T;
|
|
44
|
+
/**
|
|
45
|
+
* Wraps a single live instance via Proxy without touching the class prototype.
|
|
46
|
+
* Use for singletons exported directly (e.g. `dbCursor`, `distributionWorkflows`).
|
|
47
|
+
*
|
|
48
|
+
* ```ts
|
|
49
|
+
* export const dbCursor = interceptInstance(new DBCursor(), otelHooks());
|
|
50
|
+
* // only this exported instance is traced; other DBCursor instances are unaffected
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* Hooks contract: `before` fires before the call; `after` fires on success with
|
|
54
|
+
* the resolved value; `onError` fires on throw or rejection — the error is always
|
|
55
|
+
* re-thrown after `onError` completes.
|
|
56
|
+
*/
|
|
57
|
+
export declare function interceptInstance<T extends object>(instance: T, hooks: InterceptorHooks): T;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
function isPlainFunction(descriptor) {
|
|
2
|
+
if (!descriptor)
|
|
3
|
+
return false;
|
|
4
|
+
if (typeof descriptor.value !== "function")
|
|
5
|
+
return false;
|
|
6
|
+
if (descriptor.get || descriptor.set)
|
|
7
|
+
return false;
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
function wrap(original, className, methodName, hooks) {
|
|
11
|
+
return function wrapped(...args) {
|
|
12
|
+
const call = { className, methodName, args };
|
|
13
|
+
hooks.before?.(call);
|
|
14
|
+
try {
|
|
15
|
+
const result = original.apply(this, args);
|
|
16
|
+
if (result instanceof Promise) {
|
|
17
|
+
return result.then((value) => {
|
|
18
|
+
hooks.after?.(call, value);
|
|
19
|
+
return value;
|
|
20
|
+
}, (error) => {
|
|
21
|
+
hooks.onError?.(call, error);
|
|
22
|
+
throw error;
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
hooks.after?.(call, result);
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
hooks.onError?.(call, error);
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const INTERCEPTED = Symbol("sincpro.intercepted");
|
|
35
|
+
function ownMethodNames(prototype) {
|
|
36
|
+
return Object.getOwnPropertyNames(prototype).filter((name) => name !== "constructor");
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Mutates the class prototype so every instance is automatically traced.
|
|
40
|
+
* Use when the class is instantiated multiple times, or as the implementation
|
|
41
|
+
* behind a class decorator.
|
|
42
|
+
*
|
|
43
|
+
* ```ts
|
|
44
|
+
* interceptClass(CustomerService, otelHooks());
|
|
45
|
+
* // all CustomerService instances are now traced
|
|
46
|
+
*
|
|
47
|
+
* // as a decorator:
|
|
48
|
+
* function Traced(hooks: InterceptorHooks) {
|
|
49
|
+
* return <T extends new (...args: never[]) => object>(target: T) =>
|
|
50
|
+
* interceptClass(target, hooks);
|
|
51
|
+
* }
|
|
52
|
+
* @Traced(otelHooks())
|
|
53
|
+
* class OrderService { ... }
|
|
54
|
+
* ```
|
|
55
|
+
*
|
|
56
|
+
* Throws if called twice on the same class — hooks stack silently otherwise.
|
|
57
|
+
* Getters, setters, and the constructor are never wrapped.
|
|
58
|
+
*/
|
|
59
|
+
export function interceptClass(target, hooks) {
|
|
60
|
+
if (target[INTERCEPTED]) {
|
|
61
|
+
throw new Error(`interceptClass: ${target.name} is already intercepted`);
|
|
62
|
+
}
|
|
63
|
+
const prototype = target.prototype;
|
|
64
|
+
for (const methodName of ownMethodNames(prototype)) {
|
|
65
|
+
const descriptor = Object.getOwnPropertyDescriptor(prototype, methodName);
|
|
66
|
+
if (!isPlainFunction(descriptor))
|
|
67
|
+
continue;
|
|
68
|
+
const original = descriptor.value;
|
|
69
|
+
descriptor.value = wrap(original, target.name, methodName, hooks);
|
|
70
|
+
Object.defineProperty(prototype, methodName, descriptor);
|
|
71
|
+
}
|
|
72
|
+
Object.defineProperty(target, INTERCEPTED, { value: true, enumerable: false });
|
|
73
|
+
return target;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Wraps a standalone function. Use for module-level functions or factory outputs.
|
|
77
|
+
*
|
|
78
|
+
* ```ts
|
|
79
|
+
* export const fetchOrders = interceptFunction(rawFetchOrders, otelHooks());
|
|
80
|
+
* ```
|
|
81
|
+
*
|
|
82
|
+
* `MethodCall.className` is empty for standalone functions; `methodName` is the
|
|
83
|
+
* function's `.name` (or `"anonymous"` if unnamed).
|
|
84
|
+
*/
|
|
85
|
+
export function interceptFunction(fn, hooks) {
|
|
86
|
+
return wrap(fn, "", fn.name || "anonymous", hooks);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Wraps a single live instance via Proxy without touching the class prototype.
|
|
90
|
+
* Use for singletons exported directly (e.g. `dbCursor`, `distributionWorkflows`).
|
|
91
|
+
*
|
|
92
|
+
* ```ts
|
|
93
|
+
* export const dbCursor = interceptInstance(new DBCursor(), otelHooks());
|
|
94
|
+
* // only this exported instance is traced; other DBCursor instances are unaffected
|
|
95
|
+
* ```
|
|
96
|
+
*
|
|
97
|
+
* Hooks contract: `before` fires before the call; `after` fires on success with
|
|
98
|
+
* the resolved value; `onError` fires on throw or rejection — the error is always
|
|
99
|
+
* re-thrown after `onError` completes.
|
|
100
|
+
*/
|
|
101
|
+
export function interceptInstance(instance, hooks) {
|
|
102
|
+
const className = instance.constructor?.name ?? "Object";
|
|
103
|
+
return new Proxy(instance, {
|
|
104
|
+
get(target, prop, receiver) {
|
|
105
|
+
const value = Reflect.get(target, prop, receiver);
|
|
106
|
+
if (typeof value !== "function" || typeof prop === "symbol") {
|
|
107
|
+
return value;
|
|
108
|
+
}
|
|
109
|
+
return wrap(value, className, prop, hooks).bind(target);
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { InterceptorHooks } from "./intercept";
|
|
2
|
+
/** Minimal logger contract — compatible with the framework's ILogger and any custom logger. */
|
|
3
|
+
export interface Logger {
|
|
4
|
+
debug(...args: unknown[]): void;
|
|
5
|
+
info(...args: unknown[]): void;
|
|
6
|
+
error(...args: unknown[]): void;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Produces `InterceptorHooks` that log every method call through the given logger.
|
|
10
|
+
*
|
|
11
|
+
* - `before` → debug: method entry with class and method name
|
|
12
|
+
* - `after` → info: method exited successfully
|
|
13
|
+
* - `onError` → error: method threw or rejected, includes the error
|
|
14
|
+
*
|
|
15
|
+
* Args and return values are intentionally not logged to avoid leaking sensitive data.
|
|
16
|
+
*
|
|
17
|
+
* ```ts
|
|
18
|
+
* interceptClass(DistributionWorkflowsImp, loggingHooks(loggerUseCases));
|
|
19
|
+
* interceptInstance(dbCursor, loggingHooks(loggerRepositories));
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export declare function loggingHooks(logger: Logger): InterceptorHooks;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Produces `InterceptorHooks` that log every method call through the given logger.
|
|
3
|
+
*
|
|
4
|
+
* - `before` → debug: method entry with class and method name
|
|
5
|
+
* - `after` → info: method exited successfully
|
|
6
|
+
* - `onError` → error: method threw or rejected, includes the error
|
|
7
|
+
*
|
|
8
|
+
* Args and return values are intentionally not logged to avoid leaking sensitive data.
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* interceptClass(DistributionWorkflowsImp, loggingHooks(loggerUseCases));
|
|
12
|
+
* interceptInstance(dbCursor, loggingHooks(loggerRepositories));
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export function loggingHooks(logger) {
|
|
16
|
+
return {
|
|
17
|
+
before(call) {
|
|
18
|
+
logger.debug(`→ ${call.className}.${call.methodName}`);
|
|
19
|
+
},
|
|
20
|
+
after(call) {
|
|
21
|
+
logger.info(`✓ ${call.className}.${call.methodName}`);
|
|
22
|
+
},
|
|
23
|
+
onError(call, error) {
|
|
24
|
+
logger.error(`✗ ${call.className}.${call.methodName}`, error);
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -5,21 +5,16 @@ interface ILogger {
|
|
|
5
5
|
warn(...args: LoggerArgs): void;
|
|
6
6
|
error(...args: LoggerArgs): void;
|
|
7
7
|
}
|
|
8
|
-
declare
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
REPOSITORIES = "REPOSITORIES",
|
|
19
|
-
USE_CASES = "USE_CASES",
|
|
20
|
-
QUEUE_PROCESSOR = "EVENT_BUS",
|
|
21
|
-
CRON_JOBS = "CRON_JOBS"
|
|
22
|
-
}
|
|
8
|
+
declare const EApplicationLogger: {
|
|
9
|
+
readonly GLOBAL: "GLOBAL";
|
|
10
|
+
readonly ODOO_CLIENT: "ODOO_CLIENT";
|
|
11
|
+
readonly ADAPTER: "ADAPTER";
|
|
12
|
+
readonly REPOSITORIES: "REPOSITORIES";
|
|
13
|
+
readonly USE_CASES: "USE_CASES";
|
|
14
|
+
readonly QUEUE_PROCESSOR: "EVENT_BUS";
|
|
15
|
+
readonly CRON_JOBS: "CRON_JOBS";
|
|
16
|
+
};
|
|
17
|
+
type EApplicationLogger = (typeof EApplicationLogger)[keyof typeof EApplicationLogger];
|
|
23
18
|
/**
|
|
24
19
|
* Production flag - manually toggle to disable all logs
|
|
25
20
|
* Set to true to disable logging, false to enable
|
|
@@ -28,7 +23,7 @@ export declare const IS_PRODUCTION = false;
|
|
|
28
23
|
/**
|
|
29
24
|
* Global log level for the application.
|
|
30
25
|
*/
|
|
31
|
-
export declare const LOG_LEVEL
|
|
26
|
+
export declare const LOG_LEVEL: "INFO";
|
|
32
27
|
/**
|
|
33
28
|
* Configuration for enabling or disabling logs for specific application contexts.
|
|
34
29
|
*/
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { getLokiClient } from "./telemetry/logging/loki_registry";
|
|
2
|
+
import { activeTraceLabel } from "./telemetry/tracing/active_span";
|
|
1
3
|
// ANSI color codes for terminal output
|
|
2
4
|
const Colors = {
|
|
3
5
|
Reset: "\x1b[0m",
|
|
@@ -15,23 +17,21 @@ const Colors = {
|
|
|
15
17
|
BrightYellow: "\x1b[93m",
|
|
16
18
|
White: "\x1b[37m",
|
|
17
19
|
};
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
EApplicationLogger["CRON_JOBS"] = "CRON_JOBS";
|
|
34
|
-
})(EApplicationLogger || (EApplicationLogger = {}));
|
|
20
|
+
const ELogLevel = {
|
|
21
|
+
DEBUG: "DEBUG",
|
|
22
|
+
INFO: "INFO",
|
|
23
|
+
WARN: "WARN",
|
|
24
|
+
ERROR: "ERROR",
|
|
25
|
+
};
|
|
26
|
+
const EApplicationLogger = {
|
|
27
|
+
GLOBAL: "GLOBAL",
|
|
28
|
+
ODOO_CLIENT: "ODOO_CLIENT",
|
|
29
|
+
ADAPTER: "ADAPTER",
|
|
30
|
+
REPOSITORIES: "REPOSITORIES",
|
|
31
|
+
USE_CASES: "USE_CASES",
|
|
32
|
+
QUEUE_PROCESSOR: "EVENT_BUS",
|
|
33
|
+
CRON_JOBS: "CRON_JOBS",
|
|
34
|
+
};
|
|
35
35
|
/**
|
|
36
36
|
* Color mapping for each logger context
|
|
37
37
|
*/
|
|
@@ -93,6 +93,9 @@ class BaseLogger {
|
|
|
93
93
|
if (this.shouldLog(level)) {
|
|
94
94
|
this.output(method, level, args);
|
|
95
95
|
}
|
|
96
|
+
// Remote push is independent of console filters — IS_PRODUCTION and ENABLED_LOGS
|
|
97
|
+
// control console verbosity, not what reaches Loki in production.
|
|
98
|
+
this.pushRemote(level, args);
|
|
96
99
|
}
|
|
97
100
|
shouldLog(level) {
|
|
98
101
|
if (IS_PRODUCTION)
|
|
@@ -105,17 +108,57 @@ class BaseLogger {
|
|
|
105
108
|
return false;
|
|
106
109
|
return true;
|
|
107
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* Remote logging filter — intentionally decoupled from console config.
|
|
113
|
+
* ERROR and WARN always reach Loki (production incidents must be observable).
|
|
114
|
+
* INFO reaches Loki only for enabled contexts (avoids noise from disabled ones).
|
|
115
|
+
* DEBUG never goes remote (too verbose for a push store).
|
|
116
|
+
* IS_PRODUCTION does NOT gate this — it controls the console, not telemetry.
|
|
117
|
+
*/
|
|
118
|
+
shouldLogRemote(level) {
|
|
119
|
+
if (level === ELogLevel.ERROR || level === ELogLevel.WARN)
|
|
120
|
+
return true;
|
|
121
|
+
if (level === ELogLevel.INFO)
|
|
122
|
+
return ENABLED_LOGS[this.context];
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
pushRemote(level, args) {
|
|
126
|
+
if (!this.shouldLogRemote(level))
|
|
127
|
+
return;
|
|
128
|
+
const client = getLokiClient();
|
|
129
|
+
if (!client)
|
|
130
|
+
return;
|
|
131
|
+
// Include context in the message so logs are filterable in Grafana
|
|
132
|
+
// without needing a high-cardinality label: `{app="..."} |= "[USE_CASES]"`
|
|
133
|
+
const prefix = this.context !== EApplicationLogger.GLOBAL ? `[${this.context}] ` : "";
|
|
134
|
+
const lvl = level.toLowerCase();
|
|
135
|
+
const traceLabel = activeTraceLabel();
|
|
136
|
+
queueMicrotask(() => client.push(lvl, prefix + this.serialize(args) + traceLabel));
|
|
137
|
+
}
|
|
108
138
|
output(method, level, args) {
|
|
109
139
|
const timestamp = new Date().toISOString();
|
|
110
140
|
const contextColor = this.getContextColor();
|
|
111
141
|
if (this.contextPrefix) {
|
|
112
|
-
// Colorear todo el mensaje con el color del contexto
|
|
113
142
|
method(`${Colors.Gray}${timestamp}${Colors.Reset}`, `${contextColor}${this.contextPrefix}`, ...args, Colors.Reset);
|
|
114
143
|
}
|
|
115
144
|
else {
|
|
116
145
|
method(`${Colors.Gray}${timestamp}${Colors.Reset}`, ...args);
|
|
117
146
|
}
|
|
118
147
|
}
|
|
148
|
+
serialize(args) {
|
|
149
|
+
const MAX = 2048;
|
|
150
|
+
const msg = args
|
|
151
|
+
.map((a) => {
|
|
152
|
+
try {
|
|
153
|
+
return typeof a === "string" ? a : JSON.stringify(a);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return "[unserializable]";
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
.join(" ");
|
|
160
|
+
return msg.length > MAX ? `${msg.slice(0, MAX)}…` : msg;
|
|
161
|
+
}
|
|
119
162
|
debug(...args) {
|
|
120
163
|
this.log(ELogLevel.DEBUG, console.debug, args);
|
|
121
164
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { LogQueueRepository } from "./logging/log_queue_repository";
|
|
2
|
+
import type { SpanQueueRepository } from "./tracing/span_queue_repository";
|
|
3
|
+
import type { TelemetryBufferStats } from "./types";
|
|
4
|
+
/** @internal — wired by initTelemetry so consumers can inspect buffer pressure. */
|
|
5
|
+
export declare function _registerBuffers(logQueue: LogQueueRepository | null, spanQueue: SpanQueueRepository | null): void;
|
|
6
|
+
/** @internal — test use only. */
|
|
7
|
+
export declare function _resetBuffers(): void;
|
|
8
|
+
/**
|
|
9
|
+
* Current size and drop pressure of the telemetry buffers.
|
|
10
|
+
*
|
|
11
|
+
* Use this to surface offline backlog in dashboards or health checks — a high
|
|
12
|
+
* `dropped` count means the device has been offline long enough to lose data.
|
|
13
|
+
* Returns null buffers when the corresponding signal is disabled.
|
|
14
|
+
*/
|
|
15
|
+
export declare function bufferStats(): Promise<TelemetryBufferStats>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
let _logQueue = null;
|
|
2
|
+
let _spanQueue = null;
|
|
3
|
+
/** @internal — wired by initTelemetry so consumers can inspect buffer pressure. */
|
|
4
|
+
export function _registerBuffers(logQueue, spanQueue) {
|
|
5
|
+
_logQueue = logQueue;
|
|
6
|
+
_spanQueue = spanQueue;
|
|
7
|
+
}
|
|
8
|
+
/** @internal — test use only. */
|
|
9
|
+
export function _resetBuffers() {
|
|
10
|
+
_logQueue = null;
|
|
11
|
+
_spanQueue = null;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Current size and drop pressure of the telemetry buffers.
|
|
15
|
+
*
|
|
16
|
+
* Use this to surface offline backlog in dashboards or health checks — a high
|
|
17
|
+
* `dropped` count means the device has been offline long enough to lose data.
|
|
18
|
+
* Returns null buffers when the corresponding signal is disabled.
|
|
19
|
+
*/
|
|
20
|
+
export async function bufferStats() {
|
|
21
|
+
const [logs, spans] = await Promise.all([
|
|
22
|
+
_logQueue?.stats() ?? Promise.resolve(null),
|
|
23
|
+
_spanQueue?.stats() ?? Promise.resolve(null),
|
|
24
|
+
]);
|
|
25
|
+
return { logs, spans };
|
|
26
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-correcting online/offline flag for the telemetry pipeline.
|
|
3
|
+
*
|
|
4
|
+
* The framework does NOT depend on the app's connectivity cron — that is
|
|
5
|
+
* client-specific and optional. Instead this flag is corrected from two cheap,
|
|
6
|
+
* always-available sources:
|
|
7
|
+
* - the outcome of each delivery attempt (success → online, network failure → offline)
|
|
8
|
+
* - optional connectivity events, when the host app emits them
|
|
9
|
+
*
|
|
10
|
+
* It exists to skip delivery attempts while known-offline (avoiding hung HTTP
|
|
11
|
+
* and wasted work); the send timeout in the HTTP clients is the backstop when
|
|
12
|
+
* the flag is optimistically wrong.
|
|
13
|
+
*/
|
|
14
|
+
export declare class ConnectivityState {
|
|
15
|
+
private online;
|
|
16
|
+
constructor(initiallyOnline?: boolean);
|
|
17
|
+
isOnline(): boolean;
|
|
18
|
+
markOnline(): void;
|
|
19
|
+
markOffline(): void;
|
|
20
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-correcting online/offline flag for the telemetry pipeline.
|
|
3
|
+
*
|
|
4
|
+
* The framework does NOT depend on the app's connectivity cron — that is
|
|
5
|
+
* client-specific and optional. Instead this flag is corrected from two cheap,
|
|
6
|
+
* always-available sources:
|
|
7
|
+
* - the outcome of each delivery attempt (success → online, network failure → offline)
|
|
8
|
+
* - optional connectivity events, when the host app emits them
|
|
9
|
+
*
|
|
10
|
+
* It exists to skip delivery attempts while known-offline (avoiding hung HTTP
|
|
11
|
+
* and wasted work); the send timeout in the HTTP clients is the backstop when
|
|
12
|
+
* the flag is optimistically wrong.
|
|
13
|
+
*/
|
|
14
|
+
export class ConnectivityState {
|
|
15
|
+
online;
|
|
16
|
+
constructor(initiallyOnline = true) {
|
|
17
|
+
this.online = initiallyOnline;
|
|
18
|
+
}
|
|
19
|
+
isOnline() {
|
|
20
|
+
return this.online;
|
|
21
|
+
}
|
|
22
|
+
markOnline() {
|
|
23
|
+
this.online = true;
|
|
24
|
+
}
|
|
25
|
+
markOffline() {
|
|
26
|
+
this.online = false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Context as OtelContext } from "@opentelemetry/api";
|
|
2
|
+
/**
|
|
3
|
+
* Key under which the active OTel Context (carrying the current Span) is
|
|
4
|
+
* stored inside the framework Context. Shared between tracing_hooks (writes)
|
|
5
|
+
* and active_span (reads) so both sides reference the same Symbol.
|
|
6
|
+
*/
|
|
7
|
+
export declare const OTEL_CTX_KEY: import("../context_manager/context_key").ContextKey<OtelContext>;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { createContextKey } from "../context_manager/index";
|
|
2
|
+
/**
|
|
3
|
+
* Key under which the active OTel Context (carrying the current Span) is
|
|
4
|
+
* stored inside the framework Context. Shared between tracing_hooks (writes)
|
|
5
|
+
* and active_span (reads) so both sides reference the same Symbol.
|
|
6
|
+
*/
|
|
7
|
+
export const OTEL_CTX_KEY = createContextKey("sincpro.telemetry.otel_context");
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Default send timeout — fail fast instead of hanging when the network is flaky/offline. */
|
|
2
|
+
export declare const DEFAULT_SEND_TIMEOUT_MS = 4000;
|
|
3
|
+
/**
|
|
4
|
+
* `fetch` with a hard timeout via AbortController. On timeout the request is
|
|
5
|
+
* aborted and the returned promise rejects, so an offline/slow network never
|
|
6
|
+
* blocks the telemetry pipeline indefinitely.
|
|
7
|
+
*/
|
|
8
|
+
export declare function fetchWithTimeout(input: string, init: RequestInit, timeoutMs?: number): Promise<Response>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Default send timeout — fail fast instead of hanging when the network is flaky/offline. */
|
|
2
|
+
export const DEFAULT_SEND_TIMEOUT_MS = 4000;
|
|
3
|
+
/**
|
|
4
|
+
* `fetch` with a hard timeout via AbortController. On timeout the request is
|
|
5
|
+
* aborted and the returned promise rejects, so an offline/slow network never
|
|
6
|
+
* blocks the telemetry pipeline indefinitely.
|
|
7
|
+
*/
|
|
8
|
+
export async function fetchWithTimeout(input, init, timeoutMs = DEFAULT_SEND_TIMEOUT_MS) {
|
|
9
|
+
const controller = new AbortController();
|
|
10
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
11
|
+
try {
|
|
12
|
+
return await fetch(input, { ...init, signal: controller.signal });
|
|
13
|
+
}
|
|
14
|
+
finally {
|
|
15
|
+
clearTimeout(timer);
|
|
16
|
+
}
|
|
17
|
+
}
|