@rbxts/rlog
Version:
Context-based server-side logging solution for ROBLOX projects.
361 lines (360 loc) • 11.1 kB
TypeScript
/**
* @license
* Copyright 2024 Daymon Littrell-Reyes
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { LogEnricherCallback, LogLevel, LogSinkCallback } from "../common";
/**
* Configuration settings for serialization.
*
* @see {@link RLogConfig}
*
* @public
*/
export interface SerializationConfig {
/**
* Whether to encode Roblox-specific types.
*
* @remarks
*
* When this setting is disabled, all roblox-specific types will
* instead just be represented as `"<TYPE_NAME>"`.
*
* @defaultValue `true`
*
* @example
* ```ts
* logger.i("Player died", { player: player, location: player.Position });
* // > [INFO]: Player died
* // > { "data": { "player": 1338, "location": "<Vector3>" } }
* ```
*/
readonly encodeRobloxTypes: boolean;
/**
* Whether to encode function types.
*
* @remarks
*
* When this setting is disabled, all function types will
* be represented as `"<Function>"`. Otherwise, they'll be excluded
* from the outputted JSON.
*
* @defaultValue `false`
*
* @example
* ```ts
* function createPlayer(name: string) {
* return {
* name: name,
* eatFood: () => {
* // ...
* }
* }
* }
*
* const player = createPlayer("daymon");
*
* let logger = new RLog({ serialization: { encodeFunctions: true } });
*
* logger.i("Player created", { player: player });
* // > [INFO]: Player created
* // > { "data": { "player": { "name": "daymon", "eatFood": "<Function>" } } }
*
* logger = new RLog({ serialization: { encodeFunctions: false } });
*
* logger.i("Player created", { player: player });
* // > [INFO]: Player created
* // > { "data": { "player": { "name": "daymon" } } }
* ```
*/
readonly encodeFunctions: boolean;
/**
* Whether to perform deep encoding on tables.
*
* @remarks
*
* When disabled, tables will not be recursively encoded, which may cause you
* to miss out on certain data types being properly translated (e.g., roblox data types).
*
* This will occur even if you have {@link SerializationConfig.encodeRobloxTypes | encodeRobloxTypes}
* enabled.
*
* But if you have deeply nested data types, or are wanting to save on performance,
* this may be desirable.
*
* @defaultValue `true`
*
* @example
* ```
* const event = {
* source: {
* position: new Vector2(1, 1),
* distance: 100
* },
* target: new Vector2(2, 2),
* };
*
* let logger = new RLog({ serialization: { deepEncodeTables: true } });
* logger.i("Gun was fired", event);
* // > [INFO]: Gun was fired
* // > { "data": { "source": { "position": { "X": 1, "Y": 1}, "distance": 100 }, "target": { "X": 2, "Y": 2} } }
*
* logger = new RLog({ serialization: { deepEncodeTables: false } });
* logger.i("Gun was fired", event);
* // > [INFO]: Gun was fired
* // > { "data": { "source": { "position": null, "distance": 100 }, "target": { "X": 2, "Y": 2} } }
* ```
*/
readonly deepEncodeTables: boolean;
/**
* The method name to use for class encoding.
*
* @remarks
*
* When encoding an object, the encoder will first check if the object has
* a method with this name. If it does, it will call that method instead of
* trying to manually encode it.
*
* @defaultValue `__tostring`
*
* @example
* ```ts
* class PlayerClass {
* constructor(public name: string) {};
*
* public encode() {
* return { name: this.name };
* }
* }
*
* const player = new PlayerClass("daymon");
*
* let logger = new RLog();
*
* logger.i("Player created", { player: player });
* // > [INFO]: Player created
* // > { "data": { "player": "PlayerClass" } }
*
* logger = new RLog({ serialization: { encodeMethod: "encode" } });
*
* logger.i("Player created", { player: player });
* // > [INFO]: Player created
* // > { "data": { "player": { "name": "daymon" } } }
* ```
*/
readonly encodeMethod: string;
}
/**
* Default configuration for serialization.
*
* @internal
*/
export declare const defaultSerializationConfig: Readonly<SerializationConfig>;
/**
* Configuration settings for {@link RLog}.
*
* @public
*/
export interface RLogConfig {
/**
* Sets the minimum {@link LogLevel} for data to be logged.
*
* Messages below the minimum level will be ignored.
*
* @defaultValue If in studio {@link LogLevel.VERBOSE | VERBOSE}, else {@link LogLevel.WARNING | WARNING}
*
* @example
* ```ts
* let logger = new RLog();
*
* logger.v("Hello verbose!");
* logger.d("Hello debug!");
* // > [VERBOSE]: Hello verbose!
* // > [DEBUG]: Hello debug!
*
* logger = logger.withMinLogLevel(LogLevel.DEBUG);
* logger.v("Hello verbose!");
* logger.d("Hello debug!");
* // > [DEBUG]: Hello debug!
* ```
*/
readonly minLogLevel: LogLevel;
/** Settings to use when encoding {@link LogEntry.data | data} in logs. */
readonly serialization: SerializationConfig;
/**
* Function to generate correlation IDs.
*
* @remarks
*
* By default, Correlation IDs are generated via a combination of
* {@link https://create.roblox.com/docs/en-us/reference/engine/classes/HttpService#GenerateGUID | HttpService.GenerateGUID }
* and the current time- to avoid conflicts.
*
* If you specify your own function, it will be called anytime a
* new Correlation ID is requested.
*
* Especially useful if you want to create Correlation IDs to match
* ids in your external database.
*
* @example
* ```ts
* function generateCorrelationID(): string {
* return "1";
* }
*
* const config = { correlationGenerator: generateCorrelationID };
*
* withLogContext(config, (context) => {
* const logger = context.use();
* logger.i("Player created", { player: player });
* });
* // > [INFO]: Player created
* // > { "correlation_id": "1", "data": { "player": 1338 } }
* ```
*/
readonly correlationGenerator?: () => string;
/**
* String to prefix to all logs.
*
* Will be followed by a `->` between the log message and the log level.
*
* @remarks
*
* This setting is ignored when merging configs.
*
* @example
* ```ts
* const logger = new RLog({ tag: "Main" });
*
* logger.i("Hello world!");
* // > [INFO]: Main -> Hello world!
* ```
*/
readonly tag?: string;
/**
* An array of {@link LogSinkCallback} to call whenever sending a message.
*/
readonly sinks?: LogSinkCallback[];
/**
* An array of {@link LogEnricherCallback} to call whenever sending a message.
*/
readonly enrichers?: LogEnricherCallback[];
/**
* Allows logs that have context to bypass {@link RLogConfig.minLogLevel | minLogLevel} under certain
* circumstances.
*
* @remarks
*
* With this setting enabled, even if the {@link RLogConfig.minLogLevel | minLogLevel} is set
* to filter out logs below {@link LogLevel.WARNING | WARNING}, if one of the logs in the context
* is that of {@link LogLevel.WARNING | WARNING} or above, then _all_ of the logs in the context
* will be sent through.
*
* Allows you to set a high {@link RLogConfig.minLogLevel | minLogLevel} without sacrificing
* a proper log trace whenever something bad happens.
*
* @defaultValue `false`
*
* @see {@link RLogConfig.suspendContext | suspendContext}
*
* @example
* ```ts
* const config = { minLogLevel: LogLevel.DEBUG, contextBypass: true }
*
* withLogContext(config, (context) => {
* const logger = context.use();
*
* logger.i("Hello world!");
* logger.w("Oh no!");
* logger.i("Goodbye world!");
* });
* // > [WARNING]: Oh no!
* // > { correlation_id: "QQLRSFsPfoTfgD7b" }
* //
* // > [INFO]: Hello world!
* // > { correlation_id: "QQLRSFsPfoTfgD7b" }
* //
* // > [INFO]: Goodbye world!
* // > { correlation_id: "QQLRSFsPfoTfgD7b" }
* ```
*/
readonly contextBypass: boolean;
/**
* Prevents logs from propogating until the context is killed.
*
* @remarks
*
* With this setting enabled, logs with context will not be sent until the context is stopped.
*
* All of the messages will be sent at once when the context is stopped.
*
* Can be used in tangent with {@link RLogConfig.contextBypass | contextBypass} to
* retain log order.
*
* @defaultValue `false`
*
* @example
* ```ts
* const config = { minLogLevel: LogLevel.DEBUG, suspendContext: true }
*
* withLogContext(config, (context) => {
* const logger = context.use();
*
* logger.i("Hello world!");
* logger.w("Oh no!");
* logger.i("Goodbye world!");
* });
* // > [INFO]: Hello world!
* // > { correlation_id: "QQLRSFsPfoTfgD7b" }
* //
* // > [WARNING]: Oh no!
* // > { correlation_id: "QQLRSFsPfoTfgD7b" }
* //
* // > [INFO]: Goodbye world!
* // > { correlation_id: "QQLRSFsPfoTfgD7b" }
* ```
*/
readonly suspendContext: boolean;
}
/**
* Default configuration for {@link RLog}.
*
* @internal
*/
export declare const defaultRLogConfig: Readonly<RLogConfig>;
/**
* Version of {@link RLogConfig} that allows all data to be absent.
*
* @public
*/
export type PartialRLogConfig = Partial<ExcludeMembers<RLogConfig, SerializationConfig>> & {
readonly serialization?: Partial<SerializationConfig>;
};
/**
* Merges a variable amount of config files.
*
* @remarks
*
* Configs that come later take precedence over those before.
*
* Uses the {@link defaultRLogConfig | default} config as a baseline.
*
* @param configs - A variable list of config files to merge together.
*
* @returns The merged config files.
*
* @internal
*/
export declare function mergeConfigs(...configs: ReadonlyArray<PartialRLogConfig | undefined>): RLogConfig;
//# sourceMappingURL=index.d.ts.map