Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Js logging setup, made api from all three languages more similar #20

Merged
merged 4 commits into from
Feb 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ jobs:
with:
node-version: "20"

- name: Install rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Install rust toolchain (doc builds use nightly features)
uses: dtolnay/rust-toolchain@nightly

- name: Install dependencies
run: |
Expand Down
2 changes: 1 addition & 1 deletion dev_scripts/docs.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ js_sub_build () {

# Builds the nested js site:
rust_sub_build () {
rm -rf ./docs/rust_ref && cargo doc --no-deps --manifest-path ./rust/Cargo.toml --target-dir ./docs/rust_ref
rm -rf ./docs/rust_ref && cargo +nightly doc --no-deps --manifest-path ./rust/Cargo.toml --target-dir ./docs/rust_ref --all-features
}

build () {
Expand Down
1 change: 1 addition & 0 deletions js/bitbazaar/log/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { GlobalLog, LOG, type LogLevel } from "./log";
63 changes: 63 additions & 0 deletions js/bitbazaar/log/log.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { describe, expect, it } from "bun:test";

import { GlobalLog, LOG, type LogLevel } from "bitbazaar/log";

describe("Logging/Tracing", () => {
it.each([
["DEBUG", ["DEBUG", "INFO", "WARN", "ERROR"]],
["INFO", ["INFO", "WARN", "ERROR"]],
["WARN", ["WARN", "ERROR"]],
["ERROR", ["ERROR"]],
[null, []],
])(
"Logs: min level %i should enable logs for: %i",
async (level_from_or_null: string | null, expected_enabled: string[]) => {
const logs: string[] = [];
new GlobalLog({
console: level_from_or_null
? {
level_from: level_from_or_null as LogLevel,
custom_out: (message, ...extra) => {
logs.push(message);
},
}
: undefined,
// Not using in this test, just can't disable:
otlp: {
endpoint: "http://localhost:4318",
// otlp can't be null, debug when null
level_from: "INFO",
service_name: "js-test",
service_version: "1.0.0",
},
});
LOG.debug("DEBUG");
LOG.info("INFO");
LOG.warn("WARN");
LOG.error("ERROR");
expect(logs).toEqual(expected_enabled);
},
);
it("Logs: oltp", async () => {
// Just confirm nothing errors, when trying to flush and measure output from tests etc seems to cause problems with bun test.
new GlobalLog({
otlp: {
endpoint: "http://localhost:4318",
level_from: "WARN",
service_name: "js-test",
service_version: "1.0.0",
},
});
const meter = LOG.meter("test");
const counter = meter.createCounter("test_counter");
counter.add(1);
LOG.withSpan("test", (span) => {
LOG.debug("DEBUG");
LOG.warn("WARN");
});
await LOG.withSpanAsync("test", async (span) => {
LOG.info("INFO");
LOG.error("ERROR");
});
});
});
291 changes: 291 additions & 0 deletions js/bitbazaar/log/log.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,291 @@
import { ZoneContextManager } from "@opentelemetry/context-zone";
import { type BufferConfig, type Tracer, WebTracerProvider } from "@opentelemetry/sdk-trace-web";

import {
CompositePropagator,
W3CBaggagePropagator,
W3CTraceContextPropagator,
} from "@opentelemetry/core";
import { DocumentLoadInstrumentation } from "@opentelemetry/instrumentation-document-load";
import { FetchInstrumentation } from "@opentelemetry/instrumentation-fetch";
import { UserInteractionInstrumentation } from "@opentelemetry/instrumentation-user-interaction";

import type { Span } from "@opentelemetry/api";
import { Resource } from "@opentelemetry/resources";
import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions";

import { registerInstrumentations } from "@opentelemetry/instrumentation";

import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";

import type { LogAttributes, Logger } from "@opentelemetry/api-logs";
import { BatchLogRecordProcessor, LoggerProvider } from "@opentelemetry/sdk-logs";

import type { Meter, MeterOptions } from "@opentelemetry/api";
import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import type { OTLPExporterNodeConfigBase } from "@opentelemetry/otlp-exporter-base";
import { MeterProvider, PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";

// Create a new proxy with a handler
let _LOG: GlobalLog | undefined;

export type LogLevel = "DEBUG" | "INFO" | "WARN" | "ERROR";

interface ConsoleArgs {
level_from: LogLevel;
custom_out?: (message: string, ...optionalParams: any[]) => void;
}

interface OltpArgs {
level_from: LogLevel;
endpoint: string; // E.g. "/otlp or http://localhost:4317/otlp"
service_name: string; // E.g. "web"
service_version: string; // E.g. "1.0.0"
}

class GlobalLog {
loggerProvider: LoggerProvider;
logger: Logger;
tracerProvider: WebTracerProvider;
tracer: Tracer;
meterProvider: MeterProvider;

console: ConsoleArgs | null;
oltp: OltpArgs;

/**
* Get a new Meter instance to record metrics with.
*
* @example
* ```typescript
* const meter = globalLog.getMeter("example-meter");
* const counter = meter.createCounter('metric_name');
* counter.add(10, { 'key': 'value' });
* ```
*/
meter(
name: string,
opts:
| MeterOptions
| {
version?: string; // The version of the meter.
} = {},
): Meter {
return this.meterProvider.getMeter(
name,
typeof opts === "object" && "version" in opts ? opts.version : undefined,
opts as MeterOptions,
);
}

/**
* Run a sync callback inside a span.
*/
withSpan<T>(name: string, cb: (span) => T): T {
return this.tracer.startActiveSpan(name, (span: Span) => {
const result = cb(span);
span.end();
return result;
});
}

/**
* Run an async callback inside a span.
*/
withSpanAsync<T>(name: string, cb: (span) => Promise<T>): Promise<T> {
return this.tracer.startActiveSpan(name, (span: Span) => {
return cb(span).then((result) => {
span.end();
return result;
});
});
}

/** Log a debug message. */
debug(message: string, attributes?: LogAttributes) {
this._log_inner("DEBUG", message, attributes);
}

/** Log an info message. */
info(message: string, attributes?: LogAttributes) {
this._log_inner("INFO", message, attributes);
}

/** Log a warning message. */
warn(message: string, attributes?: LogAttributes) {
this._log_inner("WARN", message, attributes);
}

/** Log an error message. */
error(message: string, attributes?: LogAttributes) {
this._log_inner("ERROR", message, attributes);
}

_log_inner(severityText: LogLevel, message: string, attributes: LogAttributes | undefined) {
// Log to console if enabled:
if (this.console) {
let emit = false;
let emitter: Exclude<ConsoleArgs["custom_out"], undefined>;
switch (this.console.level_from) {
case "DEBUG": {
emit = true;
emitter = console.debug;
break;
}
case "INFO": {
emit = severityText !== "DEBUG";
emitter = console.info;
break;
}
case "WARN": {
emit = severityText === "WARN" || severityText === "ERROR";
emitter = console.warn;
break;
}
case "ERROR": {
emit = severityText === "ERROR";
emitter = console.error;
break;
}
}

if (emit) {
if (this.console.custom_out) {
emitter = this.console.custom_out;
}
emitter(message, attributes);
}
}

// Emit to oltp if log level meets level_from:
let emitOltp = false;
switch (this.oltp.level_from) {
case "DEBUG": {
emitOltp = true;
break;
}
case "INFO": {
emitOltp = severityText !== "DEBUG";
break;
}
case "WARN": {
emitOltp = severityText === "WARN" || severityText === "ERROR";
break;
}
case "ERROR": {
emitOltp = severityText === "ERROR";
break;
}
}
if (emitOltp) {
this.logger.emit({
severityText,
body: message,
attributes,
});
}
}

/** Create the global logger, must setup oltp (http), console can be optionally setup and will just print logs. */
constructor({
otlp,
console = undefined,
}: {
console?: ConsoleArgs;
otlp: OltpArgs;
}) {
this.console = console ? console : null;
this.oltp = otlp;

const resource = new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: otlp.service_name,
[SemanticResourceAttributes.SERVICE_VERSION]: otlp.service_version,
});

// Url will be added for each usage, different for traces/logs/metrics
const baseExporterConfig: OTLPExporterNodeConfigBase = {
keepAlive: true,
};
const bufferConfig: BufferConfig = {
maxQueueSize: 2048,
maxExportBatchSize: 512,
scheduledDelayMillis: 5000,
exportTimeoutMillis: 30000,
};

this.meterProvider = new MeterProvider({
resource,
readers: [
new PeriodicExportingMetricReader({
exporter: new OTLPMetricExporter({
...baseExporterConfig,
url: `${otlp.endpoint}/v1/metrics`,
}),
exportIntervalMillis: 1000,
}),
],
});

this.loggerProvider = new LoggerProvider({
resource,
});
this.loggerProvider.addLogRecordProcessor(
new BatchLogRecordProcessor(
new OTLPLogExporter({
...baseExporterConfig,
url: `${otlp.endpoint}/v1/logs`,
}),
),
);
this.logger = this.loggerProvider.getLogger("GlobalLog");

this.tracerProvider = new WebTracerProvider({ resource });
this.tracerProvider.addSpanProcessor(
new BatchSpanProcessor(
new OTLPTraceExporter({
...baseExporterConfig,
url: `${otlp.endpoint}/v1/traces`,
}),
bufferConfig,
),
);
// Enable auto-context propagation within the application using zones:
this.tracerProvider.register({
contextManager: new ZoneContextManager(),
// Configure the propagator to enable context propagation between services,
// uses the W3C Trace Headers (traceparent, tracestate) and W3C Baggage Headers (baggage).
propagator: new CompositePropagator({
propagators: [new W3CBaggagePropagator(), new W3CTraceContextPropagator()],
}),
});
this.tracer = this.tracerProvider.getTracer("GlobalLog");

registerInstrumentations({
instrumentations: [
// Trace user site interactions:
new UserInteractionInstrumentation({}),
// Trace client document loading:
new DocumentLoadInstrumentation({}),
// Auto instrument fetch requests:
new FetchInstrumentation({}),
],
});

// Register it as the current global logger:
_LOG = this;
}
}

/* The global accessor for logging, will use the active global logger: */
export const LOG = new Proxy(GlobalLog.prototype, {
get: (target, property, receiver) => {
if (_LOG) {
return Reflect.get(_LOG, property, receiver);
}
throw new Error("Global log not yet initialized!");
},
});

export { GlobalLog };
5 changes: 4 additions & 1 deletion js/bitbazaar/vite/vite.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,10 @@ describe("Vite", () => {
// Check files gone from assetsPath:
expect(fs.readdir(`${assetsPath}`)).resolves.toEqual([]);
// Check files in sameDomStaticPath:
expect(fs.readdir(sameDomStaticPath)).resolves.toEqual(["site_index.html", "sworker"]);
const files = await fs.readdir(sameDomStaticPath);
// Need to sort to prevent flakiness:
files.sort();
expect(files).toEqual(["site_index.html", "sworker"]);
// Confirm sworker contains webmanifest:
expect(
fs.readFile(`${sameDomStaticPath}/sworker/manifest.webmanifest`, "utf-8"),
Expand Down
Loading
Loading