Refactor code structure for improved readability and maintainability

This commit is contained in:
Firman Ramdhani
2026-05-22 17:53:35 +07:00
parent af0fe70ac6
commit 4260e240a0
37 changed files with 2915 additions and 74 deletions
@@ -0,0 +1,5 @@
export type { IObservabilityAdapter } from './types';
export { noopObservabilityAdapter } from './noop.adapter';
export { faroAdapter } from './otel.adapter';
export { initTelemetry, getFaro } from './setup';
export type { TelemetryConfig } from './setup';
@@ -0,0 +1,17 @@
import type { IObservabilityAdapter } from './types';
/**
* No-Op Observability Adapter.
*
* Default adapter when no telemetry is configured.
* All methods are empty — V8's TurboFan JIT compiler will inline
* and dead-code-eliminate these calls during optimization,
* resulting in effectively ZERO runtime overhead.
*
* Used by apps that don't need APM (e.g., `apps/landing`).
*/
export const noopObservabilityAdapter: IObservabilityAdapter = {
onRequestStart() {},
onRequestEnd() {},
onRequestError() {},
};
@@ -0,0 +1,212 @@
import { trace, SpanStatusCode, type Span } from '@opentelemetry/api';
import { LogLevel } from '@grafana/faro-web-sdk';
import type { IObservabilityAdapter } from './types';
import type { InternalAxiosRequestConfig, AxiosResponse, AxiosError } from 'axios';
import type { TelemetryContext } from '../http-client/types';
import { getFaro } from './setup';
// ─── Symbol Keys ────────────────────────────────────────────────
/** Symbol-keyed storage for custom spans on the Axios config. */
const CUSTOM_SPAN_KEY = Symbol('__customOtelSpan');
function attachSpan(config: InternalAxiosRequestConfig, span: Span): void {
(config as unknown as Record<symbol, Span>)[CUSTOM_SPAN_KEY] = span;
}
function getSpan(config: unknown): Span | undefined {
if (!config) return undefined;
return (config as Record<symbol, Span>)?.[CUSTOM_SPAN_KEY];
}
/**
* Safely close a span, guarding against double-close.
* After ending, removes the reference from the config to prevent
* duplicate Span ID errors on potential Axios retries.
*/
function safeEndSpan(config: unknown, span: Span): void {
span.end();
// Detach from config to prevent double-close on retry
if (config) {
delete (config as Record<symbol, unknown>)[CUSTOM_SPAN_KEY];
}
}
/** Extract TelemetryContext from an Axios config. */
function getTelemetryContext(config: unknown): TelemetryContext | undefined {
if (!config) return undefined;
return (config as Record<string, unknown>)?.telemetryContext as TelemetryContext | undefined;
}
// ─── Helpers ────────────────────────────────────────────────────
/** Convert TelemetryContext tags to a string record for Faro context. */
function tagsToFaroContext(tags?: Record<string, string | number | boolean>): Record<string, string> {
if (!tags) return {};
return Object.fromEntries(
Object.entries(tags).map(([k, v]) => [k, String(v)]),
);
}
/**
* Build the standardized base context used by ALL Faro pushLog/pushError calls.
* Ensures `module.key`, `module.action`, and tags are always at the top-level
* `context` object — making them directly queryable in LogQL (Loki).
*/
function buildBaseContext(
method: string,
url: string,
moduleKey?: string,
action?: string,
tags?: Record<string, string | number | boolean>,
): Record<string, string> {
return {
'http.method': method,
'http.url': url,
...(moduleKey ? { 'module.key': moduleKey } : {}),
...(action ? { 'module.action': action } : {}),
...tagsToFaroContext(tags),
};
}
// ─── Adapter ────────────────────────────────────────────────────
/**
* Production-grade Observability Adapter.
*
* Strategy: **Opt-In Custom Spans + Faro/Loki Baseline Logging**
*
* `trace.getActiveSpan()` returns `undefined` inside Axios interceptors
* due to browser XHR/Fetch lifecycle race conditions with Faro's
* `TracingInstrumentation`. Therefore this adapter does NOT attempt
* to enrich auto-instrumented spans.
*
* Instead it focuses on two responsibilities:
*
* 1. **Custom Span Mode** (opt-in via `telemetryContext.customSpanName`):
* Creates an explicit OTel span, attaches business tags, and ensures
* the span is ALWAYS closed — even on abort, timeout, or unexpected
* errors — to prevent span leaks.
*
* 2. **Faro/Loki Baseline** (always):
* Pushes rich contextual logs (`pushLog`), errors (`pushError`), and
* success events (`pushEvent`) with standardized `baseContext` for
* direct LogQL queryability.
*
* Safety guarantees:
* - Spans are always closed via `safeEndSpan()` which detaches the
* reference after closing, preventing double-close on Axios retries.
* - Adapter errors are caught internally and never swallowed — the
* original API error always propagates to the UI.
* - `null`/`undefined` config guards prevent crashes on network timeouts
* where `error.config` may be undefined.
*/
export const faroAdapter: IObservabilityAdapter = {
onRequestStart(config: InternalAxiosRequestConfig) {
const ctx = getTelemetryContext(config);
const method = (config.method ?? 'UNKNOWN').toUpperCase();
const url = config.url ?? '/';
const moduleKey = config.headers?.['ex-module-key'] as string | undefined;
const action = config.headers?.['ex-module-action'] as string | undefined;
// ── 1. Create optional custom span ──────────────────────────
if (ctx?.customSpanName) {
const tracer = trace.getTracer('@repo/core-api', '1.0.0');
const span = tracer.startSpan(ctx.customSpanName, {
attributes: {
'http.method': method,
'http.url': url,
...(moduleKey ? { 'custom.module_key': moduleKey } : {}),
...(action ? { 'custom.module_action': action } : {}),
},
});
// Attach custom tags with `custom.` prefix
if (ctx.tags) {
for (const [key, value] of Object.entries(ctx.tags)) {
span.setAttribute(`custom.${key}`, value);
}
}
attachSpan(config, span);
}
// ── 2. Push structured log (Loki) ───────────────────────────
const faro = getFaro();
if (faro) {
const baseContext = buildBaseContext(method, url, moduleKey, action, ctx?.tags);
faro.api.pushLog(
[`[core-api] ${method} ${url}`],
{ level: LogLevel.DEBUG, context: baseContext },
);
}
},
onRequestEnd(response: AxiosResponse) {
const ctx = getTelemetryContext(response.config);
const moduleKey = response.config?.headers?.['ex-module-key'] as string | undefined;
const action = response.config?.headers?.['ex-module-action'] as string | undefined;
// ── 1. Close custom span (OK) ───────────────────────────────
const customSpan = getSpan(response.config);
if (customSpan) {
customSpan.setAttribute('http.status_code', response.status);
customSpan.setStatus({ code: SpanStatusCode.OK });
safeEndSpan(response.config, customSpan);
}
// ── 2. Push success event (Faro) ────────────────────────────
if (ctx?.pushEventOnSuccess) {
const faro = getFaro();
if (faro) {
const method = (response.config?.method ?? 'UNKNOWN').toUpperCase();
const url = response.config?.url ?? '/';
const baseContext = buildBaseContext(method, url, moduleKey, action, ctx.tags);
faro.api.pushEvent(ctx.pushEventOnSuccess, {
...baseContext,
'http.status_code': String(response.status),
});
}
}
},
onRequestError(error: AxiosError) {
const ctx = getTelemetryContext(error.config);
const status = error.response?.status ?? 0;
const method = (error.config?.method ?? 'UNKNOWN').toUpperCase();
const url = error.config?.url ?? '/';
const moduleKey = error.config?.headers?.['ex-module-key'] as string | undefined;
const action = error.config?.headers?.['ex-module-action'] as string | undefined;
// Standardized context for ALL Faro calls in this handler
const baseContext = buildBaseContext(method, url, moduleKey, action, ctx?.tags);
const errorContext: Record<string, string> = {
...baseContext,
'http.status_code': String(status),
'error.message': error.message,
};
// ── 1. Close custom span with error (if present) ────────────
const customSpan = getSpan(error.config);
if (customSpan) {
customSpan.setAttribute('http.status_code', status);
customSpan.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
customSpan.recordException(error);
safeEndSpan(error.config, customSpan);
}
// ── 2. Push structured error + log (Faro → Loki) ────────────
const faro = getFaro();
if (faro) {
faro.api.pushError(error, {
type: 'api_error',
context: errorContext,
});
faro.api.pushLog(
[`[core-api] ERROR ${method} ${url}${status}`],
{ level: LogLevel.ERROR, context: errorContext },
);
}
},
};
@@ -0,0 +1,151 @@
/**
* Centralized Telemetry Setup — Grafana Faro + OpenTelemetry.
*
* Provides a plug-and-play `initTelemetry()` function that hides
* all Faro/OTel complexity behind a simple config interface.
*
* @example
* ```ts
* // apps/web/src/main.tsx (top of file)
* import { initTelemetry } from '@repo/core-api/observability/setup';
* initTelemetry({
* appName: 'web',
* appVersion: '1.0.0',
* telemetryUrl: 'https://telemetry.eigen.co.id/collect',
* environment: 'production',
* });
* ```
*/
import {
getWebInstrumentations,
initializeFaro,
type Faro,
} from '@grafana/faro-react';
import { TracingInstrumentation } from '@grafana/faro-web-tracing';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-web';
// ─── Configuration Interface ────────────────────────────────────
/**
* Configuration for initializing the full telemetry stack.
* Apps pass this once at startup — everything else is automatic.
*/
export interface TelemetryConfig {
/** Application name for Faro + OTel resource attributes. */
appName: string;
/** Application version (SemVer). */
appVersion: string;
/** Grafana Faro collector URL (e.g., 'https://telemetry.eigen.co.id/collect'). */
telemetryUrl: string;
/** Deployment environment ('production', 'staging', 'development'). */
environment: string;
/**
* Optional: Separate OTLP trace endpoint for direct Tempo ingestion.
* If not provided, traces are only sent through Faro's built-in exporter.
*/
otlpTraceUrl?: string;
/**
* Optional: CORS URL patterns for W3C trace context propagation.
* @default [/.* /]
*/
propagateTraceHeaderCorsUrls?: Array<string | RegExp>;
}
// ─── Session Persistence ────────────────────────────────────────
const FARO_SESSION_KEY = 'faroSession';
function getStoredSession(): Record<string, unknown> | null {
try {
const data = localStorage.getItem(FARO_SESSION_KEY);
return data ? JSON.parse(data) : null;
} catch {
return null;
}
}
// ─── Singleton ──────────────────────────────────────────────────
let faroInstance: Faro | null = null;
/**
* Initializes the Grafana Faro + OpenTelemetry observability stack.
*
* Call this ONCE at the top of your app's entry point, before any
* React code, HTTP requests, or other imports execute.
*
* `TracingInstrumentation` internally handles:
* - WebTracerProvider setup with resource attributes
* - FaroMetaAttributesSpanProcessor (session/user enrichment)
* - FaroTraceExporter (sends spans to the Faro collector)
* - Auto-instrumentation for fetch/XHR
* - `faro.api.initOTEL(trace, context)` bridge
*
* @returns The initialized Faro instance for advanced usage.
*/
export function initTelemetry(config: TelemetryConfig): Faro {
if (faroInstance) return faroInstance;
const storedSession = getStoredSession();
// Build optional extra span processors
const tracingOptions: Record<string, unknown> = {};
if (config.otlpTraceUrl) {
tracingOptions.spanProcessor = new BatchSpanProcessor(
new OTLPTraceExporter({
url: config.otlpTraceUrl,
headers: {},
}),
);
}
faroInstance = initializeFaro({
url: config.telemetryUrl,
app: {
name: config.appName,
version: config.appVersion,
environment: config.environment,
},
sessionTracking: {
enabled: true,
persistent: true,
session: storedSession ?? undefined,
onSessionChange: (_oldSession, newSession) => {
if (newSession) {
localStorage.setItem(FARO_SESSION_KEY, JSON.stringify(newSession));
}
},
},
instrumentations: [
...getWebInstrumentations(),
new TracingInstrumentation({
...tracingOptions,
instrumentationOptions: {
propagateTraceHeaderCorsUrls:
config.propagateTraceHeaderCorsUrls ?? [/.*/],
fetchInstrumentationOptions: {
applyCustomAttributesOnSpan(span) {
span.setAttribute('app.synthetic_request', 'false');
},
},
xhrInstrumentationOptions: {
applyCustomAttributesOnSpan(span) {
span.setAttribute('app.synthetic_request', 'false');
},
},
},
}),
],
});
return faroInstance;
}
/** Returns the Faro instance (null if not yet initialized). */
export function getFaro(): Faro | null {
return faroInstance;
}
@@ -0,0 +1,22 @@
import type { InternalAxiosRequestConfig, AxiosResponse, AxiosError } from 'axios';
/**
* Interface-driven observability contract.
*
* The HTTP client calls these hooks at request lifecycle points.
* Implementations decide whether to trace, log, metric, or do nothing.
*
* This interface is the ONLY dependency between the HTTP client and
* any telemetry library. The core-api package NEVER imports
* OpenTelemetry, Datadog, Sentry, or any vendor SDK directly.
*/
export interface IObservabilityAdapter {
/** Called immediately before a request is dispatched. */
onRequestStart(config: InternalAxiosRequestConfig): void;
/** Called when a response is successfully received. */
onRequestEnd(response: AxiosResponse): void;
/** Called when a request fails (network error or error status). */
onRequestError(error: AxiosError): void;
}