@fluidframework/telemetry-utils 0.52.1 → 0.54.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/config.ts ADDED
@@ -0,0 +1,265 @@
1
+ /*!
2
+ * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+ import { ITelemetryBaseLogger, ITelemetryLogger } from "@fluidframework/common-definitions";
6
+ import { Lazy } from "@fluidframework/common-utils";
7
+
8
+ export type ConfigTypes = string | number | boolean | number[] | string[] | boolean[] | undefined;
9
+
10
+ /**
11
+ * Base interface for providing configurations to enable/disable/control features
12
+ */
13
+ export interface IConfigProviderBase {
14
+ getRawConfig(name: string): ConfigTypes;
15
+ }
16
+
17
+ /**
18
+ * Explicitly typed interface for reading configurations
19
+ */
20
+ export interface IConfigProvider extends IConfigProviderBase {
21
+ getBoolean(name: string): boolean | undefined;
22
+ getNumber(name: string): number | undefined;
23
+ getString(name: string): string | undefined;
24
+ getBooleanArray(name: string): boolean[] | undefined;
25
+ getNumberArray(name: string): number[] | undefined;
26
+ getStringArray(name: string): string[] | undefined;
27
+ }
28
+ /**
29
+ * Creates a base configuration provider based on `sessionStorage`
30
+ *
31
+ * @returns A lazy initialized base configuration provider with `sessionStorage` as the underlying config store
32
+ */
33
+ export const sessionStorageConfigProvider =
34
+ new Lazy<IConfigProviderBase>(() => inMemoryConfigProvider(safeSessionStorage()));
35
+
36
+ const NullConfigProvider: IConfigProviderBase = {
37
+ getRawConfig: () => undefined,
38
+ };
39
+
40
+ /**
41
+ * Creates a base configuration provider based on the supplied `Storage` instance
42
+ *
43
+ * @param storage - instance of `Storage` to be used as storage media for the config
44
+ * @returns A base configuration provider with
45
+ * the supplied `Storage` instance as the underlying config store
46
+ */
47
+ export const inMemoryConfigProvider =
48
+ (storage: Storage | undefined): IConfigProviderBase => {
49
+ if (storage !== undefined && storage !== null) {
50
+ return new CachedConfigProvider({
51
+ getRawConfig: (name: string) => {
52
+ try {
53
+ return stronglyTypedParse(storage.getItem(name) ?? undefined)?.raw;
54
+ } catch { }
55
+ return undefined;
56
+ },
57
+ });
58
+ }
59
+ return NullConfigProvider;
60
+ };
61
+
62
+ interface ConfigTypeStringToType {
63
+ number: number;
64
+ string: string;
65
+ boolean: boolean;
66
+ ["number[]"]: number[];
67
+ ["string[]"]: string[];
68
+ ["boolean[]"]: boolean[];
69
+ }
70
+
71
+ type PrimitiveTypeStrings = "number" | "string" | "boolean";
72
+
73
+ function isPrimitiveType(type: string): type is PrimitiveTypeStrings {
74
+ switch (type) {
75
+ case "boolean":
76
+ case "number":
77
+ case "string":
78
+ return true;
79
+ default:
80
+ return false;
81
+ }
82
+ }
83
+
84
+ interface StronglyTypedValue extends Partial<ConfigTypeStringToType> {
85
+ raw: ConfigTypes;
86
+ }
87
+ /**
88
+ * Takes any supported config type, and returns the value with a strong type. If the type of
89
+ * the config is not a supported type undefined will be returned.
90
+ * The user of this function should cache the result to avoid duplicated work.
91
+ *
92
+ * Strings will be attempted to be parsed and coerced into a strong config type.
93
+ * if it is not possible to parsed and coerce a string to a strong config type the original string
94
+ * will be return with a string type for the consumer to handle further if necessary.
95
+ */
96
+ function stronglyTypedParse(input: ConfigTypes): StronglyTypedValue | undefined {
97
+ let output: ConfigTypes = input;
98
+ let defaultReturn: Pick<StronglyTypedValue,"raw" | "string"> | undefined;
99
+ // we do special handling for strings to try and coerce
100
+ // them into a config type if we can. This makes it easy
101
+ // for config sources like sessionStorage which only
102
+ // holds strings
103
+ if (typeof input === "string") {
104
+ try {
105
+ output = JSON.parse(input);
106
+ // we succeeded in parsing, but we don't support parsing
107
+ // for any object as we can't do it type safely
108
+ // so in this case, the default return will be string
109
+ // rather than undefined, and the consumer
110
+ // can parse, as we don't want to provide
111
+ // a false sense of security by just
112
+ // casting.
113
+ defaultReturn = { raw: input, string: input };
114
+ } catch { }
115
+ }
116
+
117
+ if (output === undefined) {
118
+ return defaultReturn;
119
+ }
120
+
121
+ const outputType = typeof output;
122
+ if (isPrimitiveType(outputType)) {
123
+ return { ...defaultReturn, raw: input, [outputType]: output };
124
+ }
125
+
126
+ if (Array.isArray(output)) {
127
+ const firstType = typeof output[0];
128
+ // ensure the first elements is a primitive type
129
+ if (!isPrimitiveType(firstType)) {
130
+ return defaultReturn;
131
+ }
132
+ // ensue all the elements types are homogeneous
133
+ // aka they all have the same type as the first
134
+ for (const v of output) {
135
+ if (typeof v !== firstType) {
136
+ return defaultReturn;
137
+ }
138
+ }
139
+ return { ...defaultReturn, raw: input, [`${firstType}[]`]: output };
140
+ }
141
+
142
+ return defaultReturn;
143
+ }
144
+
145
+ /** Referencing the `sessionStorage` variable can throw in some environments such as Node */
146
+ const safeSessionStorage = (): Storage | undefined => {
147
+ try {
148
+ return sessionStorage !== null ? sessionStorage : undefined;
149
+ } catch { return undefined; }
150
+ };
151
+
152
+ /**
153
+ * Implementation of {@link IConfigProvider} which contains nested {@link IConfigProviderBase} instances
154
+ */
155
+ export class CachedConfigProvider implements IConfigProvider {
156
+ private readonly configCache = new Map<string, StronglyTypedValue>();
157
+ private readonly orderedBaseProviders: (IConfigProviderBase | undefined)[];
158
+
159
+ constructor(
160
+ ... orderedBaseProviders: (IConfigProviderBase | undefined)[]
161
+ ) {
162
+ this.orderedBaseProviders = [];
163
+ const knownProviders = new Set<IConfigProviderBase>();
164
+ const candidateProviders = [...orderedBaseProviders];
165
+ while (candidateProviders.length > 0) {
166
+ const baseProvider = candidateProviders.shift()!;
167
+ if (baseProvider !== undefined
168
+ && isConfigProviderBase(baseProvider)
169
+ && !knownProviders.has(baseProvider)
170
+ ) {
171
+ knownProviders.add(baseProvider);
172
+ if (baseProvider instanceof CachedConfigProvider) {
173
+ candidateProviders.push(...baseProvider.orderedBaseProviders);
174
+ } else {
175
+ this.orderedBaseProviders.push(baseProvider);
176
+ }
177
+ }
178
+ }
179
+ }
180
+ getBoolean(name: string): boolean | undefined {
181
+ return this.getCacheEntry(name)?.boolean;
182
+ }
183
+ getNumber(name: string): number | undefined {
184
+ return this.getCacheEntry(name)?.number;
185
+ }
186
+ getString(name: string): string | undefined {
187
+ return this.getCacheEntry(name)?.string;
188
+ }
189
+ getBooleanArray(name: string): boolean[] | undefined {
190
+ return this.getCacheEntry(name)?.["boolean[]"];
191
+ }
192
+ getNumberArray(name: string): number[] | undefined {
193
+ return this.getCacheEntry(name)?.["number[]"];
194
+ }
195
+ getStringArray(name: string): string[] | undefined {
196
+ return this.getCacheEntry(name)?.["string[]"];
197
+ }
198
+
199
+ getRawConfig(name: string): ConfigTypes {
200
+ return this.getCacheEntry(name)?.raw;
201
+ }
202
+
203
+ private getCacheEntry(name: string): StronglyTypedValue | undefined {
204
+ if (!this.configCache.has(name)) {
205
+ for (const provider of this.orderedBaseProviders) {
206
+ const parsed = stronglyTypedParse(provider?.getRawConfig(name));
207
+ if (parsed !== undefined) {
208
+ this.configCache.set(name, parsed);
209
+ return parsed;
210
+ }
211
+ }
212
+ // configs are immutable, if the first lookup returned no results, all lookups should
213
+ this.configCache.set(name, { raw: undefined });
214
+ }
215
+ return this.configCache.get(name);
216
+ }
217
+ }
218
+
219
+ /**
220
+ * A type containing both a telemetry logger and a configuration provider
221
+ */
222
+ export interface MonitoringContext<
223
+ L extends ITelemetryBaseLogger = ITelemetryLogger
224
+ > {
225
+ config: IConfigProvider;
226
+ logger: L;
227
+ }
228
+
229
+ export function loggerIsMonitoringContext<L extends ITelemetryBaseLogger = ITelemetryLogger>(
230
+ obj: L): obj is L & MonitoringContext<L> {
231
+ const maybeConfig = obj as Partial<MonitoringContext<L>> | undefined;
232
+ return isConfigProviderBase(maybeConfig?.config) && maybeConfig?.logger !== undefined;
233
+ }
234
+
235
+ export function loggerToMonitoringContext<L extends ITelemetryBaseLogger = ITelemetryLogger>(
236
+ logger: L): MonitoringContext<L> {
237
+ if(loggerIsMonitoringContext<L>(logger)) {
238
+ return logger;
239
+ }
240
+ return mixinMonitoringContext<L>(logger, sessionStorageConfigProvider.value);
241
+ }
242
+
243
+ export function mixinMonitoringContext<L extends ITelemetryBaseLogger = ITelemetryLogger>(
244
+ logger: L, ... configs: (IConfigProviderBase | undefined)[]) {
245
+ if (loggerIsMonitoringContext<L>(logger)) {
246
+ throw new Error("Logger is already a monitoring context");
247
+ }
248
+ /**
249
+ * this is the tricky bit we use for now to smuggle monitoring context around.
250
+ * To the logger we mixin both config and itself, so mc.logger === logger as it is self-referential.
251
+ * We then expose it as a Monitoring context, so via types we hide the outer logger methods.
252
+ * To layers that expect just a logger we can pass mc.logger, but this is still a MonitoringContext
253
+ * so if a deeper layer then converts that logger to a monitoring context it can find the smuggled properties
254
+ * of the MonitoringContext and get the config provider.
255
+ */
256
+ const mc: L & Partial<MonitoringContext<L>> = logger;
257
+ mc.config = new CachedConfigProvider(...configs);
258
+ mc.logger = logger;
259
+ return mc as MonitoringContext<L>;
260
+ }
261
+
262
+ function isConfigProviderBase(obj: unknown): obj is IConfigProviderBase {
263
+ const maybeConfig = obj as Partial<IConfigProviderBase> | undefined;
264
+ return typeof (maybeConfig?.getRawConfig) === "function";
265
+ }
@@ -78,42 +78,6 @@ export interface IFluidErrorAnnotations {
78
78
  props?: ITelemetryProperties;
79
79
  }
80
80
 
81
- /** Simplest possible implementation of IFluidErrorBase */
82
- class SimpleFluidError implements IFluidErrorBase {
83
- private readonly telemetryProps: ITelemetryProperties = {};
84
-
85
- readonly errorType: string;
86
- readonly fluidErrorCode: string;
87
- readonly message: string;
88
- readonly stack?: string;
89
- readonly name: string = "Error";
90
- readonly errorInstanceId: string;
91
-
92
- constructor(
93
- errorProps: Omit<IFluidErrorBase,
94
- | "getTelemetryProperties"
95
- | "addTelemetryProperties"
96
- | "errorInstanceId"
97
- | "name">,
98
- ) {
99
- this.errorType = errorProps.errorType;
100
- this.fluidErrorCode = errorProps.fluidErrorCode;
101
- this.message = errorProps.message;
102
- this.stack = errorProps.stack;
103
- this.errorInstanceId = uuid();
104
-
105
- this.addTelemetryProperties(errorProps);
106
- }
107
-
108
- getTelemetryProperties(): ITelemetryProperties {
109
- return this.telemetryProps;
110
- }
111
-
112
- addTelemetryProperties(props: ITelemetryProperties) {
113
- copyProps(this.telemetryProps, props);
114
- }
115
- }
116
-
117
81
  /** For backwards compatibility with pre-fluidErrorCode valid errors */
118
82
  function patchWithErrorCode(
119
83
  legacyError: Omit<IFluidErrorBase, "fluidErrorCode">,
@@ -151,7 +115,7 @@ export function normalizeError(
151
115
  errorType: "genericError", // Match Container/Driver generic error type
152
116
  fluidErrorCode: "",
153
117
  message,
154
- stack: stack ?? generateStack(),
118
+ stack,
155
119
  });
156
120
 
157
121
  fluidError.addTelemetryProperties({
@@ -166,17 +130,37 @@ export function normalizeError(
166
130
  return fluidError;
167
131
  }
168
132
 
169
- export function generateStack(): string | undefined {
170
- // Some browsers will populate stack right away, others require throwing Error
171
- let stack = new Error("<<generated stack>>").stack;
172
- if (!stack) {
173
- try {
174
- throw new Error("<<generated stack>>");
175
- } catch (e) {
176
- stack = e.stack;
177
- }
133
+ let stackPopulatedOnCreation: boolean | undefined;
134
+
135
+ /**
136
+ * The purpose of this function is to provide ability to capture stack context quickly.
137
+ * Accessing new Error().stack is slow, and the slowest part is accessing stack property itself.
138
+ * There are scenarios where we generate error with stack, but error is handled in most cases and
139
+ * stack property is not accessed.
140
+ * For such cases it's better to not read stack property right away, but rather delay it until / if it's needed
141
+ * Some browsers will populate stack right away, others require throwing Error, so we do auto-detection on the fly.
142
+ * @returns Error object that has stack populated.
143
+ */
144
+ export function generateErrorWithStack(): Error {
145
+ const err = new Error("<<generated stack>>");
146
+
147
+ if (stackPopulatedOnCreation === undefined) {
148
+ stackPopulatedOnCreation = (err.stack !== undefined);
178
149
  }
179
- return stack;
150
+
151
+ if (stackPopulatedOnCreation) {
152
+ return err;
153
+ }
154
+
155
+ try {
156
+ throw err;
157
+ } catch (e) {
158
+ return e as Error;
159
+ }
160
+ }
161
+
162
+ export function generateStack(): string | undefined {
163
+ return generateErrorWithStack().stack;
180
164
  }
181
165
 
182
166
  /**
@@ -199,12 +183,7 @@ export function generateStack(): string | undefined {
199
183
  const newError = newErrorFn(message);
200
184
 
201
185
  if (stack !== undefined) {
202
- // supposedly setting stack on an Error can throw.
203
- try {
204
- Object.assign(newError, { stack });
205
- } catch (errorSettingStack) {
206
- newError.addTelemetryProperties({ stack2: stack });
207
- }
186
+ overwriteStack(newError, stack);
208
187
  }
209
188
 
210
189
  if (hasErrorInstanceId(innerError)) {
@@ -233,6 +212,15 @@ export function wrapErrorAndLog<T extends IFluidErrorBase>(
233
212
  return newError;
234
213
  }
235
214
 
215
+ function overwriteStack(error: IFluidErrorBase, stack: string) {
216
+ // supposedly setting stack on an Error can throw.
217
+ try {
218
+ Object.assign(error, { stack });
219
+ } catch (errorSettingStack) {
220
+ error.addTelemetryProperties({ stack2: stack });
221
+ }
222
+ }
223
+
236
224
  /**
237
225
  * Type guard to identify if a particular value (loosely) appears to be a tagged telemetry property
238
226
  */
@@ -322,3 +310,24 @@ export class LoggingError extends Error implements ILoggingError, Pick<IFluidErr
322
310
  };
323
311
  }
324
312
  }
313
+
314
+ /** Simple implementation of IFluidErrorBase, extending LoggingError */
315
+ class SimpleFluidError extends LoggingError implements IFluidErrorBase {
316
+ readonly errorType: string;
317
+ readonly fluidErrorCode: string;
318
+
319
+ constructor(
320
+ errorProps: Omit<IFluidErrorBase,
321
+ | "getTelemetryProperties"
322
+ | "addTelemetryProperties"
323
+ | "errorInstanceId"
324
+ | "name">,
325
+ ) {
326
+ super(errorProps.message);
327
+ this.errorType = errorProps.errorType;
328
+ this.fluidErrorCode = errorProps.fluidErrorCode;
329
+ if (errorProps.stack !== undefined) {
330
+ overwriteStack(this, errorProps.stack);
331
+ }
332
+ }
333
+ }
package/src/index.ts CHANGED
@@ -11,3 +11,12 @@ export * from "./logger";
11
11
  export * from "./mockLogger";
12
12
  export * from "./thresholdCounter";
13
13
  export * from "./utils";
14
+ export {
15
+ MonitoringContext,
16
+ IConfigProviderBase,
17
+ sessionStorageConfigProvider,
18
+ mixinMonitoringContext,
19
+ IConfigProvider,
20
+ ConfigTypes,
21
+ loggerToMonitoringContext,
22
+ } from "./config";
package/src/logger.ts CHANGED
@@ -16,6 +16,11 @@ import {
16
16
  TelemetryEventCategory,
17
17
  } from "@fluidframework/common-definitions";
18
18
  import { BaseTelemetryNullLogger, performance } from "@fluidframework/common-utils";
19
+ import {
20
+ CachedConfigProvider,
21
+ loggerIsMonitoringContext,
22
+ mixinMonitoringContext,
23
+ } from "./config";
19
24
  import {
20
25
  isILoggingError,
21
26
  extractLogSafeErrorProperties,
@@ -313,9 +318,17 @@ export class ChildLogger extends TelemetryLogger {
313
318
 
314
319
  private constructor(
315
320
  protected readonly baseLogger: ITelemetryBaseLogger,
316
- namespace?: string,
317
- properties?: ITelemetryLoggerPropertyBags) {
321
+ namespace: string | undefined,
322
+ properties: ITelemetryLoggerPropertyBags | undefined,
323
+ ) {
318
324
  super(namespace, properties);
325
+
326
+ // propagate the monitoring context
327
+ if(loggerIsMonitoringContext(baseLogger)) {
328
+ mixinMonitoringContext(
329
+ this,
330
+ new CachedConfigProvider(baseLogger.config));
331
+ }
319
332
  }
320
333
 
321
334
  /**
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export const pkgName = "@fluidframework/telemetry-utils";
9
- export const pkgVersion = "0.52.1";
9
+ export const pkgVersion = "0.54.0";