@google-cloud/spanner
Version:
Cloud Spanner Client Library for Node.js
281 lines • 10.3 kB
JavaScript
;
/*!
* Copyright 2024 Google LLC. All Rights Reserved.
*
* 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.SPAN_NAMESPACE_PREFIX = exports.TRACER_VERSION = exports.TRACER_NAME = void 0;
exports.getTracer = getTracer;
exports.ensureInitialContextManagerSet = ensureInitialContextManagerSet;
exports.isTracingEnabled = isTracingEnabled;
exports._resetTracingEnabledForTest = _resetTracingEnabledForTest;
exports.startTrace = startTrace;
exports.setSpanError = setSpanError;
exports.setSpanErrorAndException = setSpanErrorAndException;
exports.getActiveOrNoopSpan = getActiveOrNoopSpan;
const semantic_conventions_1 = require("@opentelemetry/semantic-conventions");
const api_1 = require("@opentelemetry/api");
const optedInPII = process.env.SPANNER_ENABLE_EXTENDED_TRACING === 'true';
const TRACER_NAME = 'cloud.google.com/nodejs/spanner';
exports.TRACER_NAME = TRACER_NAME;
const TRACER_VERSION = require('../../package.json').version;
exports.TRACER_VERSION = TRACER_VERSION;
/**
* getTracer fetches the tracer from the provided tracerProvider.
* @param {TracerProvider} [tracerProvider] optional custom tracer provider
* to use for fetching the tracer. If not provided, the global provider will be used.
*
* @returns {Tracer} The tracer instance associated with the provided or global provider.
*/
function getTracer(tracerProvider) {
if (tracerProvider) {
return tracerProvider.getTracer(TRACER_NAME, TRACER_VERSION);
}
// Otherwise use the global tracer.
return api_1.trace.getTracer(TRACER_NAME, TRACER_VERSION);
}
const SPAN_NAMESPACE_PREFIX = 'CloudSpanner'; // TODO: discuss & standardize this prefix.
exports.SPAN_NAMESPACE_PREFIX = SPAN_NAMESPACE_PREFIX;
const { AsyncHooksContextManager, } = require('@opentelemetry/context-async-hooks');
let contextManagerInstallAttempted = false;
/*
* If no global ContextManager is registered, install an AsyncHooksContextManager
* so that async/await trace context propagation works for apps that haven't
* configured OpenTelemetry themselves. If the host app has already installed a
* ContextManager, leave it alone — tearing down a working manager breaks the
* host's baggage and span parentage on the next gRPC call.
*
* setGlobalContextManager() returns false when a manager is already registered,
* which is the documented signal that we shouldn't replace it. The
* `contextManagerInstallAttempted` latch makes the call idempotent so we don't
* allocate a new AsyncHooksContextManager on each Spanner client construction.
*/
function ensureInitialContextManagerSet() {
if (contextManagerInstallAttempted)
return;
contextManagerInstallAttempted = true;
const contextManager = new AsyncHooksContextManager();
if (api_1.context.setGlobalContextManager(contextManager)) {
contextManager.enable();
}
}
let globalTracingEnabled = undefined;
let lastCheckTime = 0;
const CACHE_TTL_MS = 10000; // 10 seconds TTL
/**
* isGlobalTracingEnabled returns true if tracing is enabled globally,
* respecting cached status and active recording spans.
*
* @returns {boolean} True if global tracing is enabled.
*/
function isGlobalTracingEnabled() {
const now = Date.now();
if (globalTracingEnabled !== undefined &&
(globalTracingEnabled || now - lastCheckTime < CACHE_TTL_MS)) {
return globalTracingEnabled;
}
lastCheckTime = now;
const globalProvider = api_1.trace.getTracerProvider();
if (globalProvider) {
let delegate = globalProvider;
if (typeof globalProvider.getDelegate === 'function') {
delegate = globalProvider.getDelegate();
}
if (delegate) {
const name = delegate.constructor.name;
// Exclude the dummy NoopTracerProvider and uninitialized ProxyTracerProvider
if (name !== 'NoopTracerProvider' && name !== 'ProxyTracerProvider') {
globalTracingEnabled = true;
ensureInitialContextManagerSet();
return true;
}
}
}
globalTracingEnabled = false;
return false;
}
/**
* isTracingEnabled returns true if tracing is enabled for the given options
* or globally.
*
* @param {ObservabilityOptions} [opts] The observability options.
* @returns {boolean} True if tracing is enabled.
*/
function isTracingEnabled(opts) {
if (opts?.tracerProvider) {
return true;
}
return isGlobalTracingEnabled();
}
/** Only exported for resetting state in unit tests. */
function _resetTracingEnabledForTest() {
globalTracingEnabled = undefined;
lastCheckTime = 0;
}
/**
* startTrace begins an active span in the current active context
* and passes it back to the set callback function. Each span will
* be prefixed with "cloud.google.com/nodejs/spanner". It is the
* responsibility of the caller to invoke [span.end] when finished tracing.
*
* @returns {Span} The created span.
*/
function startTrace(spanNameSuffix, config, cb) {
if (!isTracingEnabled(config?.opts)) {
return cb(new noopSpan());
}
if (!config) {
config = {};
}
return getTracer(config.opts?.tracerProvider).startActiveSpan(SPAN_NAMESPACE_PREFIX + '.' + spanNameSuffix, { kind: api_1.SpanKind.CLIENT }, span => {
span.setAttribute(semantic_conventions_1.ATTR_OTEL_SCOPE_NAME, TRACER_NAME);
span.setAttribute(semantic_conventions_1.ATTR_OTEL_SCOPE_VERSION, TRACER_VERSION);
span.setAttribute('gcp.client.service', 'spanner');
span.setAttribute('gcp.client.version', TRACER_VERSION);
span.setAttribute('gcp.client.repo', 'googleapis/nodejs-spanner');
if (config.tableName) {
span.setAttribute('db.sql.table', config.tableName);
}
if (config.dbName) {
span.setAttribute('gcp.resource.name', `//spanner.googleapis.com/${config.dbName}`);
span.setAttribute('db.name', config.dbName);
}
if (config.requestTag) {
span.setAttribute('request.tag', config.requestTag);
}
if (config.transactionTag) {
span.setAttribute('transaction.tag', config.transactionTag);
}
const allowExtendedTracing = optedInPII || config.opts?.enableExtendedTracing;
if (config.sql && allowExtendedTracing) {
const sql = config.sql;
if (typeof sql === 'string') {
span.setAttribute('db.statement', sql);
}
else {
const stmt = sql;
span.setAttribute('db.statement', stmt.sql);
}
}
// If at all the invoked function throws an exception,
// record the exception and then end this span.
try {
return cb(span);
}
catch (e) {
setSpanErrorAndException(span, e);
span.end();
// Finally re-throw the exception.
throw e;
}
});
}
/**
* Sets the span status with err, if non-null onto the span with
* status.code=ERROR and the message of err.toString()
*
* @returns {boolean} to signify if the status was set.
*/
function setSpanError(span, err) {
if (!err || !span) {
return false;
}
let message = '';
if (typeof err === 'object' && 'message' in err) {
message = err.message;
}
else {
message = err.toString();
}
span.setStatus({
code: api_1.SpanStatusCode.ERROR,
message: message,
});
return true;
}
/**
* Sets err, if non-null onto the span with
* status.code=ERROR and the message of err.toString()
* as well as recording an exception on the span.
* @param {Span} [span] the subject span
* @param {Error} [err] the error whose message to use to record
* the span error and the span exception.
*
* @returns {boolean} to signify if the status and exception were set.
*/
function setSpanErrorAndException(span, err) {
if (setSpanError(span, err)) {
span.recordException(err);
return true;
}
return false;
}
/**
* getActiveOrNoopSpan queries the global tracer for the currently active
* span and returns it, otherwise if there is no active span available, it'll
* simply create a NoopSpan. This is important in the cases where we don't
* want to create a new span, such as in sensitive and frequently called code
* for which the new spans would be too many and thus pollute the trace,
* but yet we'd like to record an important annotation.
*
* @returns {Span} the non-null span.
*/
function getActiveOrNoopSpan() {
const span = api_1.trace.getActiveSpan();
if (span) {
return span;
}
return new noopSpan();
}
/**
* noopSpan is a pass-through Span that does nothing and shall not
* be exported, nor added into any context. It serves as a placeholder
* to allow calls in sensitive areas like sessionPools to transparently
* add attributes to spans without lots of ugly null checks.
*
* It exists because OpenTelemetry-JS does not seem to export the NoopSpan.
*/
class noopSpan {
constructor() { }
spanContext() {
return api_1.INVALID_SPAN_CONTEXT;
}
setAttribute(key, value) {
return this;
}
setAttributes(attributes) {
return this;
}
addEvent(name, attributes) {
return this;
}
addLink(link) {
return this;
}
addLinks(links) {
return this;
}
setStatus(status) {
return this;
}
end(endTime) { }
isRecording() {
return false;
}
recordException(exc, timeAt) { }
updateName(name) {
return this;
}
}
//# sourceMappingURL=instrument.js.map