@jaypie/logger 1.2.17 → 1.2.19

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 (39) hide show
  1. package/dist/cjs/index.cjs +302 -7
  2. package/dist/cjs/index.cjs.map +1 -1
  3. package/dist/cjs/index.d.cts +142 -0
  4. package/dist/cjs/{JaypieLogger.d.ts → src/JaypieLogger.d.ts} +10 -2
  5. package/dist/cjs/{Logger.d.ts → src/Logger.d.ts} +9 -2
  6. package/dist/cjs/{constants.d.ts → src/constants.d.ts} +6 -0
  7. package/dist/cjs/{index.d.ts → src/index.d.ts} +1 -0
  8. package/dist/cjs/src/limits.d.ts +55 -0
  9. package/dist/esm/index.d.ts +142 -8
  10. package/dist/esm/index.js +302 -7
  11. package/dist/esm/index.js.map +1 -1
  12. package/dist/esm/{JaypieLogger.d.ts → src/JaypieLogger.d.ts} +10 -2
  13. package/dist/esm/{Logger.d.ts → src/Logger.d.ts} +9 -2
  14. package/dist/esm/src/__tests__/limits.spec.d.ts +1 -0
  15. package/dist/esm/src/__tests__/sanitizeAuth.spec.d.ts +1 -0
  16. package/dist/esm/{constants.d.ts → src/constants.d.ts} +6 -0
  17. package/dist/esm/src/index.d.ts +9 -0
  18. package/dist/esm/src/limits.d.ts +55 -0
  19. package/package.json +10 -4
  20. /package/dist/cjs/{__tests__ → src/__tests__}/datadogTransport.spec.d.ts +0 -0
  21. /package/dist/cjs/{__tests__ → src/__tests__}/index.spec.d.ts +0 -0
  22. /package/dist/cjs/{__tests__/sanitizeAuth.spec.d.ts → src/__tests__/limits.spec.d.ts} +0 -0
  23. /package/dist/{esm → cjs/src}/__tests__/sanitizeAuth.spec.d.ts +0 -0
  24. /package/dist/cjs/{datadogTransport.d.ts → src/datadogTransport.d.ts} +0 -0
  25. /package/dist/cjs/{forceVar.d.ts → src/forceVar.d.ts} +0 -0
  26. /package/dist/cjs/{logTags.d.ts → src/logTags.d.ts} +0 -0
  27. /package/dist/cjs/{logVar.d.ts → src/logVar.d.ts} +0 -0
  28. /package/dist/cjs/{pipelines.d.ts → src/pipelines.d.ts} +0 -0
  29. /package/dist/cjs/{sanitizeAuth.d.ts → src/sanitizeAuth.d.ts} +0 -0
  30. /package/dist/cjs/{utils.d.ts → src/utils.d.ts} +0 -0
  31. /package/dist/esm/{__tests__ → src/__tests__}/datadogTransport.spec.d.ts +0 -0
  32. /package/dist/esm/{__tests__ → src/__tests__}/index.spec.d.ts +0 -0
  33. /package/dist/esm/{datadogTransport.d.ts → src/datadogTransport.d.ts} +0 -0
  34. /package/dist/esm/{forceVar.d.ts → src/forceVar.d.ts} +0 -0
  35. /package/dist/esm/{logTags.d.ts → src/logTags.d.ts} +0 -0
  36. /package/dist/esm/{logVar.d.ts → src/logVar.d.ts} +0 -0
  37. /package/dist/esm/{pipelines.d.ts → src/pipelines.d.ts} +0 -0
  38. /package/dist/esm/{sanitizeAuth.d.ts → src/sanitizeAuth.d.ts} +0 -0
  39. /package/dist/esm/{utils.d.ts → src/utils.d.ts} +0 -0
@@ -8,8 +8,16 @@ var node_https = require('node:https');
8
8
 
9
9
  const DEFAULT = {
10
10
  LEVEL: "debug",
11
+ // CloudWatch Logs caps events at 256KB and fronts Datadog in Lambda;
12
+ // Datadog's own per-log cap is 1MB. Truncate deliberately below both.
13
+ MAX_ENTRY_BYTES: 262144,
11
14
  VAR_LEVEL: "debug",
12
15
  };
16
+ const LIMIT_ENV = {
17
+ MAX_DEPTH: "LOG_MAX_DEPTH",
18
+ MAX_ENTRY_BYTES: "LOG_MAX_ENTRY_BYTES",
19
+ MAX_STRING: "LOG_MAX_STRING",
20
+ };
13
21
  const ERROR_PREFIX = "[logger]";
14
22
  const ERROR = {
15
23
  VAR: {
@@ -63,6 +71,207 @@ const DATADOG_TRANSPORT = {
63
71
  MAX_BATCH_SIZE: 100,
64
72
  };
65
73
 
74
+ //
75
+ //
76
+ // Constants
77
+ //
78
+ const CIRCULAR_PLACEHOLDER = "[Circular]";
79
+ const DISABLED_ENV_VALUES = ["", "0", "false", "none", "off"];
80
+ const ELLIPSIS = "…";
81
+ // Reserve headroom for the truncation marker when fitting a string to a
82
+ // byte budget
83
+ const MARKER_RESERVE_BYTES = 64;
84
+ /** Characters preserved when an oversized entry truncates an attribute */
85
+ const ENTRY_PREVIEW_LENGTH = 72;
86
+ //
87
+ //
88
+ // Helpers
89
+ //
90
+ function isPlainObject(value) {
91
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
92
+ return false;
93
+ }
94
+ const proto = Object.getPrototypeOf(value);
95
+ return proto === Object.prototype || proto === null;
96
+ }
97
+ function normalizeLimit(option) {
98
+ if (option === false)
99
+ return undefined;
100
+ if (typeof option === "number" && Number.isFinite(option) && option > 0) {
101
+ return Math.floor(option);
102
+ }
103
+ return undefined;
104
+ }
105
+ function resolveLimit(option, envKey, defaultValue) {
106
+ if (option !== undefined) {
107
+ return normalizeLimit(option);
108
+ }
109
+ const raw = process.env[envKey];
110
+ if (raw !== undefined) {
111
+ if (DISABLED_ENV_VALUES.includes(raw.toLowerCase()))
112
+ return undefined;
113
+ const parsed = Number.parseInt(raw, 10);
114
+ if (Number.isFinite(parsed) && parsed > 0)
115
+ return parsed;
116
+ }
117
+ return defaultValue;
118
+ }
119
+ function truncationMarker(droppedChars) {
120
+ return `${ELLIPSIS} [truncated ${droppedChars.toLocaleString("en-US")} chars]`;
121
+ }
122
+ //
123
+ //
124
+ // Main
125
+ //
126
+ /**
127
+ * Resolve limits from explicit options, env vars, then defaults.
128
+ * `maxEntryBytes` defaults on (payloads must fit the log pipeline);
129
+ * `maxDepth` and `maxStringLength` default off.
130
+ */
131
+ function resolveSerializationLimits(options = {}) {
132
+ return {
133
+ maxDepth: resolveLimit(options.maxDepth, LIMIT_ENV.MAX_DEPTH),
134
+ maxEntryBytes: resolveLimit(options.maxEntryBytes, LIMIT_ENV.MAX_ENTRY_BYTES, DEFAULT.MAX_ENTRY_BYTES),
135
+ maxStringLength: resolveLimit(options.maxStringLength, LIMIT_ENV.MAX_STRING),
136
+ };
137
+ }
138
+ function hasValueLimits(limits) {
139
+ return limits.maxDepth !== undefined || limits.maxStringLength !== undefined;
140
+ }
141
+ function byteLength(value) {
142
+ try {
143
+ const str = typeof value === "string" ? value : JSON.stringify(value);
144
+ return Buffer.byteLength(str ?? "", "utf8");
145
+ }
146
+ catch {
147
+ return 0;
148
+ }
149
+ }
150
+ /**
151
+ * Keep the first `maxLength` characters and append a visible marker
152
+ * preserving the dropped size
153
+ */
154
+ function truncateString(value, maxLength) {
155
+ if (value.length <= maxLength)
156
+ return value;
157
+ return value.slice(0, maxLength) + truncationMarker(value.length - maxLength);
158
+ }
159
+ /**
160
+ * Fit a string to a byte budget, reserving room for the marker and
161
+ * never cutting below the preview length
162
+ */
163
+ function truncateToBudget(value, budgetBytes) {
164
+ if (Buffer.byteLength(value, "utf8") <= budgetBytes)
165
+ return value;
166
+ const maxLength = Math.max(ENTRY_PREVIEW_LENGTH, budgetBytes - MARKER_RESERVE_BYTES);
167
+ return truncateString(value, maxLength);
168
+ }
169
+ /**
170
+ * Walk a value applying maxStringLength and maxDepth. Returns a new value;
171
+ * never mutates the input. Only plain objects and arrays are traversed so
172
+ * class instances (Error, Date, ...) keep their serialization behavior.
173
+ */
174
+ function applyValueLimits(value, limits, depth = 0, seen = new WeakSet()) {
175
+ const { maxDepth, maxStringLength } = limits;
176
+ if (typeof value === "string") {
177
+ return maxStringLength !== undefined
178
+ ? truncateString(value, maxStringLength)
179
+ : value;
180
+ }
181
+ if (Array.isArray(value)) {
182
+ if (seen.has(value))
183
+ return CIRCULAR_PLACEHOLDER;
184
+ if (maxDepth !== undefined && depth > maxDepth) {
185
+ return `[Array(${value.length})]`;
186
+ }
187
+ seen.add(value);
188
+ const result = value.map((item) => applyValueLimits(item, limits, depth + 1, seen));
189
+ seen.delete(value);
190
+ return result;
191
+ }
192
+ if (isPlainObject(value)) {
193
+ if (seen.has(value))
194
+ return CIRCULAR_PLACEHOLDER;
195
+ if (maxDepth !== undefined && depth > maxDepth) {
196
+ return "[Object]";
197
+ }
198
+ seen.add(value);
199
+ const result = {};
200
+ for (const key of Object.keys(value)) {
201
+ result[key] = applyValueLimits(value[key], limits, depth + 1, seen);
202
+ }
203
+ seen.delete(value);
204
+ return result;
205
+ }
206
+ return value;
207
+ }
208
+ function truncateToPreview(value) {
209
+ const str = typeof value === "string"
210
+ ? value
211
+ : (JSON.stringify(value) ?? String(value));
212
+ if (str.length <= ENTRY_PREVIEW_LENGTH)
213
+ return value;
214
+ return truncateString(str, ENTRY_PREVIEW_LENGTH);
215
+ }
216
+ /**
217
+ * Fit a serialized log entry under maxEntryBytes. Truncates the top-level
218
+ * attributes of `data` largest-first to short previews until the entry fits,
219
+ * collapsing `data` to a byte-count marker only as a last resort. When
220
+ * `syncMessageToData` is set (var entries and single-object messages, where
221
+ * `message` mirrors `data`), the message is rebuilt from the truncated data.
222
+ * Returns a new entry; never mutates the input.
223
+ */
224
+ function enforceEntryLimit(entry, { maxEntryBytes, syncMessageToData = false, }) {
225
+ const originalBytes = byteLength(entry);
226
+ if (originalBytes <= maxEntryBytes)
227
+ return entry;
228
+ const result = { ...entry };
229
+ const sync = () => {
230
+ if (syncMessageToData) {
231
+ result.message =
232
+ typeof result.data === "string"
233
+ ? result.data
234
+ : (JSON.stringify(result.data) ?? String(result.data));
235
+ }
236
+ };
237
+ const data = result.data;
238
+ if (typeof data === "string") {
239
+ result.data = truncateToPreview(data);
240
+ sync();
241
+ }
242
+ else if (Array.isArray(data) || isPlainObject(data)) {
243
+ const container = Array.isArray(data)
244
+ ? [...data]
245
+ : { ...data };
246
+ result.data = container;
247
+ const keys = Object.keys(container).sort((a, b) => byteLength(container[b]) - byteLength(container[a]));
248
+ for (const key of keys) {
249
+ container[key] = truncateToPreview(container[key]);
250
+ sync();
251
+ if (byteLength(result) <= maxEntryBytes)
252
+ return result;
253
+ }
254
+ }
255
+ else if (typeof result.message === "string") {
256
+ // No structured data: the message itself is oversized
257
+ const overhead = originalBytes - byteLength(result.message);
258
+ result.message = truncateToBudget(result.message, Math.max(ENTRY_PREVIEW_LENGTH, maxEntryBytes - overhead));
259
+ }
260
+ if (byteLength(result) <= maxEntryBytes)
261
+ return result;
262
+ // Last resort: entry is still oversized after attribute-level truncation
263
+ const marker = `[truncated ${originalBytes.toLocaleString("en-US")} bytes]`;
264
+ if ("data" in result) {
265
+ result.data = marker;
266
+ if (syncMessageToData)
267
+ result.message = marker;
268
+ }
269
+ else if (typeof result.message === "string") {
270
+ result.message = marker;
271
+ }
272
+ return result;
273
+ }
274
+
66
275
  //
67
276
  // Key-based pipelines (match on var key name)
68
277
  //
@@ -654,12 +863,22 @@ function resolveLevelField(value) {
654
863
  return value;
655
864
  }
656
865
  class Logger {
657
- constructor({ format = process.env.LOG_FORMAT || DEFAULT.LEVEL, level = process.env.LOG_LEVEL || DEFAULT.LEVEL, levelField, tags = {}, varLevel = process.env.LOG_VAR_LEVEL || DEFAULT.VAR_LEVEL, } = {}) {
866
+ constructor({ format = process.env.LOG_FORMAT || DEFAULT.LEVEL, level = process.env.LOG_LEVEL || DEFAULT.LEVEL, levelField, maxDepth, maxEntryBytes, maxStringLength, tags = {}, varLevel = process.env.LOG_VAR_LEVEL || DEFAULT.VAR_LEVEL, } = {}) {
658
867
  this.levelField = resolveLevelField(levelField);
868
+ this.limits = resolveSerializationLimits({
869
+ maxDepth,
870
+ maxEntryBytes,
871
+ maxStringLength,
872
+ });
659
873
  this.options = {
660
874
  format,
661
875
  level,
662
876
  levelField: this.levelField || undefined,
877
+ // Pin resolved limits (false = explicitly off) so child loggers
878
+ // created via with() inherit this config instead of re-resolving
879
+ maxDepth: this.limits.maxDepth ?? false,
880
+ maxEntryBytes: this.limits.maxEntryBytes ?? false,
881
+ maxStringLength: this.limits.maxStringLength ?? false,
663
882
  varLevel,
664
883
  };
665
884
  this.tags = {};
@@ -678,10 +897,16 @@ class Logger {
678
897
  createLogMethod(logLevel, format, checkLevel) {
679
898
  const logFn = (...messages) => {
680
899
  if (LEVEL_VALUES[logLevel] <= LEVEL_VALUES[checkLevel]) {
681
- const sanitized = messages.map(sanitizeAuth);
900
+ let sanitized = messages.map(sanitizeAuth);
901
+ if (hasValueLimits(this.limits)) {
902
+ sanitized = sanitized.map((item) => applyValueLimits(item, this.limits));
903
+ }
682
904
  if (format === FORMAT.JSON) {
683
905
  let message = stringify(...sanitized);
684
906
  let parses = parsesTo(message);
907
+ // When data comes from the full message they mirror each other;
908
+ // entry-limit truncation must keep them in sync
909
+ let syncMessageToData = parses.parses;
685
910
  const last = sanitized[sanitized.length - 1];
686
911
  if (sanitized.length > 1 &&
687
912
  typeof last === "object" &&
@@ -693,6 +918,7 @@ class Logger {
693
918
  if (lastParses.parses) {
694
919
  message = stringify(...sanitized.slice(0, -1));
695
920
  parses = lastParses;
921
+ syncMessageToData = false;
696
922
  }
697
923
  }
698
924
  const json = {
@@ -705,10 +931,20 @@ class Logger {
705
931
  if (this.levelField) {
706
932
  json[this.levelField] = logLevel;
707
933
  }
708
- out(json, { level: logLevel });
934
+ let entry = json;
935
+ if (this.limits.maxEntryBytes !== undefined) {
936
+ entry = enforceEntryLimit(json, {
937
+ maxEntryBytes: this.limits.maxEntryBytes,
938
+ syncMessageToData,
939
+ });
940
+ }
941
+ out(entry, { level: logLevel });
709
942
  }
710
943
  else {
711
- const message = stringify(...sanitized);
944
+ let message = stringify(...sanitized);
945
+ if (this.limits.maxEntryBytes !== undefined) {
946
+ message = truncateToBudget(message, this.limits.maxEntryBytes);
947
+ }
712
948
  out(message, { level: logLevel });
713
949
  }
714
950
  }
@@ -750,6 +986,9 @@ class Logger {
750
986
  }
751
987
  }
752
988
  messageVal = filterByType(messageVal);
989
+ if (hasValueLimits(this.limits)) {
990
+ messageVal = applyValueLimits(messageVal, this.limits);
991
+ }
753
992
  const json = {
754
993
  data: parse(messageVal),
755
994
  dataType: typeof messageVal,
@@ -761,7 +1000,14 @@ class Logger {
761
1000
  json[this.levelField] = logLevel;
762
1001
  }
763
1002
  if (LEVEL_VALUES[logLevel] <= LEVEL_VALUES[checkLevel]) {
764
- out(json, { level: logLevel });
1003
+ let entry = json;
1004
+ if (this.limits.maxEntryBytes !== undefined) {
1005
+ entry = enforceEntryLimit(json, {
1006
+ maxEntryBytes: this.limits.maxEntryBytes,
1007
+ syncMessageToData: true,
1008
+ });
1009
+ }
1010
+ out(entry, { level: logLevel });
765
1011
  }
766
1012
  }
767
1013
  else {
@@ -770,6 +1016,20 @@ class Logger {
770
1016
  };
771
1017
  return logFn;
772
1018
  }
1019
+ /**
1020
+ * Update serialization limits at runtime. Pass a number to set a limit,
1021
+ * `false` to disable one; omitted keys are unchanged.
1022
+ */
1023
+ config(options = {}) {
1024
+ this.limits = resolveSerializationLimits({
1025
+ maxDepth: options.maxDepth ?? this.limits.maxDepth ?? false,
1026
+ maxEntryBytes: options.maxEntryBytes ?? this.limits.maxEntryBytes ?? false,
1027
+ maxStringLength: options.maxStringLength ?? this.limits.maxStringLength ?? false,
1028
+ });
1029
+ this.options.maxDepth = this.limits.maxDepth ?? false;
1030
+ this.options.maxEntryBytes = this.limits.maxEntryBytes ?? false;
1031
+ this.options.maxStringLength = this.limits.maxStringLength ?? false;
1032
+ }
773
1033
  tag(key, value) {
774
1034
  if (value) {
775
1035
  this.tags[forceString(key)] = forceString(value);
@@ -899,12 +1159,12 @@ function envBoolean(key, { defaultValue }) {
899
1159
  lower === "no");
900
1160
  }
901
1161
  class JaypieLogger {
902
- constructor({ level = process.env.LOG_LEVEL, tags = {}, } = {}) {
1162
+ constructor({ level = process.env.LOG_LEVEL, maxDepth, maxEntryBytes, maxStringLength, tags = {}, } = {}) {
903
1163
  this._errorCount = 0;
904
1164
  this._report = {};
905
1165
  this._sessionActive = false;
906
1166
  this._warnCount = 0;
907
- this._params = { level, tags };
1167
+ this._params = { level, maxDepth, maxEntryBytes, maxStringLength, tags };
908
1168
  this._loggers = [];
909
1169
  this._tags = {};
910
1170
  this._withLoggers = {};
@@ -913,6 +1173,9 @@ class JaypieLogger {
913
1173
  this._logger = new Logger({
914
1174
  format: FORMAT.JSON,
915
1175
  level: this.level,
1176
+ maxDepth,
1177
+ maxEntryBytes,
1178
+ maxStringLength,
916
1179
  tags: this._tags,
917
1180
  });
918
1181
  this._loggers = [this._logger];
@@ -954,6 +1217,29 @@ class JaypieLogger {
954
1217
  };
955
1218
  this.var = (messageObject, messageValue) => this._logger.var(logVar(messageObject, messageValue));
956
1219
  }
1220
+ /**
1221
+ * Update serialization limits at runtime for this logger and all loggers
1222
+ * derived from it (lib, with, flag). Pass a number to set a limit,
1223
+ * `false` to disable one; omitted keys are unchanged. Persists across
1224
+ * init().
1225
+ */
1226
+ config(options = {}) {
1227
+ if (options.maxDepth !== undefined) {
1228
+ this._params.maxDepth = options.maxDepth;
1229
+ }
1230
+ if (options.maxEntryBytes !== undefined) {
1231
+ this._params.maxEntryBytes = options.maxEntryBytes;
1232
+ }
1233
+ if (options.maxStringLength !== undefined) {
1234
+ this._params.maxStringLength = options.maxStringLength;
1235
+ }
1236
+ for (const logger of this._loggers) {
1237
+ logger.config(options);
1238
+ }
1239
+ for (const key of Object.keys(this._withLoggers)) {
1240
+ this._withLoggers[key].config(options);
1241
+ }
1242
+ }
957
1243
  flag(flag) {
958
1244
  if (typeof flag !== "string" || flag === "") {
959
1245
  return this;
@@ -974,6 +1260,9 @@ class JaypieLogger {
974
1260
  this._logger = new Logger({
975
1261
  format: FORMAT.JSON,
976
1262
  level: this.level,
1263
+ maxDepth: this._params.maxDepth,
1264
+ maxEntryBytes: this._params.maxEntryBytes,
1265
+ maxStringLength: this._params.maxStringLength,
977
1266
  tags: this._tags,
978
1267
  });
979
1268
  this._loggers = [this._logger];
@@ -1056,6 +1345,9 @@ class JaypieLogger {
1056
1345
  }
1057
1346
  return LEVEL.SILENT;
1058
1347
  })(),
1348
+ maxDepth: this._params.maxDepth,
1349
+ maxEntryBytes: this._params.maxEntryBytes,
1350
+ maxStringLength: this._params.maxStringLength,
1059
1351
  tags: newTags,
1060
1352
  });
1061
1353
  this._loggers.push(logger._logger);
@@ -1138,6 +1430,9 @@ class JaypieLogger {
1138
1430
  }
1139
1431
  const logger = new JaypieLogger({
1140
1432
  level: this.level,
1433
+ maxDepth: this._params.maxDepth,
1434
+ maxEntryBytes: this._params.maxEntryBytes,
1435
+ maxStringLength: this._params.maxStringLength,
1141
1436
  tags: { ...this._tags },
1142
1437
  });
1143
1438
  logger._logger = this._logger.with(key, value);