logixia 1.12.4 → 1.12.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -27
- package/dist/.tsbuildinfo +1 -1
- package/dist/async-context-CSHWoHMB.js +88 -0
- package/dist/async-context-CSHWoHMB.js.map +1 -0
- package/dist/async-context-CwBNMm2E.mjs +68 -0
- package/dist/async-context-CwBNMm2E.mjs.map +1 -0
- package/dist/azure-monitor.transport-CG7ClkYj.d.ts +62 -0
- package/dist/azure-monitor.transport-CG7ClkYj.d.ts.map +1 -0
- package/dist/azure-monitor.transport-CHnjVRF-.mjs +152 -0
- package/dist/azure-monitor.transport-CHnjVRF-.mjs.map +1 -0
- package/dist/azure-monitor.transport-Cm74kXZS.d.mts +62 -0
- package/dist/azure-monitor.transport-Cm74kXZS.d.mts.map +1 -0
- package/dist/azure-monitor.transport-CrvfHd9C.js +157 -0
- package/dist/azure-monitor.transport-CrvfHd9C.js.map +1 -0
- package/dist/azure.d.mts +3 -0
- package/dist/azure.d.ts +3 -0
- package/dist/azure.js +6 -0
- package/dist/azure.mjs +6 -0
- package/dist/batch-buffer-BY-mRWIK.js +170 -0
- package/dist/batch-buffer-BY-mRWIK.js.map +1 -0
- package/dist/batch-buffer-PGvSgbmd.mjs +132 -0
- package/dist/batch-buffer-PGvSgbmd.mjs.map +1 -0
- package/dist/browser.d.mts +427 -0
- package/dist/browser.d.mts.map +1 -0
- package/dist/browser.d.ts +427 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +590 -0
- package/dist/browser.js.map +1 -0
- package/dist/browser.mjs +586 -0
- package/dist/browser.mjs.map +1 -0
- package/dist/chunk-DLSkYcPk.mjs +7 -0
- package/dist/chunk-nOFOJqeH.js +30 -0
- package/dist/cloudwatch.d.mts +3 -0
- package/dist/cloudwatch.d.ts +3 -0
- package/dist/cloudwatch.js +6 -0
- package/dist/cloudwatch.mjs +6 -0
- package/dist/cloudwatch.transport-CGHY4buE.d.mts +81 -0
- package/dist/cloudwatch.transport-CGHY4buE.d.mts.map +1 -0
- package/dist/cloudwatch.transport-DK_8cKd3.d.ts +81 -0
- package/dist/cloudwatch.transport-DK_8cKd3.d.ts.map +1 -0
- package/dist/cloudwatch.transport-pNBNWJLt.js +370 -0
- package/dist/cloudwatch.transport-pNBNWJLt.js.map +1 -0
- package/dist/cloudwatch.transport-yBwm4OeO.mjs +360 -0
- package/dist/cloudwatch.transport-yBwm4OeO.mjs.map +1 -0
- package/dist/coerce.utils-BN980Y9V.js +44 -0
- package/dist/coerce.utils-BN980Y9V.js.map +1 -0
- package/dist/coerce.utils-Dxv4_ikC.mjs +38 -0
- package/dist/coerce.utils-Dxv4_ikC.mjs.map +1 -0
- package/dist/correlation.d.mts +238 -0
- package/dist/correlation.d.mts.map +1 -0
- package/dist/correlation.d.ts +238 -0
- package/dist/correlation.d.ts.map +1 -0
- package/dist/correlation.js +272 -0
- package/dist/correlation.js.map +1 -0
- package/dist/correlation.mjs +261 -0
- package/dist/correlation.mjs.map +1 -0
- package/dist/gcp.d.mts +3 -0
- package/dist/gcp.d.ts +3 -0
- package/dist/gcp.js +6 -0
- package/dist/gcp.mjs +6 -0
- package/dist/gcp.transport-BJuNRFe_.mjs +192 -0
- package/dist/gcp.transport-BJuNRFe_.mjs.map +1 -0
- package/dist/gcp.transport-BOhkVKlI.d.ts +71 -0
- package/dist/gcp.transport-BOhkVKlI.d.ts.map +1 -0
- package/dist/gcp.transport-Bi6CUqZR.d.mts +71 -0
- package/dist/gcp.transport-Bi6CUqZR.d.mts.map +1 -0
- package/dist/gcp.transport-SHdtc1n0.js +198 -0
- package/dist/gcp.transport-SHdtc1n0.js.map +1 -0
- package/dist/{index-CwFww7VC.d.ts → index-BPKwwERo.d.mts} +18 -174
- package/dist/index-BPKwwERo.d.mts.map +1 -0
- package/dist/{index-DnyeaoZU.d.mts → index-C1X-TE5I.d.ts} +18 -174
- package/dist/index-C1X-TE5I.d.ts.map +1 -0
- package/dist/index.d.mts +41 -11
- package/dist/index.d.mts.map +1 -1
- package/dist/index.d.ts +40 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +87 -147
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +62 -132
- package/dist/index.mjs.map +1 -1
- package/dist/internal-log-BUJYMZbE.js +70 -0
- package/dist/internal-log-BUJYMZbE.js.map +1 -0
- package/dist/internal-log-Cm8SOWSV.mjs +52 -0
- package/dist/internal-log-Cm8SOWSV.mjs.map +1 -0
- package/dist/{logitron-logger.module-BIGTm2V6.mjs → logitron-logger.module-Bs3XYZgF.mjs} +266 -166
- package/dist/logitron-logger.module-Bs3XYZgF.mjs.map +1 -0
- package/dist/{logitron-logger.module-BTxDpK6u.js → logitron-logger.module-BsDZR5BH.js} +297 -203
- package/dist/logitron-logger.module-BsDZR5BH.js.map +1 -0
- package/dist/{logitron-logger.module-BmT-ewin.d.ts → logitron-logger.module-CxT1F6Cm.d.ts} +23 -10
- package/dist/logitron-logger.module-CxT1F6Cm.d.ts.map +1 -0
- package/dist/{logitron-logger.module-BCq3sF27.d.mts → logitron-logger.module-zDKVd_o0.d.mts} +23 -10
- package/dist/logitron-logger.module-zDKVd_o0.d.mts.map +1 -0
- package/dist/middleware.d.mts +5 -2
- package/dist/middleware.d.mts.map +1 -1
- package/dist/middleware.d.ts +5 -2
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js +13 -4
- package/dist/middleware.js.map +1 -1
- package/dist/middleware.mjs +14 -4
- package/dist/middleware.mjs.map +1 -1
- package/dist/nest.d.mts +3 -2
- package/dist/nest.d.mts.map +1 -1
- package/dist/nest.d.ts +3 -2
- package/dist/nest.d.ts.map +1 -1
- package/dist/nest.js +6 -2
- package/dist/nest.mjs +6 -2
- package/dist/safe-stringify-DJ66tW5I.mjs +118 -0
- package/dist/safe-stringify-DJ66tW5I.mjs.map +1 -0
- package/dist/safe-stringify-DWo3aZp_.js +142 -0
- package/dist/safe-stringify-DWo3aZp_.js.map +1 -0
- package/dist/search-CFQRISUv.mjs +1596 -0
- package/dist/search-CFQRISUv.mjs.map +1 -0
- package/dist/search-CQOo-Iq6.d.mts +816 -0
- package/dist/search-CQOo-Iq6.d.mts.map +1 -0
- package/dist/search-DujhR94O.d.ts +816 -0
- package/dist/search-DujhR94O.d.ts.map +1 -0
- package/dist/search-njTI2hdo.js +1631 -0
- package/dist/search-njTI2hdo.js.map +1 -0
- package/dist/search.d.mts +4 -1
- package/dist/search.d.ts +4 -1
- package/dist/search.js +9 -1
- package/dist/search.mjs +3 -2
- package/dist/testing.d.mts +2 -1
- package/dist/testing.d.mts.map +1 -1
- package/dist/testing.d.ts +2 -1
- package/dist/testing.d.ts.map +1 -1
- package/dist/{transport.manager-ciG4r5uz.js → transport.manager-BivyUTtU.js} +497 -437
- package/dist/transport.manager-BivyUTtU.js.map +1 -0
- package/dist/{transport.manager-DETpXuaY.mjs → transport.manager-D6KL3t9V.mjs} +473 -364
- package/dist/transport.manager-D6KL3t9V.mjs.map +1 -0
- package/dist/transport.types-B5vcyv2k.d.mts +187 -0
- package/dist/transport.types-B5vcyv2k.d.mts.map +1 -0
- package/dist/transport.types-BuY84TwO.d.ts +187 -0
- package/dist/transport.types-BuY84TwO.d.ts.map +1 -0
- package/dist/transports.d.mts +90 -29
- package/dist/transports.d.mts.map +1 -1
- package/dist/transports.d.ts +90 -29
- package/dist/transports.d.ts.map +1 -1
- package/dist/transports.js +14 -2
- package/dist/transports.mjs +10 -2
- package/dist/worker-entry.js +1241 -0
- package/dist/worker.d.mts +3 -0
- package/dist/worker.d.ts +3 -0
- package/dist/worker.js +4 -0
- package/dist/worker.mjs +4 -0
- package/dist/worker.transport-BP8Q28Z2.d.mts +76 -0
- package/dist/worker.transport-BP8Q28Z2.d.mts.map +1 -0
- package/dist/worker.transport-BhMyvqQ2.mjs +167 -0
- package/dist/worker.transport-BhMyvqQ2.mjs.map +1 -0
- package/dist/worker.transport-CfH6z37Z.js +168 -0
- package/dist/worker.transport-CfH6z37Z.js.map +1 -0
- package/dist/worker.transport-mKXgNK0L.d.ts +76 -0
- package/dist/worker.transport-mKXgNK0L.d.ts.map +1 -0
- package/package.json +23 -18
- package/dist/index-CwFww7VC.d.ts.map +0 -1
- package/dist/index-DnyeaoZU.d.mts.map +0 -1
- package/dist/logitron-logger.module-BCq3sF27.d.mts.map +0 -1
- package/dist/logitron-logger.module-BIGTm2V6.mjs.map +0 -1
- package/dist/logitron-logger.module-BTxDpK6u.js.map +0 -1
- package/dist/logitron-logger.module-BmT-ewin.d.ts.map +0 -1
- package/dist/search-Cg_OasF-.d.ts +0 -1
- package/dist/search-DN676Dnz.d.mts +0 -1
- package/dist/search-DOvSI-mb.mjs +0 -1
- package/dist/search-DoZF3RZj.js +0 -0
- package/dist/transport.manager-DETpXuaY.mjs.map +0 -1
- package/dist/transport.manager-ciG4r5uz.js.map +0 -1
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import { r as internalWarn } from "./internal-log-Cm8SOWSV.mjs";
|
|
2
|
+
import { i as stringifyForLog } from "./safe-stringify-DJ66tW5I.mjs";
|
|
3
|
+
import { a as httpRequest, i as HttpError, n as DEFAULT_MAX_BUFFER_SIZE, o as isPermanentHttpError, r as DEFAULT_REQUEST_TIMEOUT_MS, t as BatchBuffer } from "./batch-buffer-PGvSgbmd.mjs";
|
|
4
|
+
import * as fs from "node:fs";
|
|
5
|
+
import * as os from "node:os";
|
|
6
|
+
import * as path$1 from "node:path";
|
|
7
|
+
import * as crypto from "node:crypto";
|
|
8
|
+
|
|
9
|
+
//#region src/transports/aws-credentials.ts
|
|
10
|
+
const REFRESH_MARGIN_MS = 300 * 1e3;
|
|
11
|
+
const FAILURE_BACKOFF_MS = 30 * 1e3;
|
|
12
|
+
const METADATA_TIMEOUT_MS = 2e3;
|
|
13
|
+
/** Parse the sections of an AWS ini file into `{ section: { key: value } }`. */
|
|
14
|
+
function parseIni(text) {
|
|
15
|
+
const out = {};
|
|
16
|
+
let section;
|
|
17
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
18
|
+
const line = raw.trim();
|
|
19
|
+
if (!line || line.startsWith("#") || line.startsWith(";")) continue;
|
|
20
|
+
const header = /^\[(.+)\]$/.exec(line);
|
|
21
|
+
if (header) {
|
|
22
|
+
section = out[header[1].trim()] = out[header[1].trim()] ?? {};
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
const eq = line.indexOf("=");
|
|
26
|
+
if (eq > 0 && section) section[line.slice(0, eq).trim()] = line.slice(eq + 1).trim();
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
function fromEnv() {
|
|
31
|
+
const accessKeyId = process.env["AWS_ACCESS_KEY_ID"];
|
|
32
|
+
const secretAccessKey = process.env["AWS_SECRET_ACCESS_KEY"];
|
|
33
|
+
if (!accessKeyId || !secretAccessKey) return void 0;
|
|
34
|
+
return {
|
|
35
|
+
accessKeyId,
|
|
36
|
+
secretAccessKey,
|
|
37
|
+
sessionToken: process.env["AWS_SESSION_TOKEN"]
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function fromSharedFile() {
|
|
41
|
+
const file = process.env["AWS_SHARED_CREDENTIALS_FILE"] ?? path$1.join(os.homedir(), ".aws", "credentials");
|
|
42
|
+
const profile = process.env["AWS_PROFILE"] ?? "default";
|
|
43
|
+
let text;
|
|
44
|
+
try {
|
|
45
|
+
text = fs.readFileSync(file, "utf8");
|
|
46
|
+
} catch {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const section = parseIni(text)[profile];
|
|
50
|
+
const accessKeyId = section === null || section === void 0 ? void 0 : section["aws_access_key_id"];
|
|
51
|
+
const secretAccessKey = section === null || section === void 0 ? void 0 : section["aws_secret_access_key"];
|
|
52
|
+
if (!accessKeyId || !secretAccessKey) return void 0;
|
|
53
|
+
return {
|
|
54
|
+
accessKeyId,
|
|
55
|
+
secretAccessKey,
|
|
56
|
+
sessionToken: section === null || section === void 0 ? void 0 : section["aws_session_token"]
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function fromDocument(raw) {
|
|
60
|
+
const doc = JSON.parse(raw);
|
|
61
|
+
if (!doc.AccessKeyId || !doc.SecretAccessKey) return void 0;
|
|
62
|
+
const expiresAt = doc.Expiration ? Date.parse(doc.Expiration) : NaN;
|
|
63
|
+
return {
|
|
64
|
+
accessKeyId: doc.AccessKeyId,
|
|
65
|
+
secretAccessKey: doc.SecretAccessKey,
|
|
66
|
+
sessionToken: doc.Token,
|
|
67
|
+
expiresAt: Number.isNaN(expiresAt) ? void 0 : expiresAt
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
const CONTAINER_HOSTS = new Set([
|
|
71
|
+
"169.254.170.2",
|
|
72
|
+
"169.254.170.23",
|
|
73
|
+
"[fd00:ec2::23]",
|
|
74
|
+
"localhost",
|
|
75
|
+
"[::1]"
|
|
76
|
+
]);
|
|
77
|
+
/** Same rule as the AWS SDKs: never send the container token to an arbitrary http host. */
|
|
78
|
+
function isAllowedContainerHost(url) {
|
|
79
|
+
if (url.protocol === "https:") return true;
|
|
80
|
+
if (url.protocol !== "http:") return false;
|
|
81
|
+
return CONTAINER_HOSTS.has(url.hostname) || /^127(?:\.\d{1,3}){3}$/.test(url.hostname);
|
|
82
|
+
}
|
|
83
|
+
async function fromContainer() {
|
|
84
|
+
const relative = process.env["AWS_CONTAINER_CREDENTIALS_RELATIVE_URI"];
|
|
85
|
+
const full = process.env["AWS_CONTAINER_CREDENTIALS_FULL_URI"];
|
|
86
|
+
if (!relative && !full) return void 0;
|
|
87
|
+
const url = new URL(relative ? `http://169.254.170.2${relative}` : full);
|
|
88
|
+
if (!relative && !isAllowedContainerHost(url)) {
|
|
89
|
+
internalWarn(`ignoring AWS_CONTAINER_CREDENTIALS_FULL_URI: ${url.host} is not https or a loopback / ECS / EKS address`);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const headers = {};
|
|
93
|
+
const tokenFile = process.env["AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE"];
|
|
94
|
+
const token = tokenFile ? fs.readFileSync(tokenFile, "utf8").trim() : process.env["AWS_CONTAINER_AUTHORIZATION_TOKEN"];
|
|
95
|
+
if (token) headers["Authorization"] = token;
|
|
96
|
+
return fromDocument((await httpRequest({
|
|
97
|
+
url,
|
|
98
|
+
method: "GET",
|
|
99
|
+
headers,
|
|
100
|
+
timeoutMs: METADATA_TIMEOUT_MS,
|
|
101
|
+
label: "AWS container credentials"
|
|
102
|
+
})).body);
|
|
103
|
+
}
|
|
104
|
+
async function fromInstanceMetadata() {
|
|
105
|
+
var _roles$body$split$;
|
|
106
|
+
if (process.env["AWS_EC2_METADATA_DISABLED"] === "true") return void 0;
|
|
107
|
+
const base = process.env["AWS_EC2_METADATA_SERVICE_ENDPOINT"] ?? "http://169.254.169.254";
|
|
108
|
+
const get = (p, method, headers) => httpRequest({
|
|
109
|
+
url: new URL(p, base),
|
|
110
|
+
method,
|
|
111
|
+
headers,
|
|
112
|
+
timeoutMs: METADATA_TIMEOUT_MS,
|
|
113
|
+
label: "EC2 instance metadata"
|
|
114
|
+
});
|
|
115
|
+
const session = await get("/latest/api/token", "PUT", { "X-aws-ec2-metadata-token-ttl-seconds": "21600" });
|
|
116
|
+
const auth = { "X-aws-ec2-metadata-token": session.body.trim() };
|
|
117
|
+
const role = (_roles$body$split$ = (await get("/latest/meta-data/iam/security-credentials/", "GET", auth)).body.split("\n")[0]) === null || _roles$body$split$ === void 0 ? void 0 : _roles$body$split$.trim();
|
|
118
|
+
if (!role) return void 0;
|
|
119
|
+
return fromDocument((await get(`/latest/meta-data/iam/security-credentials/${encodeURIComponent(role)}`, "GET", auth)).body);
|
|
120
|
+
}
|
|
121
|
+
var AwsCredentialProvider = class {
|
|
122
|
+
constructor(config) {
|
|
123
|
+
this.failedAt = 0;
|
|
124
|
+
if (config.accessKeyId && config.secretAccessKey) this.explicit = {
|
|
125
|
+
accessKeyId: config.accessKeyId,
|
|
126
|
+
secretAccessKey: config.secretAccessKey,
|
|
127
|
+
sessionToken: config.sessionToken
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/** Throws when no source yields credentials, so the caller keeps its buffer. */
|
|
131
|
+
async get() {
|
|
132
|
+
if (this.explicit) return this.explicit;
|
|
133
|
+
const env = fromEnv();
|
|
134
|
+
if (env) return env;
|
|
135
|
+
if (this.cached && !this.isExpiring(this.cached)) return this.cached;
|
|
136
|
+
if (Date.now() - this.failedAt < FAILURE_BACKOFF_MS) throw new Error("no AWS credentials found (retrying shortly)");
|
|
137
|
+
let found = fromSharedFile();
|
|
138
|
+
for (const source of [fromContainer, fromInstanceMetadata]) {
|
|
139
|
+
if (found) break;
|
|
140
|
+
try {
|
|
141
|
+
found = await source();
|
|
142
|
+
} catch {}
|
|
143
|
+
}
|
|
144
|
+
if (!found) {
|
|
145
|
+
this.failedAt = Date.now();
|
|
146
|
+
throw new Error("no AWS credentials found: set AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY, a shared credentials file, container credentials, or run on EC2 with an instance role");
|
|
147
|
+
}
|
|
148
|
+
this.cached = found;
|
|
149
|
+
return found;
|
|
150
|
+
}
|
|
151
|
+
invalidate() {
|
|
152
|
+
this.cached = void 0;
|
|
153
|
+
this.failedAt = 0;
|
|
154
|
+
}
|
|
155
|
+
isExpiring(c) {
|
|
156
|
+
return c.expiresAt !== void 0 && c.expiresAt - Date.now() < REFRESH_MARGIN_MS;
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
//#endregion
|
|
161
|
+
//#region src/transports/aws-sigv4.ts
|
|
162
|
+
const sha256Hex = (data) => crypto.createHash("sha256").update(data).digest("hex");
|
|
163
|
+
const hmac = (key, msg) => crypto.createHmac("sha256", key).update(msg).digest();
|
|
164
|
+
/** Returns the request headers (lowercase names) with `x-amz-date` and `authorization` added. */
|
|
165
|
+
function signV4(opts) {
|
|
166
|
+
const { method, path: path$2, query = "", body, region, service, credentials, headers: input, date } = opts;
|
|
167
|
+
const amzDate = (date ?? /* @__PURE__ */ new Date()).toISOString().replace(/[:-]|\.\d{3}/g, "");
|
|
168
|
+
const dateStamp = amzDate.slice(0, 8);
|
|
169
|
+
const headers = {};
|
|
170
|
+
for (const [k, v] of Object.entries(input)) headers[k.toLowerCase()] = v;
|
|
171
|
+
headers["x-amz-date"] = amzDate;
|
|
172
|
+
if (credentials.sessionToken) headers["x-amz-security-token"] = credentials.sessionToken;
|
|
173
|
+
const names = Object.keys(headers).sort();
|
|
174
|
+
const canonicalHeaders = names.map((n) => `${n}:${headers[n].trim().replace(/\s+/g, " ")}\n`).join("");
|
|
175
|
+
const signedHeaders = names.join(";");
|
|
176
|
+
const canonicalRequest = [
|
|
177
|
+
method,
|
|
178
|
+
path$2,
|
|
179
|
+
query,
|
|
180
|
+
canonicalHeaders,
|
|
181
|
+
signedHeaders,
|
|
182
|
+
sha256Hex(body)
|
|
183
|
+
].join("\n");
|
|
184
|
+
const scope = `${dateStamp}/${region}/${service}/aws4_request`;
|
|
185
|
+
const stringToSign = [
|
|
186
|
+
"AWS4-HMAC-SHA256",
|
|
187
|
+
amzDate,
|
|
188
|
+
scope,
|
|
189
|
+
sha256Hex(canonicalRequest)
|
|
190
|
+
].join("\n");
|
|
191
|
+
const signingKey = hmac(hmac(hmac(hmac(`AWS4${credentials.secretAccessKey}`, dateStamp), region), service), "aws4_request");
|
|
192
|
+
const signature = crypto.createHmac("sha256", signingKey).update(stringToSign).digest("hex");
|
|
193
|
+
headers["authorization"] = `AWS4-HMAC-SHA256 Credential=${credentials.accessKeyId}/${scope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
|
|
194
|
+
return headers;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
//#endregion
|
|
198
|
+
//#region src/transports/cloudwatch.transport.ts
|
|
199
|
+
const MAX_EVENTS_PER_PUT = 1e4;
|
|
200
|
+
/** Error types worth retrying even though CloudWatch reports them as HTTP 400. */
|
|
201
|
+
const RETRYABLE_TYPES = new Set([
|
|
202
|
+
"ThrottlingException",
|
|
203
|
+
"ThrottledException",
|
|
204
|
+
"LimitExceededException",
|
|
205
|
+
"ServiceUnavailableException",
|
|
206
|
+
"ExpiredTokenException",
|
|
207
|
+
"ResourceNotFoundException"
|
|
208
|
+
]);
|
|
209
|
+
function awsErrorType(err) {
|
|
210
|
+
if (!(err instanceof HttpError)) return void 0;
|
|
211
|
+
try {
|
|
212
|
+
const type = JSON.parse(err.body).__type;
|
|
213
|
+
return type === null || type === void 0 ? void 0 : type.split("#").pop();
|
|
214
|
+
} catch {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* AWS CloudWatch Logs transport with EMF support and batched PutLogEvents calls.
|
|
220
|
+
*/
|
|
221
|
+
var CloudWatchTransport = class {
|
|
222
|
+
constructor(config) {
|
|
223
|
+
this.name = "cloudwatch";
|
|
224
|
+
this.flushTimer = null;
|
|
225
|
+
this.streamReady = false;
|
|
226
|
+
this.region = config.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? "us-east-1";
|
|
227
|
+
this.logGroupName = config.logGroupName;
|
|
228
|
+
this.logStreamName = config.logStreamName ?? `${process.env["HOSTNAME"] ?? "node"}-${process.pid}`;
|
|
229
|
+
this.credentials = new AwsCredentialProvider(config);
|
|
230
|
+
this.endpoint = new URL(config.endpoint ?? `https://logs.${this.region}.amazonaws.com`);
|
|
231
|
+
this.batchSize = Math.min(config.batchSize ?? 100, MAX_EVENTS_PER_PUT);
|
|
232
|
+
this.emfNamespace = config.emfNamespace ?? "Logixia";
|
|
233
|
+
this.flushInterval = config.flushIntervalMs ?? 5e3;
|
|
234
|
+
this.emf = config.emf ?? false;
|
|
235
|
+
this.level = config.level;
|
|
236
|
+
this.requestTimeoutMs = config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
237
|
+
this.buffer = new BatchBuffer({
|
|
238
|
+
name: "CloudWatchTransport",
|
|
239
|
+
batchSize: this.batchSize,
|
|
240
|
+
maxBufferSize: config.maxBufferSize ?? DEFAULT_MAX_BUFFER_SIZE,
|
|
241
|
+
send: (events) => this.putLogEvents(events),
|
|
242
|
+
shouldRetry: (err) => !isPermanentHttpError(err) || RETRYABLE_TYPES.has(awsErrorType(err) ?? "")
|
|
243
|
+
});
|
|
244
|
+
this.flushTimer = setInterval(() => {
|
|
245
|
+
this.flush().catch(() => {});
|
|
246
|
+
}, this.flushInterval);
|
|
247
|
+
if (this.flushTimer.unref) this.flushTimer.unref();
|
|
248
|
+
}
|
|
249
|
+
write(entry) {
|
|
250
|
+
const timestamp = entry.timestamp ? new Date(entry.timestamp).getTime() : Date.now();
|
|
251
|
+
const message = this.emf ? this.toEmf(entry) : stringifyForLog(entry);
|
|
252
|
+
this.buffer.push({
|
|
253
|
+
timestamp,
|
|
254
|
+
message
|
|
255
|
+
});
|
|
256
|
+
this.buffer.flushIfFull();
|
|
257
|
+
}
|
|
258
|
+
flush() {
|
|
259
|
+
return this.buffer.flush();
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Flush remaining events and stop the interval timer on shutdown. Without this
|
|
263
|
+
* the manager's close() skips the transport (close was undefined) and any
|
|
264
|
+
* buffered events were lost on deploy. Waits for any in-flight send first.
|
|
265
|
+
*/
|
|
266
|
+
async close() {
|
|
267
|
+
if (this.flushTimer) {
|
|
268
|
+
clearInterval(this.flushTimer);
|
|
269
|
+
this.flushTimer = null;
|
|
270
|
+
}
|
|
271
|
+
await this.buffer.close();
|
|
272
|
+
}
|
|
273
|
+
toEmf(entry) {
|
|
274
|
+
const { level, message, data: payload, timestamp } = entry;
|
|
275
|
+
const numericFields = {};
|
|
276
|
+
for (const [k, v] of Object.entries(payload ?? {})) if (typeof v === "number") numericFields[k] = v;
|
|
277
|
+
return stringifyForLog({
|
|
278
|
+
_aws: {
|
|
279
|
+
Timestamp: timestamp ? new Date(timestamp).getTime() : Date.now(),
|
|
280
|
+
CloudWatchMetrics: Object.keys(numericFields).length > 0 ? [{
|
|
281
|
+
Namespace: this.emfNamespace,
|
|
282
|
+
Dimensions: [["level"]],
|
|
283
|
+
Metrics: Object.keys(numericFields).map((n) => ({
|
|
284
|
+
Name: n,
|
|
285
|
+
Unit: "None"
|
|
286
|
+
}))
|
|
287
|
+
}] : []
|
|
288
|
+
},
|
|
289
|
+
level,
|
|
290
|
+
message,
|
|
291
|
+
...payload,
|
|
292
|
+
...numericFields
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
async putLogEvents(events) {
|
|
296
|
+
await this.ensureStream();
|
|
297
|
+
const logEvents = [...events].sort((a, b) => a.timestamp - b.timestamp);
|
|
298
|
+
try {
|
|
299
|
+
await this.call("PutLogEvents", {
|
|
300
|
+
logGroupName: this.logGroupName,
|
|
301
|
+
logStreamName: this.logStreamName,
|
|
302
|
+
logEvents
|
|
303
|
+
});
|
|
304
|
+
} catch (err) {
|
|
305
|
+
const type = awsErrorType(err);
|
|
306
|
+
if (type === "DataAlreadyAcceptedException") return;
|
|
307
|
+
if (type === "ResourceNotFoundException") this.streamReady = false;
|
|
308
|
+
if (type === "ExpiredTokenException") this.credentials.invalidate();
|
|
309
|
+
throw err;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
async ensureStream() {
|
|
313
|
+
if (this.streamReady) return;
|
|
314
|
+
try {
|
|
315
|
+
await this.call("CreateLogGroup", { logGroupName: this.logGroupName });
|
|
316
|
+
} catch (err) {
|
|
317
|
+
if (awsErrorType(err) === "ExpiredTokenException") this.credentials.invalidate();
|
|
318
|
+
if (!(err instanceof HttpError)) throw err;
|
|
319
|
+
if (awsErrorType(err) !== "ResourceAlreadyExistsException") internalWarn(`CloudWatchTransport: CreateLogGroup failed, continuing (${err.message})`);
|
|
320
|
+
}
|
|
321
|
+
try {
|
|
322
|
+
await this.call("CreateLogStream", {
|
|
323
|
+
logGroupName: this.logGroupName,
|
|
324
|
+
logStreamName: this.logStreamName
|
|
325
|
+
});
|
|
326
|
+
} catch (err) {
|
|
327
|
+
if (awsErrorType(err) !== "ResourceAlreadyExistsException") throw err;
|
|
328
|
+
}
|
|
329
|
+
this.streamReady = true;
|
|
330
|
+
}
|
|
331
|
+
async call(action, payload) {
|
|
332
|
+
const credentials = await this.credentials.get();
|
|
333
|
+
const body = stringifyForLog(payload);
|
|
334
|
+
const { host } = this.endpoint;
|
|
335
|
+
const headers = signV4({
|
|
336
|
+
method: "POST",
|
|
337
|
+
path: "/",
|
|
338
|
+
headers: {
|
|
339
|
+
host,
|
|
340
|
+
"content-type": "application/x-amz-json-1.1",
|
|
341
|
+
"x-amz-target": `Logs_20140328.${action}`
|
|
342
|
+
},
|
|
343
|
+
body,
|
|
344
|
+
region: this.region,
|
|
345
|
+
service: "logs",
|
|
346
|
+
credentials
|
|
347
|
+
});
|
|
348
|
+
await httpRequest({
|
|
349
|
+
url: this.endpoint,
|
|
350
|
+
headers,
|
|
351
|
+
body,
|
|
352
|
+
timeoutMs: this.requestTimeoutMs,
|
|
353
|
+
label: `CloudWatch ${action}`
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
//#endregion
|
|
359
|
+
export { CloudWatchTransport as t };
|
|
360
|
+
//# sourceMappingURL=cloudwatch.transport-yBwm4OeO.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cloudwatch.transport-yBwm4OeO.mjs","names":["out: Record<string, Record<string, string>>","section: Record<string, string> | undefined","path","text: string","headers: Record<string, string>","headers: Record<string, string>","path","numericFields: Record<string, number>"],"sources":["../src/transports/aws-credentials.ts","../src/transports/aws-sigv4.ts","../src/transports/cloudwatch.transport.ts"],"sourcesContent":["/**\n * AWS credential resolution without the SDK.\n *\n * Order: explicit config, environment variables, shared credentials file,\n * ECS/container credentials, EC2 instance metadata (IMDSv2).\n * Not supported: SSO, assume-role profiles, credential_process, web identity.\n */\n\nimport * as fs from 'node:fs';\nimport * as os from 'node:os';\nimport * as path from 'node:path';\n\nimport { internalWarn } from '../utils/internal-log';\nimport { type AwsCredentials } from './aws-sigv4';\nimport { httpRequest } from './batch-buffer';\n\nconst REFRESH_MARGIN_MS = 5 * 60 * 1000;\nconst FAILURE_BACKOFF_MS = 30 * 1000;\nconst METADATA_TIMEOUT_MS = 2000;\n\nexport interface AwsCredentialConfig {\n accessKeyId?: string | undefined;\n secretAccessKey?: string | undefined;\n sessionToken?: string | undefined;\n}\n\n/** Parse the sections of an AWS ini file into `{ section: { key: value } }`. */\nexport function parseIni(text: string): Record<string, Record<string, string>> {\n const out: Record<string, Record<string, string>> = {};\n let section: Record<string, string> | undefined;\n for (const raw of text.split(/\\r?\\n/)) {\n const line = raw.trim();\n if (!line || line.startsWith('#') || line.startsWith(';')) continue;\n const header = /^\\[(.+)\\]$/.exec(line);\n if (header) {\n section = out[header[1]!.trim()] = out[header[1]!.trim()] ?? {};\n continue;\n }\n const eq = line.indexOf('=');\n if (eq > 0 && section) section[line.slice(0, eq).trim()] = line.slice(eq + 1).trim();\n }\n return out;\n}\n\nfunction fromEnv(): AwsCredentials | undefined {\n const accessKeyId = process.env['AWS_ACCESS_KEY_ID'];\n const secretAccessKey = process.env['AWS_SECRET_ACCESS_KEY'];\n if (!accessKeyId || !secretAccessKey) return undefined;\n return { accessKeyId, secretAccessKey, sessionToken: process.env['AWS_SESSION_TOKEN'] };\n}\n\nfunction fromSharedFile(): AwsCredentials | undefined {\n const file =\n process.env['AWS_SHARED_CREDENTIALS_FILE'] ?? path.join(os.homedir(), '.aws', 'credentials');\n const profile = process.env['AWS_PROFILE'] ?? 'default';\n let text: string;\n try {\n text = fs.readFileSync(file, 'utf8');\n } catch {\n return undefined;\n }\n const section = parseIni(text)[profile];\n const accessKeyId = section?.['aws_access_key_id'];\n const secretAccessKey = section?.['aws_secret_access_key'];\n if (!accessKeyId || !secretAccessKey) return undefined;\n return { accessKeyId, secretAccessKey, sessionToken: section?.['aws_session_token'] };\n}\n\ninterface CredentialDocument {\n AccessKeyId?: string;\n SecretAccessKey?: string;\n Token?: string;\n Expiration?: string;\n}\n\nfunction fromDocument(raw: string): AwsCredentials | undefined {\n const doc = JSON.parse(raw) as CredentialDocument;\n if (!doc.AccessKeyId || !doc.SecretAccessKey) return undefined;\n const expiresAt = doc.Expiration ? Date.parse(doc.Expiration) : Number.NaN;\n return {\n accessKeyId: doc.AccessKeyId,\n secretAccessKey: doc.SecretAccessKey,\n sessionToken: doc.Token,\n expiresAt: Number.isNaN(expiresAt) ? undefined : expiresAt,\n };\n}\n\nconst CONTAINER_HOSTS = new Set([\n // eslint-disable-next-line sonarjs/no-hardcoded-ip -- fixed ECS credentials endpoint\n '169.254.170.2',\n // eslint-disable-next-line sonarjs/no-hardcoded-ip -- fixed EKS pod identity endpoint\n '169.254.170.23',\n '[fd00:ec2::23]',\n 'localhost',\n '[::1]',\n]);\n\n/** Same rule as the AWS SDKs: never send the container token to an arbitrary http host. */\nfunction isAllowedContainerHost(url: URL): boolean {\n if (url.protocol === 'https:') return true;\n if (url.protocol !== 'http:') return false;\n return CONTAINER_HOSTS.has(url.hostname) || /^127(?:\\.\\d{1,3}){3}$/.test(url.hostname);\n}\n\nasync function fromContainer(): Promise<AwsCredentials | undefined> {\n const relative = process.env['AWS_CONTAINER_CREDENTIALS_RELATIVE_URI'];\n const full = process.env['AWS_CONTAINER_CREDENTIALS_FULL_URI'];\n if (!relative && !full) return undefined;\n const url = new URL(relative ? `http://169.254.170.2${relative}` : (full as string));\n if (!relative && !isAllowedContainerHost(url)) {\n internalWarn(\n `ignoring AWS_CONTAINER_CREDENTIALS_FULL_URI: ${url.host} is not https or a loopback / ECS / EKS address`\n );\n return undefined;\n }\n const headers: Record<string, string> = {};\n const tokenFile = process.env['AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE'];\n const token = tokenFile\n ? fs.readFileSync(tokenFile, 'utf8').trim()\n : process.env['AWS_CONTAINER_AUTHORIZATION_TOKEN'];\n if (token) headers['Authorization'] = token;\n const res = await httpRequest({\n url,\n method: 'GET',\n headers,\n timeoutMs: METADATA_TIMEOUT_MS,\n label: 'AWS container credentials',\n });\n return fromDocument(res.body);\n}\n\nasync function fromInstanceMetadata(): Promise<AwsCredentials | undefined> {\n if (process.env['AWS_EC2_METADATA_DISABLED'] === 'true') return undefined;\n const base = process.env['AWS_EC2_METADATA_SERVICE_ENDPOINT'] ?? 'http://169.254.169.254';\n const get = (p: string, method: string, headers: Record<string, string>) =>\n httpRequest({\n url: new URL(p, base),\n method,\n headers,\n timeoutMs: METADATA_TIMEOUT_MS,\n label: 'EC2 instance metadata',\n });\n const session = await get('/latest/api/token', 'PUT', {\n 'X-aws-ec2-metadata-token-ttl-seconds': '21600',\n });\n const auth = { 'X-aws-ec2-metadata-token': session.body.trim() };\n const roles = await get('/latest/meta-data/iam/security-credentials/', 'GET', auth);\n const role = roles.body.split('\\n')[0]?.trim();\n if (!role) return undefined;\n const creds = await get(\n `/latest/meta-data/iam/security-credentials/${encodeURIComponent(role)}`,\n 'GET',\n auth\n );\n return fromDocument(creds.body);\n}\n\nexport class AwsCredentialProvider {\n private cached: AwsCredentials | undefined;\n private failedAt = 0;\n private readonly explicit: AwsCredentials | undefined;\n\n constructor(config: AwsCredentialConfig) {\n if (config.accessKeyId && config.secretAccessKey) {\n this.explicit = {\n accessKeyId: config.accessKeyId,\n secretAccessKey: config.secretAccessKey,\n sessionToken: config.sessionToken,\n };\n }\n }\n\n /** Throws when no source yields credentials, so the caller keeps its buffer. */\n async get(): Promise<AwsCredentials> {\n if (this.explicit) return this.explicit;\n const env = fromEnv();\n if (env) return env;\n if (this.cached && !this.isExpiring(this.cached)) return this.cached;\n if (Date.now() - this.failedAt < FAILURE_BACKOFF_MS) {\n throw new Error('no AWS credentials found (retrying shortly)');\n }\n\n let found = fromSharedFile();\n for (const source of [fromContainer, fromInstanceMetadata]) {\n if (found) break;\n try {\n found = await source();\n } catch {\n /* source not available here; try the next */\n }\n }\n if (!found) {\n this.failedAt = Date.now();\n throw new Error(\n 'no AWS credentials found: set AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY, a shared credentials file, container credentials, or run on EC2 with an instance role'\n );\n }\n this.cached = found;\n return found;\n }\n\n invalidate(): void {\n this.cached = undefined;\n this.failedAt = 0;\n }\n\n private isExpiring(c: AwsCredentials): boolean {\n return c.expiresAt !== undefined && c.expiresAt - Date.now() < REFRESH_MARGIN_MS;\n }\n}\n","/**\n * AWS Signature Version 4 request signing, dependency-free.\n */\n\nimport * as crypto from 'node:crypto';\n\nexport interface AwsCredentials {\n accessKeyId: string;\n secretAccessKey: string;\n sessionToken?: string | undefined;\n /** Epoch ms after which the credentials are no longer valid. */\n expiresAt?: number | undefined;\n}\n\nexport interface SignV4Options {\n method: string;\n path: string;\n query?: string;\n /** Must include `host`. Names are lowercased before signing. */\n headers: Record<string, string>;\n body: string;\n region: string;\n service: string;\n credentials: AwsCredentials;\n date?: Date;\n}\n\nconst sha256Hex = (data: string): string => crypto.createHash('sha256').update(data).digest('hex');\n\nconst hmac = (key: Buffer | string, msg: string): Buffer =>\n crypto.createHmac('sha256', key).update(msg).digest();\n\n/** Returns the request headers (lowercase names) with `x-amz-date` and `authorization` added. */\nexport function signV4(opts: SignV4Options): Record<string, string> {\n const {\n method,\n path,\n query = '',\n body,\n region,\n service,\n credentials,\n headers: input,\n date,\n } = opts;\n const amzDate = (date ?? new Date()).toISOString().replace(/[:-]|\\.\\d{3}/g, '');\n const dateStamp = amzDate.slice(0, 8);\n\n const headers: Record<string, string> = {};\n for (const [k, v] of Object.entries(input)) headers[k.toLowerCase()] = v;\n headers['x-amz-date'] = amzDate;\n if (credentials.sessionToken) headers['x-amz-security-token'] = credentials.sessionToken;\n\n const names = Object.keys(headers).sort();\n const canonicalHeaders = names\n .map((n) => `${n}:${headers[n]!.trim().replace(/\\s+/g, ' ')}\\n`)\n .join('');\n const signedHeaders = names.join(';');\n\n const canonicalRequest = [\n method,\n path,\n query,\n canonicalHeaders,\n signedHeaders,\n sha256Hex(body),\n ].join('\\n');\n const scope = `${dateStamp}/${region}/${service}/aws4_request`;\n const stringToSign = ['AWS4-HMAC-SHA256', amzDate, scope, sha256Hex(canonicalRequest)].join('\\n');\n\n const signingKey = hmac(\n hmac(hmac(hmac(`AWS4${credentials.secretAccessKey}`, dateStamp), region), service),\n 'aws4_request'\n );\n const signature = crypto.createHmac('sha256', signingKey).update(stringToSign).digest('hex');\n\n headers['authorization'] =\n `AWS4-HMAC-SHA256 Credential=${credentials.accessKeyId}/${scope}, ` +\n `SignedHeaders=${signedHeaders}, Signature=${signature}`;\n return headers;\n}\n","/**\n * logixia — AWS CloudWatch Logs transport\n *\n * Sends log entries to AWS CloudWatch Logs via the PutLogEvents API.\n * Supports EMF (Embedded Metric Format) for automatic metric extraction.\n *\n * Auth, in order: explicit config, environment variables, the shared\n * credentials file (AWS_PROFILE, AWS_SHARED_CREDENTIALS_FILE), ECS container\n * credentials, then EC2 instance metadata (IMDSv2). SSO, assume-role profiles\n * and credential_process are not supported. No SDK dependency — raw HTTPS with\n * AWS Signature Version 4.\n *\n * The log group and stream are created on first use if they do not exist.\n *\n * @example\n * ```ts\n * import { CloudWatchTransport } from 'logixia';\n *\n * const logger = createLogger({\n * transports: {\n * cloudwatch: new CloudWatchTransport({\n * region: 'us-east-1',\n * logGroupName: '/myapp/production',\n * logStreamName: `api-${process.env.HOSTNAME}`,\n * batchSize: 100,\n * flushIntervalMs: 5000,\n * }),\n * },\n * });\n * ```\n */\n\nimport type { IAsyncTransport, TransportLogEntry } from '../types/transport.types';\nimport { internalWarn } from '../utils/internal-log';\nimport { stringifyForLog } from '../utils/safe-stringify';\nimport { AwsCredentialProvider } from './aws-credentials';\nimport { signV4 } from './aws-sigv4';\nimport {\n BatchBuffer,\n DEFAULT_MAX_BUFFER_SIZE,\n DEFAULT_REQUEST_TIMEOUT_MS,\n HttpError,\n httpRequest,\n isPermanentHttpError,\n} from './batch-buffer';\n\nexport interface CloudWatchTransportConfig {\n /** AWS region, e.g. `'us-east-1'`. Falls back to `AWS_REGION` env var. */\n region?: string;\n /** CloudWatch Logs group name. Created automatically if it does not exist. */\n logGroupName: string;\n /** CloudWatch Logs stream name. Defaults to hostname + process PID. */\n logStreamName?: string;\n /** Override the Logs endpoint, e.g. for LocalStack. Default: `https://logs.<region>.amazonaws.com`. */\n endpoint?: string;\n /** AWS access key ID. Falls back to the credential chain described above. */\n accessKeyId?: string;\n /** AWS secret access key. Used together with `accessKeyId`. */\n secretAccessKey?: string;\n /** AWS session token for temporary credentials. Used together with `accessKeyId`. */\n sessionToken?: string;\n /**\n * Number of log events to batch before flushing.\n * CloudWatch allows max 10,000 events per PutLogEvents call; larger values are capped.\n * @default 100\n */\n batchSize?: number;\n /**\n * Max ms between flushes even if batchSize is not reached.\n * @default 5000\n */\n flushIntervalMs?: number;\n /**\n * Enable EMF (Embedded Metric Format) — wraps log entries as CloudWatch\n * metric namespaces so numeric fields become CloudWatch Metrics automatically.\n * @default false\n */\n emf?: boolean;\n /** CloudWatch Metrics namespace used by EMF. @default 'Logixia' */\n emfNamespace?: string;\n /** Abort a request that has not finished after this many ms. @default 10000 */\n requestTimeoutMs?: number;\n /** Max buffered events; the oldest are dropped past this. @default 10000 */\n maxBufferSize?: number;\n /** Minimum log level to forward. */\n level?: string;\n}\n\nconst MAX_EVENTS_PER_PUT = 10_000;\n\n/** Error types worth retrying even though CloudWatch reports them as HTTP 400. */\nconst RETRYABLE_TYPES = new Set([\n 'ThrottlingException',\n 'ThrottledException',\n 'LimitExceededException',\n 'ServiceUnavailableException',\n 'ExpiredTokenException',\n 'ResourceNotFoundException',\n]);\n\nfunction awsErrorType(err: unknown): string | undefined {\n if (!(err instanceof HttpError)) return undefined;\n try {\n const type = (JSON.parse(err.body) as { __type?: string }).__type;\n return type?.split('#').pop();\n } catch {\n return undefined;\n }\n}\n\ninterface CwLogEvent {\n timestamp: number;\n message: string;\n}\n\n/**\n * AWS CloudWatch Logs transport with EMF support and batched PutLogEvents calls.\n */\nexport class CloudWatchTransport implements IAsyncTransport {\n public readonly name = 'cloudwatch';\n public readonly batchSize: number;\n public readonly flushInterval: number;\n public readonly level: string | undefined;\n\n private readonly buffer: BatchBuffer<CwLogEvent>;\n private readonly requestTimeoutMs: number;\n private flushTimer: NodeJS.Timeout | null = null;\n private readonly credentials: AwsCredentialProvider;\n private readonly endpoint: URL;\n private streamReady = false;\n\n private readonly region: string;\n private readonly logGroupName: string;\n private readonly logStreamName: string;\n private readonly emf: boolean;\n private readonly emfNamespace: string;\n\n constructor(config: CloudWatchTransportConfig) {\n this.region =\n config.region ??\n process.env['AWS_REGION'] ??\n process.env['AWS_DEFAULT_REGION'] ??\n 'us-east-1';\n this.logGroupName = config.logGroupName;\n this.logStreamName =\n config.logStreamName ?? `${process.env['HOSTNAME'] ?? 'node'}-${process.pid}`;\n this.credentials = new AwsCredentialProvider(config);\n this.endpoint = new URL(config.endpoint ?? `https://logs.${this.region}.amazonaws.com`);\n this.batchSize = Math.min(config.batchSize ?? 100, MAX_EVENTS_PER_PUT);\n this.emfNamespace = config.emfNamespace ?? 'Logixia';\n this.flushInterval = config.flushIntervalMs ?? 5000;\n this.emf = config.emf ?? false;\n this.level = config.level;\n this.requestTimeoutMs = config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;\n this.buffer = new BatchBuffer<CwLogEvent>({\n name: 'CloudWatchTransport',\n batchSize: this.batchSize,\n maxBufferSize: config.maxBufferSize ?? DEFAULT_MAX_BUFFER_SIZE,\n send: (events) => this.putLogEvents(events),\n shouldRetry: (err) =>\n !isPermanentHttpError(err) || RETRYABLE_TYPES.has(awsErrorType(err) ?? ''),\n });\n\n this.flushTimer = setInterval(() => {\n this.flush().catch(() => {});\n }, this.flushInterval);\n if (this.flushTimer.unref) this.flushTimer.unref();\n }\n\n write(entry: TransportLogEntry): void {\n const timestamp = entry.timestamp ? new Date(entry.timestamp).getTime() : Date.now();\n const message = this.emf ? this.toEmf(entry) : stringifyForLog(entry);\n this.buffer.push({ timestamp, message });\n this.buffer.flushIfFull();\n }\n\n flush(): Promise<void> {\n return this.buffer.flush();\n }\n\n /**\n * Flush remaining events and stop the interval timer on shutdown. Without this\n * the manager's close() skips the transport (close was undefined) and any\n * buffered events were lost on deploy. Waits for any in-flight send first.\n */\n async close(): Promise<void> {\n if (this.flushTimer) {\n clearInterval(this.flushTimer);\n this.flushTimer = null;\n }\n await this.buffer.close();\n }\n\n private toEmf(entry: TransportLogEntry): string {\n const { level, message, data: payload, timestamp } = entry;\n const numericFields: Record<string, number> = {};\n for (const [k, v] of Object.entries(payload ?? {})) {\n if (typeof v === 'number') numericFields[k] = v;\n }\n return stringifyForLog({\n _aws: {\n Timestamp: timestamp ? new Date(timestamp).getTime() : Date.now(),\n CloudWatchMetrics:\n Object.keys(numericFields).length > 0\n ? [\n {\n Namespace: this.emfNamespace,\n Dimensions: [['level']],\n Metrics: Object.keys(numericFields).map((n) => ({ Name: n, Unit: 'None' })),\n },\n ]\n : [],\n },\n level,\n message,\n ...payload,\n ...numericFields,\n });\n }\n\n private async putLogEvents(events: CwLogEvent[]): Promise<void> {\n await this.ensureStream();\n const logEvents = [...events].sort((a, b) => a.timestamp - b.timestamp);\n try {\n await this.call('PutLogEvents', {\n logGroupName: this.logGroupName,\n logStreamName: this.logStreamName,\n logEvents,\n });\n } catch (err) {\n const type = awsErrorType(err);\n if (type === 'DataAlreadyAcceptedException') return;\n if (type === 'ResourceNotFoundException') this.streamReady = false;\n if (type === 'ExpiredTokenException') this.credentials.invalidate();\n throw err;\n }\n }\n\n private async ensureStream(): Promise<void> {\n if (this.streamReady) return;\n try {\n await this.call('CreateLogGroup', { logGroupName: this.logGroupName });\n } catch (err) {\n if (awsErrorType(err) === 'ExpiredTokenException') this.credentials.invalidate();\n if (!(err instanceof HttpError)) throw err;\n if (awsErrorType(err) !== 'ResourceAlreadyExistsException') {\n internalWarn(`CloudWatchTransport: CreateLogGroup failed, continuing (${err.message})`);\n }\n }\n try {\n await this.call('CreateLogStream', {\n logGroupName: this.logGroupName,\n logStreamName: this.logStreamName,\n });\n } catch (err) {\n if (awsErrorType(err) !== 'ResourceAlreadyExistsException') throw err;\n }\n this.streamReady = true;\n }\n\n private async call(action: string, payload: Record<string, unknown>): Promise<void> {\n const credentials = await this.credentials.get();\n const body = stringifyForLog(payload);\n const { host } = this.endpoint;\n const headers = signV4({\n method: 'POST',\n path: '/',\n headers: {\n host,\n 'content-type': 'application/x-amz-json-1.1',\n 'x-amz-target': `Logs_20140328.${action}`,\n },\n body,\n region: this.region,\n service: 'logs',\n credentials,\n });\n await httpRequest({\n url: this.endpoint,\n headers,\n body,\n timeoutMs: this.requestTimeoutMs,\n label: `CloudWatch ${action}`,\n });\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
|
|
2
|
+
//#region src/utils/coerce.utils.ts
|
|
3
|
+
/**
|
|
4
|
+
* Defensive value coercion helpers for Logixia.
|
|
5
|
+
*
|
|
6
|
+
* Logger inputs come from third-party code (NestJS framework internals,
|
|
7
|
+
* user payloads, error objects) where the runtime type is not always what
|
|
8
|
+
* the TypeScript signature promises. The formatter / transport hot paths
|
|
9
|
+
* call `.replace()` on string fields and crash if the field is anything
|
|
10
|
+
* else. Use these helpers anywhere a downstream consumer assumes a string.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Coerce any value to a string without throwing.
|
|
14
|
+
*
|
|
15
|
+
* - `undefined` / `null` → `''` (so the field is rendered as empty rather
|
|
16
|
+
* than the literal string "undefined")
|
|
17
|
+
* - strings → returned as-is (zero allocation on the hot path)
|
|
18
|
+
* - Errors → `error.message` (matches what users would expect to see)
|
|
19
|
+
* - everything else → `JSON.stringify` if possible, falling back to
|
|
20
|
+
* `String(value)` for circular refs / values without a stringifier.
|
|
21
|
+
*/
|
|
22
|
+
function safeToString(value) {
|
|
23
|
+
var _constructor;
|
|
24
|
+
if (typeof value === "string") return value;
|
|
25
|
+
if (value === void 0 || value === null) return "";
|
|
26
|
+
if (value instanceof Error) return value.message;
|
|
27
|
+
if (typeof value === "number" || typeof value === "bigint" || typeof value === "boolean") return String(value);
|
|
28
|
+
if (typeof value === "symbol") return value.toString();
|
|
29
|
+
if (typeof value === "function") return `[Function: ${value.name || "anonymous"}]`;
|
|
30
|
+
try {
|
|
31
|
+
const json = JSON.stringify(value);
|
|
32
|
+
if (json !== void 0) return json;
|
|
33
|
+
} catch {}
|
|
34
|
+
return `[${((_constructor = value.constructor) === null || _constructor === void 0 ? void 0 : _constructor.name) ?? "object"}]`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
//#endregion
|
|
38
|
+
Object.defineProperty(exports, 'safeToString', {
|
|
39
|
+
enumerable: true,
|
|
40
|
+
get: function () {
|
|
41
|
+
return safeToString;
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
//# sourceMappingURL=coerce.utils-BN980Y9V.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"coerce.utils-BN980Y9V.js","names":[],"sources":["../src/utils/coerce.utils.ts"],"sourcesContent":["/**\n * Defensive value coercion helpers for Logixia.\n *\n * Logger inputs come from third-party code (NestJS framework internals,\n * user payloads, error objects) where the runtime type is not always what\n * the TypeScript signature promises. The formatter / transport hot paths\n * call `.replace()` on string fields and crash if the field is anything\n * else. Use these helpers anywhere a downstream consumer assumes a string.\n */\n\n/**\n * Coerce any value to a string without throwing.\n *\n * - `undefined` / `null` → `''` (so the field is rendered as empty rather\n * than the literal string \"undefined\")\n * - strings → returned as-is (zero allocation on the hot path)\n * - Errors → `error.message` (matches what users would expect to see)\n * - everything else → `JSON.stringify` if possible, falling back to\n * `String(value)` for circular refs / values without a stringifier.\n */\nexport function safeToString(value: unknown): string {\n if (typeof value === 'string') return value;\n if (value === undefined || value === null) return '';\n if (value instanceof Error) return value.message;\n if (typeof value === 'number' || typeof value === 'bigint' || typeof value === 'boolean') {\n return String(value);\n }\n if (typeof value === 'symbol') return value.toString();\n if (typeof value === 'function') {\n return `[Function: ${(value as { name?: string }).name || 'anonymous'}]`;\n }\n // value is now narrowed to object — JSON-stringify, falling back to a\n // constructor-name tag for circular refs / throwing toJSON.\n try {\n const json = JSON.stringify(value);\n // JSON.stringify returns undefined for values it can't represent (e.g. an\n // object whose toJSON() returns undefined). Returning that would defeat the\n // whole point of this helper — a downstream .replace() on `undefined` throws.\n if (json !== undefined) return json;\n } catch {\n /* fall through to the constructor-name tag below */\n }\n const ctor = (value as { constructor?: { name?: string } }).constructor?.name;\n return `[${ctor ?? 'object'}]`;\n}\n\n/**\n * String-replace on a value that may not be a string.\n * Coerces non-strings before delegating to `String.prototype.replace`.\n */\nexport function safeReplace(value: unknown, pattern: string | RegExp, replacement: string): string {\n return safeToString(value).replace(pattern, replacement);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAoBA,SAAgB,aAAa,OAAwB;;AACnD,KAAI,OAAO,UAAU,SAAU,QAAO;AACtC,KAAI,UAAU,UAAa,UAAU,KAAM,QAAO;AAClD,KAAI,iBAAiB,MAAO,QAAO,MAAM;AACzC,KAAI,OAAO,UAAU,YAAY,OAAO,UAAU,YAAY,OAAO,UAAU,UAC7E,QAAO,OAAO,MAAM;AAEtB,KAAI,OAAO,UAAU,SAAU,QAAO,MAAM,UAAU;AACtD,KAAI,OAAO,UAAU,WACnB,QAAO,cAAe,MAA4B,QAAQ,YAAY;AAIxE,KAAI;EACF,MAAM,OAAO,KAAK,UAAU,MAAM;AAIlC,MAAI,SAAS,OAAW,QAAO;SACzB;AAIR,QAAO,qBADO,MAA8C,yEAAa,SACtD,SAAS"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
//#region src/utils/coerce.utils.ts
|
|
2
|
+
/**
|
|
3
|
+
* Defensive value coercion helpers for Logixia.
|
|
4
|
+
*
|
|
5
|
+
* Logger inputs come from third-party code (NestJS framework internals,
|
|
6
|
+
* user payloads, error objects) where the runtime type is not always what
|
|
7
|
+
* the TypeScript signature promises. The formatter / transport hot paths
|
|
8
|
+
* call `.replace()` on string fields and crash if the field is anything
|
|
9
|
+
* else. Use these helpers anywhere a downstream consumer assumes a string.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Coerce any value to a string without throwing.
|
|
13
|
+
*
|
|
14
|
+
* - `undefined` / `null` → `''` (so the field is rendered as empty rather
|
|
15
|
+
* than the literal string "undefined")
|
|
16
|
+
* - strings → returned as-is (zero allocation on the hot path)
|
|
17
|
+
* - Errors → `error.message` (matches what users would expect to see)
|
|
18
|
+
* - everything else → `JSON.stringify` if possible, falling back to
|
|
19
|
+
* `String(value)` for circular refs / values without a stringifier.
|
|
20
|
+
*/
|
|
21
|
+
function safeToString(value) {
|
|
22
|
+
var _constructor;
|
|
23
|
+
if (typeof value === "string") return value;
|
|
24
|
+
if (value === void 0 || value === null) return "";
|
|
25
|
+
if (value instanceof Error) return value.message;
|
|
26
|
+
if (typeof value === "number" || typeof value === "bigint" || typeof value === "boolean") return String(value);
|
|
27
|
+
if (typeof value === "symbol") return value.toString();
|
|
28
|
+
if (typeof value === "function") return `[Function: ${value.name || "anonymous"}]`;
|
|
29
|
+
try {
|
|
30
|
+
const json = JSON.stringify(value);
|
|
31
|
+
if (json !== void 0) return json;
|
|
32
|
+
} catch {}
|
|
33
|
+
return `[${((_constructor = value.constructor) === null || _constructor === void 0 ? void 0 : _constructor.name) ?? "object"}]`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
//#endregion
|
|
37
|
+
export { safeToString as t };
|
|
38
|
+
//# sourceMappingURL=coerce.utils-Dxv4_ikC.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"coerce.utils-Dxv4_ikC.mjs","names":[],"sources":["../src/utils/coerce.utils.ts"],"sourcesContent":["/**\n * Defensive value coercion helpers for Logixia.\n *\n * Logger inputs come from third-party code (NestJS framework internals,\n * user payloads, error objects) where the runtime type is not always what\n * the TypeScript signature promises. The formatter / transport hot paths\n * call `.replace()` on string fields and crash if the field is anything\n * else. Use these helpers anywhere a downstream consumer assumes a string.\n */\n\n/**\n * Coerce any value to a string without throwing.\n *\n * - `undefined` / `null` → `''` (so the field is rendered as empty rather\n * than the literal string \"undefined\")\n * - strings → returned as-is (zero allocation on the hot path)\n * - Errors → `error.message` (matches what users would expect to see)\n * - everything else → `JSON.stringify` if possible, falling back to\n * `String(value)` for circular refs / values without a stringifier.\n */\nexport function safeToString(value: unknown): string {\n if (typeof value === 'string') return value;\n if (value === undefined || value === null) return '';\n if (value instanceof Error) return value.message;\n if (typeof value === 'number' || typeof value === 'bigint' || typeof value === 'boolean') {\n return String(value);\n }\n if (typeof value === 'symbol') return value.toString();\n if (typeof value === 'function') {\n return `[Function: ${(value as { name?: string }).name || 'anonymous'}]`;\n }\n // value is now narrowed to object — JSON-stringify, falling back to a\n // constructor-name tag for circular refs / throwing toJSON.\n try {\n const json = JSON.stringify(value);\n // JSON.stringify returns undefined for values it can't represent (e.g. an\n // object whose toJSON() returns undefined). Returning that would defeat the\n // whole point of this helper — a downstream .replace() on `undefined` throws.\n if (json !== undefined) return json;\n } catch {\n /* fall through to the constructor-name tag below */\n }\n const ctor = (value as { constructor?: { name?: string } }).constructor?.name;\n return `[${ctor ?? 'object'}]`;\n}\n\n/**\n * String-replace on a value that may not be a string.\n * Coerces non-strings before delegating to `String.prototype.replace`.\n */\nexport function safeReplace(value: unknown, pattern: string | RegExp, replacement: string): string {\n return safeToString(value).replace(pattern, replacement);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAoBA,SAAgB,aAAa,OAAwB;;AACnD,KAAI,OAAO,UAAU,SAAU,QAAO;AACtC,KAAI,UAAU,UAAa,UAAU,KAAM,QAAO;AAClD,KAAI,iBAAiB,MAAO,QAAO,MAAM;AACzC,KAAI,OAAO,UAAU,YAAY,OAAO,UAAU,YAAY,OAAO,UAAU,UAC7E,QAAO,OAAO,MAAM;AAEtB,KAAI,OAAO,UAAU,SAAU,QAAO,MAAM,UAAU;AACtD,KAAI,OAAO,UAAU,WACnB,QAAO,cAAe,MAA4B,QAAQ,YAAY;AAIxE,KAAI;EACF,MAAM,OAAO,KAAK,UAAU,MAAM;AAIlC,MAAI,SAAS,OAAW,QAAO;SACzB;AAIR,QAAO,qBADO,MAA8C,yEAAa,SACtD,SAAS"}
|