@fluidframework/telemetry-utils 1.4.0-121020 → 2.0.0-dev-rc.1.0.0.225277

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.
Files changed (156) hide show
  1. package/.eslintrc.js +12 -13
  2. package/.mocharc.js +12 -0
  3. package/CHANGELOG.md +249 -0
  4. package/README.md +68 -1
  5. package/api-extractor-esm.json +5 -0
  6. package/api-extractor-lint.json +4 -0
  7. package/api-extractor.json +2 -2
  8. package/api-report/telemetry-utils.api.md +444 -0
  9. package/dist/config.d.ts +47 -16
  10. package/dist/config.d.ts.map +1 -1
  11. package/dist/config.js +88 -38
  12. package/dist/config.js.map +1 -1
  13. package/dist/error.d.ts +112 -0
  14. package/dist/error.d.ts.map +1 -0
  15. package/dist/error.js +159 -0
  16. package/dist/error.js.map +1 -0
  17. package/dist/errorLogging.d.ts +86 -20
  18. package/dist/errorLogging.d.ts.map +1 -1
  19. package/dist/errorLogging.js +190 -60
  20. package/dist/errorLogging.js.map +1 -1
  21. package/dist/eventEmitterWithErrorHandling.d.ts +9 -3
  22. package/dist/eventEmitterWithErrorHandling.d.ts.map +1 -1
  23. package/dist/eventEmitterWithErrorHandling.js +16 -3
  24. package/dist/eventEmitterWithErrorHandling.js.map +1 -1
  25. package/dist/events.d.ts +27 -3
  26. package/dist/events.d.ts.map +1 -1
  27. package/dist/events.js +26 -2
  28. package/dist/events.js.map +1 -1
  29. package/dist/fluidErrorBase.d.ts +57 -16
  30. package/dist/fluidErrorBase.d.ts.map +1 -1
  31. package/dist/fluidErrorBase.js +27 -14
  32. package/dist/fluidErrorBase.js.map +1 -1
  33. package/dist/index.d.ts +12 -11
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +55 -21
  36. package/dist/index.js.map +1 -1
  37. package/dist/logger.d.ts +269 -53
  38. package/dist/logger.d.ts.map +1 -1
  39. package/dist/logger.js +423 -132
  40. package/dist/logger.js.map +1 -1
  41. package/dist/mockLogger.d.ts +39 -12
  42. package/dist/mockLogger.d.ts.map +1 -1
  43. package/dist/mockLogger.js +105 -22
  44. package/dist/mockLogger.js.map +1 -1
  45. package/dist/sampledTelemetryHelper.d.ts +18 -12
  46. package/dist/sampledTelemetryHelper.d.ts.map +1 -1
  47. package/dist/sampledTelemetryHelper.js +28 -19
  48. package/dist/sampledTelemetryHelper.js.map +1 -1
  49. package/dist/telemetry-utils-alpha.d.ts +290 -0
  50. package/dist/telemetry-utils-beta.d.ts +264 -0
  51. package/dist/telemetry-utils-public.d.ts +264 -0
  52. package/dist/telemetry-utils-untrimmed.d.ts +1102 -0
  53. package/dist/telemetryTypes.d.ts +115 -0
  54. package/dist/telemetryTypes.d.ts.map +1 -0
  55. package/dist/telemetryTypes.js +7 -0
  56. package/dist/telemetryTypes.js.map +1 -0
  57. package/dist/thresholdCounter.d.ts +6 -5
  58. package/dist/thresholdCounter.d.ts.map +1 -1
  59. package/dist/thresholdCounter.js +4 -3
  60. package/dist/thresholdCounter.js.map +1 -1
  61. package/dist/tsdoc-metadata.json +11 -0
  62. package/dist/utils.d.ts +54 -3
  63. package/dist/utils.d.ts.map +1 -1
  64. package/dist/utils.js +58 -3
  65. package/dist/utils.js.map +1 -1
  66. package/lib/config.d.ts +47 -16
  67. package/lib/config.d.ts.map +1 -1
  68. package/lib/config.js +85 -36
  69. package/lib/config.js.map +1 -1
  70. package/lib/error.d.ts +112 -0
  71. package/lib/error.d.ts.map +1 -0
  72. package/lib/error.js +150 -0
  73. package/lib/error.js.map +1 -0
  74. package/lib/errorLogging.d.ts +86 -20
  75. package/lib/errorLogging.d.ts.map +1 -1
  76. package/lib/errorLogging.js +189 -60
  77. package/lib/errorLogging.js.map +1 -1
  78. package/lib/eventEmitterWithErrorHandling.d.ts +9 -3
  79. package/lib/eventEmitterWithErrorHandling.d.ts.map +1 -1
  80. package/lib/eventEmitterWithErrorHandling.js +15 -2
  81. package/lib/eventEmitterWithErrorHandling.js.map +1 -1
  82. package/lib/events.d.ts +27 -3
  83. package/lib/events.d.ts.map +1 -1
  84. package/lib/events.js +26 -2
  85. package/lib/events.js.map +1 -1
  86. package/lib/fluidErrorBase.d.ts +57 -16
  87. package/lib/fluidErrorBase.d.ts.map +1 -1
  88. package/lib/fluidErrorBase.js +27 -14
  89. package/lib/fluidErrorBase.js.map +1 -1
  90. package/lib/index.d.ts +12 -11
  91. package/lib/index.d.ts.map +1 -1
  92. package/lib/index.js +11 -11
  93. package/lib/index.js.map +1 -1
  94. package/lib/logger.d.ts +269 -53
  95. package/lib/logger.d.ts.map +1 -1
  96. package/lib/logger.js +415 -131
  97. package/lib/logger.js.map +1 -1
  98. package/lib/mockLogger.d.ts +39 -12
  99. package/lib/mockLogger.d.ts.map +1 -1
  100. package/lib/mockLogger.js +106 -23
  101. package/lib/mockLogger.js.map +1 -1
  102. package/lib/sampledTelemetryHelper.d.ts +18 -12
  103. package/lib/sampledTelemetryHelper.d.ts.map +1 -1
  104. package/lib/sampledTelemetryHelper.js +26 -17
  105. package/lib/sampledTelemetryHelper.js.map +1 -1
  106. package/lib/telemetry-utils-alpha.d.mts +290 -0
  107. package/lib/telemetry-utils-beta.d.mts +264 -0
  108. package/lib/telemetry-utils-public.d.mts +264 -0
  109. package/lib/telemetry-utils-untrimmed.d.mts +1102 -0
  110. package/lib/telemetryTypes.d.ts +115 -0
  111. package/lib/telemetryTypes.d.ts.map +1 -0
  112. package/lib/telemetryTypes.js +6 -0
  113. package/lib/telemetryTypes.js.map +1 -0
  114. package/lib/thresholdCounter.d.ts +6 -5
  115. package/lib/thresholdCounter.d.ts.map +1 -1
  116. package/lib/thresholdCounter.js +4 -3
  117. package/lib/thresholdCounter.js.map +1 -1
  118. package/lib/utils.d.ts +54 -3
  119. package/lib/utils.d.ts.map +1 -1
  120. package/lib/utils.js +56 -2
  121. package/lib/utils.js.map +1 -1
  122. package/package.json +86 -57
  123. package/prettier.config.cjs +8 -0
  124. package/src/config.ts +254 -189
  125. package/src/error.ts +235 -0
  126. package/src/errorLogging.ts +440 -290
  127. package/src/eventEmitterWithErrorHandling.ts +26 -14
  128. package/src/events.ts +54 -25
  129. package/src/fluidErrorBase.ts +94 -46
  130. package/src/index.ts +76 -17
  131. package/src/logger.ts +972 -505
  132. package/src/mockLogger.ts +225 -83
  133. package/src/sampledTelemetryHelper.ts +136 -128
  134. package/src/telemetryTypes.ts +140 -0
  135. package/src/thresholdCounter.ts +38 -37
  136. package/src/utils.ts +108 -17
  137. package/tsconfig.esnext.json +6 -6
  138. package/tsconfig.json +9 -13
  139. package/dist/debugLogger.d.ts +0 -39
  140. package/dist/debugLogger.d.ts.map +0 -1
  141. package/dist/debugLogger.js +0 -101
  142. package/dist/debugLogger.js.map +0 -1
  143. package/dist/packageVersion.d.ts +0 -9
  144. package/dist/packageVersion.d.ts.map +0 -1
  145. package/dist/packageVersion.js +0 -12
  146. package/dist/packageVersion.js.map +0 -1
  147. package/lib/debugLogger.d.ts +0 -39
  148. package/lib/debugLogger.d.ts.map +0 -1
  149. package/lib/debugLogger.js +0 -97
  150. package/lib/debugLogger.js.map +0 -1
  151. package/lib/packageVersion.d.ts +0 -9
  152. package/lib/packageVersion.d.ts.map +0 -1
  153. package/lib/packageVersion.js +0 -9
  154. package/lib/packageVersion.js.map +0 -1
  155. package/src/debugLogger.ts +0 -126
  156. package/src/packageVersion.ts +0 -9
package/src/logger.ts CHANGED
@@ -4,564 +4,1031 @@
4
4
  */
5
5
 
6
6
  import {
7
- ITelemetryBaseEvent,
8
- ITelemetryBaseLogger,
9
- ITelemetryErrorEvent,
10
- ITelemetryGenericEvent,
11
- ITelemetryLogger,
12
- ITelemetryPerformanceEvent,
13
- ITelemetryProperties,
14
- TelemetryEventPropertyType,
15
- ITaggedTelemetryPropertyType,
16
- TelemetryEventCategory,
17
- } from "@fluidframework/common-definitions";
18
- import { BaseTelemetryNullLogger, performance } from "@fluidframework/common-utils";
7
+ ITelemetryBaseEvent,
8
+ ITelemetryBaseLogger,
9
+ ITelemetryErrorEvent,
10
+ ITelemetryGenericEvent,
11
+ ITelemetryPerformanceEvent,
12
+ TelemetryBaseEventPropertyType as TelemetryEventPropertyType,
13
+ LogLevel,
14
+ Tagged,
15
+ ITelemetryBaseProperties,
16
+ TelemetryBaseEventPropertyType,
17
+ } from "@fluidframework/core-interfaces";
18
+ import { IsomorphicPerformance, performance } from "@fluid-internal/client-utils";
19
+ import { CachedConfigProvider, loggerIsMonitoringContext, mixinMonitoringContext } from "./config";
19
20
  import {
20
- CachedConfigProvider,
21
- loggerIsMonitoringContext,
22
- mixinMonitoringContext,
23
- } from "./config";
24
- import {
25
- isILoggingError,
26
- extractLogSafeErrorProperties,
27
- generateStack,
21
+ isILoggingError,
22
+ extractLogSafeErrorProperties,
23
+ generateStack,
24
+ isTaggedTelemetryPropertyValue,
28
25
  } from "./errorLogging";
26
+ import {
27
+ ITelemetryEventExt,
28
+ ITelemetryGenericEventExt,
29
+ ITelemetryLoggerExt,
30
+ ITelemetryPerformanceEventExt,
31
+ TelemetryEventPropertyTypeExt,
32
+ TelemetryEventCategory,
33
+ ITelemetryPropertiesExt,
34
+ } from "./telemetryTypes";
35
+
36
+ export interface Memory {
37
+ usedJSHeapSize: number;
38
+ }
39
+
40
+ export interface PerformanceWithMemory extends IsomorphicPerformance {
41
+ readonly memory: Memory;
42
+ }
29
43
 
30
44
  /**
31
45
  * Broad classifications to be applied to individual properties as they're prepared to be logged to telemetry.
32
- * Please do not modify existing entries for backwards compatibility.
46
+ *
47
+ * @privateRemarks Please do not modify existing entries, to maintain backwards compatibility.
48
+ *
49
+ * @internal
33
50
  */
34
51
  export enum TelemetryDataTag {
35
- /**
36
- * Data containing terms from code packages that may have been dynamically loaded
37
- * @deprecated 1.0, will be removed in next release (see issue #6603). Use `TelemetryDataTag.CodeArtifact` instead.
38
- */
39
- PackageData = "PackageData",
40
- /** Data containing terms or IDs from code packages that may have been dynamically loaded */
41
- CodeArtifact = "CodeArtifact",
42
- /** Personal data of a variety of classifications that pertains to the user */
43
- UserData = "UserData",
52
+ /**
53
+ * Data containing terms or IDs from code packages that may have been dynamically loaded
54
+ */
55
+ CodeArtifact = "CodeArtifact",
56
+ /**
57
+ * Personal data of a variety of classifications that pertains to the user
58
+ */
59
+ UserData = "UserData",
44
60
  }
45
61
 
46
- export type TelemetryEventPropertyTypes = TelemetryEventPropertyType | ITaggedTelemetryPropertyType;
62
+ /**
63
+ * @alpha
64
+ */
65
+ export type TelemetryEventPropertyTypes = ITelemetryBaseProperties[string];
47
66
 
67
+ /**
68
+ * @alpha
69
+ */
48
70
  export interface ITelemetryLoggerPropertyBag {
49
- [index: string]: TelemetryEventPropertyTypes | (() => TelemetryEventPropertyTypes);
71
+ [index: string]: TelemetryEventPropertyTypes | (() => TelemetryEventPropertyTypes);
72
+ }
73
+
74
+ /**
75
+ * @alpha
76
+ */
77
+ export interface ITelemetryLoggerPropertyBags {
78
+ all?: ITelemetryLoggerPropertyBag;
79
+ error?: ITelemetryLoggerPropertyBag;
50
80
  }
51
- export interface ITelemetryLoggerPropertyBags{
52
- all?: ITelemetryLoggerPropertyBag;
53
- error?: ITelemetryLoggerPropertyBag;
81
+
82
+ /**
83
+ * Attempts to parse number from string.
84
+ * If it fails, it will return the original string.
85
+ *
86
+ * @remarks
87
+ * Used to make telemetry data typed (and support math operations, like comparison),
88
+ * in places where we do expect numbers (like contentsize/duration property in http header).
89
+ *
90
+ * @internal
91
+ */
92
+ // eslint-disable-next-line @rushstack/no-new-null
93
+ export function numberFromString(str: string | null | undefined): string | number | undefined {
94
+ if (str === undefined || str === null) {
95
+ return undefined;
96
+ }
97
+ const num = Number(str);
98
+ return Number.isNaN(num) ? str : num;
54
99
  }
55
100
 
101
+ // TODO: add docs
102
+ // eslint-disable-next-line jsdoc/require-description
103
+ /**
104
+ * @internal
105
+ */
106
+ export function formatTick(tick: number): number {
107
+ return Math.floor(tick);
108
+ }
109
+
110
+ /**
111
+ * String used to concatenate the namespace of parent loggers and their child loggers.
112
+ * @internal
113
+ */
114
+ export const eventNamespaceSeparator = ":" as const;
115
+
56
116
  /**
57
117
  * TelemetryLogger class contains various helper telemetry methods,
58
118
  * encoding in one place schemas for various types of Fluid telemetry events.
59
119
  * Creates sub-logger that appends properties to all events
60
120
  */
61
- export abstract class TelemetryLogger implements ITelemetryLogger {
62
- public static readonly eventNamespaceSeparator = ":";
63
-
64
- public static formatTick(tick: number): number {
65
- return Math.floor(tick);
66
- }
67
-
68
- /**
69
- * Attempts to parse number from string.
70
- * If fails,returns original string.
71
- * Used to make telemetry data typed (and support math operations, like comparison),
72
- * in places where we do expect numbers (like contentsize/duration property in http header)
73
- */
74
- public static numberFromString(str: string | null | undefined): string | number | undefined {
75
- if (str === undefined || str === null) {
76
- return undefined;
77
- }
78
- const num = Number(str);
79
- return Number.isNaN(num) ? str : num;
80
- }
81
-
82
- public static sanitizePkgName(name: string) {
83
- return name.replace("@", "").replace("/", "-");
84
- }
85
-
86
- /**
87
- * Take an unknown error object and add the appropriate info from it to the event. Message and stack will be copied
88
- * over from the error object, along with other telemetry properties if it's an ILoggingError.
89
- * @param event - Event being logged
90
- * @param error - Error to extract info from
91
- * @param fetchStack - Whether to fetch the current callstack if error.stack is undefined
92
- */
93
- public static prepareErrorObject(event: ITelemetryBaseEvent, error: any, fetchStack: boolean) {
94
- const { message, errorType, stack } = extractLogSafeErrorProperties(error, true /* sanitizeStack */);
95
- // First, copy over error message, stack, and errorType directly (overwrite if present on event)
96
- event.stack = stack;
97
- event.error = message; // Note that the error message goes on the 'error' field
98
- event.errorType = errorType;
99
-
100
- if (isILoggingError(error)) {
101
- // Add any other telemetry properties from the LoggingError
102
- const telemetryProp = error.getTelemetryProperties();
103
- for (const key of Object.keys(telemetryProp)) {
104
- if (event[key] !== undefined) {
105
- // Don't overwrite existing properties on the event
106
- continue;
107
- }
108
- event[key] = telemetryProp[key];
109
- }
110
- }
111
-
112
- // Collect stack if we were not able to extract it from error
113
- if (event.stack === undefined && fetchStack) {
114
- event.stack = generateStack();
115
- }
116
- }
117
-
118
- public constructor(
119
- protected readonly namespace?: string,
120
- protected readonly properties?: ITelemetryLoggerPropertyBags) {
121
- }
122
-
123
- /**
124
- * Send an event with the logger
125
- *
126
- * @param event - the event to send
127
- */
128
- public abstract send(event: ITelemetryBaseEvent): void;
129
-
130
- /**
131
- * Send a telemetry event with the logger
132
- *
133
- * @param event - the event to send
134
- * @param error - optional error object to log
135
- */
136
- public sendTelemetryEvent(event: ITelemetryGenericEvent, error?: any) {
137
- this.sendTelemetryEventCore({ ...event, category: event.category ?? "generic" }, error);
138
- }
139
-
140
- /**
141
- * Send a telemetry event with the logger
142
- *
143
- * @param event - the event to send
144
- * @param error - optional error object to log
145
- */
146
- protected sendTelemetryEventCore(
147
- event: ITelemetryGenericEvent & { category: TelemetryEventCategory; },
148
- error?: any) {
149
- const newEvent = { ...event };
150
- if (error !== undefined) {
151
- TelemetryLogger.prepareErrorObject(newEvent, error, false);
152
- }
153
-
154
- // Will include Nan & Infinity, but probably we do not care
155
- if (typeof newEvent.duration === "number") {
156
- newEvent.duration = TelemetryLogger.formatTick(newEvent.duration);
157
- }
158
-
159
- this.send(newEvent);
160
- }
161
-
162
- /**
163
- * Send an error telemetry event with the logger
164
- *
165
- * @param event - the event to send
166
- * @param error - optional error object to log
167
- */
168
- public sendErrorEvent(event: ITelemetryErrorEvent, error?: any) {
169
- this.sendTelemetryEventCore({
170
- // ensure the error field has some value,
171
- // this can and will be overridden by event, or error
172
- error: event.eventName,
173
- ...event,
174
- category: "error",
175
- }, error);
176
- }
177
-
178
- /**
179
- * Send a performance telemetry event with the logger
180
- *
181
- * @param event - Event to send
182
- * @param error - optional error object to log
183
- */
184
- public sendPerformanceEvent(event: ITelemetryPerformanceEvent, error?: any): void {
185
- const perfEvent = {
186
- ...event,
187
- category: event.category ?? "performance",
188
- };
189
-
190
- this.sendTelemetryEventCore(perfEvent, error);
191
- }
192
-
193
- protected prepareEvent(event: ITelemetryBaseEvent): ITelemetryBaseEvent {
194
- const includeErrorProps = event.category === "error" || event.error !== undefined;
195
- const newEvent: ITelemetryBaseEvent = {
196
- ...event,
197
- };
198
- if (this.namespace !== undefined) {
199
- newEvent.eventName = `${this.namespace}${TelemetryLogger.eventNamespaceSeparator}${newEvent.eventName}`;
200
- }
201
- if (this.properties) {
202
- const properties: (undefined | ITelemetryLoggerPropertyBag)[] = [];
203
- properties.push(this.properties.all);
204
- if (includeErrorProps) {
205
- properties.push(this.properties.error);
206
- }
207
- for (const props of properties) {
208
- if (props !== undefined) {
209
- for (const key of Object.keys(props)) {
210
- if (event[key] !== undefined) {
211
- continue;
212
- }
213
- const getterOrValue = props[key];
214
- // If this throws, hopefully it is handled elsewhere
215
- const value = typeof getterOrValue === "function" ? getterOrValue() : getterOrValue;
216
- if (value !== undefined) {
217
- newEvent[key] = value;
218
- }
219
- }
220
- }
221
- }
222
- }
223
- return newEvent;
224
- }
121
+ export abstract class TelemetryLogger implements ITelemetryLoggerExt {
122
+ /**
123
+ * {@inheritDoc eventNamespaceSeparator}
124
+ */
125
+ public static readonly eventNamespaceSeparator = eventNamespaceSeparator;
126
+
127
+ public static sanitizePkgName(name: string): string {
128
+ return name.replace("@", "").replace("/", "-");
129
+ }
130
+
131
+ /**
132
+ * Take an unknown error object and add the appropriate info from it to the event. Message and stack will be copied
133
+ * over from the error object, along with other telemetry properties if it's an ILoggingError.
134
+ * @param event - Event being logged
135
+ * @param error - Error to extract info from
136
+ * @param fetchStack - Whether to fetch the current callstack if error.stack is undefined
137
+ */
138
+ public static prepareErrorObject(
139
+ event: ITelemetryBaseEvent,
140
+ error: unknown,
141
+ fetchStack: boolean,
142
+ ): void {
143
+ const { message, errorType, stack } = extractLogSafeErrorProperties(
144
+ error,
145
+ true /* sanitizeStack */,
146
+ );
147
+ // First, copy over error message, stack, and errorType directly (overwrite if present on event)
148
+ event.stack = stack;
149
+ event.error = message; // Note that the error message goes on the 'error' field
150
+ event.errorType = errorType;
151
+
152
+ if (isILoggingError(error)) {
153
+ // Add any other telemetry properties from the LoggingError
154
+ const telemetryProp = error.getTelemetryProperties();
155
+ for (const key of Object.keys(telemetryProp)) {
156
+ if (event[key] !== undefined) {
157
+ // Don't overwrite existing properties on the event
158
+ continue;
159
+ }
160
+ event[key] = telemetryProp[key];
161
+ }
162
+ }
163
+
164
+ // Collect stack if we were not able to extract it from error
165
+ if (event.stack === undefined && fetchStack) {
166
+ event.stack = generateStack();
167
+ }
168
+ }
169
+
170
+ public constructor(
171
+ protected readonly namespace?: string,
172
+ protected readonly properties?: ITelemetryLoggerPropertyBags,
173
+ ) {}
174
+
175
+ /**
176
+ * Send an event with the logger
177
+ *
178
+ * @param event - the event to send
179
+ */
180
+ public abstract send(event: ITelemetryBaseEvent, logLevel?: LogLevel): void;
181
+
182
+ /**
183
+ * Send a telemetry event with the logger
184
+ *
185
+ * @param event - the event to send
186
+ * @param error - optional error object to log
187
+ * @param logLevel - optional level of the log. It category of event is set as error,
188
+ * then the logLevel will be upgraded to be an error.
189
+ */
190
+ public sendTelemetryEvent(
191
+ event: ITelemetryGenericEventExt,
192
+ error?: unknown,
193
+ logLevel: typeof LogLevel.verbose | typeof LogLevel.default = LogLevel.default,
194
+ ): void {
195
+ this.sendTelemetryEventCore(
196
+ { ...event, category: event.category ?? "generic" },
197
+ error,
198
+ event.category === "error" ? LogLevel.error : logLevel,
199
+ );
200
+ }
201
+
202
+ /**
203
+ * Send a telemetry event with the logger
204
+ *
205
+ * @param event - the event to send
206
+ * @param error - optional error object to log
207
+ * @param logLevel - optional level of the log.
208
+ */
209
+ protected sendTelemetryEventCore(
210
+ event: ITelemetryGenericEventExt & { category: TelemetryEventCategory },
211
+ error?: unknown,
212
+ logLevel?: LogLevel,
213
+ ): void {
214
+ const newEvent = convertToBaseEvent(event);
215
+ if (error !== undefined) {
216
+ TelemetryLogger.prepareErrorObject(newEvent, error, false);
217
+ }
218
+
219
+ // Will include Nan & Infinity, but probably we do not care
220
+ if (typeof newEvent.duration === "number") {
221
+ newEvent.duration = formatTick(newEvent.duration);
222
+ }
223
+
224
+ this.send(newEvent, logLevel);
225
+ }
226
+
227
+ /**
228
+ * Send an error telemetry event with the logger
229
+ *
230
+ * @param event - the event to send
231
+ * @param error - optional error object to log
232
+ */
233
+ public sendErrorEvent(event: ITelemetryErrorEvent, error?: unknown): void {
234
+ this.sendTelemetryEventCore(
235
+ {
236
+ // ensure the error field has some value,
237
+ // this can and will be overridden by event, or error
238
+ error: event.eventName,
239
+ ...event,
240
+ category: "error",
241
+ },
242
+ error,
243
+ LogLevel.error,
244
+ );
245
+ }
246
+
247
+ /**
248
+ * Send a performance telemetry event with the logger
249
+ *
250
+ * @param event - Event to send
251
+ * @param error - optional error object to log
252
+ * @param logLevel - optional level of the log. It category of event is set as error,
253
+ * then the logLevel will be upgraded to be an error.
254
+ */
255
+ public sendPerformanceEvent(
256
+ event: ITelemetryPerformanceEventExt,
257
+ error?: unknown,
258
+ logLevel: typeof LogLevel.verbose | typeof LogLevel.default = LogLevel.default,
259
+ ): void {
260
+ const perfEvent = {
261
+ ...event,
262
+ category: event.category ?? "performance",
263
+ };
264
+
265
+ this.sendTelemetryEventCore(
266
+ perfEvent,
267
+ error,
268
+ perfEvent.category === "error" ? LogLevel.error : logLevel,
269
+ );
270
+ }
271
+
272
+ protected prepareEvent(event: ITelemetryBaseEvent): ITelemetryBaseEvent {
273
+ const includeErrorProps = event.category === "error" || event.error !== undefined;
274
+ const newEvent: ITelemetryBaseEvent = {
275
+ ...event,
276
+ };
277
+ if (this.namespace !== undefined) {
278
+ newEvent.eventName = `${this.namespace}${TelemetryLogger.eventNamespaceSeparator}${newEvent.eventName}`;
279
+ }
280
+ return this.extendProperties(newEvent, includeErrorProps);
281
+ }
282
+
283
+ private extendProperties<T extends ITelemetryLoggerPropertyBag = ITelemetryLoggerPropertyBag>(
284
+ toExtend: T,
285
+ includeErrorProps: boolean,
286
+ ): T {
287
+ const eventLike: ITelemetryLoggerPropertyBag = toExtend;
288
+ if (this.properties) {
289
+ const properties: (undefined | ITelemetryLoggerPropertyBag)[] = [];
290
+ properties.push(this.properties.all);
291
+ if (includeErrorProps) {
292
+ properties.push(this.properties.error);
293
+ }
294
+ for (const props of properties) {
295
+ if (props !== undefined) {
296
+ for (const key of Object.keys(props)) {
297
+ if (eventLike[key] !== undefined) {
298
+ continue;
299
+ }
300
+ const getterOrValue = props[key];
301
+ // If this throws, hopefully it is handled elsewhere
302
+ const value =
303
+ typeof getterOrValue === "function" ? getterOrValue() : getterOrValue;
304
+ if (value !== undefined) {
305
+ eventLike[key] = value;
306
+ }
307
+ }
308
+ }
309
+ }
310
+ }
311
+ return toExtend;
312
+ }
225
313
  }
226
314
 
227
315
  /**
228
316
  * @deprecated 0.56, remove TaggedLoggerAdapter once its usage is removed from
229
317
  * container-runtime. Issue: #8191
230
318
  * TaggedLoggerAdapter class can add tag handling to your logger.
319
+ *
320
+ * @internal
231
321
  */
232
- export class TaggedLoggerAdapter implements ITelemetryBaseLogger {
233
- public constructor(
234
- private readonly logger: ITelemetryBaseLogger) {
235
- }
236
-
237
- public send(eventWithTagsMaybe: ITelemetryBaseEvent) {
238
- const newEvent: ITelemetryBaseEvent = {
239
- category: eventWithTagsMaybe.category,
240
- eventName: eventWithTagsMaybe.eventName,
241
- };
242
- for (const key of Object.keys(eventWithTagsMaybe)) {
243
- const taggableProp = eventWithTagsMaybe[key];
244
- const { value, tag } = (typeof taggableProp === "object")
245
- ? taggableProp
246
- : { value: taggableProp, tag: undefined };
247
- switch (tag) {
248
- case undefined:
249
- // No tag means we can log plainly
250
- newEvent[key] = value;
251
- break;
252
- case TelemetryDataTag.PackageData:
253
- // For Microsoft applications, PackageData is safe for now
254
- // (we don't load 3P code in 1P apps)
255
- newEvent[key] = value;
256
- break;
257
- case TelemetryDataTag.UserData:
258
- // Strip out anything tagged explicitly as PII.
259
- // Alternate strategy would be to hash these props
260
- newEvent[key] = "REDACTED (UserData)";
261
- break;
262
- default:
263
- // If we encounter a tag we don't recognize
264
- // then we must assume we should scrub.
265
- newEvent[key] = "REDACTED (unknown tag)";
266
- break;
267
- }
268
- }
269
- this.logger.send(newEvent);
270
- }
322
+ export class TaggedLoggerAdapter implements ITelemetryBaseLogger {
323
+ public constructor(private readonly logger: ITelemetryBaseLogger) {}
324
+
325
+ /**
326
+ * {@inheritDoc @fluidframework/core-interfaces#ITelemetryBaseLogger.send}
327
+ */
328
+ public send(eventWithTagsMaybe: ITelemetryBaseEvent): void {
329
+ const newEvent: ITelemetryBaseEvent = {
330
+ category: eventWithTagsMaybe.category,
331
+ eventName: eventWithTagsMaybe.eventName,
332
+ };
333
+ for (const key of Object.keys(eventWithTagsMaybe)) {
334
+ const taggableProp = eventWithTagsMaybe[key];
335
+ const { value, tag } =
336
+ typeof taggableProp === "object"
337
+ ? taggableProp
338
+ : { value: taggableProp, tag: undefined };
339
+ switch (tag) {
340
+ case undefined: {
341
+ // No tag means we can log plainly
342
+ newEvent[key] = value;
343
+ break;
344
+ }
345
+ case "PackageData": // For back-compat
346
+ case TelemetryDataTag.CodeArtifact: {
347
+ // For Microsoft applications, CodeArtifact is safe for now
348
+ // (we don't load 3P code in 1P apps)
349
+ newEvent[key] = value;
350
+ break;
351
+ }
352
+ case TelemetryDataTag.UserData: {
353
+ // Strip out anything tagged explicitly as UserData.
354
+ // Alternate strategy would be to hash these props
355
+ newEvent[key] = "REDACTED (UserData)";
356
+ break;
357
+ }
358
+ default: {
359
+ // If we encounter a tag we don't recognize
360
+ // then we must assume we should scrub.
361
+ newEvent[key] = "REDACTED (unknown tag)";
362
+ break;
363
+ }
364
+ }
365
+ }
366
+ this.logger.send(newEvent);
367
+ }
368
+ }
369
+
370
+ /**
371
+ * Create a child logger based on the provided props object.
372
+ *
373
+ * @remarks
374
+ * Passing in no props object (i.e. undefined) will return a logger that is effectively a no-op.
375
+ *
376
+ * @param props - logger is the base logger the child will log to after it's processing, namespace will be prefixed to all event names, properties are default properties that will be applied events.
377
+ *
378
+ * @alpha
379
+ */
380
+ export function createChildLogger(props?: {
381
+ logger?: ITelemetryBaseLogger;
382
+ namespace?: string;
383
+ properties?: ITelemetryLoggerPropertyBags;
384
+ }): ITelemetryLoggerExt {
385
+ return ChildLogger.create(props?.logger, props?.namespace, props?.properties);
271
386
  }
272
387
 
273
388
  /**
274
389
  * ChildLogger class contains various helper telemetry methods,
275
390
  * encoding in one place schemas for various types of Fluid telemetry events.
276
- * Creates sub-logger that appends properties to all events
391
+ * Creates sub-logger that appends properties to all events.
277
392
  */
278
393
  export class ChildLogger extends TelemetryLogger {
279
- /**
280
- * Create child logger
281
- * @param baseLogger - Base logger to use to output events. If undefined, proper child logger
282
- * is created, but it does not sends telemetry events anywhere.
283
- * @param namespace - Telemetry event name prefix to add to all events
284
- * @param properties - Base properties to add to all events
285
- * @param propertyGetters - Getters to add additional properties to all events
286
- */
287
- public static create(
288
- baseLogger?: ITelemetryBaseLogger,
289
- namespace?: string,
290
- properties?: ITelemetryLoggerPropertyBags): TelemetryLogger {
291
- // if we are creating a child of a child, rather than nest, which will increase
292
- // the callstack overhead, just generate a new logger that includes everything from the previous
293
- if (baseLogger instanceof ChildLogger) {
294
- const combinedProperties: ITelemetryLoggerPropertyBags = {};
295
- for (const extendedProps of [baseLogger.properties, properties]) {
296
- if (extendedProps !== undefined) {
297
- if (extendedProps.all !== undefined) {
298
- combinedProperties.all = {
299
- ... combinedProperties.all,
300
- ... extendedProps.all,
301
- };
302
- }
303
- if (extendedProps.error !== undefined) {
304
- combinedProperties.error = {
305
- ... combinedProperties.error,
306
- ... extendedProps.error,
307
- };
308
- }
309
- }
310
- }
311
-
312
- const combinedNamespace = baseLogger.namespace === undefined
313
- ? namespace
314
- : namespace === undefined
315
- ? baseLogger.namespace
316
- : `${baseLogger.namespace}${TelemetryLogger.eventNamespaceSeparator}${namespace}`;
317
-
318
- return new ChildLogger(
319
- baseLogger.baseLogger,
320
- combinedNamespace,
321
- combinedProperties,
322
- );
323
- }
324
-
325
- return new ChildLogger(
326
- baseLogger ? baseLogger : new BaseTelemetryNullLogger(),
327
- namespace,
328
- properties);
329
- }
330
-
331
- private constructor(
332
- protected readonly baseLogger: ITelemetryBaseLogger,
333
- namespace: string | undefined,
334
- properties: ITelemetryLoggerPropertyBags | undefined,
335
- ) {
336
- super(namespace, properties);
337
-
338
- // propagate the monitoring context
339
- if (loggerIsMonitoringContext(baseLogger)) {
340
- mixinMonitoringContext(
341
- this,
342
- new CachedConfigProvider(baseLogger.config));
343
- }
344
- }
345
-
346
- /**
347
- * Send an event with the logger
348
- *
349
- * @param event - the event to send
350
- */
351
- public send(event: ITelemetryBaseEvent): void {
352
- this.baseLogger.send(this.prepareEvent(event));
353
- }
394
+ /**
395
+ * Create child logger
396
+ * @param baseLogger - Base logger to use to output events. If undefined, proper child logger
397
+ * is created, but it does not send telemetry events anywhere.
398
+ * @param namespace - Telemetry event name prefix to add to all events
399
+ * @param properties - Base properties to add to all events
400
+ */
401
+ public static create(
402
+ baseLogger?: ITelemetryBaseLogger,
403
+ namespace?: string,
404
+ properties?: ITelemetryLoggerPropertyBags,
405
+ ): TelemetryLogger {
406
+ // if we are creating a child of a child, rather than nest, which will increase
407
+ // the callstack overhead, just generate a new logger that includes everything from the previous
408
+ if (baseLogger instanceof ChildLogger) {
409
+ const combinedProperties: ITelemetryLoggerPropertyBags = {};
410
+ for (const extendedProps of [baseLogger.properties, properties]) {
411
+ if (extendedProps !== undefined) {
412
+ if (extendedProps.all !== undefined) {
413
+ combinedProperties.all = {
414
+ ...combinedProperties.all,
415
+ ...extendedProps.all,
416
+ };
417
+ }
418
+ if (extendedProps.error !== undefined) {
419
+ combinedProperties.error = {
420
+ ...combinedProperties.error,
421
+ ...extendedProps.error,
422
+ };
423
+ }
424
+ }
425
+ }
426
+
427
+ const combinedNamespace =
428
+ baseLogger.namespace === undefined
429
+ ? namespace
430
+ : namespace === undefined
431
+ ? baseLogger.namespace
432
+ : `${baseLogger.namespace}${TelemetryLogger.eventNamespaceSeparator}${namespace}`;
433
+
434
+ const child = new ChildLogger(
435
+ baseLogger.baseLogger,
436
+ combinedNamespace,
437
+ combinedProperties,
438
+ );
439
+
440
+ if (!loggerIsMonitoringContext(child) && loggerIsMonitoringContext(baseLogger)) {
441
+ mixinMonitoringContext(child, baseLogger.config);
442
+ }
443
+ return child;
444
+ }
445
+
446
+ return new ChildLogger(baseLogger ?? { send(): void {} }, namespace, properties);
447
+ }
448
+
449
+ private constructor(
450
+ protected readonly baseLogger: ITelemetryBaseLogger,
451
+ namespace: string | undefined,
452
+ properties: ITelemetryLoggerPropertyBags | undefined,
453
+ ) {
454
+ super(namespace, properties);
455
+
456
+ // propagate the monitoring context
457
+ if (loggerIsMonitoringContext(baseLogger)) {
458
+ mixinMonitoringContext(this, new CachedConfigProvider(this, baseLogger.config));
459
+ }
460
+ }
461
+
462
+ public get minLogLevel(): LogLevel | undefined {
463
+ return this.baseLogger.minLogLevel;
464
+ }
465
+
466
+ private shouldFilterOutEvent(event: ITelemetryBaseEvent, logLevel?: LogLevel): boolean {
467
+ const eventLogLevel = logLevel ?? LogLevel.default;
468
+ const configLogLevel = this.baseLogger.minLogLevel ?? LogLevel.default;
469
+ // Filter out in case event log level is below what is wanted in config.
470
+ return eventLogLevel < configLogLevel;
471
+ }
472
+
473
+ /**
474
+ * Send an event with the logger
475
+ *
476
+ * @param event - the event to send
477
+ */
478
+ public send(event: ITelemetryBaseEvent, logLevel?: LogLevel): void {
479
+ if (this.shouldFilterOutEvent(event, logLevel)) {
480
+ return;
481
+ }
482
+ this.baseLogger.send(this.prepareEvent(event), logLevel);
483
+ }
484
+ }
485
+
486
+ /**
487
+ * Input properties for {@link createMultiSinkLogger}.
488
+ *
489
+ * @internal
490
+ */
491
+ export interface MultiSinkLoggerProperties {
492
+ /**
493
+ * Will be prefixed to all event names.
494
+ */
495
+ namespace?: string;
496
+
497
+ /**
498
+ * Default properties that will be applied to all events flowing through this logger.
499
+ */
500
+ properties?: ITelemetryLoggerPropertyBags;
501
+
502
+ /**
503
+ * The base loggers that this logger will forward the logs to, after it processes them.
504
+ */
505
+ loggers?: (ITelemetryBaseLogger | undefined)[];
506
+
507
+ /**
508
+ * If true, the logger will attempt to copy the custom properties (if they are of a known type, i.e. one from this package) of all the base loggers passed to it, to apply them itself to logs that flow through.
509
+ */
510
+ tryInheritProperties?: true;
511
+ }
512
+
513
+ /**
514
+ * Create a logger which logs to multiple other loggers based on the provided props object.
515
+ *
516
+ * @internal
517
+ */
518
+ export function createMultiSinkLogger(props: MultiSinkLoggerProperties): ITelemetryLoggerExt {
519
+ return new MultiSinkLogger(
520
+ props.namespace,
521
+ props.properties,
522
+ props.loggers?.filter((l): l is ITelemetryBaseLogger => l !== undefined),
523
+ props.tryInheritProperties,
524
+ );
354
525
  }
355
526
 
356
527
  /**
357
528
  * Multi-sink logger
358
529
  * Takes multiple ITelemetryBaseLogger objects (sinks) and logs all events into each sink
359
- * Implements ITelemetryBaseLogger (through static create() method)
360
530
  */
361
531
  export class MultiSinkLogger extends TelemetryLogger {
362
- protected loggers: ITelemetryBaseLogger[] = [];
363
-
364
- /**
365
- * Create multiple sink logger (i.e. logger that sends events to multiple sinks)
366
- * @param namespace - Telemetry event name prefix to add to all events
367
- * @param properties - Base properties to add to all events
368
- * @param propertyGetters - Getters to add additional properties to all events
369
- */
370
- constructor(
371
- namespace?: string,
372
- properties?: ITelemetryLoggerPropertyBags) {
373
- super(namespace, properties);
374
- }
375
-
376
- /**
377
- * Add logger to send all events to
378
- * @param logger - Logger to add
379
- */
380
- public addLogger(logger?: ITelemetryBaseLogger) {
381
- if (logger !== undefined && logger !== null) {
382
- this.loggers.push(logger);
383
- }
384
- }
385
-
386
- /**
387
- * Send an event to the loggers
388
- *
389
- * @param event - the event to send to all the registered logger
390
- */
391
- public send(event: ITelemetryBaseEvent): void {
392
- const newEvent = this.prepareEvent(event);
393
- this.loggers.forEach((logger: ITelemetryBaseLogger) => {
394
- logger.send(newEvent);
395
- });
396
- }
532
+ protected loggers: ITelemetryBaseLogger[];
533
+ // This is minimum of minLlogLevel of all loggers.
534
+ private _minLogLevelOfAllLoggers: LogLevel;
535
+
536
+ /**
537
+ * Create multiple sink logger (i.e. logger that sends events to multiple sinks)
538
+ * @param namespace - Telemetry event name prefix to add to all events
539
+ * @param properties - Base properties to add to all events
540
+ * @param loggers - The list of loggers to use as sinks
541
+ * @param tryInheritProperties - Will attempted to copy those loggers properties to this loggers if they are of a known type e.g. one from this package
542
+ */
543
+ constructor(
544
+ namespace?: string,
545
+ properties?: ITelemetryLoggerPropertyBags,
546
+ loggers: ITelemetryBaseLogger[] = [],
547
+ tryInheritProperties?: true,
548
+ ) {
549
+ let realProperties = properties === undefined ? undefined : { ...properties };
550
+ if (tryInheritProperties === true) {
551
+ const merge = (realProperties ??= {});
552
+ loggers
553
+ .filter((l): l is this => l instanceof TelemetryLogger)
554
+ .map((l) => l.properties ?? {})
555
+ // eslint-disable-next-line unicorn/no-array-for-each
556
+ .forEach((cv) => {
557
+ // eslint-disable-next-line unicorn/no-array-for-each
558
+ Object.keys(cv).forEach((k) => {
559
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
560
+ merge[k] = { ...cv[k], ...merge?.[k] };
561
+ });
562
+ });
563
+ }
564
+
565
+ super(namespace, realProperties);
566
+ this.loggers = loggers;
567
+ this._minLogLevelOfAllLoggers = LogLevel.default;
568
+ this.calculateMinLogLevel();
569
+ }
570
+
571
+ public get minLogLevel(): LogLevel {
572
+ return this._minLogLevelOfAllLoggers;
573
+ }
574
+
575
+ private calculateMinLogLevel(): void {
576
+ if (this.loggers.length > 0) {
577
+ const logLevels: LogLevel[] = [];
578
+ for (const logger of this.loggers) {
579
+ logLevels.push(logger.minLogLevel ?? LogLevel.default);
580
+ }
581
+ this._minLogLevelOfAllLoggers = Math.min(...logLevels) as LogLevel;
582
+ }
583
+ }
584
+
585
+ /**
586
+ * Add logger to send all events to
587
+ * @param logger - Logger to add
588
+ */
589
+ public addLogger(logger?: ITelemetryBaseLogger): void {
590
+ if (logger !== undefined && logger !== null) {
591
+ this.loggers.push(logger);
592
+ // Update in case the logLevel of added logger is less than the current.
593
+ this.calculateMinLogLevel();
594
+ }
595
+ }
596
+
597
+ /**
598
+ * Send an event to the loggers
599
+ *
600
+ * @param event - the event to send to all the registered logger
601
+ */
602
+ public send(event: ITelemetryBaseEvent): void {
603
+ const newEvent = this.prepareEvent(event);
604
+ for (const logger of this.loggers) {
605
+ logger.send(newEvent);
606
+ }
607
+ }
397
608
  }
398
609
 
399
610
  /**
400
- * Describes what events PerformanceEvent should log
401
- * By default, all events are logged, but client can override this behavior
402
- * For example, there is rarely a need to record start event, as we really after
611
+ * Describes what events {@link PerformanceEvent} should log.
612
+ *
613
+ * @remarks
614
+ * By default, all events are logged, but the client can override this behavior.
615
+ *
616
+ * For example, there is rarely a need to record a start event, as we're really after
403
617
  * success / failure tracking, including duration (on success).
618
+ *
619
+ * @internal
404
620
  */
405
621
  export interface IPerformanceEventMarkers {
406
- start?: true;
407
- end?: true;
408
- cancel?: "generic" | "error"; // tells wether to issue "generic" or "error" category cancel event
622
+ start?: true;
623
+ end?: true;
624
+ cancel?: "generic" | "error"; // tells wether to issue "generic" or "error" category cancel event
409
625
  }
410
626
 
411
627
  /**
412
- * Helper class to log performance events
628
+ * Helper class to log performance events.
629
+ *
630
+ * @internal
413
631
  */
414
632
  export class PerformanceEvent {
415
- public static start(logger: ITelemetryLogger, event: ITelemetryGenericEvent, markers?: IPerformanceEventMarkers) {
416
- return new PerformanceEvent(logger, event, markers);
417
- }
418
-
419
- public static timedExec<T>(
420
- logger: ITelemetryLogger,
421
- event: ITelemetryGenericEvent,
422
- callback: (event: PerformanceEvent) => T,
423
- markers?: IPerformanceEventMarkers,
424
- ) {
425
- const perfEvent = PerformanceEvent.start(logger, event, markers);
426
- try {
427
- const ret = callback(perfEvent);
428
- perfEvent.autoEnd();
429
- return ret;
430
- } catch (error) {
431
- perfEvent.cancel(undefined, error);
432
- throw error;
433
- }
434
- }
435
-
436
- public static async timedExecAsync<T>(
437
- logger: ITelemetryLogger,
438
- event: ITelemetryGenericEvent,
439
- callback: (event: PerformanceEvent) => Promise<T>,
440
- markers?: IPerformanceEventMarkers,
441
- ) {
442
- const perfEvent = PerformanceEvent.start(logger, event, markers);
443
- try {
444
- const ret = await callback(perfEvent);
445
- perfEvent.autoEnd();
446
- return ret;
447
- } catch (error) {
448
- perfEvent.cancel(undefined, error);
449
- throw error;
450
- }
451
- }
452
-
453
- public get duration() { return performance.now() - this.startTime; }
454
-
455
- private event?: ITelemetryGenericEvent;
456
- private readonly startTime = performance.now();
457
- private startMark?: string;
458
-
459
- protected constructor(
460
- private readonly logger: ITelemetryLogger,
461
- event: ITelemetryGenericEvent,
462
- private readonly markers: IPerformanceEventMarkers = { end: true, cancel: "generic" },
463
- ) {
464
- this.event = { ...event };
465
- if (this.markers.start) {
466
- this.reportEvent("start");
467
- }
468
-
469
- if (typeof window === "object" && window != null && window.performance) {
470
- this.startMark = `${event.eventName}-start`;
471
- window.performance.mark(this.startMark);
472
- }
473
- }
474
-
475
- public reportProgress(props?: ITelemetryProperties, eventNameSuffix: string = "update"): void {
476
- this.reportEvent(eventNameSuffix, props);
477
- }
478
-
479
- private autoEnd() {
480
- // Event might have been cancelled or ended in the callback
481
- if (this.event && this.markers.end) {
482
- this.reportEvent("end");
483
- }
484
- this.performanceEndMark();
485
- this.event = undefined;
486
- }
487
-
488
- public end(props?: ITelemetryProperties): void {
489
- this.reportEvent("end", props);
490
- this.performanceEndMark();
491
- this.event = undefined;
492
- }
493
-
494
- private performanceEndMark() {
495
- if (this.startMark && this.event) {
496
- const endMark = `${this.event.eventName}-end`;
497
- window.performance.mark(endMark);
498
- window.performance.measure(`${this.event.eventName}`, this.startMark, endMark);
499
- this.startMark = undefined;
500
- }
501
- }
502
-
503
- public cancel(props?: ITelemetryProperties, error?: any): void {
504
- if (this.markers.cancel !== undefined) {
505
- this.reportEvent("cancel", { category: this.markers.cancel, ...props }, error);
506
- }
507
- this.event = undefined;
508
- }
509
-
510
- /**
511
- * Report the event, if it hasn't already been reported.
512
- */
513
- public reportEvent(eventNameSuffix: string, props?: ITelemetryProperties, error?: any) {
514
- // There are strange sequences involving multiple Promise chains
515
- // where the event can be cancelled and then later a callback is invoked
516
- // and the caller attempts to end directly, e.g. issue #3936. Just return.
517
- if (!this.event) {
518
- return;
519
- }
520
-
521
- const event: ITelemetryPerformanceEvent = { ...this.event, ...props };
522
- event.eventName = `${event.eventName}_${eventNameSuffix}`;
523
- if (eventNameSuffix !== "start") {
524
- event.duration = this.duration;
525
- }
526
-
527
- this.logger.sendPerformanceEvent(event, error);
528
- }
633
+ /**
634
+ * Creates an instance of {@link PerformanceEvent} and starts measurements
635
+ * @param logger - the logger to be used for publishing events
636
+ * @param event - the logging event details which will be published with the performance measurements
637
+ * @param markers - See {@link IPerformanceEventMarkers}
638
+ * @param recordHeapSize - whether or not to also record memory performance
639
+ * @param emitLogs - should this instance emit logs. If set to false, logs will not be emitted to the logger,
640
+ * but measurements will still be performed and any specified markers will be generated.
641
+ * @returns An instance of {@link PerformanceEvent}
642
+ */
643
+ public static start(
644
+ logger: ITelemetryLoggerExt,
645
+ event: ITelemetryGenericEventExt,
646
+ markers?: IPerformanceEventMarkers,
647
+ recordHeapSize: boolean = false,
648
+ emitLogs: boolean = true,
649
+ ): PerformanceEvent {
650
+ return new PerformanceEvent(logger, event, markers, recordHeapSize, emitLogs);
651
+ }
652
+
653
+ /**
654
+ * Measure a synchronous task
655
+ * @param logger - the logger to be used for publishing events
656
+ * @param event - the logging event details which will be published with the performance measurements
657
+ * @param callback - the task to be executed and measured
658
+ * @param markers - See {@link IPerformanceEventMarkers}
659
+ * @param sampleThreshold - events with the same name and category will be sent to the logger
660
+ * only when we hit this many executions of the task. If unspecified, all events will be sent.
661
+ * @returns The results of the executed task
662
+ *
663
+ * @remarks Note that if the "same" event (category + eventName) would be emitted by different
664
+ * tasks (`callback`), `sampleThreshold` is still applied only based on the event's category + eventName,
665
+ * so executing either of the tasks will increase the internal counter and they
666
+ * effectively "share" the sampling rate for the event.
667
+ */
668
+ public static timedExec<T>(
669
+ logger: ITelemetryLoggerExt,
670
+ event: ITelemetryGenericEventExt,
671
+ callback: (event: PerformanceEvent) => T,
672
+ markers?: IPerformanceEventMarkers,
673
+ sampleThreshold: number = 1,
674
+ ): T {
675
+ const perfEvent = PerformanceEvent.start(
676
+ logger,
677
+ event,
678
+ markers,
679
+ undefined, // recordHeapSize
680
+ PerformanceEvent.shouldReport(event, sampleThreshold),
681
+ );
682
+ try {
683
+ const ret = callback(perfEvent);
684
+ perfEvent.autoEnd();
685
+ return ret;
686
+ } catch (error) {
687
+ perfEvent.cancel(undefined, error);
688
+ throw error;
689
+ }
690
+ }
691
+
692
+ /**
693
+ * Measure an asynchronous task
694
+ * @param logger - the logger to be used for publishing events
695
+ * @param event - the logging event details which will be published with the performance measurements
696
+ * @param callback - the task to be executed and measured
697
+ * @param markers - See {@link IPerformanceEventMarkers}
698
+ * @param recordHeapSize - whether or not to also record memory performance
699
+ * @param sampleThreshold - events with the same name and category will be sent to the logger
700
+ * only when we hit this many executions of the task. If unspecified, all events will be sent.
701
+ * @returns The results of the executed task
702
+ *
703
+ * @remarks Note that if the "same" event (category + eventName) would be emitted by different
704
+ * tasks (`callback`), `sampleThreshold` is still applied only based on the event's category + eventName,
705
+ * so executing either of the tasks will increase the internal counter and they
706
+ * effectively "share" the sampling rate for the event.
707
+ */
708
+ public static async timedExecAsync<T>(
709
+ logger: ITelemetryLoggerExt,
710
+ event: ITelemetryGenericEventExt,
711
+ callback: (event: PerformanceEvent) => Promise<T>,
712
+ markers?: IPerformanceEventMarkers,
713
+ recordHeapSize?: boolean,
714
+ sampleThreshold: number = 1,
715
+ ): Promise<T> {
716
+ const perfEvent = PerformanceEvent.start(
717
+ logger,
718
+ event,
719
+ markers,
720
+ recordHeapSize,
721
+ PerformanceEvent.shouldReport(event, sampleThreshold),
722
+ );
723
+ try {
724
+ const ret = await callback(perfEvent);
725
+ perfEvent.autoEnd();
726
+ return ret;
727
+ } catch (error) {
728
+ perfEvent.cancel(undefined, error);
729
+ throw error;
730
+ }
731
+ }
732
+
733
+ public get duration(): number {
734
+ return performance.now() - this.startTime;
735
+ }
736
+
737
+ private event?: ITelemetryGenericEventExt;
738
+ private readonly startTime = performance.now();
739
+ private startMark?: string;
740
+ private startMemoryCollection: number | undefined = 0;
741
+
742
+ protected constructor(
743
+ private readonly logger: ITelemetryLoggerExt,
744
+ event: ITelemetryGenericEventExt,
745
+ private readonly markers: IPerformanceEventMarkers = { end: true, cancel: "generic" },
746
+ private readonly recordHeapSize: boolean = false,
747
+ private readonly emitLogs: boolean = true,
748
+ ) {
749
+ this.event = { ...event };
750
+ if (this.markers.start) {
751
+ this.reportEvent("start");
752
+ }
753
+
754
+ if (typeof window === "object" && window?.performance?.mark) {
755
+ this.startMark = `${event.eventName}-start`;
756
+ window.performance.mark(this.startMark);
757
+ }
758
+ }
759
+
760
+ public reportProgress(
761
+ props?: ITelemetryPropertiesExt,
762
+ eventNameSuffix: string = "update",
763
+ ): void {
764
+ this.reportEvent(eventNameSuffix, props);
765
+ }
766
+
767
+ private autoEnd(): void {
768
+ // Event might have been cancelled or ended in the callback
769
+ if (this.event && this.markers.end) {
770
+ this.reportEvent("end");
771
+ }
772
+ this.performanceEndMark();
773
+ this.event = undefined;
774
+ }
775
+
776
+ public end(props?: ITelemetryPropertiesExt): void {
777
+ this.reportEvent("end", props);
778
+ this.performanceEndMark();
779
+ this.event = undefined;
780
+ }
781
+
782
+ private performanceEndMark(): void {
783
+ if (this.startMark && this.event) {
784
+ const endMark = `${this.event.eventName}-end`;
785
+ window.performance.mark(endMark);
786
+ window.performance.measure(`${this.event.eventName}`, this.startMark, endMark);
787
+ this.startMark = undefined;
788
+ }
789
+ }
790
+
791
+ public cancel(props?: ITelemetryPropertiesExt, error?: unknown): void {
792
+ if (this.markers.cancel !== undefined) {
793
+ this.reportEvent("cancel", { category: this.markers.cancel, ...props }, error);
794
+ }
795
+ this.event = undefined;
796
+ }
797
+
798
+ /**
799
+ * Report the event, if it hasn't already been reported.
800
+ */
801
+ public reportEvent(
802
+ eventNameSuffix: string,
803
+ props?: ITelemetryPropertiesExt,
804
+ error?: unknown,
805
+ ): void {
806
+ // There are strange sequences involving multiple Promise chains
807
+ // where the event can be cancelled and then later a callback is invoked
808
+ // and the caller attempts to end directly, e.g. issue #3936. Just return.
809
+ if (!this.event) {
810
+ return;
811
+ }
812
+
813
+ if (!this.emitLogs) {
814
+ return;
815
+ }
816
+
817
+ const event: ITelemetryPerformanceEventExt = { ...this.event, ...props };
818
+ event.eventName = `${event.eventName}_${eventNameSuffix}`;
819
+ if (eventNameSuffix !== "start") {
820
+ event.duration = this.duration;
821
+ if (this.startMemoryCollection) {
822
+ const currentMemory = (performance as PerformanceWithMemory)?.memory
823
+ ?.usedJSHeapSize;
824
+ const differenceInKBytes = Math.floor(
825
+ (currentMemory - this.startMemoryCollection) / 1024,
826
+ );
827
+ if (differenceInKBytes > 0) {
828
+ event.usedJSHeapSize = differenceInKBytes;
829
+ }
830
+ }
831
+ } else if (this.recordHeapSize) {
832
+ this.startMemoryCollection = (performance as PerformanceWithMemory)?.memory
833
+ ?.usedJSHeapSize;
834
+ }
835
+
836
+ this.logger.sendPerformanceEvent(event, error);
837
+ }
838
+
839
+ private static readonly eventHits = new Map<string, number>();
840
+ private static shouldReport(
841
+ event: ITelemetryGenericEventExt,
842
+ sampleThreshold: number,
843
+ ): boolean {
844
+ const eventKey = `.${event.category}.${event.eventName}`;
845
+ const hitCount = PerformanceEvent.eventHits.get(eventKey) ?? 0;
846
+ PerformanceEvent.eventHits.set(eventKey, hitCount >= sampleThreshold ? 1 : hitCount + 1);
847
+ return hitCount % sampleThreshold === 0;
848
+ }
849
+ }
850
+
851
+ /**
852
+ * Null logger that no-ops for all telemetry events passed to it.
853
+ *
854
+ * @deprecated This will be removed in a future release.
855
+ * For internal use within the FluidFramework codebase, use {@link createChildLogger} with no arguments instead.
856
+ * For external consumers we recommend writing a trivial implementation of {@link @fluidframework/core-interfaces#ITelemetryBaseLogger}
857
+ * where the send() method does nothing and using that.
858
+ *
859
+ * @internal
860
+ */
861
+ export class TelemetryNullLogger implements ITelemetryLoggerExt {
862
+ public send(event: ITelemetryBaseEvent): void {}
863
+ public sendTelemetryEvent(event: ITelemetryGenericEvent, error?: unknown): void {}
864
+ public sendErrorEvent(event: ITelemetryErrorEvent, error?: unknown): void {}
865
+ public sendPerformanceEvent(event: ITelemetryPerformanceEvent, error?: unknown): void {}
529
866
  }
530
867
 
531
868
  /**
532
- * Logger that is useful for UT
533
- * It can be used in places where logger instance is required, but events should be not send over.
869
+ * Takes in an event object, and converts all of its values to a basePropertyType.
870
+ * In the case of an invalid property type, the value will be converted to an error string.
871
+ * @param event - Event with fields you want to stringify.
534
872
  */
535
- export class TelemetryUTLogger implements ITelemetryLogger {
536
- public send(event: ITelemetryBaseEvent): void {
537
- }
538
- public sendTelemetryEvent(event: ITelemetryGenericEvent, error?: any) {
539
- }
540
- public sendErrorEvent(event: ITelemetryErrorEvent, error?: any) {
541
- this.reportError("errorEvent in UT logger!", event, error);
542
- }
543
- public sendPerformanceEvent(event: ITelemetryPerformanceEvent, error?: any): void {
544
- }
545
- public logGenericError(eventName: string, error: any) {
546
- this.reportError(`genericError in UT logger!`, { eventName }, error);
547
- }
548
- public logException(event: ITelemetryErrorEvent, exception: any): void {
549
- this.reportError("exception in UT logger!", event, exception);
550
- }
551
- public debugAssert(condition: boolean, event?: ITelemetryErrorEvent): void {
552
- this.reportError("debugAssert in UT logger!");
553
- }
554
- public shipAssert(condition: boolean, event?: ITelemetryErrorEvent): void {
555
- this.reportError("shipAssert in UT logger!");
556
- }
557
-
558
- private reportError(message: string, event?: ITelemetryErrorEvent, err?: any) {
559
- const error = new Error(message);
560
- (error as any).error = error;
561
- (error as any).event = event;
562
- // report to console as exception can be eaten
563
- console.error(message);
564
- console.error(error);
565
- throw error;
566
- }
873
+ function convertToBaseEvent({
874
+ category,
875
+ eventName,
876
+ ...props
877
+ }: ITelemetryEventExt): ITelemetryBaseEvent {
878
+ const newEvent: ITelemetryBaseEvent = { category, eventName };
879
+ for (const key of Object.keys(props)) {
880
+ newEvent[key] = convertToBasePropertyType(props[key]);
881
+ }
882
+ return newEvent;
567
883
  }
884
+
885
+ /**
886
+ * Takes in value, and does one of 4 things.
887
+ * if value is of primitive type - returns the original value.
888
+ * If the value is a flat array or object - returns a stringified version of the array/object.
889
+ * If the value is an object of type Tagged<TelemetryEventPropertyType> - returns the object
890
+ * with its values recursively converted to base property Type.
891
+ * If none of these cases are reached - returns an error string
892
+ * @param x - value passed in to convert to a base property type
893
+ */
894
+ export function convertToBasePropertyType(
895
+ x: TelemetryEventPropertyTypeExt | Tagged<TelemetryEventPropertyTypeExt>,
896
+ ): TelemetryEventPropertyType | Tagged<TelemetryEventPropertyType> {
897
+ return isTaggedTelemetryPropertyValue(x)
898
+ ? {
899
+ value: convertToBasePropertyTypeUntagged(x.value),
900
+ tag: x.tag,
901
+ }
902
+ : convertToBasePropertyTypeUntagged(x);
903
+ }
904
+
905
+ function convertToBasePropertyTypeUntagged(
906
+ x: TelemetryEventPropertyTypeExt,
907
+ ): TelemetryEventPropertyType {
908
+ switch (typeof x) {
909
+ case "string":
910
+ case "number":
911
+ case "boolean":
912
+ case "undefined": {
913
+ return x;
914
+ }
915
+ case "object": {
916
+ // We assume this is an array or flat object based on the input types
917
+ return JSON.stringify(x);
918
+ }
919
+ default: {
920
+ // should never reach this case based on the input types
921
+ console.error(
922
+ `convertToBasePropertyTypeUntagged: INVALID PROPERTY (typed as ${typeof x})`,
923
+ );
924
+ return `INVALID PROPERTY (typed as ${typeof x})`;
925
+ }
926
+ }
927
+ }
928
+
929
+ /**
930
+ * Tags all given `values` with the same `tag`.
931
+ *
932
+ * @param tag - The tag with which all `values` will be annotated.
933
+ * @param values - The values to be tagged.
934
+ *
935
+ * @remarks
936
+ * It supports properties of type {@link @fluidframework/core-interfaces#TelemetryBaseEventPropertyType},
937
+ * as well as callbacks that return that type.
938
+ *
939
+ * @example Sample usage
940
+ * ```typescript
941
+ * {
942
+ * // ...Other properties being added to a telemetry event
943
+ * ...tagData("someTag", {foo: 1, bar: 2}),
944
+ * // ...
945
+ * }
946
+ * ```
947
+ * This will result in `foo` and `bar` added to the event with their values tagged.
948
+ *
949
+ * @internal
950
+ */
951
+ export const tagData = <
952
+ T extends TelemetryDataTag,
953
+ V extends Record<
954
+ string,
955
+ TelemetryBaseEventPropertyType | (() => TelemetryBaseEventPropertyType)
956
+ >,
957
+ >(
958
+ tag: T,
959
+ values: V,
960
+ ): {
961
+ [P in keyof V]:
962
+ | (V[P] extends () => TelemetryBaseEventPropertyType
963
+ ? () => {
964
+ value: ReturnType<V[P]>;
965
+ tag: T;
966
+ }
967
+ : {
968
+ value: Exclude<V[P], undefined>;
969
+ tag: T;
970
+ })
971
+ | (V[P] extends undefined ? undefined : never);
972
+ } =>
973
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
974
+ Object.entries(values)
975
+ .filter((e) => e[1] !== undefined)
976
+ // eslint-disable-next-line unicorn/no-array-reduce, unicorn/prefer-object-from-entries
977
+ .reduce((pv, cv) => {
978
+ const [key, value] = cv;
979
+ // The ternary form is less legible in this case.
980
+ // eslint-disable-next-line unicorn/prefer-ternary
981
+ if (typeof value === "function") {
982
+ // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
983
+ pv[key] = () => {
984
+ return { tag, value: value() };
985
+ };
986
+ } else {
987
+ pv[key] = { tag, value };
988
+ }
989
+ return pv;
990
+ }, {}) as ReturnType<typeof tagData>;
991
+
992
+ /**
993
+ * Tags all provided `values` as {@link TelemetryDataTag.CodeArtifact}.
994
+ *
995
+ * @param values - The values to be tagged.
996
+ *
997
+ * @remarks
998
+ * It supports properties of type {@link @fluidframework/core-interfaces#TelemetryBaseEventPropertyType},
999
+ * as well as callbacks that return that type.
1000
+ *
1001
+ * @example Sample usage
1002
+ * ```typescript
1003
+ * {
1004
+ * // ...Other properties being added to a telemetry event
1005
+ * ...tagCodeArtifacts("someTag", {foo: 1, bar: 2}),
1006
+ * // ...
1007
+ * }
1008
+ * ```
1009
+ * This will result in `foo` and `bar` added to the event with their values tagged as {@link TelemetryDataTag.CodeArtifact}.
1010
+ *
1011
+ * @see {@link tagData}
1012
+ *
1013
+ * @internal
1014
+ */
1015
+ export const tagCodeArtifacts = <
1016
+ T extends Record<
1017
+ string,
1018
+ TelemetryBaseEventPropertyType | (() => TelemetryBaseEventPropertyType)
1019
+ >,
1020
+ >(
1021
+ values: T,
1022
+ ): {
1023
+ [P in keyof T]:
1024
+ | (T[P] extends () => TelemetryBaseEventPropertyType
1025
+ ? () => {
1026
+ value: ReturnType<T[P]>;
1027
+ tag: TelemetryDataTag.CodeArtifact;
1028
+ }
1029
+ : {
1030
+ value: Exclude<T[P], undefined>;
1031
+ tag: TelemetryDataTag.CodeArtifact;
1032
+ })
1033
+ | (T[P] extends undefined ? undefined : never);
1034
+ } => tagData<TelemetryDataTag.CodeArtifact, T>(TelemetryDataTag.CodeArtifact, values);