serverless-ircd 0.1.0 → 0.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.
Files changed (198) hide show
  1. package/.github/workflows/ci.yml +96 -2
  2. package/.github/workflows/deploy-aws.yml +129 -0
  3. package/.github/workflows/deploy-cf.yml +0 -2
  4. package/.gitmodules +3 -0
  5. package/AGENTS.md +5 -0
  6. package/CHANGELOG.md +352 -0
  7. package/MOTD.txt +3 -0
  8. package/PLAN-FIXES.md +358 -0
  9. package/PLAN.md +420 -0
  10. package/README.md +115 -81
  11. package/apps/aws-stack/README.md +73 -0
  12. package/apps/aws-stack/bin/aws.ts +49 -0
  13. package/apps/aws-stack/cdk.json +10 -0
  14. package/apps/aws-stack/package.json +41 -0
  15. package/apps/aws-stack/scripts/smoke-helpers.d.mts +22 -0
  16. package/apps/aws-stack/scripts/smoke-helpers.mjs +89 -0
  17. package/apps/aws-stack/scripts/smoke.mjs +142 -0
  18. package/apps/aws-stack/src/aws-stack.ts +263 -0
  19. package/apps/aws-stack/src/tables.ts +10 -0
  20. package/apps/aws-stack/tests/localstack.test.ts +46 -0
  21. package/apps/aws-stack/tests/smoke-helpers.test.ts +98 -0
  22. package/apps/aws-stack/tests/stack.test.ts +464 -0
  23. package/apps/aws-stack/tsconfig.build.json +11 -0
  24. package/apps/aws-stack/tsconfig.test.json +8 -0
  25. package/apps/aws-stack/vitest.config.ts +20 -0
  26. package/apps/cf-worker/package.json +1 -1
  27. package/apps/cf-worker/wrangler.toml +25 -0
  28. package/apps/local-cli/package.json +1 -1
  29. package/apps/local-cli/src/config-loader.ts +56 -0
  30. package/apps/local-cli/src/server.ts +241 -32
  31. package/apps/local-cli/tests/config-loader.test.ts +107 -0
  32. package/apps/local-cli/tests/e2e.test.ts +74 -0
  33. package/apps/local-cli/tests/security-e2e.test.ts +239 -0
  34. package/biome.json +3 -1
  35. package/dashboards/cloudwatch-irc.json +139 -0
  36. package/docs/AWS-Adapter-Architecture.md +494 -0
  37. package/docs/AWS-Deployment.md +1107 -0
  38. package/docs/Cloudflare-Deployment-Guide.md +660 -0
  39. package/docs/Home.md +1 -0
  40. package/docs/Observability.md +87 -0
  41. package/docs/PlanIRCv3Websocket.md +489 -0
  42. package/docs/PlanWebClient.md +451 -0
  43. package/docs/Release-Process.md +440 -0
  44. package/package.json +8 -2
  45. package/packages/aws-adapter/README.md +35 -0
  46. package/packages/aws-adapter/package.json +52 -0
  47. package/packages/aws-adapter/src/aws-runtime.ts +783 -0
  48. package/packages/aws-adapter/src/cdk-table-defs.ts +69 -0
  49. package/packages/aws-adapter/src/config-loader.ts +96 -0
  50. package/packages/aws-adapter/src/dynamo.ts +61 -0
  51. package/packages/aws-adapter/src/handlers/connect.ts +44 -0
  52. package/packages/aws-adapter/src/handlers/default.ts +322 -0
  53. package/packages/aws-adapter/src/handlers/disconnect.ts +29 -0
  54. package/packages/aws-adapter/src/handlers/index.ts +248 -0
  55. package/packages/aws-adapter/src/handlers/ping-checker.ts +217 -0
  56. package/packages/aws-adapter/src/handlers/state.ts +13 -0
  57. package/packages/aws-adapter/src/handlers/sweeper.ts +84 -0
  58. package/packages/aws-adapter/src/index.ts +67 -0
  59. package/packages/aws-adapter/src/message-store.ts +34 -0
  60. package/packages/aws-adapter/src/serialize.ts +283 -0
  61. package/packages/aws-adapter/src/tables.ts +53 -0
  62. package/packages/aws-adapter/tests/aws-harness.ts +408 -0
  63. package/packages/aws-adapter/tests/aws-integration.test.ts +17 -0
  64. package/packages/aws-adapter/tests/aws-runtime.test.ts +654 -0
  65. package/packages/aws-adapter/tests/config-loader.test.ts +158 -0
  66. package/packages/aws-adapter/tests/dynamo.test.ts +57 -0
  67. package/packages/aws-adapter/tests/global-setup.ts +301 -0
  68. package/packages/aws-adapter/tests/gone-exception.test.ts +271 -0
  69. package/packages/aws-adapter/tests/handlers.test.ts +427 -0
  70. package/packages/aws-adapter/tests/message-store.test.ts +55 -0
  71. package/packages/aws-adapter/tests/ping-checker.test.ts +571 -0
  72. package/packages/aws-adapter/tests/serialize.test.ts +249 -0
  73. package/packages/aws-adapter/tests/smoke.test.ts +48 -0
  74. package/packages/aws-adapter/tests/sweeper.test.ts +420 -0
  75. package/packages/aws-adapter/tests/tables.test.ts +74 -0
  76. package/packages/aws-adapter/tests/transactions.test.ts +446 -0
  77. package/packages/aws-adapter/tsconfig.build.json +16 -0
  78. package/packages/aws-adapter/tsconfig.test.json +14 -0
  79. package/packages/aws-adapter/vitest.config.ts +23 -0
  80. package/packages/cf-adapter/package.json +2 -1
  81. package/packages/cf-adapter/src/config-loader.ts +106 -0
  82. package/packages/cf-adapter/src/connection-do.ts +34 -0
  83. package/packages/cf-adapter/tests/cf-harness.ts +2 -2
  84. package/packages/cf-adapter/tests/cf-integration.test.ts +2 -2
  85. package/packages/cf-adapter/tests/config-loader.test.ts +149 -0
  86. package/packages/cf-adapter/tests/connection-do.test.ts +42 -0
  87. package/packages/in-memory-runtime/package.json +1 -1
  88. package/packages/in-memory-runtime/src/in-memory-runtime.ts +108 -3
  89. package/packages/in-memory-runtime/src/index.ts +1 -1
  90. package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +115 -0
  91. package/packages/irc-core/package.json +12 -2
  92. package/packages/irc-core/src/admission.ts +216 -0
  93. package/packages/irc-core/src/caps/capabilities.ts +1 -0
  94. package/packages/irc-core/src/cloak.ts +81 -0
  95. package/packages/irc-core/src/commands/cap.ts +1 -2
  96. package/packages/irc-core/src/commands/chathistory.ts +305 -0
  97. package/packages/irc-core/src/commands/index.ts +7 -0
  98. package/packages/irc-core/src/commands/join.ts +59 -7
  99. package/packages/irc-core/src/commands/privmsg.ts +24 -0
  100. package/packages/irc-core/src/commands/registration.ts +71 -14
  101. package/packages/irc-core/src/commands/sasl.ts +251 -0
  102. package/packages/irc-core/src/config.ts +247 -0
  103. package/packages/irc-core/src/flood-control.ts +175 -0
  104. package/packages/irc-core/src/index.ts +5 -0
  105. package/packages/irc-core/src/ports.ts +655 -0
  106. package/packages/irc-core/src/protocol/base64.ts +16 -0
  107. package/packages/irc-core/src/protocol/index.ts +2 -1
  108. package/packages/irc-core/src/protocol/numerics.ts +11 -0
  109. package/packages/irc-core/src/protocol/parser.ts +27 -2
  110. package/packages/irc-core/src/state/connection.ts +41 -0
  111. package/packages/irc-core/src/types.ts +66 -2
  112. package/packages/irc-core/stryker.commands.conf.json +41 -0
  113. package/packages/irc-core/stryker.protocol.conf.json +26 -0
  114. package/packages/irc-core/tests/admission.test.ts +229 -0
  115. package/packages/irc-core/tests/caps/capabilities.test.ts +22 -0
  116. package/packages/irc-core/tests/cloak.test.ts +78 -0
  117. package/packages/irc-core/tests/commands/chathistory.test.ts +996 -0
  118. package/packages/irc-core/tests/commands/join.test.ts +246 -2
  119. package/packages/irc-core/tests/commands/privmsg.test.ts +146 -2
  120. package/packages/irc-core/tests/commands/registration.test.ts +277 -9
  121. package/packages/irc-core/tests/commands/sasl.test.ts +536 -0
  122. package/packages/irc-core/tests/config.test.ts +646 -0
  123. package/packages/irc-core/tests/flood-control.test.ts +394 -0
  124. package/packages/irc-core/tests/message-store.test.ts +530 -0
  125. package/packages/irc-core/tests/parser.test.ts +44 -1
  126. package/packages/irc-core/tests/ports.test.ts +263 -0
  127. package/packages/irc-server/package.json +1 -1
  128. package/packages/irc-server/src/actor.ts +162 -44
  129. package/packages/irc-server/src/dispatch.ts +89 -5
  130. package/packages/irc-server/src/index.ts +2 -0
  131. package/packages/irc-server/src/routing.ts +151 -0
  132. package/packages/irc-server/tests/actor.test.ts +470 -14
  133. package/packages/irc-server/tests/dispatch.test.ts +84 -0
  134. package/packages/irc-server/tests/routing.test.ts +201 -0
  135. package/packages/irc-test-support/README.md +32 -0
  136. package/packages/irc-test-support/package.json +37 -0
  137. package/packages/{cf-adapter/tests/integration → irc-test-support/src}/in-memory-harness.ts +1 -1
  138. package/packages/irc-test-support/src/index.ts +24 -0
  139. package/packages/{cf-adapter/tests/integration/harness.test.ts → irc-test-support/tests/in-memory-harness.test.ts} +1 -1
  140. package/packages/{cf-adapter/tests/integration/scenarios.test.ts → irc-test-support/tests/in-memory-scenarios.test.ts} +1 -2
  141. package/packages/irc-test-support/tests/smoke.test.ts +11 -0
  142. package/packages/irc-test-support/tsconfig.build.json +16 -0
  143. package/packages/irc-test-support/tsconfig.test.json +14 -0
  144. package/packages/irc-test-support/vitest.config.ts +20 -0
  145. package/progress.md +107 -0
  146. package/tickets.md +2485 -0
  147. package/tools/ci-hardening/package.json +30 -0
  148. package/tools/ci-hardening/src/index.ts +6 -0
  149. package/tools/ci-hardening/src/validate.ts +103 -0
  150. package/tools/ci-hardening/tests/__no_thresholds__.txt +11 -0
  151. package/tools/ci-hardening/tests/__partial_thresholds__.txt +16 -0
  152. package/tools/ci-hardening/tests/__stryker_bad__.json +4 -0
  153. package/tools/ci-hardening/tests/validate.test.ts +177 -0
  154. package/tools/ci-hardening/tsconfig.build.json +12 -0
  155. package/tools/ci-hardening/tsconfig.test.json +10 -0
  156. package/tools/ci-hardening/vitest.config.ts +21 -0
  157. package/tools/tcp-ws-forwarder/package.json +1 -1
  158. package/tools/tcp-ws-forwarder/src/forwarder.ts +34 -23
  159. package/tools/tcp-ws-forwarder/src/logger.ts +155 -0
  160. package/tools/tcp-ws-forwarder/src/main.ts +33 -14
  161. package/tools/tcp-ws-forwarder/tests/forwarder.test.ts +86 -0
  162. package/tools/tcp-ws-forwarder/tests/logger.test.ts +136 -0
  163. package/webircgateway/LICENSE +201 -0
  164. package/webircgateway/Makefile +44 -0
  165. package/webircgateway/README.md +134 -0
  166. package/webircgateway/config.conf.example +135 -0
  167. package/webircgateway/go.mod +16 -0
  168. package/webircgateway/go.sum +89 -0
  169. package/webircgateway/main.go +118 -0
  170. package/webircgateway/pkg/dnsbl/dnsbl.go +121 -0
  171. package/webircgateway/pkg/identd/identd.go +86 -0
  172. package/webircgateway/pkg/identd/rpcclient.go +59 -0
  173. package/webircgateway/pkg/irc/isupport.go +56 -0
  174. package/webircgateway/pkg/irc/message.go +217 -0
  175. package/webircgateway/pkg/irc/state.go +79 -0
  176. package/webircgateway/pkg/proxy/proxy.go +129 -0
  177. package/webircgateway/pkg/proxy/server.go +237 -0
  178. package/webircgateway/pkg/recaptcha/recaptcha.go +59 -0
  179. package/webircgateway/pkg/webircgateway/client.go +741 -0
  180. package/webircgateway/pkg/webircgateway/client_command_handlers.go +495 -0
  181. package/webircgateway/pkg/webircgateway/config.go +385 -0
  182. package/webircgateway/pkg/webircgateway/gateway.go +278 -0
  183. package/webircgateway/pkg/webircgateway/gateway_utils.go +133 -0
  184. package/webircgateway/pkg/webircgateway/hooks.go +152 -0
  185. package/webircgateway/pkg/webircgateway/letsencrypt.go +41 -0
  186. package/webircgateway/pkg/webircgateway/messagetags.go +103 -0
  187. package/webircgateway/pkg/webircgateway/transport_kiwiirc.go +206 -0
  188. package/webircgateway/pkg/webircgateway/transport_sockjs.go +107 -0
  189. package/webircgateway/pkg/webircgateway/transport_tcp.go +113 -0
  190. package/webircgateway/pkg/webircgateway/transport_websocket.go +126 -0
  191. package/webircgateway/pkg/webircgateway/utils.go +147 -0
  192. package/webircgateway/plugins/example/plugin.go +11 -0
  193. package/webircgateway/plugins/stats/plugin.go +52 -0
  194. package/webircgateway/staticcheck.conf +1 -0
  195. package/webircgateway/webircgateway.svg +3 -0
  196. package/packages/cf-adapter/tests/integration/index.ts +0 -17
  197. /package/packages/{cf-adapter/tests/integration → irc-test-support/src}/harness.ts +0 -0
  198. /package/packages/{cf-adapter/tests/integration → irc-test-support/src}/scenarios.ts +0 -0
@@ -0,0 +1,239 @@
1
+ /**
2
+ * End-to-end tests for the production-hardening security features:
3
+ * server password enforcement, hostmask cloaking, and
4
+ * the connection-admission gate.
5
+ *
6
+ * These tests boot a real `startLocalServer` with the relevant config
7
+ * knobs and drive it over WebSocket — the same path a real IRC client
8
+ * takes. The unit-test coverage for each limit lives next to the
9
+ * implementation; this file verifies the wiring through the local CLI.
10
+ */
11
+
12
+ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
13
+ import { WebSocket } from 'ws';
14
+ import { type LocalServer, startLocalServer } from '../src/server';
15
+
16
+ class TestClient {
17
+ readonly ws: WebSocket;
18
+ readonly received: string[] = [];
19
+
20
+ constructor(url: string) {
21
+ this.ws = new WebSocket(url);
22
+ this.ws.on('message', (data) => {
23
+ const text = data.toString('utf8');
24
+ for (const line of text.split(/\r?\n/u)) {
25
+ if (line.length > 0) this.received.push(line);
26
+ }
27
+ });
28
+ }
29
+
30
+ opened(): Promise<void> {
31
+ return new Promise((resolve, reject) => {
32
+ if (this.ws.readyState === WebSocket.OPEN) resolve();
33
+ else if (this.ws.readyState === WebSocket.CLOSED)
34
+ reject(new Error('socket closed before open'));
35
+ else {
36
+ this.ws.once('open', () => resolve());
37
+ this.ws.once('error', reject);
38
+ this.ws.once('close', () => reject(new Error('socket closed before open')));
39
+ }
40
+ });
41
+ }
42
+
43
+ /** Resolves once the underlying socket closes. */
44
+ closed(): Promise<void> {
45
+ return new Promise((resolve) => {
46
+ if (this.ws.readyState === WebSocket.CLOSED) resolve();
47
+ else this.ws.once('close', () => resolve());
48
+ });
49
+ }
50
+
51
+ send(line: string): Promise<void> {
52
+ return new Promise((resolve, reject) => {
53
+ this.ws.send(`${line}\r\n`, (err) => (err ? reject(err) : resolve()));
54
+ });
55
+ }
56
+
57
+ waitFor(predicate: (line: string) => boolean, timeoutMs = 1_000): Promise<string> {
58
+ return new Promise((resolve, reject) => {
59
+ const timeout = setTimeout(() => {
60
+ reject(new Error(`timeout; received: ${JSON.stringify(this.received)}`));
61
+ }, timeoutMs);
62
+ const check = (line: string) => {
63
+ if (predicate(line)) {
64
+ clearTimeout(timeout);
65
+ this.ws.off('message', onMessage);
66
+ resolve(line);
67
+ }
68
+ };
69
+ const onMessage = (data: { toString: (enc: string) => string }): void => {
70
+ const text = data.toString('utf8');
71
+ for (const line of text.split(/\r?\n/u)) {
72
+ if (line.length > 0) check(line);
73
+ }
74
+ };
75
+ for (const line of this.received) {
76
+ if (predicate(line)) {
77
+ clearTimeout(timeout);
78
+ resolve(line);
79
+ return;
80
+ }
81
+ }
82
+ this.ws.on('message', onMessage);
83
+ });
84
+ }
85
+
86
+ close(): Promise<void> {
87
+ return new Promise((resolve) => {
88
+ this.ws.once('close', () => resolve());
89
+ this.ws.close();
90
+ });
91
+ }
92
+ }
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // Server password
96
+ // ---------------------------------------------------------------------------
97
+
98
+ describe('local-cli e2e — server password', () => {
99
+ let server: LocalServer;
100
+
101
+ beforeAll(async () => {
102
+ server = await startLocalServer({
103
+ port: 0,
104
+ hostname: '127.0.0.1',
105
+ serverName: 'pwd.example.com',
106
+ networkName: 'PwdNet',
107
+ serverPassword: 's3cret',
108
+ });
109
+ });
110
+
111
+ afterAll(async () => {
112
+ await server.close();
113
+ });
114
+
115
+ it('rejects a client that registers without the password', async () => {
116
+ const client = new TestClient(`ws://127.0.0.1:${server.port}/`);
117
+ await client.opened();
118
+ await client.send('NICK alice');
119
+ await client.send('USER alice 0 * :Alice');
120
+ // 464 ERR_PASSWDMISMATCH followed by ERROR :Closing link.
121
+ await client.waitFor((l) => l.includes(' 464 '));
122
+ await client.waitFor((l) => l.startsWith('ERROR :Closing link'));
123
+ await client.close();
124
+ });
125
+
126
+ it('rejects a client that supplies the wrong password', async () => {
127
+ const client = new TestClient(`ws://127.0.0.1:${server.port}/`);
128
+ await client.opened();
129
+ await client.send('PASS wrong');
130
+ await client.send('NICK bob');
131
+ await client.send('USER bob 0 * :Bob');
132
+ await client.waitFor((l) => l.includes(' 464 '));
133
+ await client.close();
134
+ });
135
+
136
+ it('admits a client that supplies the correct password', async () => {
137
+ const client = new TestClient(`ws://127.0.0.1:${server.port}/`);
138
+ await client.opened();
139
+ await client.send('PASS s3cret');
140
+ await client.send('NICK carol');
141
+ await client.send('USER carol 0 * :Carol');
142
+ await client.waitFor((l) => l.startsWith(':pwd.example.com 001 '));
143
+ await client.close();
144
+ });
145
+ });
146
+
147
+ // ---------------------------------------------------------------------------
148
+ // Hostmask cloaking
149
+ // ---------------------------------------------------------------------------
150
+
151
+ describe('local-cli e2e — hostmask cloaking', () => {
152
+ let server: LocalServer;
153
+
154
+ beforeAll(async () => {
155
+ server = await startLocalServer({
156
+ port: 0,
157
+ hostname: '127.0.0.1',
158
+ serverName: 'cloak.example.com',
159
+ networkName: 'CloakNet',
160
+ cloaking: { enabled: true, secret: 'hunter2', cloakedSuffix: 'hidden.lan' },
161
+ });
162
+ });
163
+
164
+ afterAll(async () => {
165
+ await server.close();
166
+ });
167
+
168
+ it('emits the cloaked host in the 001 welcome line', async () => {
169
+ const client = new TestClient(`ws://127.0.0.1:${server.port}/`);
170
+ await client.opened();
171
+ await client.send('NICK alice');
172
+ await client.send('USER alice 0 * :Alice');
173
+ const welcome = await client.waitFor((l) => l.startsWith(':cloak.example.com 001 '));
174
+ // The cloaked host ends in the configured suffix and is NOT the raw IP.
175
+ expect(welcome.endsWith('.hidden.lan')).toBe(true);
176
+ // 001 format: :server 001 nick :Welcome to the Network IRC Network, nick!user@host
177
+ const hostSegment = welcome.split('@').slice(-1)[0] ?? '';
178
+ expect(hostSegment.endsWith('.hidden.lan')).toBe(true);
179
+ await client.close();
180
+ });
181
+ });
182
+
183
+ // ---------------------------------------------------------------------------
184
+ // Admission gate — per-IP connection cap
185
+ // ---------------------------------------------------------------------------
186
+
187
+ describe('local-cli e2e — admission gate (per-IP cap)', () => {
188
+ let server: LocalServer;
189
+
190
+ beforeAll(async () => {
191
+ server = await startLocalServer({
192
+ port: 0,
193
+ hostname: '127.0.0.1',
194
+ serverName: 'cap.example.com',
195
+ networkName: 'CapNet',
196
+ // Tight caps so the e2e test can saturate them quickly.
197
+ maxConnectionsPerIp: 2,
198
+ maxConnectionsPerUser: 100,
199
+ perIpConnectionRate: { max: 1_000, windowMs: 60_000 },
200
+ });
201
+ });
202
+
203
+ afterAll(async () => {
204
+ await server.close();
205
+ });
206
+
207
+ it('admits connections up to the cap then refuses further admissions', async () => {
208
+ const url = `ws://127.0.0.1:${server.port}/`;
209
+ const c1 = new TestClient(url);
210
+ const c2 = new TestClient(url);
211
+ await c1.opened();
212
+ await c2.opened();
213
+ // First two register fine.
214
+ await c1.send('NICK a1');
215
+ await c1.send('USER a1 0 * :A');
216
+ await c1.waitFor((l) => l.startsWith(':cap.example.com 001 '));
217
+ await c2.send('NICK a2');
218
+ await c2.send('USER a2 0 * :A');
219
+ await c2.waitFor((l) => l.startsWith(':cap.example.com 001 '));
220
+
221
+ // Third exceeds the per-IP cap (maxConnectionsPerIp: 2). The server
222
+ // sends an ERROR notice and closes the socket. Either opened() rejects
223
+ // (close-before-open) or the ERROR line arrives before close.
224
+ const c3 = new TestClient(url);
225
+ // Wait for either rejection path without racing into an unhandled
226
+ // rejection: opened() rejects on close, and we catch it.
227
+ await c3.opened().catch(() => {
228
+ // Expected: server closed the socket during/before handshake.
229
+ });
230
+ // If the socket did open, the server still sends ERROR then closes.
231
+ if (c3.ws.readyState === WebSocket.OPEN) {
232
+ await c3.waitFor((l) => l.startsWith('ERROR :Closing link'));
233
+ }
234
+ await c3.closed();
235
+
236
+ await c1.close();
237
+ await c2.close();
238
+ });
239
+ });
package/biome.json CHANGED
@@ -12,7 +12,9 @@
12
12
  "**/coverage/**",
13
13
  "**/node_modules/**",
14
14
  "**/.turbo/**",
15
- "pnpm-lock.yaml"
15
+ "pnpm-lock.yaml",
16
+ "cdk.out/",
17
+ "*.template.json"
16
18
  ]
17
19
  },
18
20
  "formatter": {
@@ -0,0 +1,139 @@
1
+ {
2
+ "widgets": [
3
+ {
4
+ "type": "metric",
5
+ "x": 0,
6
+ "y": 0,
7
+ "width": 12,
8
+ "height": 6,
9
+ "properties": {
10
+ "view": "timeSeries",
11
+ "stacked": false,
12
+ "metrics": [
13
+ ["AWS/ApiGateway", "ConnectCount", "ApiId", "${API_ID}"],
14
+ [".", "DisconnectCount", ".", "."]
15
+ ],
16
+ "region": "${AWS_REGION}",
17
+ "title": "Connections per second",
18
+ "period": 60,
19
+ "stat": "Sum"
20
+ }
21
+ },
22
+ {
23
+ "type": "metric",
24
+ "x": 12,
25
+ "y": 0,
26
+ "width": 12,
27
+ "height": 6,
28
+ "properties": {
29
+ "view": "timeSeries",
30
+ "stacked": false,
31
+ "metrics": [
32
+ [
33
+ "AWS/Lambda",
34
+ "Duration",
35
+ "FunctionName",
36
+ "${LAMBDA_NAME}",
37
+ "Resource",
38
+ "${LAMBDA_NAME}:$default"
39
+ ]
40
+ ],
41
+ "region": "${AWS_REGION}",
42
+ "title": "$default handler latency (p50/p95/p99)",
43
+ "period": 60,
44
+ "stat": ["p50", "p95", "p99"]
45
+ }
46
+ },
47
+ {
48
+ "type": "metric",
49
+ "x": 0,
50
+ "y": 6,
51
+ "width": 12,
52
+ "height": 6,
53
+ "properties": {
54
+ "view": "timeSeries",
55
+ "stacked": false,
56
+ "metrics": [["AWS/Lambda", "Errors", "FunctionName", "${LAMBDA_NAME}"]],
57
+ "region": "${AWS_REGION}",
58
+ "title": "Lambda errors per minute",
59
+ "period": 60,
60
+ "stat": "Sum"
61
+ }
62
+ },
63
+ {
64
+ "type": "log",
65
+ "x": 12,
66
+ "y": 6,
67
+ "width": 12,
68
+ "height": 6,
69
+ "properties": {
70
+ "view": "logs",
71
+ "stacked": false,
72
+ "query": "SOURCE '${LOG_GROUP_ARN}' | fields @timestamp, msg, level, traceId, connectionId, effect\n| filter level = \"error\" or msg = \"dispatch.error\" or msg = \"frame.parse-error\"\n| sort @timestamp desc\n| limit 100",
73
+ "region": "${AWS_REGION}",
74
+ "title": "Recent errors (level=error / dispatch.error / frame.parse-error)",
75
+ "period": 60
76
+ }
77
+ },
78
+ {
79
+ "type": "log",
80
+ "x": 0,
81
+ "y": 12,
82
+ "width": 24,
83
+ "height": 6,
84
+ "properties": {
85
+ "view": "logs",
86
+ "stacked": false,
87
+ "query": "SOURCE '${LOG_GROUP_ARN}' | filter msg = \"dispatch\"\n| stats count(*) as effects_per_minute by histogram(@timestamp, 1m)\n| sort @timestamp desc",
88
+ "region": "${AWS_REGION}",
89
+ "title": "Dispatched effects per minute (from structured log msg=dispatch)",
90
+ "period": 60
91
+ }
92
+ }
93
+ ],
94
+ "dashboardNameTemplate": "ServerlessIRCd-${ENVIRONMENT}",
95
+ "dashboardFilters": {
96
+ "variables": [
97
+ {
98
+ "name": "ENVIRONMENT",
99
+ "type": "Pattern",
100
+ "value": "staging",
101
+ "input": {
102
+ "id": "input/text"
103
+ }
104
+ },
105
+ {
106
+ "name": "API_ID",
107
+ "type": "Pattern",
108
+ "value": "",
109
+ "input": {
110
+ "id": "input/text"
111
+ }
112
+ },
113
+ {
114
+ "name": "LAMBDA_NAME",
115
+ "type": "Pattern",
116
+ "value": "serverless-ircd-handler",
117
+ "input": {
118
+ "id": "input/text"
119
+ }
120
+ },
121
+ {
122
+ "name": "LOG_GROUP_ARN",
123
+ "type": "Pattern",
124
+ "value": "",
125
+ "input": {
126
+ "id": "input/text"
127
+ }
128
+ },
129
+ {
130
+ "name": "AWS_REGION",
131
+ "type": "Pattern",
132
+ "value": "us-east-1",
133
+ "input": {
134
+ "id": "input/text"
135
+ }
136
+ }
137
+ ]
138
+ }
139
+ }