From 730125ab23150f72a9863506e1af94a9259e262c Mon Sep 17 00:00:00 2001 From: Hector Hernandez <39923391+hectorhdzg@users.noreply.github.com> Date: Thu, 11 Jul 2024 17:09:33 -0700 Subject: [PATCH 1/2] Integrate @opentelemetry/api-logs package into @opentelemetry/api --- .gitignore | 4 +- api/src/experimental/logs/NoopLogger.ts | 24 +++++ .../experimental/logs/NoopLoggerProvider.ts | 32 +++++++ api/src/experimental/logs/api/logs.ts | 77 ++++++++++++++++ api/src/experimental/logs/index.ts | 26 ++++++ api/src/experimental/logs/types/AnyValue.ts | 29 +++++++ api/src/experimental/logs/types/LogRecord.ts | 87 +++++++++++++++++++ api/src/experimental/logs/types/Logger.ts | 26 ++++++ .../experimental/logs/types/LoggerOptions.ts | 36 ++++++++ .../experimental/logs/types/LoggerProvider.ts | 34 ++++++++ api/src/index.ts | 17 +++- api/src/internal/global-utils.ts | 2 + api/test/common/logs/logs.test.ts | 69 +++++++++++++++ .../noop-logger-provider.test.ts | 35 ++++++++ .../noop-implementations/noop-logger.test.ts | 35 ++++++++ 15 files changed, 530 insertions(+), 3 deletions(-) create mode 100644 api/src/experimental/logs/NoopLogger.ts create mode 100644 api/src/experimental/logs/NoopLoggerProvider.ts create mode 100644 api/src/experimental/logs/api/logs.ts create mode 100644 api/src/experimental/logs/index.ts create mode 100644 api/src/experimental/logs/types/AnyValue.ts create mode 100644 api/src/experimental/logs/types/LogRecord.ts create mode 100644 api/src/experimental/logs/types/Logger.ts create mode 100644 api/src/experimental/logs/types/LoggerOptions.ts create mode 100644 api/src/experimental/logs/types/LoggerProvider.ts create mode 100644 api/test/common/logs/logs.test.ts create mode 100644 api/test/common/noop-implementations/noop-logger-provider.test.ts create mode 100644 api/test/common/noop-implementations/noop-logger.test.ts diff --git a/.gitignore b/.gitignore index 6c3473bdbd0..ca7c212ac15 100644 --- a/.gitignore +++ b/.gitignore @@ -2,8 +2,8 @@ version.ts -# Logs -logs +# Logs in root +/logs *.log npm-debug.log* yarn-debug.log* diff --git a/api/src/experimental/logs/NoopLogger.ts b/api/src/experimental/logs/NoopLogger.ts new file mode 100644 index 00000000000..b3d092167f8 --- /dev/null +++ b/api/src/experimental/logs/NoopLogger.ts @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * + * 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 + * + * https://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 { Logger } from './types/Logger'; +import { LogRecord } from './types/LogRecord'; + +export class NoopLogger implements Logger { + emit(_logRecord: LogRecord): void {} +} + +export const NOOP_LOGGER = new NoopLogger(); diff --git a/api/src/experimental/logs/NoopLoggerProvider.ts b/api/src/experimental/logs/NoopLoggerProvider.ts new file mode 100644 index 00000000000..aea947ed809 --- /dev/null +++ b/api/src/experimental/logs/NoopLoggerProvider.ts @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * + * 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 + * + * https://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 { LoggerProvider } from './types/LoggerProvider'; +import { Logger } from './types/Logger'; +import { LoggerOptions } from './types/LoggerOptions'; +import { NoopLogger } from './NoopLogger'; + +export class NoopLoggerProvider implements LoggerProvider { + getLogger( + _name: string, + _version?: string | undefined, + _options?: LoggerOptions | undefined + ): Logger { + return new NoopLogger(); + } +} + +export const NOOP_LOGGER_PROVIDER = new NoopLoggerProvider(); diff --git a/api/src/experimental/logs/api/logs.ts b/api/src/experimental/logs/api/logs.ts new file mode 100644 index 00000000000..096475fe652 --- /dev/null +++ b/api/src/experimental/logs/api/logs.ts @@ -0,0 +1,77 @@ +/* + * Copyright The OpenTelemetry Authors + * + * 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 + * + * https://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 { LoggerProvider } from '../types/LoggerProvider'; +import { NOOP_LOGGER_PROVIDER } from '../NoopLoggerProvider'; +import { Logger } from '../types/Logger'; +import { LoggerOptions } from '../types/LoggerOptions'; +import { + getGlobal, + registerGlobal, + unregisterGlobal, +} from '../../../internal/global-utils'; +import { DiagAPI } from '../../../api/diag'; + +const API_NAME = 'logs'; + +export class LogsAPI { + private static _instance?: LogsAPI; + + private constructor() {} + + public static getInstance(): LogsAPI { + if (!this._instance) { + this._instance = new LogsAPI(); + } + + return this._instance; + } + + /** + * Set the current global logger provider. + * Returns true if the logger provider was successfully registered, else false. + */ + public setGlobalLoggerProvider(provider: LoggerProvider): boolean { + return registerGlobal(API_NAME, provider, DiagAPI.instance()); + } + + /** + * Returns the global logger provider. + * + * @returns LoggerProvider + */ + public getLoggerProvider(): LoggerProvider { + return getGlobal(API_NAME) || NOOP_LOGGER_PROVIDER; + } + + /** + * Returns a logger from the global logger provider. + * + * @returns Logger + */ + public getLogger( + name: string, + version?: string, + options?: LoggerOptions + ): Logger { + return this.getLoggerProvider().getLogger(name, version, options); + } + + /** Remove the global logger provider */ + public disable(): void { + unregisterGlobal(API_NAME, DiagAPI.instance()); + } +} diff --git a/api/src/experimental/logs/index.ts b/api/src/experimental/logs/index.ts new file mode 100644 index 00000000000..06449bcd9a8 --- /dev/null +++ b/api/src/experimental/logs/index.ts @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * + * 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 + * + * https://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. + */ +export * from './types/Logger'; +export * from './types/LoggerProvider'; +export * from './types/LogRecord'; +export * from './types/LoggerOptions'; +export * from './types/AnyValue'; +export * from './NoopLogger'; +export * from './NoopLoggerProvider'; + +import { LogsAPI } from './api/logs'; +export { LogsAPI }; +export const logs = LogsAPI.getInstance(); diff --git a/api/src/experimental/logs/types/AnyValue.ts b/api/src/experimental/logs/types/AnyValue.ts new file mode 100644 index 00000000000..521aaa9c9a9 --- /dev/null +++ b/api/src/experimental/logs/types/AnyValue.ts @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * + * 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 + * + * https://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 { AttributeValue } from '../../../common/Attributes'; + +/** + * AnyValueMap is a map from string to AnyValue (attribute value or a nested map) + */ +export interface AnyValueMap { + [attributeKey: string]: AnyValue | undefined; +} + +/** + * AnyValue is a either an attribute value or a map of AnyValue(s) + */ +export type AnyValue = AttributeValue | AnyValueMap; diff --git a/api/src/experimental/logs/types/LogRecord.ts b/api/src/experimental/logs/types/LogRecord.ts new file mode 100644 index 00000000000..8068907e8ce --- /dev/null +++ b/api/src/experimental/logs/types/LogRecord.ts @@ -0,0 +1,87 @@ +/* + * Copyright The OpenTelemetry Authors + * + * 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 + * + * https://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 { Attributes } from '../../../common/Attributes'; +import { TimeInput } from '../../../common/Time'; +import { Context } from '../../../context/types'; +import { AnyValue } from './AnyValue'; + +export type LogBody = AnyValue; + +export enum SeverityNumber { + UNSPECIFIED = 0, + TRACE = 1, + TRACE2 = 2, + TRACE3 = 3, + TRACE4 = 4, + DEBUG = 5, + DEBUG2 = 6, + DEBUG3 = 7, + DEBUG4 = 8, + INFO = 9, + INFO2 = 10, + INFO3 = 11, + INFO4 = 12, + WARN = 13, + WARN2 = 14, + WARN3 = 15, + WARN4 = 16, + ERROR = 17, + ERROR2 = 18, + ERROR3 = 19, + ERROR4 = 20, + FATAL = 21, + FATAL2 = 22, + FATAL3 = 23, + FATAL4 = 24, +} + +export interface LogRecord { + /** + * The time when the log record occurred as UNIX Epoch time in nanoseconds. + */ + timestamp?: TimeInput; + + /** + * Time when the event was observed by the collection system. + */ + observedTimestamp?: TimeInput; + + /** + * Numerical value of the severity. + */ + severityNumber?: SeverityNumber; + + /** + * The severity text. + */ + severityText?: string; + + /** + * A value containing the body of the log record. + */ + body?: LogBody; + + /** + * Attributes that define the log record. + */ + attributes?: Attributes; + + /** + * The Context associated with the LogRecord. + */ + context?: Context; +} diff --git a/api/src/experimental/logs/types/Logger.ts b/api/src/experimental/logs/types/Logger.ts new file mode 100644 index 00000000000..e6d63940aa7 --- /dev/null +++ b/api/src/experimental/logs/types/Logger.ts @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * + * 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 + * + * https://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 { LogRecord } from './LogRecord'; + +export interface Logger { + /** + * Emit a log record. This method should only be used by log appenders. + * + * @param logRecord + */ + emit(logRecord: LogRecord): void; +} diff --git a/api/src/experimental/logs/types/LoggerOptions.ts b/api/src/experimental/logs/types/LoggerOptions.ts new file mode 100644 index 00000000000..215045e31ef --- /dev/null +++ b/api/src/experimental/logs/types/LoggerOptions.ts @@ -0,0 +1,36 @@ +/* + * Copyright The OpenTelemetry Authors + * + * 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 + * + * https://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 { Attributes } from '../../../common/Attributes'; + +export interface LoggerOptions { + /** + * The schemaUrl of the tracer or instrumentation library + * @default '' + */ + schemaUrl?: string; + + /** + * The instrumentation scope attributes to associate with emitted telemetry + */ + scopeAttributes?: Attributes; + + /** + * Specifies whether the Trace Context should automatically be passed on to the LogRecords emitted by the Logger. + * @default true + */ + includeTraceContext?: boolean; +} diff --git a/api/src/experimental/logs/types/LoggerProvider.ts b/api/src/experimental/logs/types/LoggerProvider.ts new file mode 100644 index 00000000000..10ed6debf2f --- /dev/null +++ b/api/src/experimental/logs/types/LoggerProvider.ts @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * + * 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 + * + * https://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 { Logger } from './Logger'; +import { LoggerOptions } from './LoggerOptions'; + +/** + * A registry for creating named {@link Logger}s. + */ +export interface LoggerProvider { + /** + * Returns a Logger, creating one if one with the given name, version, and + * schemaUrl pair is not already created. + * + * @param name The name of the logger or instrumentation library. + * @param version The version of the logger or instrumentation library. + * @param options The options of the logger or instrumentation library. + * @returns Logger A Logger with the given name and version + */ + getLogger(name: string, version?: string, options?: LoggerOptions): Logger; +} diff --git a/api/src/index.ts b/api/src/index.ts index c5dbe1685bf..9f19e117a07 100644 --- a/api/src/index.ts +++ b/api/src/index.ts @@ -102,6 +102,19 @@ export { } from './trace/invalid-span-constants'; export type { TraceAPI } from './api/trace'; +// Logs +export { Logger } from './experimental/logs'; +export { LoggerProvider } from './experimental/logs'; +export { LogRecord } from './experimental/logs'; +export { LogBody } from './experimental/logs'; +export { SeverityNumber } from './experimental/logs'; +export { LoggerOptions } from './experimental/logs'; +export { AnyValue } from './experimental/logs'; +export { AnyValueMap } from './experimental/logs'; +export { NoopLogger } from './experimental/logs'; +export { NoopLoggerProvider } from './experimental/logs'; +export type { LogsAPI } from './experimental/logs'; + // Split module-level variable definition into separate files to allow // tree-shaking on each api instance. import { context } from './context-api'; @@ -109,9 +122,10 @@ import { diag } from './diag-api'; import { metrics } from './metrics-api'; import { propagation } from './propagation-api'; import { trace } from './trace-api'; +import { logs } from './experimental/logs'; // Named export. -export { context, diag, metrics, propagation, trace }; +export { context, diag, metrics, propagation, trace, logs }; // Default export. export default { context, @@ -119,4 +133,5 @@ export default { metrics, propagation, trace, + logs, }; diff --git a/api/src/internal/global-utils.ts b/api/src/internal/global-utils.ts index b8c5fb16dbf..dbe92d0caaa 100644 --- a/api/src/internal/global-utils.ts +++ b/api/src/internal/global-utils.ts @@ -22,6 +22,7 @@ import { TextMapPropagator } from '../propagation/TextMapPropagator'; import type { TracerProvider } from '../trace/tracer_provider'; import { VERSION } from '../version'; import { isCompatible } from './semver'; +import { LoggerProvider } from '../experimental/logs'; const major = VERSION.split('.')[0]; const GLOBAL_OPENTELEMETRY_API_KEY = Symbol.for( @@ -101,4 +102,5 @@ type OTelGlobalAPI = { context?: ContextManager; metrics?: MeterProvider; propagation?: TextMapPropagator; + logs?: LoggerProvider; }; diff --git a/api/test/common/logs/logs.test.ts b/api/test/common/logs/logs.test.ts new file mode 100644 index 00000000000..d832e1e0b25 --- /dev/null +++ b/api/test/common/logs/logs.test.ts @@ -0,0 +1,69 @@ +/* + * Copyright The OpenTelemetry Authors + * + * 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 + * + * https://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 * as assert from 'assert'; +import { Logger, logs } from '../../../src/experimental/logs/'; +import { NoopLogger } from '../../../src/experimental/logs/NoopLogger'; +import { NoopLoggerProvider } from '../../../src/experimental/logs/NoopLoggerProvider'; + +describe('API', () => { + const dummyLogger = new NoopLogger(); + + it('should expose a logger provider via getLoggerProvider', () => { + const provider = logs.getLoggerProvider(); + assert.ok(provider); + assert.strictEqual(typeof provider, 'object'); + }); + + describe('GlobalLoggerProvider', () => { + beforeEach(() => { + logs.disable(); + }); + + it('should use the global logger provider', () => { + logs.setGlobalLoggerProvider(new TestLoggerProvider()); + const logger = logs.getLoggerProvider().getLogger('name'); + assert.deepStrictEqual(logger, dummyLogger); + }); + + it('should not allow overriding global provider if already set', () => { + const provider1 = new TestLoggerProvider(); + const provider2 = new TestLoggerProvider(); + logs.setGlobalLoggerProvider(provider1); + assert.equal(logs.getLoggerProvider(), provider1); + logs.setGlobalLoggerProvider(provider2); + assert.equal(logs.getLoggerProvider(), provider1); + }); + }); + + describe('getLogger', () => { + beforeEach(() => { + logs.disable(); + }); + + it('should return a logger instance from global provider', () => { + logs.setGlobalLoggerProvider(new TestLoggerProvider()); + const logger = logs.getLogger('myLogger'); + assert.deepStrictEqual(logger, dummyLogger); + }); + }); + + class TestLoggerProvider extends NoopLoggerProvider { + override getLogger(): Logger { + return dummyLogger; + } + } +}); diff --git a/api/test/common/noop-implementations/noop-logger-provider.test.ts b/api/test/common/noop-implementations/noop-logger-provider.test.ts new file mode 100644 index 00000000000..0ba83762dd9 --- /dev/null +++ b/api/test/common/noop-implementations/noop-logger-provider.test.ts @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * + * 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 + * + * https://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 * as assert from 'assert'; +import { NoopLogger } from '../../../src/experimental/logs/NoopLogger'; +import { NoopLoggerProvider } from '../../../src/experimental/logs/NoopLoggerProvider'; + +describe('NoopLoggerProvider', () => { + it('should not crash', () => { + const loggerProvider = new NoopLoggerProvider(); + + assert.ok(loggerProvider.getLogger('logger-name') instanceof NoopLogger); + assert.ok( + loggerProvider.getLogger('logger-name', 'v1') instanceof NoopLogger + ); + assert.ok( + loggerProvider.getLogger('logger-name', 'v1', { + schemaUrl: 'https://opentelemetry.io/schemas/1.7.0', + }) instanceof NoopLogger + ); + }); +}); diff --git a/api/test/common/noop-implementations/noop-logger.test.ts b/api/test/common/noop-implementations/noop-logger.test.ts new file mode 100644 index 00000000000..1acc958ffa8 --- /dev/null +++ b/api/test/common/noop-implementations/noop-logger.test.ts @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * + * 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 + * + * https://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 * as assert from 'assert'; +import { SeverityNumber } from '../../../src/experimental/logs/'; +import { NoopLogger } from '../../../src/experimental/logs/NoopLogger'; +import { NoopLoggerProvider } from '../../../src/experimental/logs/NoopLoggerProvider'; + +describe('NoopLogger', () => { + it('constructor should not crash', () => { + const logger = new NoopLoggerProvider().getLogger('test-noop'); + assert(logger instanceof NoopLogger); + }); + + it('calling emit should not crash', () => { + const logger = new NoopLoggerProvider().getLogger('test-noop'); + logger.emit({ + severityNumber: SeverityNumber.TRACE, + body: 'log body', + }); + }); +}); From 3574df5ac07a60d82bd058c4d8f8aae2055f3290 Mon Sep 17 00:00:00 2001 From: Hector Hernandez <39923391+hectorhdzg@users.noreply.github.com> Date: Thu, 11 Jul 2024 17:15:07 -0700 Subject: [PATCH 2/2] Update changelog --- api/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md index 946d39be863..d6bfc6948cc 100644 --- a/api/CHANGELOG.md +++ b/api/CHANGELOG.md @@ -11,6 +11,7 @@ All notable changes to this project will be documented in this file. * feat(api): allow adding span links after span creation [#4536](https://github.com/open-telemetry/opentelemetry-js/pull/4536) @seemk * This change is non-breaking for end-users, but breaking for Trace SDK implmentations in accordance with the [specification](https://github.com/open-telemetry/opentelemetry-specification/blob/a03382ada8afa9415266a84dafac0510ec8c160f/specification/upgrading.md?plain=1#L97-L122) as new features need to be implemented. * feat: support node 22 [#4666](https://github.com/open-telemetry/opentelemetry-js/pull/4666) @dyladan +* feat(api): Integrate @opentelemetry/api-logs package into @opentelemetry/api as experimental [#4862](https://github.com/open-telemetry/opentelemetry-js/pull/4862) @hectorhdzg ### :bug: (Bug Fix)