@fluidframework/telemetry-utils 2.0.0-internal.7.0.0 → 2.0.0-internal.7.2.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.
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAEN,oBAAoB,EACpB,sBAAsB,EACtB,MAAM,iCAAiC,CAAC;AAEzC;;;;;;GAMG;AACH,wBAAgB,UAAU,CACzB,SAAS,EAAE,OAAO,EAClB,MAAM,EAAE,oBAAoB,EAC5B,KAAK,EAAE,MAAM,GAAG,sBAAsB,GACpC,SAAS,IAAI,IAAI,CAUnB"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAEN,oBAAoB,EACpB,sBAAsB,EACtB,MAAM,iCAAiC,CAAC;AAEzC,OAAO,EAA6B,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAElF;;;;;;GAMG;AACH,wBAAgB,UAAU,CACzB,SAAS,EAAE,OAAO,EAClB,MAAM,EAAE,oBAAoB,EAC5B,KAAK,EAAE,MAAM,GAAG,sBAAsB,GACpC,SAAS,IAAI,IAAI,CAUnB;AAED;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC7B;;OAEG;IACH,MAAM,EAAE,MAAM,OAAO,GAAG,SAAS,CAAC;CAClC;AAED;;;;GAIG;AACH,MAAM,WAAW,uBAAwB,SAAQ,mBAAmB;IACnE;;;;;;;OAOG;IACH,kBAAkB,EAAE,OAAO,CAAC;CAC5B;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,mBAAmB,CAClC,MAAM,EAAE,mBAAmB,EAC3B,YAAY,CAAC,EAAE,aAAa,GAC1B,uBAAuB,CAiCzB"}
package/lib/utils.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { loggerToMonitoringContext } from "./config";
1
2
  /**
2
3
  * Like assert, but logs only if the condition is false, rather than throwing
3
4
  * @param condition - The condition to attest too
@@ -15,4 +16,51 @@ export function logIfFalse(condition, logger, event) {
15
16
  logger.send(newEvent);
16
17
  return false;
17
18
  }
19
+ /**
20
+ * Wraps around an existing logger matching the {@link ITelemetryLoggerExt} interface and provides the ability to only log a subset of events using a sampling strategy provided by an ${@link IEventSampler}.
21
+ * You can chose to not provide an event sampler which is effectively a no-op, meaning that it will be treated as if the sampler always returns true.
22
+ *
23
+ * @remarks
24
+ * The sampling functionality uses the Fluid telemetry logging configuration along with the optionally provided event sampling callback to determine whether an event should
25
+ * be logged or not.
26
+ *
27
+ * Configuration object parameters:
28
+ * 'Fluid.Telemetry.DisableSampling': if this config value is set to true, all events will be unsampled and therefore logged.
29
+ * Otherwise only a sample will be logged according to the provided event sampler callback.
30
+ *
31
+ * Note that the same sampler is used for all APIs of the returned logger. If you want separate events flowing through the returned logger to be sampled separately, the {@link IEventSampler} you provide should track them separately.
32
+ *
33
+ * @internal
34
+ */
35
+ export function createSampledLogger(logger, eventSampler) {
36
+ const monitoringContext = loggerToMonitoringContext(logger);
37
+ const isSamplingDisabled = monitoringContext.config.getBoolean("Fluid.Telemetry.DisableSampling") ?? false;
38
+ const sampledLogger = {
39
+ send: (event) => {
40
+ // The sampler uses the following logic for sending events:
41
+ // 1. If isSamplingDisabled is true, then this means events should be unsampled. Therefore we send the event without any checks.
42
+ // 2. If isSamplingDisabled is false, then event should be sampled using the event sampler, if the sampler is not defined just send all events, other use the eventSampler.sample() method.
43
+ if (isSamplingDisabled || eventSampler === undefined || eventSampler.sample()) {
44
+ logger.send(event);
45
+ }
46
+ },
47
+ sendTelemetryEvent: (event) => {
48
+ if (isSamplingDisabled || eventSampler === undefined || eventSampler.sample()) {
49
+ logger.sendTelemetryEvent(event);
50
+ }
51
+ },
52
+ sendErrorEvent: (event) => {
53
+ if (isSamplingDisabled || eventSampler === undefined || eventSampler.sample()) {
54
+ logger.sendErrorEvent(event);
55
+ }
56
+ },
57
+ sendPerformanceEvent: (event) => {
58
+ if (isSamplingDisabled || eventSampler === undefined || eventSampler.sample()) {
59
+ logger.sendPerformanceEvent(event);
60
+ }
61
+ },
62
+ isSamplingDisabled,
63
+ };
64
+ return sampledLogger;
65
+ }
18
66
  //# sourceMappingURL=utils.js.map
package/lib/utils.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAUA;;;;;;GAMG;AACH,MAAM,UAAU,UAAU,CACzB,SAAkB,EAClB,MAA4B,EAC5B,KAAsC;IAEtC,IAAI,SAAS,EAAE;QACd,OAAO,IAAI,CAAC;KACZ;IACD,MAAM,QAAQ,GACb,OAAO,KAAK,KAAK,QAAQ;QACxB,CAAC,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE;QACzC,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,KAAK,EAAE,CAAC;IACpC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACtB,OAAO,KAAK,CAAC;AACd,CAAC","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n */\nimport {\n\tITelemetryBaseEvent,\n\tITelemetryBaseLogger,\n\tITelemetryGenericEvent,\n} from \"@fluidframework/core-interfaces\";\n\n/**\n * Like assert, but logs only if the condition is false, rather than throwing\n * @param condition - The condition to attest too\n * @param logger - The logger to log with\n * @param event - The string or event to log\n * @returns The outcome of the condition\n */\nexport function logIfFalse(\n\tcondition: unknown,\n\tlogger: ITelemetryBaseLogger,\n\tevent: string | ITelemetryGenericEvent,\n): condition is true {\n\tif (condition) {\n\t\treturn true;\n\t}\n\tconst newEvent: ITelemetryBaseEvent =\n\t\ttypeof event === \"string\"\n\t\t\t? { eventName: event, category: \"error\" }\n\t\t\t: { category: \"error\", ...event };\n\tlogger.send(newEvent);\n\treturn false;\n}\n"]}
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,yBAAyB,EAAE,MAAM,UAAU,CAAC;AAGrD;;;;;;GAMG;AACH,MAAM,UAAU,UAAU,CACzB,SAAkB,EAClB,MAA4B,EAC5B,KAAsC;IAEtC,IAAI,SAAS,EAAE;QACd,OAAO,IAAI,CAAC;KACZ;IACD,MAAM,QAAQ,GACb,OAAO,KAAK,KAAK,QAAQ;QACxB,CAAC,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE;QACzC,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,KAAK,EAAE,CAAC;IACpC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACtB,OAAO,KAAK,CAAC;AACd,CAAC;AA+BD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,mBAAmB,CAClC,MAA2B,EAC3B,YAA4B;IAE5B,MAAM,iBAAiB,GAAG,yBAAyB,CAAC,MAAM,CAAC,CAAC;IAC5D,MAAM,kBAAkB,GACvB,iBAAiB,CAAC,MAAM,CAAC,UAAU,CAAC,iCAAiC,CAAC,IAAI,KAAK,CAAC;IAEjF,MAAM,aAAa,GAAG;QACrB,IAAI,EAAE,CAAC,KAA0B,EAAQ,EAAE;YAC1C,2DAA2D;YAC3D,gIAAgI;YAChI,2LAA2L;YAC3L,IAAI,kBAAkB,IAAI,YAAY,KAAK,SAAS,IAAI,YAAY,CAAC,MAAM,EAAE,EAAE;gBAC9E,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;aACnB;QACF,CAAC;QACD,kBAAkB,EAAE,CAAC,KAAgC,EAAQ,EAAE;YAC9D,IAAI,kBAAkB,IAAI,YAAY,KAAK,SAAS,IAAI,YAAY,CAAC,MAAM,EAAE,EAAE;gBAC9E,MAAM,CAAC,kBAAkB,CAAC,KAAK,CAAC,CAAC;aACjC;QACF,CAAC;QACD,cAAc,EAAE,CAAC,KAAgC,EAAQ,EAAE;YAC1D,IAAI,kBAAkB,IAAI,YAAY,KAAK,SAAS,IAAI,YAAY,CAAC,MAAM,EAAE,EAAE;gBAC9E,MAAM,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;aAC7B;QACF,CAAC;QACD,oBAAoB,EAAE,CAAC,KAAgC,EAAQ,EAAE;YAChE,IAAI,kBAAkB,IAAI,YAAY,KAAK,SAAS,IAAI,YAAY,CAAC,MAAM,EAAE,EAAE;gBAC9E,MAAM,CAAC,oBAAoB,CAAC,KAAK,CAAC,CAAC;aACnC;QACF,CAAC;QACD,kBAAkB;KAClB,CAAC;IAEF,OAAO,aAAa,CAAC;AACtB,CAAC","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n */\nimport {\n\tITelemetryBaseEvent,\n\tITelemetryBaseLogger,\n\tITelemetryGenericEvent,\n} from \"@fluidframework/core-interfaces\";\nimport { loggerToMonitoringContext } from \"./config\";\nimport { ITelemetryGenericEventExt, ITelemetryLoggerExt } from \"./telemetryTypes\";\n\n/**\n * Like assert, but logs only if the condition is false, rather than throwing\n * @param condition - The condition to attest too\n * @param logger - The logger to log with\n * @param event - The string or event to log\n * @returns The outcome of the condition\n */\nexport function logIfFalse(\n\tcondition: unknown,\n\tlogger: ITelemetryBaseLogger,\n\tevent: string | ITelemetryGenericEvent,\n): condition is true {\n\tif (condition) {\n\t\treturn true;\n\t}\n\tconst newEvent: ITelemetryBaseEvent =\n\t\ttypeof event === \"string\"\n\t\t\t? { eventName: event, category: \"error\" }\n\t\t\t: { category: \"error\", ...event };\n\tlogger.send(newEvent);\n\treturn false;\n}\n\n/**\n * An object that contains a callback used in conjunction with the {@link createSampledLogger} utility function to provide custom logic for sampling events.\n *\n * @internal\n */\nexport interface IEventSampler {\n\t/**\n\t * @returns true if the event should be sampled or false if not\n\t */\n\tsample: () => boolean | undefined;\n}\n\n/**\n * A telemetry logger that has sampling capabilities\n *\n * @internal\n */\nexport interface ISampledTelemetryLogger extends ITelemetryLoggerExt {\n\t/**\n\t * Indicates if the feature flag to disable sampling is set.\n\t *\n\t * @remarks Exposed to enable some advanced scenarios where the code using the sampled logger\n\t * could take advantage of skipping the execution of some logic when it can determine\n\t * it won't be necessary because the telemetry event that needs it wouldn't be\n\t * emitted anyway.\n\t */\n\tisSamplingDisabled: boolean;\n}\n\n/**\n * Wraps around an existing logger matching the {@link ITelemetryLoggerExt} interface and provides the ability to only log a subset of events using a sampling strategy provided by an ${@link IEventSampler}.\n * You can chose to not provide an event sampler which is effectively a no-op, meaning that it will be treated as if the sampler always returns true.\n *\n * @remarks\n * The sampling functionality uses the Fluid telemetry logging configuration along with the optionally provided event sampling callback to determine whether an event should\n * be logged or not.\n *\n * Configuration object parameters:\n * 'Fluid.Telemetry.DisableSampling': if this config value is set to true, all events will be unsampled and therefore logged.\n * Otherwise only a sample will be logged according to the provided event sampler callback.\n *\n * Note that the same sampler is used for all APIs of the returned logger. If you want separate events flowing through the returned logger to be sampled separately, the {@link IEventSampler} you provide should track them separately.\n *\n * @internal\n */\nexport function createSampledLogger(\n\tlogger: ITelemetryLoggerExt,\n\teventSampler?: IEventSampler,\n): ISampledTelemetryLogger {\n\tconst monitoringContext = loggerToMonitoringContext(logger);\n\tconst isSamplingDisabled =\n\t\tmonitoringContext.config.getBoolean(\"Fluid.Telemetry.DisableSampling\") ?? false;\n\n\tconst sampledLogger = {\n\t\tsend: (event: ITelemetryBaseEvent): void => {\n\t\t\t// The sampler uses the following logic for sending events:\n\t\t\t// 1. If isSamplingDisabled is true, then this means events should be unsampled. Therefore we send the event without any checks.\n\t\t\t// 2. If isSamplingDisabled is false, then event should be sampled using the event sampler, if the sampler is not defined just send all events, other use the eventSampler.sample() method.\n\t\t\tif (isSamplingDisabled || eventSampler === undefined || eventSampler.sample()) {\n\t\t\t\tlogger.send(event);\n\t\t\t}\n\t\t},\n\t\tsendTelemetryEvent: (event: ITelemetryGenericEventExt): void => {\n\t\t\tif (isSamplingDisabled || eventSampler === undefined || eventSampler.sample()) {\n\t\t\t\tlogger.sendTelemetryEvent(event);\n\t\t\t}\n\t\t},\n\t\tsendErrorEvent: (event: ITelemetryGenericEventExt): void => {\n\t\t\tif (isSamplingDisabled || eventSampler === undefined || eventSampler.sample()) {\n\t\t\t\tlogger.sendErrorEvent(event);\n\t\t\t}\n\t\t},\n\t\tsendPerformanceEvent: (event: ITelemetryGenericEventExt): void => {\n\t\t\tif (isSamplingDisabled || eventSampler === undefined || eventSampler.sample()) {\n\t\t\t\tlogger.sendPerformanceEvent(event);\n\t\t\t}\n\t\t},\n\t\tisSamplingDisabled,\n\t};\n\n\treturn sampledLogger;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fluidframework/telemetry-utils",
3
- "version": "2.0.0-internal.7.0.0",
3
+ "version": "2.0.0-internal.7.2.0",
4
4
  "description": "Collection of telemetry relates utilities for Fluid",
5
5
  "homepage": "https://fluidframework.com",
6
6
  "repository": {
@@ -39,21 +39,21 @@
39
39
  "temp-directory": "nyc/.nyc_output"
40
40
  },
41
41
  "dependencies": {
42
- "@fluid-internal/client-utils": ">=2.0.0-internal.7.0.0 <2.0.0-internal.7.1.0",
43
- "@fluidframework/core-interfaces": ">=2.0.0-internal.7.0.0 <2.0.0-internal.7.1.0",
44
- "@fluidframework/core-utils": ">=2.0.0-internal.7.0.0 <2.0.0-internal.7.1.0",
42
+ "@fluid-internal/client-utils": ">=2.0.0-internal.7.2.0 <2.0.0-internal.7.3.0",
43
+ "@fluidframework/core-interfaces": ">=2.0.0-internal.7.2.0 <2.0.0-internal.7.3.0",
44
+ "@fluidframework/core-utils": ">=2.0.0-internal.7.2.0 <2.0.0-internal.7.3.0",
45
45
  "@fluidframework/protocol-definitions": "^3.0.0",
46
- "debug": "^4.1.1",
46
+ "debug": "^4.3.4",
47
47
  "events": "^3.1.0",
48
48
  "uuid": "^9.0.0"
49
49
  },
50
50
  "devDependencies": {
51
- "@fluid-tools/build-cli": "^0.24.0",
52
- "@fluidframework/build-common": "^2.0.0",
53
- "@fluidframework/build-tools": "^0.24.0",
54
- "@fluidframework/eslint-config-fluid": "^2.1.0",
55
- "@fluidframework/mocha-test-setup": ">=2.0.0-internal.7.0.0 <2.0.0-internal.7.1.0",
56
- "@fluidframework/telemetry-utils-previous": "npm:@fluidframework/telemetry-utils@2.0.0-internal.6.3.0",
51
+ "@fluid-tools/build-cli": "^0.26.1",
52
+ "@fluidframework/build-common": "^2.0.3",
53
+ "@fluidframework/build-tools": "^0.26.1",
54
+ "@fluidframework/eslint-config-fluid": "^3.0.0",
55
+ "@fluidframework/mocha-test-setup": ">=2.0.0-internal.7.2.0 <2.0.0-internal.7.3.0",
56
+ "@fluidframework/telemetry-utils-previous": "npm:@fluidframework/telemetry-utils@2.0.0-internal.7.1.0",
57
57
  "@microsoft/api-extractor": "^7.37.0",
58
58
  "@types/debug": "^4.1.5",
59
59
  "@types/events": "^3.0.0",
@@ -61,14 +61,13 @@
61
61
  "@types/node": "^16.18.38",
62
62
  "@types/uuid": "^9.0.2",
63
63
  "c8": "^7.7.1",
64
- "copyfiles": "^2.4.1",
65
64
  "cross-env": "^7.0.3",
66
- "eslint": "~8.6.0",
65
+ "eslint": "~8.50.0",
67
66
  "mocha": "^10.2.0",
68
67
  "mocha-json-output-reporter": "^2.0.1",
69
68
  "mocha-multi-reporters": "^1.5.1",
70
69
  "moment": "^2.21.0",
71
- "prettier": "~2.6.2",
70
+ "prettier": "~3.0.3",
72
71
  "rimraf": "^4.4.0",
73
72
  "sinon": "^7.4.2",
74
73
  "typescript": "~5.1.6"
@@ -80,12 +79,12 @@
80
79
  "build": "fluid-build . --task build",
81
80
  "build:commonjs": "fluid-build . --task commonjs",
82
81
  "build:compile": "fluid-build . --task compile",
83
- "build:docs": "api-extractor run --local --typescript-compiler-folder ../../../node_modules/typescript && copyfiles -u 1 ./_api-extractor-temp/doc-models/* ../../../_api-extractor-temp/",
82
+ "build:docs": "api-extractor run --local",
84
83
  "build:esnext": "tsc --project ./tsconfig.esnext.json",
85
84
  "build:test": "tsc --project ./src/test/tsconfig.json",
86
85
  "bump-version": "npm version minor --no-push --no-git-tag-version && npm run build:genver",
87
- "ci:build:docs": "api-extractor run --typescript-compiler-folder ../../../node_modules/typescript && copyfiles -u 1 ./_api-extractor-temp/* ../../../_api-extractor-temp/",
88
- "clean": "rimraf --glob 'dist' 'lib' '*.tsbuildinfo' '*.build.log' '_api-extractor-temp' 'nyc'",
86
+ "ci:build:docs": "api-extractor run",
87
+ "clean": "rimraf --glob dist lib \"*.tsbuildinfo\" \"*.build.log\" _api-extractor-temp nyc",
89
88
  "eslint": "eslint --format stylish src",
90
89
  "eslint:fix": "eslint --format stylish src --fix --fix-type problem,suggestion,layout",
91
90
  "format": "npm run prettier:fix",
package/src/error.ts CHANGED
@@ -35,9 +35,13 @@ export class GenericError extends LoggingError implements IGenericError, IFluidE
35
35
  * @param error - inner error object
36
36
  * @param props - Telemetry props to include when the error is logged
37
37
  */
38
- // TODO: Use `unknown` instead (API breaking change because error is not just an input parameter, but a public member of the class)
39
- // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
40
- constructor(message: string, public readonly error?: any, props?: ITelemetryBaseProperties) {
38
+ constructor(
39
+ message: string,
40
+ // TODO: Use `unknown` instead (API breaking change because error is not just an input parameter, but a public member of the class)
41
+ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
42
+ public readonly error?: any,
43
+ props?: ITelemetryBaseProperties,
44
+ ) {
41
45
  // Don't try to log the inner error
42
46
  super(message, props, new Set(["error"]));
43
47
  }
package/src/index.ts CHANGED
@@ -68,7 +68,7 @@ export {
68
68
  export { MockLogger } from "./mockLogger";
69
69
  export { ThresholdCounter } from "./thresholdCounter";
70
70
  export { SampledTelemetryHelper } from "./sampledTelemetryHelper";
71
- export { logIfFalse } from "./utils";
71
+ export { logIfFalse, createSampledLogger, IEventSampler, ISampledTelemetryLogger } from "./utils";
72
72
  export {
73
73
  TelemetryEventPropertyTypeExt,
74
74
  ITelemetryEventExt,
package/src/logger.ts CHANGED
@@ -566,22 +566,55 @@ export interface IPerformanceEventMarkers {
566
566
  * Helper class to log performance events
567
567
  */
568
568
  export class PerformanceEvent {
569
+ /**
570
+ * Creates an instance of {@link PerformanceEvent} and starts measurements
571
+ * @param logger - the logger to be used for publishing events
572
+ * @param event - the logging event details which will be published with the performance measurements
573
+ * @param markers - See {@link IPerformanceEventMarkers}
574
+ * @param recordHeapSize - whether or not to also record memory performance
575
+ * @param emitLogs - should this instance emit logs. If set to false, logs will not be emitted to the logger,
576
+ * but measurements will still be performed and any specified markers will be generated.
577
+ * @returns An instance of {@link PerformanceEvent}
578
+ */
569
579
  public static start(
570
580
  logger: ITelemetryLoggerExt,
571
581
  event: ITelemetryGenericEvent,
572
582
  markers?: IPerformanceEventMarkers,
573
583
  recordHeapSize: boolean = false,
584
+ emitLogs: boolean = true,
574
585
  ): PerformanceEvent {
575
- return new PerformanceEvent(logger, event, markers, recordHeapSize);
586
+ return new PerformanceEvent(logger, event, markers, recordHeapSize, emitLogs);
576
587
  }
577
588
 
589
+ /**
590
+ * Measure a synchronous task
591
+ * @param logger - the logger to be used for publishing events
592
+ * @param event - the logging event details which will be published with the performance measurements
593
+ * @param callback - the task to be executed and measured
594
+ * @param markers - See {@link IPerformanceEventMarkers}
595
+ * @param sampleThreshold - events with the same name and category will be sent to the logger
596
+ * only when we hit this many executions of the task. If unspecified, all events will be sent.
597
+ * @returns The results of the executed task
598
+ *
599
+ * @remarks Note that if the "same" event (category + eventName) would be emitted by different
600
+ * tasks (`callback`), `sampleThreshold` is still applied only based on the event's category + eventName,
601
+ * so executing either of the tasks will increase the internal counter and they
602
+ * effectively "share" the sampling rate for the event.
603
+ */
578
604
  public static timedExec<T>(
579
605
  logger: ITelemetryLoggerExt,
580
606
  event: ITelemetryGenericEvent,
581
607
  callback: (event: PerformanceEvent) => T,
582
608
  markers?: IPerformanceEventMarkers,
609
+ sampleThreshold: number = 1,
583
610
  ): T {
584
- const perfEvent = PerformanceEvent.start(logger, event, markers);
611
+ const perfEvent = PerformanceEvent.start(
612
+ logger,
613
+ event,
614
+ markers,
615
+ undefined, // recordHeapSize
616
+ PerformanceEvent.shouldReport(event, sampleThreshold),
617
+ );
585
618
  try {
586
619
  const ret = callback(perfEvent);
587
620
  perfEvent.autoEnd();
@@ -592,14 +625,37 @@ export class PerformanceEvent {
592
625
  }
593
626
  }
594
627
 
628
+ /**
629
+ * Measure an asynchronous task
630
+ * @param logger - the logger to be used for publishing events
631
+ * @param event - the logging event details which will be published with the performance measurements
632
+ * @param callback - the task to be executed and measured
633
+ * @param markers - See {@link IPerformanceEventMarkers}
634
+ * @param recordHeapSize - whether or not to also record memory performance
635
+ * @param sampleThreshold - events with the same name and category will be sent to the logger
636
+ * only when we hit this many executions of the task. If unspecified, all events will be sent.
637
+ * @returns The results of the executed task
638
+ *
639
+ * @remarks Note that if the "same" event (category + eventName) would be emitted by different
640
+ * tasks (`callback`), `sampleThreshold` is still applied only based on the event's category + eventName,
641
+ * so executing either of the tasks will increase the internal counter and they
642
+ * effectively "share" the sampling rate for the event.
643
+ */
595
644
  public static async timedExecAsync<T>(
596
645
  logger: ITelemetryLoggerExt,
597
646
  event: ITelemetryGenericEvent,
598
647
  callback: (event: PerformanceEvent) => Promise<T>,
599
648
  markers?: IPerformanceEventMarkers,
600
649
  recordHeapSize?: boolean,
650
+ sampleThreshold: number = 1,
601
651
  ): Promise<T> {
602
- const perfEvent = PerformanceEvent.start(logger, event, markers, recordHeapSize);
652
+ const perfEvent = PerformanceEvent.start(
653
+ logger,
654
+ event,
655
+ markers,
656
+ recordHeapSize,
657
+ PerformanceEvent.shouldReport(event, sampleThreshold),
658
+ );
603
659
  try {
604
660
  const ret = await callback(perfEvent);
605
661
  perfEvent.autoEnd();
@@ -624,6 +680,7 @@ export class PerformanceEvent {
624
680
  event: ITelemetryGenericEvent,
625
681
  private readonly markers: IPerformanceEventMarkers = { end: true, cancel: "generic" },
626
682
  private readonly recordHeapSize: boolean = false,
683
+ private readonly emitLogs: boolean = true,
627
684
  ) {
628
685
  this.event = { ...event };
629
686
  if (this.markers.start) {
@@ -686,6 +743,10 @@ export class PerformanceEvent {
686
743
  return;
687
744
  }
688
745
 
746
+ if (!this.emitLogs) {
747
+ return;
748
+ }
749
+
689
750
  const event: ITelemetryPerformanceEvent = { ...this.event, ...props };
690
751
  event.eventName = `${event.eventName}_${eventNameSuffix}`;
691
752
  if (eventNameSuffix !== "start") {
@@ -701,18 +762,25 @@ export class PerformanceEvent {
701
762
  }
702
763
  }
703
764
  } else if (this.recordHeapSize) {
704
- this.startMemoryCollection = (
705
- performance as PerformanceWithMemory
706
- )?.memory?.usedJSHeapSize;
765
+ this.startMemoryCollection = (performance as PerformanceWithMemory)?.memory
766
+ ?.usedJSHeapSize;
707
767
  }
708
768
 
709
769
  this.logger.sendPerformanceEvent(event, error);
710
770
  }
771
+
772
+ private static readonly eventHits = new Map<string, number>();
773
+ private static shouldReport(event: ITelemetryGenericEvent, sampleThreshold: number): boolean {
774
+ const eventKey = `.${event.category}.${event.eventName}`;
775
+ const hitCount = PerformanceEvent.eventHits.get(eventKey) ?? 0;
776
+ PerformanceEvent.eventHits.set(eventKey, hitCount >= sampleThreshold ? 1 : hitCount + 1);
777
+ return hitCount % sampleThreshold === 0;
778
+ }
711
779
  }
712
780
 
713
781
  /**
714
782
  * Null logger that no-ops for all telemetry events passed to it.
715
- * @deprecated - This will be removed in a future release.
783
+ * @deprecated This will be removed in a future release.
716
784
  * For internal use within the FluidFramework codebase, use {@link createChildLogger} with no arguments instead.
717
785
  * For external consumers we recommend writing a trivial implementation of {@link @fluidframework/core-interfaces#ITelemetryBaseLogger}
718
786
  * where the send() method does nothing and using that.
package/src/utils.ts CHANGED
@@ -7,6 +7,8 @@ import {
7
7
  ITelemetryBaseLogger,
8
8
  ITelemetryGenericEvent,
9
9
  } from "@fluidframework/core-interfaces";
10
+ import { loggerToMonitoringContext } from "./config";
11
+ import { ITelemetryGenericEventExt, ITelemetryLoggerExt } from "./telemetryTypes";
10
12
 
11
13
  /**
12
14
  * Like assert, but logs only if the condition is false, rather than throwing
@@ -30,3 +32,86 @@ export function logIfFalse(
30
32
  logger.send(newEvent);
31
33
  return false;
32
34
  }
35
+
36
+ /**
37
+ * An object that contains a callback used in conjunction with the {@link createSampledLogger} utility function to provide custom logic for sampling events.
38
+ *
39
+ * @internal
40
+ */
41
+ export interface IEventSampler {
42
+ /**
43
+ * @returns true if the event should be sampled or false if not
44
+ */
45
+ sample: () => boolean | undefined;
46
+ }
47
+
48
+ /**
49
+ * A telemetry logger that has sampling capabilities
50
+ *
51
+ * @internal
52
+ */
53
+ export interface ISampledTelemetryLogger extends ITelemetryLoggerExt {
54
+ /**
55
+ * Indicates if the feature flag to disable sampling is set.
56
+ *
57
+ * @remarks Exposed to enable some advanced scenarios where the code using the sampled logger
58
+ * could take advantage of skipping the execution of some logic when it can determine
59
+ * it won't be necessary because the telemetry event that needs it wouldn't be
60
+ * emitted anyway.
61
+ */
62
+ isSamplingDisabled: boolean;
63
+ }
64
+
65
+ /**
66
+ * Wraps around an existing logger matching the {@link ITelemetryLoggerExt} interface and provides the ability to only log a subset of events using a sampling strategy provided by an ${@link IEventSampler}.
67
+ * You can chose to not provide an event sampler which is effectively a no-op, meaning that it will be treated as if the sampler always returns true.
68
+ *
69
+ * @remarks
70
+ * The sampling functionality uses the Fluid telemetry logging configuration along with the optionally provided event sampling callback to determine whether an event should
71
+ * be logged or not.
72
+ *
73
+ * Configuration object parameters:
74
+ * 'Fluid.Telemetry.DisableSampling': if this config value is set to true, all events will be unsampled and therefore logged.
75
+ * Otherwise only a sample will be logged according to the provided event sampler callback.
76
+ *
77
+ * Note that the same sampler is used for all APIs of the returned logger. If you want separate events flowing through the returned logger to be sampled separately, the {@link IEventSampler} you provide should track them separately.
78
+ *
79
+ * @internal
80
+ */
81
+ export function createSampledLogger(
82
+ logger: ITelemetryLoggerExt,
83
+ eventSampler?: IEventSampler,
84
+ ): ISampledTelemetryLogger {
85
+ const monitoringContext = loggerToMonitoringContext(logger);
86
+ const isSamplingDisabled =
87
+ monitoringContext.config.getBoolean("Fluid.Telemetry.DisableSampling") ?? false;
88
+
89
+ const sampledLogger = {
90
+ send: (event: ITelemetryBaseEvent): void => {
91
+ // The sampler uses the following logic for sending events:
92
+ // 1. If isSamplingDisabled is true, then this means events should be unsampled. Therefore we send the event without any checks.
93
+ // 2. If isSamplingDisabled is false, then event should be sampled using the event sampler, if the sampler is not defined just send all events, other use the eventSampler.sample() method.
94
+ if (isSamplingDisabled || eventSampler === undefined || eventSampler.sample()) {
95
+ logger.send(event);
96
+ }
97
+ },
98
+ sendTelemetryEvent: (event: ITelemetryGenericEventExt): void => {
99
+ if (isSamplingDisabled || eventSampler === undefined || eventSampler.sample()) {
100
+ logger.sendTelemetryEvent(event);
101
+ }
102
+ },
103
+ sendErrorEvent: (event: ITelemetryGenericEventExt): void => {
104
+ if (isSamplingDisabled || eventSampler === undefined || eventSampler.sample()) {
105
+ logger.sendErrorEvent(event);
106
+ }
107
+ },
108
+ sendPerformanceEvent: (event: ITelemetryGenericEventExt): void => {
109
+ if (isSamplingDisabled || eventSampler === undefined || eventSampler.sample()) {
110
+ logger.sendPerformanceEvent(event);
111
+ }
112
+ },
113
+ isSamplingDisabled,
114
+ };
115
+
116
+ return sampledLogger;
117
+ }