Skip to content

Commit 8cd55db

Browse files
committed
Implement heartbeats in Node.js
Implement heartbeats with a conceptually similar implementation to that of the [Ruby][ruby] and [Elixir][elixir] integrations. Note, however, that unlike the Ruby and Elixir implementations, the heartbeats are sent asynchronously, in a "fire and forget" manner, without blocking the execution of the heartbeat-wrapped function.
1 parent 3020fd8 commit 8cd55db

File tree

3 files changed

+109
-0
lines changed

3 files changed

+109
-0
lines changed

.changesets/add-heartbeats-support.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
bump: "patch"
3+
type: "add"
4+
---
5+
6+
Add heartbeats support. You can send heartbeats directly from your code, to
7+
track the execution of certain processes:
8+
9+
```javascript
10+
import { heartbeat } from "@appsignal/nodejs"
11+
12+
function sendInvoices() {
13+
// ... your code here ...
14+
heartbeat("send_invoices")
15+
}
16+
```
17+
18+
You can pass a function to `heartbeat`, to report to AppSignal both when the
19+
process starts, and when it finishes, allowing you to see the duration of the
20+
process:
21+
22+
```javascript
23+
import { heartbeat } from "@appsignal/nodejs"
24+
25+
function sendInvoices() {
26+
heartbeat("send_invoices", () => {
27+
// ... your code here ...
28+
})
29+
}
30+
```
31+
32+
If an exception is raised within the function, the finish event will not be
33+
reported to AppSignal, triggering a notification about the missing heartbeat.
34+
The exception will bubble outside of the heartbeat function.

src/heartbeat.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import crypto from "crypto"
2+
import { Transmitter } from "./transmitter"
3+
import { Client } from "./client"
4+
5+
export type EventKind = "start" | "finish"
6+
7+
export type Event = {
8+
name: string
9+
id: string
10+
kind: EventKind
11+
timestamp: number
12+
}
13+
14+
export class Heartbeat {
15+
name: string
16+
id: string
17+
18+
constructor(name: string) {
19+
this.name = name
20+
this.id = crypto.randomBytes(8).toString("hex")
21+
}
22+
23+
public start() {
24+
this.transmit(this.event("start"))
25+
}
26+
27+
public finish() {
28+
this.transmit(this.event("finish"))
29+
}
30+
31+
private event(kind: EventKind): Event {
32+
return {
33+
name: this.name,
34+
id: this.id,
35+
kind: kind,
36+
timestamp: Date.now()
37+
}
38+
}
39+
40+
private transmit(event: Event) {
41+
if (Client.client === undefined || !Client.client.isActive) {
42+
Client.internalLogger.debug(
43+
"AppSignal not started; not sending heartbeat"
44+
)
45+
return
46+
}
47+
48+
new Transmitter(
49+
`${Client.config.data.loggingEndpoint}/heartbeats/json`,
50+
JSON.stringify(event)
51+
).transmit().then(({status}: {status: number}) => {
52+
if (status !== 200) {
53+
Client.internalLogger.warn(`Failed to transmit heartbeat: status code ${status}`)
54+
}
55+
}).catch(({error}: {error: Error}) => {
56+
Client.internalLogger.warn(`Failed to transmit heartbeat: ${error.message}`)
57+
})
58+
}
59+
}
60+
61+
export function heartbeat(name: string): void
62+
export function heartbeat<T>(name: string, fn: () => T): T
63+
export function heartbeat<T>(name: string, fn?: () => T): T | undefined {
64+
const heartbeat = new Heartbeat(name)
65+
let output
66+
67+
if (fn) {
68+
heartbeat.start()
69+
output = fn()
70+
}
71+
72+
heartbeat.finish()
73+
return output
74+
}

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ export { expressErrorHandler } from "./instrumentation/express/error_handler"
1111
export { LoggerLevel, LoggerAttributes } from "./logger"
1212
export { WinstonTransport } from "./winston_transport"
1313
export * from "./helpers"
14+
export { Heartbeat, heartbeat } from "./heartbeat"

0 commit comments

Comments
 (0)