@google-cloud/logging-winston
Version:
Cloud Logging transport for Winston
272 lines • 12.4 kB
JavaScript
"use strict";
// Copyright 2016 Google LLC
//
// 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.
Object.defineProperty(exports, "__esModule", { value: true });
exports.getNodejsLibraryVersion = exports.LoggingCommon = exports.getCurrentTraceFromAgent = exports.NODEJS_WINSTON_DEFAULT_LIBRARY_VERSION = exports.LOGGING_SAMPLED_KEY = exports.LOGGING_SPAN_KEY = exports.LOGGING_TRACE_KEY = void 0;
const util = require("util");
const logging_1 = require("@google-cloud/logging");
const instrumentation_1 = require("@google-cloud/logging/build/src/utils/instrumentation");
const mapValues = require("lodash.mapvalues");
// Map of npm output levels to Cloud Logging levels.
// See https://github.com/winstonjs/winston#logging-levels for more info.
const NPM_LEVEL_NAME_TO_CODE = {
error: 3,
warn: 4,
info: 6,
http: 6,
verbose: 7,
debug: 7,
silly: 7,
};
// Map of Cloud Logging levels.
const CLOUD_LOGGING_LEVEL_CODE_TO_NAME = {
0: 'emergency',
1: 'alert',
2: 'critical',
3: 'error',
4: 'warning',
5: 'notice',
6: 'info',
7: 'debug',
};
/*!
* Log entry data key to allow users to indicate a trace for the request.
*/
exports.LOGGING_TRACE_KEY = 'logging.googleapis.com/trace';
/*!
* Log entry data key to allow users to indicate a spanId for the request.
*/
exports.LOGGING_SPAN_KEY = 'logging.googleapis.com/spanId';
/*!
* Log entry data key to allow users to indicate a traceSampled flag for the request.
*/
exports.LOGGING_SAMPLED_KEY = 'logging.googleapis.com/trace_sampled';
/**
* Default library version to be used
* Using release-please annotations to update DEFAULT_INSTRUMENTATION_VERSION with latest version.
* See https://github.com/googleapis/release-please/blob/main/docs/customizing.md#updating-arbitrary-files
*/
exports.NODEJS_WINSTON_DEFAULT_LIBRARY_VERSION = '6.0.0'; // {x-release-please-version}
/*!
* Gets the current fully qualified trace ID when available from the
* @google-cloud/trace-agent library in the LogEntry.trace field format of:
* "projects/[PROJECT-ID]/traces/[TRACE-ID]".
*/
function getCurrentTraceFromAgent() {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const agent = global._google_trace_agent;
if (!agent || !agent.getCurrentContextId || !agent.getWriterProjectId) {
return null;
}
const traceId = agent.getCurrentContextId();
if (!traceId) {
return null;
}
const traceProjectId = agent.getWriterProjectId();
if (!traceProjectId) {
return null;
}
return `projects/${traceProjectId}/traces/${traceId}`;
}
exports.getCurrentTraceFromAgent = getCurrentTraceFromAgent;
class LoggingCommon {
constructor(options) {
var _a, _b;
options = Object.assign({
scopes: ['https://www.googleapis.com/auth/logging.write'],
}, options);
this.logName = options.logName || 'winston_log';
this.inspectMetadata = options.inspectMetadata === true;
this.levels = options.levels || NPM_LEVEL_NAME_TO_CODE;
this.redirectToStdout = (_a = options.redirectToStdout) !== null && _a !== void 0 ? _a : false;
if (!this.redirectToStdout) {
this.cloudLog = new logging_1.Logging(options).log(this.logName, {
removeCircular: true,
// See: https://cloud.google.com/logging/quotas, a log size of
// 250,000 has been chosen to keep us comfortably within the
// 256,000 limit.
maxEntrySize: options.maxEntrySize || 250000,
});
}
else {
const logSyncOptions = {
useMessageField: (_b = options.useMessageField) !== null && _b !== void 0 ? _b : true,
};
this.cloudLog = new logging_1.Logging(options).logSync(this.logName, undefined, logSyncOptions);
}
this.resource = options.resource;
this.serviceContext = options.serviceContext;
this.prefix = options.prefix;
this.labels = options.labels;
this.defaultCallback = options.defaultCallback;
}
log(level, message, metadata, callback) {
metadata = metadata || {};
// First create instrumentation record if it is never written before
let instrumentationEntry;
if (!(0, instrumentation_1.setInstrumentationStatus)(true)) {
instrumentationEntry = (0, instrumentation_1.createDiagnosticEntry)('nodejs-winston', getNodejsLibraryVersion());
// Update instrumentation record resource, logName and timestamp
instrumentationEntry.metadata.resource = this.resource;
instrumentationEntry.metadata.logName = metadata.logName;
instrumentationEntry.metadata.timestamp = metadata.timestamp;
}
message = message || '';
const hasMetadata = Object.keys(metadata).length;
if (this.levels[level] === undefined) {
throw new Error('Unknown log level: ' + level);
}
const levelCode = this.levels[level];
const cloudLevel = CLOUD_LOGGING_LEVEL_CODE_TO_NAME[levelCode];
const data = {};
// Cloud Logs Viewer picks up the summary line from the `message`
// property of the jsonPayload.
// https://cloud.google.com/logging/docs/view/logs_viewer_v2#expanding.
//
// For error messages at severity 'error' and higher,
// Error Reporting will pick up error messages if the full stack trace is
// included in the textPayload or the message property of the jsonPayload.
// https://cloud.google.com/error-reporting/docs/formatting-error-messages
// We prefer to format messages as jsonPayload (by putting it as a message
// property on an object) as that works and is accepted by Error Reporting
// in far more resource types.
//
if (metadata.stack) {
message += (message ? ' ' : '') + metadata.stack;
data.serviceContext = this.serviceContext;
}
data.message = this.prefix ? `[${this.prefix}] ` : '';
data.message += message;
const entryMetadata = {
resource: this.resource,
};
// If the metadata contains a logName property, promote it to the entry
// metadata.
if (metadata.logName) {
entryMetadata.logName = metadata.logName;
}
// If the metadata contains a httpRequest property, promote it to the
// entry metadata. This allows Cloud Logging to use request log formatting.
// https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#HttpRequest
// Note that the httpRequest field must properly validate as HttpRequest
// proto message, or the log entry would be rejected by the API. We no do
// validation here.
if (metadata.httpRequest) {
entryMetadata.httpRequest = metadata.httpRequest;
}
// If the metadata contains a timestamp property, promote it to the entry
// metadata. As Winston 3 buffers logs when a transport (such as this one)
// invokes its log callback asynchronously, a timestamp assigned at log time
// is more accurate than one assigned in a transport.
if (metadata.timestamp instanceof Date) {
entryMetadata.timestamp = metadata.timestamp;
}
// If the metadata contains a labels property, promote it to the entry
// metadata.
// https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry
if (this.labels || metadata.labels) {
entryMetadata.labels = !this.labels
? metadata.labels
: Object.assign({}, this.labels, metadata.labels);
}
const trace = metadata[exports.LOGGING_TRACE_KEY] || getCurrentTraceFromAgent();
if (trace) {
entryMetadata.trace = trace;
}
const spanId = metadata[exports.LOGGING_SPAN_KEY];
if (spanId) {
entryMetadata.spanId = spanId;
}
if (exports.LOGGING_SAMPLED_KEY in metadata) {
entryMetadata.traceSampled = metadata[exports.LOGGING_SAMPLED_KEY] === '1';
}
// we have tests that assert that metadata is always passed.
// not sure if its correct but for now we always set it even if it has
// nothing in it
data.metadata = this.inspectMetadata
? mapValues(metadata, util.inspect)
: metadata;
if (hasMetadata) {
// clean entryMetadata props
delete data.metadata[exports.LOGGING_TRACE_KEY];
delete data.metadata[exports.LOGGING_SPAN_KEY];
delete data.metadata[exports.LOGGING_SAMPLED_KEY];
delete data.metadata.httpRequest;
delete data.metadata.labels;
delete data.metadata.timestamp;
delete data.metadata.logName;
}
const entries = [];
entries.push(this.entry(entryMetadata, data));
// Check if instrumentation entry needs to be added as well
if (instrumentationEntry) {
// Make sure instrumentation entry is updated by underlying logger
instrumentationEntry = this.entry(instrumentationEntry.metadata, instrumentationEntry.data);
if (levelCode !== NPM_LEVEL_NAME_TO_CODE.info) {
// We using info level for diagnostic records
this.cloudLog[CLOUD_LOGGING_LEVEL_CODE_TO_NAME[NPM_LEVEL_NAME_TO_CODE.info]]([instrumentationEntry], this.defaultCallback);
}
else
entries.push(instrumentationEntry);
}
// Make sure that both callbacks are called in case if provided
const newCallback = (err, apiResponse) => {
let callbackError;
if (callback) {
try {
callback(err, apiResponse);
}
catch (error) {
callbackError = error;
}
}
if (this.defaultCallback) {
this.defaultCallback(err, apiResponse);
}
// In case if original error was null and callback thrown exception, rethrow it to make sure
// we do not swallow it since upon success the exceptions normally should not be thrown. Also
// we should retrhrow callbackError when defaultCallback was not provided to keep
// prevous behaviour intact
if ((!this.defaultCallback || err === null) && callbackError) {
throw callbackError;
}
};
this.cloudLog[cloudLevel](entries, newCallback);
// The LogSync class does not supports callback. However Writable class always
// provides onwrite() callback which needs to be called after each log is written,
// so the stream would remove writing state. Since this.defaultCallback can also be set, we
// should call it explicitly as well.
if (this.redirectToStdout) {
newCallback(null, undefined);
}
}
entry(metadata, data) {
if (this.redirectToStdout) {
return this.cloudLog.entry(metadata, data);
}
return this.cloudLog.entry(metadata, data);
}
}
exports.LoggingCommon = LoggingCommon;
// LOGGING_TRACE_KEY is Cloud Logging specific and has the format:
// logging.googleapis.com/trace
LoggingCommon.LOGGING_TRACE_KEY = exports.LOGGING_TRACE_KEY;
// LOGGING_TRACE_KEY is Cloud Logging specific and has the format:
// logging.googleapis.com/spanId
LoggingCommon.LOGGING_SPAN_KEY = exports.LOGGING_SPAN_KEY;
function getNodejsLibraryVersion() {
return exports.NODEJS_WINSTON_DEFAULT_LIBRARY_VERSION;
}
exports.getNodejsLibraryVersion = getNodejsLibraryVersion;
//# sourceMappingURL=common.js.map