@forge-ops/tracker 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -4
- package/package.json +1 -1
- package/src/index.js +43 -2
package/README.md
CHANGED
|
@@ -539,10 +539,39 @@ app.use(forgeOpsTrackerExpressMiddleware);
|
|
|
539
539
|
app.listen(3000);
|
|
540
540
|
```
|
|
541
541
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
542
|
+
### Database spans with their SQL
|
|
543
|
+
|
|
544
|
+
Database queries aren't captured automatically (this SDK has no query or ORM instrumentation hook
|
|
545
|
+
to extend), so wrap a query by hand with `kind: "database"` to have it show up as its own span. Pass
|
|
546
|
+
the SQL as `statement` and ForgeOps shows which query a slow request spent its time in:
|
|
547
|
+
|
|
548
|
+
```js
|
|
549
|
+
import pg from "pg";
|
|
550
|
+
import * as forgeOpsTracker from "@forge-ops/tracker";
|
|
551
|
+
|
|
552
|
+
const pool = new pg.Pool();
|
|
553
|
+
|
|
554
|
+
app.get("/orders", async (req, res) => {
|
|
555
|
+
const sql = "SELECT id, total FROM orders WHERE customer_id = $1 AND status = 'open'";
|
|
556
|
+
const { rows } = await forgeOpsTracker.span("Load open orders", () => pool.query(sql, [req.user.id]), {
|
|
557
|
+
kind: "database",
|
|
558
|
+
statement: sql,
|
|
559
|
+
dbSystem: "postgresql",
|
|
560
|
+
});
|
|
561
|
+
res.json(rows);
|
|
562
|
+
});
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
The statement is masked before it's stored on the span: every string and number becomes `?`, so the
|
|
566
|
+
span above carries `SELECT id, total FROM orders WHERE customer_id = $1 AND status = ?`, and
|
|
567
|
+
ForgeOps masks it again on arrival. It's cut to 4000 characters. Bind values (the `[req.user.id]`
|
|
568
|
+
array) are never read or sent. `dbSystem` is optional (`"postgresql"`, `"mysql"`, `"sqlite"`,
|
|
569
|
+
`"mssql"`, `"oracle"`, or any other lowercase name) and both options are ignored on spans of any
|
|
570
|
+
other kind. They go out in the span's data as `db.statement` and `db.system`.
|
|
571
|
+
|
|
572
|
+
**Known gaps:** no automatic database span capture (see above) and no Redis span capture (no
|
|
573
|
+
existing Redis hook or dependency exists here either); a Redis call inside a traced request just
|
|
574
|
+
won't show up as its own span for now.
|
|
546
575
|
|
|
547
576
|
## Database errors
|
|
548
577
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forge-ops/tracker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "ForgeOps error tracking client: captures unhandled exceptions (Express/Fastify integration, plus explicit capture anywhere else) and delivers them to ForgeOps over HTTP.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
package/src/index.js
CHANGED
|
@@ -12,6 +12,7 @@ import { Reporter } from "./reporter.js";
|
|
|
12
12
|
import { requestStateFor } from "./requestState.js";
|
|
13
13
|
import { SessionFlusher } from "./sessionFlusher.js";
|
|
14
14
|
import { randomSpanId, SpanBuffer } from "./spanBuffer.js";
|
|
15
|
+
import { mask as maskSql } from "./sqlStatement.js";
|
|
15
16
|
import { buildTraceparent } from "./traceParent.js";
|
|
16
17
|
|
|
17
18
|
export { Configuration };
|
|
@@ -610,17 +611,30 @@ export function runWithUser(user, callback) {
|
|
|
610
611
|
* request-level trace around it, and no middleware left running to ever flush it, would just
|
|
611
612
|
* accumulate in memory forever with nothing to ever send it, worse than not recording it at all.
|
|
612
613
|
*
|
|
614
|
+
* A "database" span can also carry the SQL it ran, so ForgeOps can show which query was slow:
|
|
615
|
+
*
|
|
616
|
+
* await forgeOpsTracker.span("Load orders", () => pool.query(sql, [userId]), {
|
|
617
|
+
* kind: "database",
|
|
618
|
+
* statement: sql,
|
|
619
|
+
* dbSystem: "postgresql",
|
|
620
|
+
* });
|
|
621
|
+
*
|
|
622
|
+
* The statement is masked (every string and number replaced by "?") before it's ever stored on
|
|
623
|
+
* the span, and sent as data["db.statement"], with dbSystem lowercased as data["db.system"]. Bind
|
|
624
|
+
* values are never taken, and both options are ignored on any other kind of span.
|
|
625
|
+
*
|
|
613
626
|
* @template T
|
|
614
627
|
* @param {string} name
|
|
615
628
|
* @param {() => T} callback
|
|
616
|
-
* @param {{ kind?: string, data?: Record<string, unknown
|
|
629
|
+
* @param {{ kind?: string, data?: Record<string, unknown>, statement?: string, dbSystem?: string }} [options]
|
|
617
630
|
* @returns {T}
|
|
618
631
|
*/
|
|
619
|
-
export function span(name, callback, { kind = "service", data = {} } = {}) {
|
|
632
|
+
export function span(name, callback, { kind = "service", data = {}, statement, dbSystem } = {}) {
|
|
620
633
|
const buffer = spanStorage.getStore();
|
|
621
634
|
if (!buffer) {
|
|
622
635
|
return callback();
|
|
623
636
|
}
|
|
637
|
+
data = databaseSpanData(kind, data, statement, dbSystem);
|
|
624
638
|
|
|
625
639
|
const spanId = randomSpanId();
|
|
626
640
|
const parentSpanId = spanParentStorage.getStore() ?? buffer.rootSpanId;
|
|
@@ -652,6 +666,33 @@ export function span(name, callback, { kind = "service", data = {} } = {}) {
|
|
|
652
666
|
}
|
|
653
667
|
}
|
|
654
668
|
|
|
669
|
+
/**
|
|
670
|
+
* The data a span is recorded with: unchanged unless it's a database span, which gets the masked
|
|
671
|
+
* statement as "db.statement" and the lowercased database name as "db.system". A "db.statement"
|
|
672
|
+
* passed in `data` directly is masked as well, so the raw SQL can't reach the payload either way.
|
|
673
|
+
* @param {string} kind
|
|
674
|
+
* @param {Record<string, unknown>} data
|
|
675
|
+
* @param {string | undefined} statement
|
|
676
|
+
* @param {string | undefined} dbSystem
|
|
677
|
+
* @returns {Record<string, unknown>}
|
|
678
|
+
*/
|
|
679
|
+
function databaseSpanData(kind, data, statement, dbSystem) {
|
|
680
|
+
if (kind !== "database") {
|
|
681
|
+
return data;
|
|
682
|
+
}
|
|
683
|
+
const result = { ...data };
|
|
684
|
+
const masked = maskSql(typeof statement === "string" ? statement : result["db.statement"]);
|
|
685
|
+
if (masked === null) {
|
|
686
|
+
delete result["db.statement"];
|
|
687
|
+
} else {
|
|
688
|
+
result["db.statement"] = masked;
|
|
689
|
+
}
|
|
690
|
+
if (typeof dbSystem === "string" && dbSystem.trim() !== "") {
|
|
691
|
+
result["db.system"] = dbSystem.trim().toLowerCase();
|
|
692
|
+
}
|
|
693
|
+
return result;
|
|
694
|
+
}
|
|
695
|
+
|
|
655
696
|
/**
|
|
656
697
|
* Reports anything that would otherwise crash the process outright (a
|
|
657
698
|
* plain script, an unhandled promise rejection) with no further wiring:
|