Skip to content
This repository was archived by the owner on Mar 11, 2026. It is now read-only.

Commit bb864c8

Browse files
feat: Add support for library instrumentation (#1261)
* feat: Add support for library instrumentation * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * Fix lint and test failures * Fix timeout * Fix failing tests and move instrumentation into data * Add support for partialSuccess * Add version fix for filing installation tests * Fix the version retrieval logic * Address PR comments * Fix a comment * Fix comments Co-authored-by: Owl Bot <gcf-owl-bot[bot]@users.noreply.github.com>
1 parent ef6d6bb commit bb864c8

6 files changed

Lines changed: 417 additions & 8 deletions

File tree

src/log-sync.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@
1818
* This is a helper library for synchronously writing logs to any transport.
1919
*/
2020

21-
import arrify = require('arrify');
2221
import {Logging} from '.';
2322
import {Entry, LABELS_KEY, LogEntry, StructuredJson} from './entry';
2423
import {Writable} from 'stream';
24+
import {populateInstrumentationInfo} from './utils/instrumentation';
2525
import {
2626
LogSeverityFunctions,
2727
assignSeverityToEntries,
@@ -412,7 +412,8 @@ class LogSync implements LogSeverityFunctions {
412412
let structuredEntries: StructuredJson[];
413413
this.formattedName_ = formatLogName(this.logging.projectId, this.name);
414414
try {
415-
structuredEntries = (arrify(entry) as Entry[]).map(entry => {
415+
// Make sure to add instrumentation info
416+
structuredEntries = populateInstrumentationInfo(entry).map(entry => {
416417
if (!(entry instanceof Entry)) {
417418
entry = this.entry(entry);
418419
}

src/log.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,16 @@
1414
* limitations under the License.
1515
*/
1616

17-
import arrify = require('arrify');
1817
import {callbackifyAll} from '@google-cloud/promisify';
1918
import * as dotProp from 'dot-prop';
2019
import * as extend from 'extend';
2120
import {CallOptions} from 'google-gax';
2221
import {GetEntriesCallback, GetEntriesResponse, Logging} from '.';
2322
import {Entry, EntryJson, LogEntry} from './entry';
23+
import {
24+
populateInstrumentationInfo,
25+
getInstrumentationInfoStatus,
26+
} from './utils/instrumentation';
2427
import {
2528
LogSeverityFunctions,
2629
assignSeverityToEntries,
@@ -959,13 +962,23 @@ class Log implements LogSeverityFunctions {
959962
entry: Entry | Entry[],
960963
opts?: WriteOptions | ApiResponseCallback
961964
): Promise<ApiResponse> {
965+
const isInfoAdded = getInstrumentationInfoStatus();
962966
const options = opts ? (opts as WriteOptions) : {};
967+
// If instrumentation info was not added, means that this is first time
968+
// log entry is written and that the instrumentation log entry could be
969+
// generated for this request. If yes, then make sure we set partialSuccess, so entire
970+
// request will make it through and only oversized entries will be dropped
971+
if (!isInfoAdded) {
972+
options.partialSuccess = true;
973+
}
963974
// Extract projectId & resource from Logging - inject & memoize if not.
964975
await this.logging.setProjectId();
965976
this.formattedName_ = formatLogName(this.logging.projectId, this.name);
966977
const resource = await this.getOrSetResource(options);
967-
// Extract & format additional context from individual entries.
968-
const decoratedEntries = this.decorateEntries(arrify(entry) as Entry[]);
978+
// Extract & format additional context from individual entries. Make sure to add instrumentation info
979+
const decoratedEntries = this.decorateEntries(
980+
populateInstrumentationInfo(entry)
981+
);
969982
this.truncateEntries(decoratedEntries);
970983
// Clobber `labels` and `resource` fields with WriteOptions from the user.
971984
const reqOpts = extend(

src/utils/instrumentation.ts

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
/*!
2+
* Copyright 2022 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import arrify = require('arrify');
18+
import path = require('path');
19+
import {google} from '../../protos/protos';
20+
import {Entry} from '../entry';
21+
22+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
23+
declare const global: {[index: string]: any};
24+
25+
// The global variable keeping track if instrumentation record was already written or not.
26+
// The instrumentation record should be generated only once per process and contain logging
27+
// libraries related info.
28+
global.instrumentationAdded = false;
29+
30+
// The variable to hold cached library version
31+
let libraryVersion: string;
32+
33+
// Max length for instrumentation library name and version values
34+
const maxDiagnosticValueLen = 14;
35+
36+
export const DIAGNOSTIC_INFO_KEY = 'logging.googleapis.com/diagnostic';
37+
export const INSTRUMENTATION_SOURCE_KEY = 'instrumentation_source';
38+
export const NODEJS_LIBRARY_NAME_PREFIX = 'nodejs';
39+
export type InstrumentationInfo = {name: string; version: string};
40+
41+
/**
42+
* This method returns the status if instrumentation info was already added or not.
43+
* @returns true if the log record with instrumentation info was already added, false otherwise.
44+
*/
45+
export function getInstrumentationInfoStatus() {
46+
return global.instrumentationAdded;
47+
}
48+
49+
/**
50+
* This method helps to populate entries with instrumentation data
51+
* @param entry {Entry} The entry or array of entries to be populated with instrumentation info
52+
* @returns {Entry} Array of entries which contains an entry with current library instrumentation info
53+
*/
54+
export function populateInstrumentationInfo(entry: Entry | Entry[]): Entry[] {
55+
// Update the flag indicating that instrumentation entry was already added once,
56+
// so any subsequent calls to this method will not add a separate instrumentation log entry
57+
let isWritten = global.instrumentationAdded;
58+
global.instrumentationAdded = true;
59+
const entries: Entry[] = [];
60+
if (entry) {
61+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
62+
for (const entryItem of arrify(entry) as any[]) {
63+
if (entryItem) {
64+
const info =
65+
entryItem.data?.[DIAGNOSTIC_INFO_KEY]?.[INSTRUMENTATION_SOURCE_KEY];
66+
if (info) {
67+
// Validate and update the instrumentation info with current library info
68+
entryItem.data[DIAGNOSTIC_INFO_KEY][INSTRUMENTATION_SOURCE_KEY] =
69+
validateAndUpdateInstrumentation(info);
70+
// Indicate that instrumentation info log entry already exists
71+
// and that current library info was added to existing log entry
72+
isWritten = true;
73+
}
74+
entries.push(entryItem);
75+
}
76+
}
77+
}
78+
// If no instrumentation info was added before, append a separate log entry with
79+
// instrumentation data for this library
80+
if (!isWritten) {
81+
entries.push(createDiagnosticEntry(undefined, undefined));
82+
}
83+
return entries;
84+
}
85+
86+
/**
87+
* The helper method to generate a log entry with diagnostic instrumentation data.
88+
* @param libraryName {string} The name of the logging library to be reported. Should be prefixed with 'nodejs'.
89+
* Will be truncated if longer than 14 characters.
90+
* @param libraryVersion {string} The version of the logging library to be reported. Will be truncated if longer than 14 characters.
91+
* @returns {Entry} The entry with diagnostic instrumentation data.
92+
*/
93+
export function createDiagnosticEntry(
94+
libraryName: string | undefined,
95+
libraryVersion: string | undefined
96+
): Entry {
97+
// Validate the libraryName first and make sure it starts with 'nodejs' prefix.
98+
if (!libraryName || !libraryName.startsWith(NODEJS_LIBRARY_NAME_PREFIX)) {
99+
libraryName = NODEJS_LIBRARY_NAME_PREFIX;
100+
}
101+
const entry = new Entry(
102+
{
103+
severity: google.logging.type.LogSeverity.INFO,
104+
},
105+
{
106+
[DIAGNOSTIC_INFO_KEY]: {
107+
[INSTRUMENTATION_SOURCE_KEY]: [
108+
{
109+
// Truncate libraryName and libraryVersion if more than 14 characters length
110+
name: truncateValue(libraryName, maxDiagnosticValueLen),
111+
version: truncateValue(
112+
libraryVersion ?? getNodejsLibraryVersion(),
113+
maxDiagnosticValueLen
114+
),
115+
},
116+
],
117+
},
118+
}
119+
);
120+
return entry;
121+
}
122+
123+
/**
124+
* This method validates that provided instrumentation info list is valid and also adds current library info to a list.
125+
* @param infoList {InstrumentationInfo} The array of InstrumentationInfo to be validated and updated.
126+
* @returns {InstrumentationInfo} The updated list of InstrumentationInfo.
127+
*/
128+
function validateAndUpdateInstrumentation(
129+
infoList: InstrumentationInfo[]
130+
): InstrumentationInfo[] {
131+
const finalInfo: InstrumentationInfo[] = [];
132+
// First, add current library information
133+
finalInfo.push({
134+
name: NODEJS_LIBRARY_NAME_PREFIX,
135+
version: getNodejsLibraryVersion(),
136+
});
137+
// Iterate through given list of libraries and for each entry perform validations and transformations
138+
// Limit amount of entries to be up to 3
139+
let count = 1;
140+
for (const info of infoList) {
141+
if (isValidInfo(info)) {
142+
finalInfo.push({
143+
name: truncateValue(info.name, maxDiagnosticValueLen),
144+
version: truncateValue(info.version, maxDiagnosticValueLen),
145+
});
146+
}
147+
if (++count === 3) break;
148+
}
149+
return finalInfo;
150+
}
151+
152+
/**
153+
* A helper function to truncate a value (library name or version for example). The value is truncated
154+
* when it is longer than {maxLen} chars and '*' is added instead of truncated suffix.
155+
* @param value {string} The value to be truncated.
156+
* @param maxLen {number} The max length to be used for truncation.
157+
* @returns {string} The truncated value.
158+
*/
159+
function truncateValue(value: string, maxLen: number) {
160+
if (value && value.length > maxLen) {
161+
return value.substring(0, maxLen).concat('*');
162+
}
163+
return value;
164+
}
165+
166+
/**
167+
* The helper function to retrieve current library version from 'package.json' file. Note that
168+
* since we use {path.resolve}, the search for 'package.json' could be impacted by current working directory.
169+
* @returns {string} A current library version.
170+
*/
171+
export function getNodejsLibraryVersion() {
172+
if (libraryVersion) {
173+
return libraryVersion;
174+
}
175+
libraryVersion = require(path.resolve(
176+
__dirname,
177+
'../../../',
178+
'package.json'
179+
)).version;
180+
return libraryVersion;
181+
}
182+
183+
/**
184+
* The helper function which checks if given InstrumentationInfo is valid.
185+
* @param info {InstrumentationInfo} The info to be validated.
186+
* @returns true if given info is valid, false otherwise
187+
*/
188+
function isValidInfo(info: InstrumentationInfo) {
189+
if (
190+
!info ||
191+
!info.name ||
192+
!info.version ||
193+
!info.name.startsWith(NODEJS_LIBRARY_NAME_PREFIX)
194+
) {
195+
return false;
196+
}
197+
return true;
198+
}
199+
200+
/**
201+
* The helper method used to reset a status of a flag which indicates if instrumentation info already written or not.
202+
*/
203+
export function resetInstrumentationStatus() {
204+
global.instrumentationAdded = false;
205+
}

system-test/logging.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {after, before} from 'mocha';
2828
const http2spy = require('http2spy');
2929
import {Logging, Sink, Log, Entry, TailEntriesResponse} from '../src';
3030
import * as http from 'http';
31+
import * as instrumentation from '../src/utils/instrumentation';
3132

3233
// block all attempts to chat with the metadata server (kokoro runs on GCE)
3334
nock(HOST_ADDRESS)
@@ -372,7 +373,26 @@ describe('Logging', () => {
372373
{numExpectedMessages: logEntries.length},
373374
(err, entries) => {
374375
assert.ifError(err);
375-
assert.strictEqual(entries!.length, logEntries.length);
376+
// Instrumentation log entry is added automatically, so we should discount it
377+
assert.strictEqual(entries!.length - 1, logEntries.length);
378+
let entry: Entry | undefined;
379+
entries!.forEach(ent => {
380+
if (
381+
ent &&
382+
ent.data?.[instrumentation.DIAGNOSTIC_INFO_KEY]?.[
383+
instrumentation.INSTRUMENTATION_SOURCE_KEY
384+
]
385+
) {
386+
entry = ent;
387+
}
388+
});
389+
assert.ok(entry);
390+
assert.equal(
391+
entry.data?.[instrumentation.DIAGNOSTIC_INFO_KEY]?.[
392+
instrumentation.INSTRUMENTATION_SOURCE_KEY
393+
]?.[0]?.['name'],
394+
instrumentation.NODEJS_LIBRARY_NAME_PREFIX
395+
);
376396
done();
377397
}
378398
);

0 commit comments

Comments
 (0)