@fjall/components-infrastructure 12.0.0 → 12.1.1
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/dist/lib/patterns/aws/clickhouseDatabase.d.ts +49 -13
- package/dist/lib/patterns/aws/clickhouseDatabase.js +112 -3
- package/dist/lib/patterns/aws/database.d.ts +2 -2
- package/dist/lib/resources/aws/database/clickhouseUserData.js +7 -4
- package/dist/lib/resources/aws/monitoring/alarmDefaults.d.ts +3 -3
- package/dist/lib/resources/aws/monitoring/alarmDefaults.js +1 -1
- package/dist/lib/resources/aws/monitoring/clickhouseAlarms.d.ts +77 -5
- package/dist/lib/resources/aws/monitoring/clickhouseAlarms.js +192 -30
- package/dist/lib/resources/aws/monitoring/ecsAlarms.d.ts +11 -3
- package/dist/lib/resources/aws/monitoring/ecsAlarms.js +49 -43
- package/dist/lib/resources/aws/monitoring/index.d.ts +2 -2
- package/dist/lib/resources/aws/monitoring/index.js +2 -2
- package/dist/lib/resources/aws/monitoring/metricNamespaces.d.ts +13 -0
- package/dist/lib/resources/aws/monitoring/metricNamespaces.js +16 -0
- package/package.json +4 -4
|
@@ -3,6 +3,7 @@ import { type TaskDefinition } from "aws-cdk-lib/aws-ecs";
|
|
|
3
3
|
import { type IBucket } from "aws-cdk-lib/aws-s3";
|
|
4
4
|
import { Construct } from "constructs";
|
|
5
5
|
import { Secret, type SecretImport } from "../../resources/aws/secrets/secret.js";
|
|
6
|
+
import { type ClickHouseAlarmThresholds } from "../../resources/aws/monitoring/index.js";
|
|
6
7
|
import { type ClickHouseSchemaAdmin, type ProfileSpec } from "../../resources/aws/database/clickhouseSchemas.js";
|
|
7
8
|
import { type ISecret } from "aws-cdk-lib/aws-secretsmanager";
|
|
8
9
|
import { type ITopic } from "aws-cdk-lib/aws-sns";
|
|
@@ -73,8 +74,15 @@ export interface ClickHouseDatabaseProps {
|
|
|
73
74
|
/**
|
|
74
75
|
* BACKUP DATABASE TO S3 schedule (D19). Schedule string validated by CDK
|
|
75
76
|
* `Schedule.expression(...)` at synth time. `false` disables the sidecar
|
|
76
|
-
* (and skips the backup-task log group).
|
|
77
|
+
* (and skips the backup-task log group, so no backup alarms materialise).
|
|
77
78
|
* Default: `BACKUP_SCHEDULE` from `clickhouseConstants.ts`.
|
|
79
|
+
*
|
|
80
|
+
* Coupled to `alarms.backupHeartbeatWindowHours`: the backup heartbeat
|
|
81
|
+
* pages when no success lands within the window (default 26 h, sized for
|
|
82
|
+
* the daily default). A customised schedule without a matching window
|
|
83
|
+
* override draws a synth-time warning — sparser-than-daily schedules must
|
|
84
|
+
* widen the window (max 143 h — the paging-guarantee ceiling), and
|
|
85
|
+
* schedules sparser than ~5.8 days must disable the heartbeat.
|
|
78
86
|
*/
|
|
79
87
|
backupSchedule?: string | false;
|
|
80
88
|
/**
|
|
@@ -136,22 +144,50 @@ export interface ClickHouseDatabaseProps {
|
|
|
136
144
|
*/
|
|
137
145
|
tls?: ClickHouseTlsOptions;
|
|
138
146
|
/**
|
|
139
|
-
* Ops alarm SNS topic for the ClickHouse host-posture
|
|
140
|
-
* disk warn+critical)
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
147
|
+
* Ops alarm SNS topic for the full ClickHouse alarm surface: host-posture
|
|
148
|
+
* alarms (CPU / disk warn+critical), the ECS-layer service alarms (cgroup
|
|
149
|
+
* memory, `RunningTaskCount` liveness, task-stop watchdog), the server-log
|
|
150
|
+
* signal alarms (errors, failed queries, backpressure, cold-tier S3), and —
|
|
151
|
+
* when `backupSchedule` is enabled — the backup-failure alarm plus the
|
|
152
|
+
* backup success heartbeat (absence + arming-gate alarms and the paging
|
|
153
|
+
* composite). Accepts an
|
|
154
|
+
* `ITopic`, an `arn:` string, or the `"import:<ExportName>"` form the
|
|
155
|
+
* generator scaffolds onto production apps (`"import:SharedAlarmTopicArn"`).
|
|
156
|
+
* Forwarded automatically by `DatabaseFactory.build`. Omitted (the default)
|
|
157
|
+
* → no alarms, matching the dormant pre-dogfood behaviour.
|
|
146
158
|
*
|
|
147
|
-
* Resolved internally via `resolveAlertsTopic
|
|
148
|
-
* `
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
159
|
+
* Resolved internally via `resolveAlertsTopic` and threaded into the inner
|
|
160
|
+
* `EcsCompute` so the generic ECS alarm pipeline materialises alongside the
|
|
161
|
+
* curated host set; the container-CPU service alarm is deliberately
|
|
162
|
+
* disabled (host CPU owns that signal — a 1-vCPU box pins container CPU on
|
|
163
|
+
* routine merges). The construct owns the `instanceRole` / `asgName` /
|
|
164
|
+
* `backupTaskLogGroup` the alarms need, so no caller wiring is required.
|
|
165
|
+
* The stuck-merge alarm is NOT declared here — `"Stuck merge detected"` is
|
|
166
|
+
* emitted by the webapp app process, so it lives on the app service's
|
|
167
|
+
* declarative `logAlarms` instead.
|
|
152
168
|
*/
|
|
153
169
|
alertsTopic?: ITopic | string;
|
|
170
|
+
/**
|
|
171
|
+
* Alarm-threshold tuning knob, fanned out across the host alarms, the ECS
|
|
172
|
+
* service memory alarm, and the server-log signal alarms — see
|
|
173
|
+
* `ClickHouseAlarmThresholds` for the per-field defaults. `false` suppresses
|
|
174
|
+
* the whole ClickHouse alarm surface (host, service, log, watchdog) even
|
|
175
|
+
* when `alertsTopic` is set; the persistent-data-volume alarms are
|
|
176
|
+
* infrastructure-level and unaffected. Omitted → defaults.
|
|
177
|
+
*/
|
|
178
|
+
alarms?: ClickHouseAlarmThresholds | false;
|
|
179
|
+
/**
|
|
180
|
+
* Application ID for webhook-to-application alarm mapping. Tagged onto every
|
|
181
|
+
* ClickHouse alarm and appended to alarm descriptions — same contract as the
|
|
182
|
+
* RDS branch of `DatabaseFactory.build`.
|
|
183
|
+
*/
|
|
184
|
+
applicationId?: string;
|
|
154
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* Public-facing re-export of the alarm knob so factory consumers can type
|
|
188
|
+
* `alarms:` from the patterns layer (same convention as `ServiceLogAlarm`).
|
|
189
|
+
*/
|
|
190
|
+
export type { ClickHouseAlarmThresholds };
|
|
155
191
|
/**
|
|
156
192
|
* ClickHouse analytics database wrapper implementing IClickHouseDatabase.
|
|
157
193
|
*
|
|
@@ -19,9 +19,9 @@ import { createClickHouseSecurityGroup } from "../../resources/aws/database/clic
|
|
|
19
19
|
import { buildClickHouseEntrypointWrapper, buildClickHouseUserData, generateUsersConfigXml } from "../../resources/aws/database/clickhouseUserData.js";
|
|
20
20
|
import { toPascalCase } from "../../utils/capitaliseString.js";
|
|
21
21
|
import { resolveAlertsTopic } from "../../utils/resolveAlertsTopic.js";
|
|
22
|
-
import { createClickHouseAlarms } from "../../resources/aws/monitoring/index.js";
|
|
22
|
+
import { createClickHouseAlarms, validateClickHouseAlarmThresholds, BACKUP_HEARTBEAT_DEFAULT_WINDOW_HOURS, BACKUP_HEARTBEAT_MAX_WINDOW_HOURS, METRIC_NAMESPACE, stackScopedMetricNamespace } from "../../resources/aws/monitoring/index.js";
|
|
23
23
|
import { ClickHouseSchemaAdminSchema, ManagedPasswordNameSchema, ProfileSpecSchema, PROFILE_NAME_PATTERN } from "../../resources/aws/database/clickhouseSchemas.js";
|
|
24
|
-
import { deriveClickHouseDefaultProfiles } from "../../resources/aws/database/clickhouseTuning.js";
|
|
24
|
+
import { deriveClickHouseDefaultProfiles, deriveClickHouseServerTuning } from "../../resources/aws/database/clickhouseTuning.js";
|
|
25
25
|
import { inferAmiHardwareType } from "../../resources/aws/compute/ecsConstants.js";
|
|
26
26
|
import { CLICKHOUSE_DATABASE_NAME, DEFAULT_CLICKHOUSE_INSTANCE_TYPE, CLICKHOUSE_IMAGE, CLICKHOUSE_EBS_VOLUME_SIZE_GB, CLICKHOUSE_EBS_IOPS, CLICKHOUSE_EBS_THROUGHPUT_MBPS, clickHouseTaskMemoryMiB, CLICKHOUSE_HTTP_PORT, CLICKHOUSE_HTTPS_PORT, CLICKHOUSE_NATIVE_PORT, CLICKHOUSE_TCP_SECURE_PORT, CLICKHOUSE_TLS_CERT_MOUNT_PATH, CLICKHOUSE_PROMETHEUS_PORT, CLICKHOUSE_DATA_MOUNT_PATH, CLICKHOUSE_SECRET_OPTIONS, CLICKHOUSE_SERVER_ROLE_TAG, CLICKHOUSE_HOST_METRICS, clickHouseUserSecretName, CLICKHOUSE_HEALTH_CHECK, CLICKHOUSE_STOP_TIMEOUT_SECONDS, CLICKHOUSE_EBS_DEVICE_NAME, CLICKHOUSE_CONFIG_SUBDIR, CLICKHOUSE_USERS_SUBDIR, userPasswordEnvName, OPTIMISE_FINAL_SCHEDULE, REPLACING_MERGE_TREE_TABLES, OPTIMISE_MV_TABLES, CLICKHOUSE_CLOUDMAP_SERVICE_NAME, CLICKHOUSE_SERVER_CONTAINER_NAME, CLICKHOUSE_SERVICE_NAME, OPTIMISE_TASK_MEMORY_MIB, OPTIMISE_TASK_CPU_UNITS, BACKUP_SCHEDULE, BACKUP_TASK_MEMORY_MIB, BACKUP_TASK_CPU_UNITS, BACKUP_RETENTION_DAYS } from "../../resources/aws/database/clickhouseConstants.js";
|
|
27
27
|
import { TlsCertGenerator } from "../../resources/aws/utilities/tlsCertGenerator.js";
|
|
@@ -50,6 +50,75 @@ function resolveClickHouseDesiredCount(contextValue, propValue) {
|
|
|
50
50
|
return propValue;
|
|
51
51
|
return 1;
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Positional (space-delimited) CloudWatch filter patterns for ClickHouse
|
|
55
|
+
* server-log lines, e.g.:
|
|
56
|
+
*
|
|
57
|
+
* `2026.08.12 10:15:42.123456 [ 456 ] {query-id} <Error> executeQuery: …`
|
|
58
|
+
*
|
|
59
|
+
* CloudWatch tokenises on spaces, treating the bracketed thread id `[ 456 ]`
|
|
60
|
+
* as ONE field, so the severity token is always field 5 and the component
|
|
61
|
+
* token field 6. Positional matching is the only reliable shape here:
|
|
62
|
+
* CloudWatch's regex subset rejects `<`/`>` and `()` grouping outright, and a
|
|
63
|
+
* plain substring term (`"<Error>"` unanchored) false-positives on query text
|
|
64
|
+
* echoed into log lines (a SELECT containing the literal). The severity
|
|
65
|
+
* position cannot.
|
|
66
|
+
*/
|
|
67
|
+
const CLICKHOUSE_ERROR_LINE_PATTERN = '[w1, w2, w3, w4, w5="<Error>" || w5="<Fatal>", ...]';
|
|
68
|
+
const CLICKHOUSE_FAILED_QUERY_PATTERN = '[w1, w2, w3, w4, w5="<Error>", w6="executeQuery:", ...]';
|
|
69
|
+
/**
|
|
70
|
+
* Server-log signal alarm specs for the ClickHouse service's declarative
|
|
71
|
+
* `logAlarms`. Three always-on signals plus a cold-tier S3 signal when the
|
|
72
|
+
* cold tier is enabled. Thresholds come from the `alarms` knob with defaults
|
|
73
|
+
* documented on `ClickHouseAlarmThresholds`; the failed-queries default
|
|
74
|
+
* derives from the instance's `maxConcurrentQueries` so it tracks host size.
|
|
75
|
+
*/
|
|
76
|
+
function buildClickHouseLogAlarmSpecs(thresholds, coldTierEnabled, maxConcurrentQueries) {
|
|
77
|
+
const specs = [
|
|
78
|
+
{
|
|
79
|
+
idStem: "ClickHouseServerErrors",
|
|
80
|
+
metricName: "ClickHouseServerErrorCount",
|
|
81
|
+
description: "ClickHouse server error/fatal log rate exceeds threshold — check system.errors, recent merges, and disk headroom",
|
|
82
|
+
literal: CLICKHOUSE_ERROR_LINE_PATTERN,
|
|
83
|
+
threshold: thresholds.serverErrorsPer5Min ?? 30,
|
|
84
|
+
period: Duration.minutes(5),
|
|
85
|
+
evaluationPeriods: 3,
|
|
86
|
+
datapointsToAlarm: 2
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
idStem: "ClickHouseFailedQueries",
|
|
90
|
+
metricName: "ClickHouseFailedQueryCount",
|
|
91
|
+
description: "ClickHouse failed-query rate exceeds threshold — check system.query_log for the failing statement and error code",
|
|
92
|
+
literal: CLICKHOUSE_FAILED_QUERY_PATTERN,
|
|
93
|
+
threshold: thresholds.failedQueriesPer5Min ?? 3 * maxConcurrentQueries,
|
|
94
|
+
period: Duration.minutes(5),
|
|
95
|
+
evaluationPeriods: 3,
|
|
96
|
+
datapointsToAlarm: 2
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
idStem: "ClickHouseBackpressure",
|
|
100
|
+
metricName: "ClickHouseBackpressureCount",
|
|
101
|
+
description: "ClickHouse background backpressure — merge scheduling pausing or parts accumulating; check background pool sizing and partition granularity",
|
|
102
|
+
anyTerms: ["Temporarily pause scheduling", "Too many parts"],
|
|
103
|
+
threshold: thresholds.backpressurePer15Min ?? 50,
|
|
104
|
+
period: Duration.minutes(15),
|
|
105
|
+
evaluationPeriods: 2,
|
|
106
|
+
datapointsToAlarm: 2
|
|
107
|
+
}
|
|
108
|
+
];
|
|
109
|
+
if (coldTierEnabled) {
|
|
110
|
+
specs.push({
|
|
111
|
+
idStem: "ClickHouseColdTierS3Errors",
|
|
112
|
+
metricName: "ClickHouseColdTierS3ErrorCount",
|
|
113
|
+
description: "ClickHouse server-side S3 errors (cold-tier moves or BACKUP TO S3) — check the instance role grants and bucket policies for the cold-tier and backup buckets",
|
|
114
|
+
anyTerms: ["S3_ERROR", "AccessDenied"],
|
|
115
|
+
threshold: 1,
|
|
116
|
+
period: Duration.minutes(15),
|
|
117
|
+
evaluationPeriods: 1
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
return specs;
|
|
121
|
+
}
|
|
53
122
|
/**
|
|
54
123
|
* Narrow `T | undefined` to `T`. Used at the 3 sites where a `Map.get(...)` /
|
|
55
124
|
* `Array.find(...)` result is structurally undefined-able but invariants
|
|
@@ -367,9 +436,22 @@ export class ClickHouseDatabase extends Construct {
|
|
|
367
436
|
userSecret.getImport("password");
|
|
368
437
|
}
|
|
369
438
|
const resolvedAlertsTopic = resolveAlertsTopic(this, "AlertsTopic", props.alertsTopic);
|
|
439
|
+
const alarmsDisabled = props.alarms === false;
|
|
440
|
+
const alarmThresholds = typeof props.alarms === "object" ? props.alarms : {};
|
|
441
|
+
validateClickHouseAlarmThresholds(alarmThresholds);
|
|
442
|
+
const serverTuning = deriveClickHouseServerTuning(instanceType);
|
|
443
|
+
const serviceLogAlarms = alarmsDisabled
|
|
444
|
+
? undefined
|
|
445
|
+
: buildClickHouseLogAlarmSpecs(alarmThresholds, coldTierEnabled, serverTuning.maxConcurrentQueries);
|
|
370
446
|
const ecsCompute = new EcsCompute(this, "Compute", {
|
|
371
447
|
type: "ecs",
|
|
372
448
|
vpc,
|
|
449
|
+
...(resolvedAlertsTopic !== undefined && {
|
|
450
|
+
alertsTopic: resolvedAlertsTopic
|
|
451
|
+
}),
|
|
452
|
+
...(props.applicationId !== undefined && {
|
|
453
|
+
applicationId: props.applicationId
|
|
454
|
+
}),
|
|
373
455
|
cluster: {
|
|
374
456
|
loadBalancer: false,
|
|
375
457
|
securityGroup,
|
|
@@ -380,6 +462,22 @@ export class ClickHouseDatabase extends Construct {
|
|
|
380
462
|
name: CLICKHOUSE_SERVICE_NAME,
|
|
381
463
|
capacityProvider: "EC2",
|
|
382
464
|
desiredCount,
|
|
465
|
+
alarms: alarmsDisabled
|
|
466
|
+
? false
|
|
467
|
+
: {
|
|
468
|
+
// Container CPU is deliberately NOT alarmed: the host CPU
|
|
469
|
+
// alarm (AWS/EC2) owns that signal on this single-purpose
|
|
470
|
+
// box, and merges routinely pin container CPU on 1-vCPU
|
|
471
|
+
// hosts. Memory IS alarmed here rather than via CWAgent —
|
|
472
|
+
// the ECS denominator is `clickHouseTaskMemoryMiB()`, so
|
|
473
|
+
// the % means the same headroom on every instance size.
|
|
474
|
+
cpuThreshold: false,
|
|
475
|
+
memoryThreshold: alarmThresholds.memoryThreshold ?? 90
|
|
476
|
+
},
|
|
477
|
+
...(serviceLogAlarms !== undefined && {
|
|
478
|
+
logAlarms: serviceLogAlarms
|
|
479
|
+
}),
|
|
480
|
+
logMetricNamespace: stackScopedMetricNamespace(METRIC_NAMESPACE.CLICKHOUSE),
|
|
383
481
|
// Omitting `scaling` attaches default CPU target tracking. A scaled-out
|
|
384
482
|
// second task is an EMPTY second database behind the same Cloud Map name
|
|
385
483
|
// (plain MergeTree does not replicate) — desired-count scaling stays off.
|
|
@@ -527,12 +625,23 @@ export class ClickHouseDatabase extends Construct {
|
|
|
527
625
|
// it the alarms cannot be built; no topic is the default, so skip silently
|
|
528
626
|
// rather than throw.
|
|
529
627
|
const asgName = ecsCompute.getAutoScalingGroupName();
|
|
530
|
-
if (resolvedAlertsTopic !== undefined &&
|
|
628
|
+
if (resolvedAlertsTopic !== undefined &&
|
|
629
|
+
asgName !== undefined &&
|
|
630
|
+
!alarmsDisabled) {
|
|
631
|
+
if (backupEnabled &&
|
|
632
|
+
typeof props.backupSchedule === "string" &&
|
|
633
|
+
alarmThresholds.backupHeartbeatWindowHours === undefined) {
|
|
634
|
+
Annotations.of(this).addWarningV2("fjall:clickhouse:backup-heartbeat-window", `backupSchedule is customised but alarms.backupHeartbeatWindowHours is not set — the backup heartbeat pages when no successful backup lands within ${BACKUP_HEARTBEAT_DEFAULT_WINDOW_HOURS}h. For sparser schedules set backupHeartbeatWindowHours (schedule interval + 2h margin, max ${BACKUP_HEARTBEAT_MAX_WINDOW_HOURS}); schedules sparser than ~5.8 days cannot be heartbeat-monitored — set false to disable the heartbeat instead.`);
|
|
635
|
+
}
|
|
531
636
|
createClickHouseAlarms({
|
|
532
637
|
scope: this,
|
|
533
638
|
instanceRole,
|
|
534
639
|
asgName,
|
|
535
640
|
alarmTopic: resolvedAlertsTopic,
|
|
641
|
+
config: alarmThresholds,
|
|
642
|
+
...(props.applicationId !== undefined && {
|
|
643
|
+
applicationId: props.applicationId
|
|
644
|
+
}),
|
|
536
645
|
...(backupTaskLogGroup !== undefined && { backupTaskLogGroup })
|
|
537
646
|
});
|
|
538
647
|
}
|
|
@@ -4,7 +4,7 @@ import type { ITopic } from "aws-cdk-lib/aws-sns";
|
|
|
4
4
|
import type { RdsAlarmThresholds } from "../../resources/aws/monitoring/index.js";
|
|
5
5
|
import type App from "../../app.js";
|
|
6
6
|
import { type DynamoDBKeySchema, type DynamoDBGlobalSecondaryIndex } from "../../resources/aws/database/dynamodb.js";
|
|
7
|
-
import { ClickHouseDatabase, type ClickHouseDatabaseProps } from "./clickhouseDatabase.js";
|
|
7
|
+
import { ClickHouseDatabase, type ClickHouseDatabaseProps, type ClickHouseAlarmThresholds } from "./clickhouseDatabase.js";
|
|
8
8
|
import { type Connections, type IConnectable, type IVpc } from "aws-cdk-lib/aws-ec2";
|
|
9
9
|
import { type ITable } from "aws-cdk-lib/aws-dynamodb";
|
|
10
10
|
import { type Secret, type SecretImport } from "../../resources/aws/secrets/index.js";
|
|
@@ -331,7 +331,7 @@ export declare class RelationalDatabase extends Construct implements IRelational
|
|
|
331
331
|
*/
|
|
332
332
|
grantIamConnect(grantee: IGrantable, dbUsername: string): Grant;
|
|
333
333
|
}
|
|
334
|
-
export { ClickHouseDatabase, type ClickHouseDatabaseProps };
|
|
334
|
+
export { ClickHouseDatabase, type ClickHouseDatabaseProps, type ClickHouseAlarmThresholds };
|
|
335
335
|
export { ClickHouseDefaultProfiles, ClickHouseSchemaAdminSchema, ManagedPasswordNameSchema, ProfileSpecSchema, type ClickHouseSchemaAdmin, type ManagedPasswordName, type ProfileSpec } from "../../resources/aws/database/clickhouseSchemas.js";
|
|
336
336
|
export { renderUsersXml } from "../../resources/aws/database/clickhouseXmlRenderer.js";
|
|
337
337
|
export type { RenderUsersXmlOptions } from "../../resources/aws/database/clickhouseXmlRenderer.js";
|
|
@@ -119,8 +119,9 @@ export function generateServerConfigXml(options) {
|
|
|
119
119
|
max_bytes_before_external_group_by) live in the profile blocks of
|
|
120
120
|
users.xml — ClickHouse 26.3.10 rejects them at top level with
|
|
121
121
|
UNKNOWN_ELEMENT_IN_CONFIG. The default localhost user inherits
|
|
122
|
-
ClickHouse's built-in defaults; workload users
|
|
123
|
-
audit_writer, backup_reader
|
|
122
|
+
ClickHouse's built-in defaults; the SQL-managed workload users
|
|
123
|
+
(app_writer, audit_writer, backup_reader) and the XML-defined
|
|
124
|
+
schema_admin all carry explicit caps via their bound profiles. -->
|
|
124
125
|
<!-- Mark + index-mark caches scale linearly with instance memory
|
|
125
126
|
(384 MiB + 128 MiB at the 4 GiB baseline). CH counts these against
|
|
126
127
|
max_server_memory_usage. On a single-tenant dashboard workload they
|
|
@@ -360,8 +361,10 @@ chmod 600 "$MOUNT_POINT/server-certs/server.key"
|
|
|
360
361
|
chmod 644 "$MOUNT_POINT/server-certs/client.xml"
|
|
361
362
|
`
|
|
362
363
|
: "";
|
|
363
|
-
// The CWAgent-namespace
|
|
364
|
-
//
|
|
364
|
+
// The CWAgent-namespace metrics have no producer without this — no
|
|
365
|
+
// CloudWatch Agent is installed. disk_used_percent feeds the host disk
|
|
366
|
+
// alarms; mem_used_percent is dashboards-only (the CWAgent memory alarm is
|
|
367
|
+
// retired — memory alarming lives on ECS MemoryUtilization). The ASG name
|
|
365
368
|
// (the alarms' dimension) is unknowable at synth (LaunchTemplate → ASG is a
|
|
366
369
|
// CFN cycle), so it resolves at run time: IMDS instance tags first, with a
|
|
367
370
|
// describe-auto-scaling-instances fallback. AL2023 ECS AMIs ship no cron;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Duration } from "aws-cdk-lib";
|
|
2
|
-
import type { Alarm } from "aws-cdk-lib/aws-cloudwatch";
|
|
2
|
+
import type { Alarm, AlarmBase } from "aws-cdk-lib/aws-cloudwatch";
|
|
3
3
|
import type { SnsAction } from "aws-cdk-lib/aws-cloudwatch-actions";
|
|
4
4
|
/**
|
|
5
5
|
* Tag key for mapping CloudWatch alarms to Fjall applications.
|
|
@@ -36,5 +36,5 @@ export declare const ALARM_DEFAULTS: {
|
|
|
36
36
|
export declare function buildAlarmDescription(baseDescription: string, applicationId: string | undefined): string;
|
|
37
37
|
/** Wire an alarm to the SNS topic (both ALARM and OK transitions) and collect it. */
|
|
38
38
|
export declare function registerAlarm(alarm: Alarm, snsAction: SnsAction, alarms: Alarm[]): void;
|
|
39
|
-
/** Tag all alarms with the application ID for webhook-to-application mapping. */
|
|
40
|
-
export declare function tagAlarmsWithApplicationId(alarms:
|
|
39
|
+
/** Tag all alarms (incl. composite alarms) with the application ID for webhook-to-application mapping. */
|
|
40
|
+
export declare function tagAlarmsWithApplicationId(alarms: readonly AlarmBase[], applicationId: string | undefined): void;
|
|
@@ -25,7 +25,7 @@ export function registerAlarm(alarm, snsAction, alarms) {
|
|
|
25
25
|
alarm.addOkAction(snsAction);
|
|
26
26
|
alarms.push(alarm);
|
|
27
27
|
}
|
|
28
|
-
/** Tag all alarms with the application ID for webhook-to-application mapping. */
|
|
28
|
+
/** Tag all alarms (incl. composite alarms) with the application ID for webhook-to-application mapping. */
|
|
29
29
|
export function tagAlarmsWithApplicationId(alarms, applicationId) {
|
|
30
30
|
if (!applicationId)
|
|
31
31
|
return;
|
|
@@ -3,16 +3,75 @@ import type { IRole } from "aws-cdk-lib/aws-iam";
|
|
|
3
3
|
import type { ITopic } from "aws-cdk-lib/aws-sns";
|
|
4
4
|
import type { ILogGroup } from "aws-cdk-lib/aws-logs";
|
|
5
5
|
import type { Construct } from "constructs";
|
|
6
|
+
/**
|
|
7
|
+
* The single alarm-tuning knob on `ClickHouseDatabaseProps.alarms`. The
|
|
8
|
+
* ClickHouseDatabase pattern fans it out across three mechanisms: host-level
|
|
9
|
+
* fields feed `createClickHouseAlarms` (this factory), `memoryThreshold` feeds
|
|
10
|
+
* the generic ECS service memory alarm (cgroup-honest — the container limit
|
|
11
|
+
* IS `clickHouseTaskMemoryMiB()`, so the % means the same headroom on every
|
|
12
|
+
* instance size), and the log-signal fields feed the service's declarative
|
|
13
|
+
* `logAlarms` specs.
|
|
14
|
+
*/
|
|
6
15
|
export interface ClickHouseAlarmThresholds {
|
|
7
|
-
/** EC2 host CPU % over 5 min. Default 90.
|
|
16
|
+
/** EC2 host CPU % over 5 min. Default 90. Informational on a 1-vCPU host —
|
|
17
|
+
* merges routinely pin CPU; liveness is owned by the ECS running-tasks
|
|
18
|
+
* alarm. */
|
|
8
19
|
cpuThreshold?: number;
|
|
9
|
-
/**
|
|
20
|
+
/** ECS `MemoryUtilization` % over 5 min (container cgroup vs the task
|
|
21
|
+
* memory limit). Default 90. Consumed by the service-level memory alarm
|
|
22
|
+
* the ClickHouseDatabase pattern wires, NOT by this factory — the former
|
|
23
|
+
* CWAgent host-memory alarm is retired (its threshold meant a different
|
|
24
|
+
* headroom per instance size; the timer metric remains for dashboards). */
|
|
10
25
|
memoryThreshold?: number;
|
|
11
26
|
/** ClickHouse data volume disk % used. Default 70 (warn) — paired with critical at 85. */
|
|
12
27
|
diskWarnThreshold?: number;
|
|
13
28
|
/** ClickHouse data volume disk % used. Default 85. */
|
|
14
29
|
diskCriticalThreshold?: number;
|
|
30
|
+
/** `<Error>`/`<Fatal>` server-log lines per 5 min. Default 30. */
|
|
31
|
+
serverErrorsPer5Min?: number;
|
|
32
|
+
/** Failed `executeQuery:` error lines per 5 min. Default derived:
|
|
33
|
+
* 3 × `deriveClickHouseServerTuning(instanceType).maxConcurrentQueries`. */
|
|
34
|
+
failedQueriesPer5Min?: number;
|
|
35
|
+
/** Background-backpressure lines ("Temporarily pause scheduling" /
|
|
36
|
+
* "Too many parts") per 15 min. Default 50. */
|
|
37
|
+
backpressurePer15Min?: number;
|
|
38
|
+
/** Backup heartbeat window in hours (integer, 1–143): the maximum tolerated
|
|
39
|
+
* gap between backup successes before the heartbeat composite pages.
|
|
40
|
+
* Default 26 — the daily schedule interval plus a 2 h completion-drift
|
|
41
|
+
* margin. The 143 ceiling keeps the paging guarantee ahead of the arming
|
|
42
|
+
* gate's 7-day memory (see `BACKUP_HEARTBEAT_MAX_WINDOW_HOURS`); schedules
|
|
43
|
+
* sparser than ~5.8 days cannot be heartbeat-monitored — set `false` to
|
|
44
|
+
* disable the heartbeat trio (the success metric filter remains for
|
|
45
|
+
* dashboards). */
|
|
46
|
+
backupHeartbeatWindowHours?: number | false;
|
|
15
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* The subset of the knob this factory actually reads — host CPU + disk + the
|
|
50
|
+
* backup heartbeat window. The remaining fields are consumed by the pattern's
|
|
51
|
+
* service-alarm and logAlarms wiring (see `ClickHouseAlarmThresholds`).
|
|
52
|
+
*/
|
|
53
|
+
export type ClickHouseHostAlarmThresholds = Pick<ClickHouseAlarmThresholds, "cpuThreshold" | "diskWarnThreshold" | "diskCriticalThreshold" | "backupHeartbeatWindowHours">;
|
|
54
|
+
export declare const BACKUP_HEARTBEAT_DEFAULT_WINDOW_HOURS = 26;
|
|
55
|
+
/**
|
|
56
|
+
* Ceiling derivation — an ORDERING bound, not CloudWatch expressibility.
|
|
57
|
+
* The 7-day evaluation-span cap would allow 168 hourly buckets, but the
|
|
58
|
+
* composite only pages while the arming gate still reads OK, and the gate's
|
|
59
|
+
* 7 daily buckets are UTC-midnight-aligned: after a success at UTC hour h
|
|
60
|
+
* the gate re-alarms 168 − h hours later (h ≤ 23), while the absence alarm
|
|
61
|
+
* needs windowHours + 1 hours. Paging is therefore guaranteed for every
|
|
62
|
+
* schedule time only when windowHours ≤ 143; above that the gate can
|
|
63
|
+
* re-alarm first and the composite never fires. Net: schedules sparser than
|
|
64
|
+
* ~5.8 days (141 h interval + the 2 h margin) cannot be heartbeat-monitored
|
|
65
|
+
* — disable with `false` instead.
|
|
66
|
+
*/
|
|
67
|
+
export declare const BACKUP_HEARTBEAT_MAX_WINDOW_HOURS = 143;
|
|
68
|
+
/**
|
|
69
|
+
* Pure validation for the alarm-thresholds knob (synth-contract corpus
|
|
70
|
+
* surface). Called from both the `ClickHouseDatabase` constructor and
|
|
71
|
+
* `createClickHouseAlarms` — the factory call is defence-in-depth; the
|
|
72
|
+
* constructor call is what a direct instantiation hits.
|
|
73
|
+
*/
|
|
74
|
+
export declare function validateClickHouseAlarmThresholds(config: ClickHouseHostAlarmThresholds): void;
|
|
16
75
|
export interface ClickHouseAlarmsProps {
|
|
17
76
|
scope: Construct;
|
|
18
77
|
/**
|
|
@@ -36,12 +95,14 @@ export interface ClickHouseAlarmsProps {
|
|
|
36
95
|
* Omitted when `backupSchedule: false` — no backup task, no log group.
|
|
37
96
|
*/
|
|
38
97
|
backupTaskLogGroup?: ILogGroup;
|
|
39
|
-
config?:
|
|
98
|
+
config?: ClickHouseHostAlarmThresholds;
|
|
99
|
+
/** Application ID for webhook-to-application alarm mapping. */
|
|
100
|
+
applicationId?: string;
|
|
40
101
|
}
|
|
41
102
|
/**
|
|
42
103
|
* Single-node ClickHouse host-posture alarms. Covers host-level CPU (AWS/EC2)
|
|
43
|
-
* plus
|
|
44
|
-
*
|
|
104
|
+
* plus disk in the `CWAgent` namespace — fed NOT by the CloudWatch Agent
|
|
105
|
+
* (never installed on these hosts) but by the lightweight put-metric-data
|
|
45
106
|
* timer the user-data installs (see `buildClickHouseUserData`), which publishes
|
|
46
107
|
* `mem_used_percent` / `disk_used_percent` under the same names and
|
|
47
108
|
* `AutoScalingGroupName` dimension the agent would use — plus the
|
|
@@ -51,9 +112,20 @@ export interface ClickHouseAlarmsProps {
|
|
|
51
112
|
* task's BACKUP DATABASE TO S3 statement. Closes the silent-failure mode
|
|
52
113
|
* that masked the original IAM-grant misconfiguration (see
|
|
53
114
|
* `designs/2026-04-27-clickhouse-backup-iam-role.md`).
|
|
115
|
+
* - **Backup success heartbeat** — pages when no `BACKUP_CREATED` success
|
|
116
|
+
* marker lands within the heartbeat window on a stack whose backups
|
|
117
|
+
* previously succeeded. The complete-but-slow complement of the failure
|
|
118
|
+
* alarm: catches hangs, dead schedulers, and tasks that never start (see
|
|
119
|
+
* `createBackupHeartbeat` and
|
|
120
|
+
* `designs/2026-08-15-clickhouse-backup-success-heartbeat.md`).
|
|
54
121
|
*
|
|
55
122
|
* The stuck-merge alarm — a `"Stuck merge detected"` line emitted by the webapp
|
|
56
123
|
* app process, not this construct — lives on the app service's declarative
|
|
57
124
|
* `logAlarms` instead; it is an app-log alarm, not a database-host concern.
|
|
125
|
+
* Memory, liveness, and server-log signals are likewise NOT here: the
|
|
126
|
+
* ClickHouseDatabase pattern wires those through the generic ECS service
|
|
127
|
+
* alarms (cgroup memory ≥ 90, `RunningTaskCount` < 1) and the service's
|
|
128
|
+
* declarative `logAlarms` — this factory owns only what the ECS layer cannot
|
|
129
|
+
* see (the EC2 host and the backup task).
|
|
58
130
|
*/
|
|
59
131
|
export declare function createClickHouseAlarms(props: ClickHouseAlarmsProps): Alarm[];
|
|
@@ -1,15 +1,58 @@
|
|
|
1
1
|
import { Duration } from "aws-cdk-lib";
|
|
2
|
-
import { Alarm, ComparisonOperator, TreatMissingData } from "aws-cdk-lib/aws-cloudwatch";
|
|
2
|
+
import { Alarm, AlarmRule, AlarmState, ComparisonOperator, CompositeAlarm, MathExpression, TreatMissingData } from "aws-cdk-lib/aws-cloudwatch";
|
|
3
3
|
import { SnsAction } from "aws-cdk-lib/aws-cloudwatch-actions";
|
|
4
4
|
import { Metric } from "aws-cdk-lib/aws-cloudwatch";
|
|
5
5
|
import { FilterPattern, MetricFilter } from "aws-cdk-lib/aws-logs";
|
|
6
|
-
import { ALARM_DEFAULTS, registerAlarm, buildAlarmDescription } from "./alarmDefaults.js";
|
|
7
|
-
import { METRIC_NAMESPACE } from "./metricNamespaces.js";
|
|
6
|
+
import { ALARM_DEFAULTS, registerAlarm, tagAlarmsWithApplicationId, buildAlarmDescription } from "./alarmDefaults.js";
|
|
7
|
+
import { METRIC_NAMESPACE, stackScopedMetricNamespace } from "./metricNamespaces.js";
|
|
8
8
|
import { CLICKHOUSE_HOST_METRICS } from "../database/clickhouseConstants.js";
|
|
9
|
+
export const BACKUP_HEARTBEAT_DEFAULT_WINDOW_HOURS = 26;
|
|
10
|
+
/**
|
|
11
|
+
* Ceiling derivation — an ORDERING bound, not CloudWatch expressibility.
|
|
12
|
+
* The 7-day evaluation-span cap would allow 168 hourly buckets, but the
|
|
13
|
+
* composite only pages while the arming gate still reads OK, and the gate's
|
|
14
|
+
* 7 daily buckets are UTC-midnight-aligned: after a success at UTC hour h
|
|
15
|
+
* the gate re-alarms 168 − h hours later (h ≤ 23), while the absence alarm
|
|
16
|
+
* needs windowHours + 1 hours. Paging is therefore guaranteed for every
|
|
17
|
+
* schedule time only when windowHours ≤ 143; above that the gate can
|
|
18
|
+
* re-alarm first and the composite never fires. Net: schedules sparser than
|
|
19
|
+
* ~5.8 days (141 h interval + the 2 h margin) cannot be heartbeat-monitored
|
|
20
|
+
* — disable with `false` instead.
|
|
21
|
+
*/
|
|
22
|
+
export const BACKUP_HEARTBEAT_MAX_WINDOW_HOURS = 143;
|
|
23
|
+
/** Arming memory of the heartbeat: 7 daily buckets — the 7-day CloudWatch
|
|
24
|
+
* evaluation-span cap, the longest "a backup has succeeded recently" memory
|
|
25
|
+
* an alarm can carry. */
|
|
26
|
+
const BACKUP_HEARTBEAT_ARMING_DAYS = 7;
|
|
27
|
+
/** Composite actions wait this long for the suppressor (arming gate) to
|
|
28
|
+
* reach ALARM before firing — the real page's only added latency. */
|
|
29
|
+
const BACKUP_HEARTBEAT_SUPPRESSOR_WAIT = Duration.minutes(2);
|
|
30
|
+
/** Composite actions stay suppressed this long after the gate LEAVES ALARM:
|
|
31
|
+
* covers the arming transition, where both children flip ALARM→OK on the
|
|
32
|
+
* same first-success datapoint but on unsynchronised ~1-minute evaluation
|
|
33
|
+
* cycles — a gate-first ordering would otherwise transiently satisfy the
|
|
34
|
+
* rule and page the very stacks the design promises stay quiet. */
|
|
35
|
+
const BACKUP_HEARTBEAT_SUPPRESSOR_EXTENSION = Duration.minutes(5);
|
|
36
|
+
/**
|
|
37
|
+
* Pure validation for the alarm-thresholds knob (synth-contract corpus
|
|
38
|
+
* surface). Called from both the `ClickHouseDatabase` constructor and
|
|
39
|
+
* `createClickHouseAlarms` — the factory call is defence-in-depth; the
|
|
40
|
+
* constructor call is what a direct instantiation hits.
|
|
41
|
+
*/
|
|
42
|
+
export function validateClickHouseAlarmThresholds(config) {
|
|
43
|
+
const windowHours = config.backupHeartbeatWindowHours;
|
|
44
|
+
if (windowHours === undefined || windowHours === false)
|
|
45
|
+
return;
|
|
46
|
+
if (!Number.isInteger(windowHours) ||
|
|
47
|
+
windowHours < 1 ||
|
|
48
|
+
windowHours > BACKUP_HEARTBEAT_MAX_WINDOW_HOURS) {
|
|
49
|
+
throw new Error(`ClickHouseDatabase: alarms.backupHeartbeatWindowHours must be an integer between 1 and ${BACKUP_HEARTBEAT_MAX_WINDOW_HOURS} (hours), or false to disable the backup heartbeat; got ${windowHours}.`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
9
52
|
/**
|
|
10
53
|
* Single-node ClickHouse host-posture alarms. Covers host-level CPU (AWS/EC2)
|
|
11
|
-
* plus
|
|
12
|
-
*
|
|
54
|
+
* plus disk in the `CWAgent` namespace — fed NOT by the CloudWatch Agent
|
|
55
|
+
* (never installed on these hosts) but by the lightweight put-metric-data
|
|
13
56
|
* timer the user-data installs (see `buildClickHouseUserData`), which publishes
|
|
14
57
|
* `mem_used_percent` / `disk_used_percent` under the same names and
|
|
15
58
|
* `AutoScalingGroupName` dimension the agent would use — plus the
|
|
@@ -19,17 +62,29 @@ import { CLICKHOUSE_HOST_METRICS } from "../database/clickhouseConstants.js";
|
|
|
19
62
|
* task's BACKUP DATABASE TO S3 statement. Closes the silent-failure mode
|
|
20
63
|
* that masked the original IAM-grant misconfiguration (see
|
|
21
64
|
* `designs/2026-04-27-clickhouse-backup-iam-role.md`).
|
|
65
|
+
* - **Backup success heartbeat** — pages when no `BACKUP_CREATED` success
|
|
66
|
+
* marker lands within the heartbeat window on a stack whose backups
|
|
67
|
+
* previously succeeded. The complete-but-slow complement of the failure
|
|
68
|
+
* alarm: catches hangs, dead schedulers, and tasks that never start (see
|
|
69
|
+
* `createBackupHeartbeat` and
|
|
70
|
+
* `designs/2026-08-15-clickhouse-backup-success-heartbeat.md`).
|
|
22
71
|
*
|
|
23
72
|
* The stuck-merge alarm — a `"Stuck merge detected"` line emitted by the webapp
|
|
24
73
|
* app process, not this construct — lives on the app service's declarative
|
|
25
74
|
* `logAlarms` instead; it is an app-log alarm, not a database-host concern.
|
|
75
|
+
* Memory, liveness, and server-log signals are likewise NOT here: the
|
|
76
|
+
* ClickHouseDatabase pattern wires those through the generic ECS service
|
|
77
|
+
* alarms (cgroup memory ≥ 90, `RunningTaskCount` < 1) and the service's
|
|
78
|
+
* declarative `logAlarms` — this factory owns only what the ECS layer cannot
|
|
79
|
+
* see (the EC2 host and the backup task).
|
|
26
80
|
*/
|
|
27
81
|
export function createClickHouseAlarms(props) {
|
|
28
|
-
const { scope, instanceRole, asgName, alarmTopic, backupTaskLogGroup, config = {} } = props;
|
|
82
|
+
const { scope, instanceRole, asgName, alarmTopic, backupTaskLogGroup, config = {}, applicationId } = props;
|
|
83
|
+
validateClickHouseAlarmThresholds(config);
|
|
29
84
|
const alarms = [];
|
|
30
85
|
const snsAction = new SnsAction(alarmTopic);
|
|
31
86
|
const cpuAlarm = new Alarm(scope, "ClickHouseCpuAlarm", {
|
|
32
|
-
alarmDescription: buildAlarmDescription("ClickHouse host CPU utilisation exceeds threshold",
|
|
87
|
+
alarmDescription: buildAlarmDescription("ClickHouse host CPU utilisation exceeds threshold", applicationId),
|
|
33
88
|
metric: new Metric({
|
|
34
89
|
namespace: "AWS/EC2",
|
|
35
90
|
metricName: "CPUUtilization",
|
|
@@ -44,24 +99,9 @@ export function createClickHouseAlarms(props) {
|
|
|
44
99
|
treatMissingData: TreatMissingData.NOT_BREACHING
|
|
45
100
|
});
|
|
46
101
|
registerAlarm(cpuAlarm, snsAction, alarms);
|
|
47
|
-
const
|
|
48
|
-
alarmDescription: buildAlarmDescription("ClickHouse host memory utilisation exceeds threshold (CWAgent)", undefined),
|
|
49
|
-
metric: new Metric({
|
|
50
|
-
namespace: CLICKHOUSE_HOST_METRICS.namespace,
|
|
51
|
-
metricName: CLICKHOUSE_HOST_METRICS.memoryMetric,
|
|
52
|
-
dimensionsMap: { [CLICKHOUSE_HOST_METRICS.asgDimension]: asgName },
|
|
53
|
-
period: ALARM_DEFAULTS.EVALUATION_PERIOD,
|
|
54
|
-
statistic: "Average"
|
|
55
|
-
}),
|
|
56
|
-
threshold: config.memoryThreshold ?? 80,
|
|
57
|
-
evaluationPeriods: 3,
|
|
58
|
-
datapointsToAlarm: 2,
|
|
59
|
-
comparisonOperator: ComparisonOperator.GREATER_THAN_THRESHOLD,
|
|
60
|
-
treatMissingData: TreatMissingData.NOT_BREACHING
|
|
61
|
-
});
|
|
62
|
-
registerAlarm(memoryAlarm, snsAction, alarms);
|
|
102
|
+
const diskWarnThreshold = config.diskWarnThreshold ?? 70;
|
|
63
103
|
const diskWarnAlarm = new Alarm(scope, "ClickHouseDiskWarnAlarm", {
|
|
64
|
-
alarmDescription: buildAlarmDescription(
|
|
104
|
+
alarmDescription: buildAlarmDescription(`ClickHouse data volume above ${diskWarnThreshold}% used — plan growth response`, applicationId),
|
|
65
105
|
metric: new Metric({
|
|
66
106
|
namespace: CLICKHOUSE_HOST_METRICS.namespace,
|
|
67
107
|
metricName: CLICKHOUSE_HOST_METRICS.diskMetric,
|
|
@@ -69,15 +109,16 @@ export function createClickHouseAlarms(props) {
|
|
|
69
109
|
period: Duration.minutes(15),
|
|
70
110
|
statistic: "Average"
|
|
71
111
|
}),
|
|
72
|
-
threshold:
|
|
112
|
+
threshold: diskWarnThreshold,
|
|
73
113
|
evaluationPeriods: 2,
|
|
74
114
|
datapointsToAlarm: 2,
|
|
75
115
|
comparisonOperator: ComparisonOperator.GREATER_THAN_THRESHOLD,
|
|
76
116
|
treatMissingData: TreatMissingData.NOT_BREACHING
|
|
77
117
|
});
|
|
78
118
|
registerAlarm(diskWarnAlarm, snsAction, alarms);
|
|
119
|
+
const diskCriticalThreshold = config.diskCriticalThreshold ?? 85;
|
|
79
120
|
const diskCriticalAlarm = new Alarm(scope, "ClickHouseDiskCriticalAlarm", {
|
|
80
|
-
alarmDescription: buildAlarmDescription(
|
|
121
|
+
alarmDescription: buildAlarmDescription(`ClickHouse data volume above ${diskCriticalThreshold}% used — imminent insert failures`, applicationId),
|
|
81
122
|
metric: new Metric({
|
|
82
123
|
namespace: CLICKHOUSE_HOST_METRICS.namespace,
|
|
83
124
|
metricName: CLICKHOUSE_HOST_METRICS.diskMetric,
|
|
@@ -85,7 +126,7 @@ export function createClickHouseAlarms(props) {
|
|
|
85
126
|
period: Duration.minutes(5),
|
|
86
127
|
statistic: "Average"
|
|
87
128
|
}),
|
|
88
|
-
threshold:
|
|
129
|
+
threshold: diskCriticalThreshold,
|
|
89
130
|
evaluationPeriods: 2,
|
|
90
131
|
datapointsToAlarm: 2,
|
|
91
132
|
comparisonOperator: ComparisonOperator.GREATER_THAN_THRESHOLD,
|
|
@@ -98,18 +139,19 @@ export function createClickHouseAlarms(props) {
|
|
|
98
139
|
registerAlarm(diskCriticalAlarm, snsAction, alarms);
|
|
99
140
|
if (backupTaskLogGroup !== undefined) {
|
|
100
141
|
const backupFailureMetricName = "ClickHouseBackupFailureCount";
|
|
142
|
+
const backupFailureNamespace = stackScopedMetricNamespace(METRIC_NAMESPACE.CLICKHOUSE);
|
|
101
143
|
new MetricFilter(scope, "ClickHouseBackupFailureMetricFilter", {
|
|
102
144
|
logGroup: backupTaskLogGroup,
|
|
103
|
-
metricNamespace:
|
|
145
|
+
metricNamespace: backupFailureNamespace,
|
|
104
146
|
metricName: backupFailureMetricName,
|
|
105
147
|
filterPattern: FilterPattern.anyTerm("AccessDenied", "S3Exception"),
|
|
106
148
|
metricValue: "1",
|
|
107
149
|
defaultValue: 0
|
|
108
150
|
});
|
|
109
151
|
const backupFailureAlarm = new Alarm(scope, "ClickHouseBackupFailureAlarm", {
|
|
110
|
-
alarmDescription: buildAlarmDescription(`ClickHouse BACKUP TO S3 emitted AccessDenied/S3Exception — verify instance role '${instanceRole.roleName}' grant on backup bucket`,
|
|
152
|
+
alarmDescription: buildAlarmDescription(`ClickHouse BACKUP TO S3 emitted AccessDenied/S3Exception — verify instance role '${instanceRole.roleName}' grant on backup bucket`, applicationId),
|
|
111
153
|
metric: new Metric({
|
|
112
|
-
namespace:
|
|
154
|
+
namespace: backupFailureNamespace,
|
|
113
155
|
metricName: backupFailureMetricName,
|
|
114
156
|
period: Duration.hours(1),
|
|
115
157
|
statistic: "Sum"
|
|
@@ -121,6 +163,126 @@ export function createClickHouseAlarms(props) {
|
|
|
121
163
|
treatMissingData: TreatMissingData.NOT_BREACHING
|
|
122
164
|
});
|
|
123
165
|
registerAlarm(backupFailureAlarm, snsAction, alarms);
|
|
166
|
+
createBackupHeartbeat({
|
|
167
|
+
scope,
|
|
168
|
+
backupTaskLogGroup,
|
|
169
|
+
namespace: backupFailureNamespace,
|
|
170
|
+
windowHours: config.backupHeartbeatWindowHours,
|
|
171
|
+
snsAction,
|
|
172
|
+
applicationId,
|
|
173
|
+
alarms
|
|
174
|
+
});
|
|
124
175
|
}
|
|
176
|
+
tagAlarmsWithApplicationId(alarms, applicationId);
|
|
125
177
|
return alarms;
|
|
126
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* Backup success heartbeat — the complete-but-slow half of the backup alarm
|
|
181
|
+
* pair (the AccessDenied/S3Exception failure alarm above is the fast half).
|
|
182
|
+
* Closes the failure modes the error filter cannot see: a hung BACKUP, a
|
|
183
|
+
* scheduler that never fires, a task that cannot start.
|
|
184
|
+
*
|
|
185
|
+
* The success marker is the backup task's single stdout line per successful
|
|
186
|
+
* synchronous `BACKUP DATABASE … TO S3(…)` — `<uuid>\tBACKUP_CREATED`
|
|
187
|
+
* (ClickHouse's terminal success status; a failed backup prints exception
|
|
188
|
+
* text, which never contains the token, and a run that never happens prints
|
|
189
|
+
* nothing). Verified against production backup-task logs 2026-08-15.
|
|
190
|
+
*
|
|
191
|
+
* Three alarms, one pager:
|
|
192
|
+
*
|
|
193
|
+
* - **Absence** (`FILL(m1, 0) < 1`, 1 h buckets × windowHours, no actions) —
|
|
194
|
+
* ALARM when no success lands for windowHours consecutive hours. FILL
|
|
195
|
+
* materialises zeros for empty buckets, so this is deterministic — but it
|
|
196
|
+
* also zero-fills the buckets before the metric existed, putting every
|
|
197
|
+
* fresh stack (and every existing stack on the deploy that introduces the
|
|
198
|
+
* filter) into ALARM until its first backup.
|
|
199
|
+
* - **Unestablished** (success Sum < 1 per daily bucket, 7 × 7, no actions) —
|
|
200
|
+
* ALARM while no success has been observed in the trailing 7 days: true on
|
|
201
|
+
* stacks younger than one backup cycle, false from the first success
|
|
202
|
+
* onward. This is the arming gate that absorbs the absence alarm's
|
|
203
|
+
* cold-start.
|
|
204
|
+
* - **Heartbeat composite** (the pager): Absence in ALARM AND Unestablished
|
|
205
|
+
* in OK — "backups previously succeeded here and have now stopped". Quiet
|
|
206
|
+
* from stack birth, armed by the first observed success. The gate doubles
|
|
207
|
+
* as the composite's actions SUPPRESSOR: at the arming transition both
|
|
208
|
+
* children flip ALARM→OK on the same datapoint but on unsynchronised
|
|
209
|
+
* evaluation cycles, and a gate-first ordering would transiently satisfy
|
|
210
|
+
* the rule — the suppressor's extension period absorbs that race, at the
|
|
211
|
+
* cost of the real page arriving `BACKUP_HEARTBEAT_SUPPRESSOR_WAIT` late.
|
|
212
|
+
* Bounded caveat: after 7 further days of unresolved absence the arming
|
|
213
|
+
* gate re-enters ALARM and the composite returns to OK — with its actions
|
|
214
|
+
* suppressed by the gate, so no misleading "resolved" notification fires;
|
|
215
|
+
* the page fired on day one and the absence alarm stays visibly in ALARM
|
|
216
|
+
* in the console.
|
|
217
|
+
*
|
|
218
|
+
* Both child alarms treat missing data as breaching so the shape is
|
|
219
|
+
* deterministic whichever way the metric-math engine renders a
|
|
220
|
+
* never-populated metric (empty series vs zero-filled grid) — the composite
|
|
221
|
+
* gate makes the breaching reading safe at birth.
|
|
222
|
+
*/
|
|
223
|
+
function createBackupHeartbeat(props) {
|
|
224
|
+
const { scope, backupTaskLogGroup, namespace, windowHours, snsAction, applicationId, alarms } = props;
|
|
225
|
+
const successMetricName = "ClickHouseBackupSuccessCount";
|
|
226
|
+
// The filter always materialises (dashboards + the heartbeat both read it);
|
|
227
|
+
// defaultValue 0 emits real zero datapoints whenever backup-task log events
|
|
228
|
+
// arrive without the success marker — an actively failing backup produces
|
|
229
|
+
// deterministic breaching data instead of relying on missing-data handling.
|
|
230
|
+
new MetricFilter(scope, "ClickHouseBackupSuccessMetricFilter", {
|
|
231
|
+
logGroup: backupTaskLogGroup,
|
|
232
|
+
metricNamespace: namespace,
|
|
233
|
+
metricName: successMetricName,
|
|
234
|
+
filterPattern: FilterPattern.allTerms("BACKUP_CREATED"),
|
|
235
|
+
metricValue: "1",
|
|
236
|
+
defaultValue: 0
|
|
237
|
+
});
|
|
238
|
+
if (windowHours === false)
|
|
239
|
+
return;
|
|
240
|
+
const effectiveWindowHours = windowHours ?? BACKUP_HEARTBEAT_DEFAULT_WINDOW_HOURS;
|
|
241
|
+
const hourlySuccesses = new Metric({
|
|
242
|
+
namespace,
|
|
243
|
+
metricName: successMetricName,
|
|
244
|
+
period: Duration.hours(1),
|
|
245
|
+
statistic: "Sum"
|
|
246
|
+
});
|
|
247
|
+
const absenceAlarm = new Alarm(scope, "ClickHouseBackupAbsenceAlarm", {
|
|
248
|
+
alarmDescription: buildAlarmDescription(`No ClickHouse backup success in the trailing ${effectiveWindowHours}h. No actions here — ClickHouseBackupHeartbeatAlarm pages when this coincides with backups having previously succeeded`, applicationId),
|
|
249
|
+
metric: new MathExpression({
|
|
250
|
+
expression: "FILL(m1, 0)",
|
|
251
|
+
usingMetrics: { m1: hourlySuccesses },
|
|
252
|
+
label: "Backup successes per hour",
|
|
253
|
+
period: Duration.hours(1)
|
|
254
|
+
}),
|
|
255
|
+
threshold: 1,
|
|
256
|
+
evaluationPeriods: effectiveWindowHours,
|
|
257
|
+
datapointsToAlarm: effectiveWindowHours,
|
|
258
|
+
comparisonOperator: ComparisonOperator.LESS_THAN_THRESHOLD,
|
|
259
|
+
treatMissingData: TreatMissingData.BREACHING
|
|
260
|
+
});
|
|
261
|
+
alarms.push(absenceAlarm);
|
|
262
|
+
const dailySuccesses = new Metric({
|
|
263
|
+
namespace,
|
|
264
|
+
metricName: successMetricName,
|
|
265
|
+
period: Duration.days(1),
|
|
266
|
+
statistic: "Sum"
|
|
267
|
+
});
|
|
268
|
+
const unestablishedAlarm = new Alarm(scope, "ClickHouseBackupUnestablishedAlarm", {
|
|
269
|
+
alarmDescription: buildAlarmDescription(`No ClickHouse backup success in the trailing ${BACKUP_HEARTBEAT_ARMING_DAYS} days — expected (and harmless) on stacks younger than one backup cycle; arms ClickHouseBackupHeartbeatAlarm once the first success lands. No actions attached`, applicationId),
|
|
270
|
+
metric: dailySuccesses,
|
|
271
|
+
threshold: 1,
|
|
272
|
+
evaluationPeriods: BACKUP_HEARTBEAT_ARMING_DAYS,
|
|
273
|
+
datapointsToAlarm: BACKUP_HEARTBEAT_ARMING_DAYS,
|
|
274
|
+
comparisonOperator: ComparisonOperator.LESS_THAN_THRESHOLD,
|
|
275
|
+
treatMissingData: TreatMissingData.BREACHING
|
|
276
|
+
});
|
|
277
|
+
alarms.push(unestablishedAlarm);
|
|
278
|
+
const heartbeat = new CompositeAlarm(scope, "ClickHouseBackupHeartbeatAlarm", {
|
|
279
|
+
alarmDescription: buildAlarmDescription(`ClickHouse backups have stopped succeeding: no success in the trailing ${effectiveWindowHours}h on a stack where backups previously succeeded — check the backup scheduled task's ECS run history, then the backup task log group, then the instance role's S3 grants`, applicationId),
|
|
280
|
+
alarmRule: AlarmRule.allOf(AlarmRule.fromAlarm(absenceAlarm, AlarmState.ALARM), AlarmRule.fromAlarm(unestablishedAlarm, AlarmState.OK)),
|
|
281
|
+
actionsSuppressor: unestablishedAlarm,
|
|
282
|
+
actionsSuppressorWaitPeriod: BACKUP_HEARTBEAT_SUPPRESSOR_WAIT,
|
|
283
|
+
actionsSuppressorExtensionPeriod: BACKUP_HEARTBEAT_SUPPRESSOR_EXTENSION
|
|
284
|
+
});
|
|
285
|
+
heartbeat.addAlarmAction(snsAction);
|
|
286
|
+
heartbeat.addOkAction(snsAction);
|
|
287
|
+
tagAlarmsWithApplicationId([heartbeat], applicationId);
|
|
288
|
+
}
|
|
@@ -4,10 +4,18 @@ import { type IApplicationTargetGroup } from "aws-cdk-lib/aws-elasticloadbalanci
|
|
|
4
4
|
import type { ITopic } from "aws-cdk-lib/aws-sns";
|
|
5
5
|
import type { Construct } from "constructs";
|
|
6
6
|
export interface EcsServiceAlarmThresholds {
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
/** CPU utilisation % threshold. `false` disables the CPU alarm entirely —
|
|
8
|
+
* for services whose CPU signal is owned elsewhere (e.g. the ClickHouse
|
|
9
|
+
* pattern alarms host CPU via AWS/EC2 and routinely pins container CPU
|
|
10
|
+
* during merges). */
|
|
11
|
+
cpuThreshold?: number | false;
|
|
12
|
+
/** Memory utilisation % threshold. `false` disables the memory alarm. */
|
|
13
|
+
memoryThreshold?: number | false;
|
|
14
|
+
/** Minimum running tasks; `0` disables the running-tasks alarm. */
|
|
9
15
|
runningTasksMinimum?: number;
|
|
10
|
-
|
|
16
|
+
/** 5xx error-rate % threshold (ALB services only). `false` disables the
|
|
17
|
+
* 5xx alarm; the p99 response-time alarm is unaffected. */
|
|
18
|
+
http5xxThreshold?: number | false;
|
|
11
19
|
}
|
|
12
20
|
export interface EcsServiceAlarmsProps {
|
|
13
21
|
scope: Construct;
|
|
@@ -7,30 +7,34 @@ export function createEcsServiceAlarms(props) {
|
|
|
7
7
|
const { scope, serviceName, service, targetGroup, config, alarmTopic, applicationId } = props;
|
|
8
8
|
const alarms = [];
|
|
9
9
|
const snsAction = new SnsAction(alarmTopic);
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
10
|
+
if (config.cpuThreshold !== false) {
|
|
11
|
+
const cpuAlarm = new Alarm(scope, `${serviceName}CpuAlarm`, {
|
|
12
|
+
alarmDescription: buildAlarmDescription(`ECS service ${serviceName} CPU utilisation exceeds threshold`, applicationId),
|
|
13
|
+
metric: service.metricCpuUtilization({
|
|
14
|
+
period: ALARM_DEFAULTS.EVALUATION_PERIOD
|
|
15
|
+
}),
|
|
16
|
+
threshold: config.cpuThreshold ?? ALARM_DEFAULTS.ECS.CPU,
|
|
17
|
+
evaluationPeriods: 3,
|
|
18
|
+
datapointsToAlarm: 2,
|
|
19
|
+
comparisonOperator: ComparisonOperator.GREATER_THAN_THRESHOLD,
|
|
20
|
+
treatMissingData: TreatMissingData.NOT_BREACHING
|
|
21
|
+
});
|
|
22
|
+
registerAlarm(cpuAlarm, snsAction, alarms);
|
|
23
|
+
}
|
|
24
|
+
if (config.memoryThreshold !== false) {
|
|
25
|
+
const memoryAlarm = new Alarm(scope, `${serviceName}MemoryAlarm`, {
|
|
26
|
+
alarmDescription: buildAlarmDescription(`ECS service ${serviceName} memory utilisation exceeds threshold`, applicationId),
|
|
27
|
+
metric: service.metricMemoryUtilization({
|
|
28
|
+
period: ALARM_DEFAULTS.EVALUATION_PERIOD
|
|
29
|
+
}),
|
|
30
|
+
threshold: config.memoryThreshold ?? ALARM_DEFAULTS.ECS.MEMORY,
|
|
31
|
+
evaluationPeriods: 3,
|
|
32
|
+
datapointsToAlarm: 2,
|
|
33
|
+
comparisonOperator: ComparisonOperator.GREATER_THAN_THRESHOLD,
|
|
34
|
+
treatMissingData: TreatMissingData.NOT_BREACHING
|
|
35
|
+
});
|
|
36
|
+
registerAlarm(memoryAlarm, snsAction, alarms);
|
|
37
|
+
}
|
|
34
38
|
const runningMin = config.runningTasksMinimum ?? ALARM_DEFAULTS.ECS.RUNNING_TASKS_MIN;
|
|
35
39
|
if (runningMin > 0) {
|
|
36
40
|
const runningTasksAlarm = new Alarm(scope, `${serviceName}RunningTasksAlarm`, {
|
|
@@ -53,25 +57,27 @@ export function createEcsServiceAlarms(props) {
|
|
|
53
57
|
}
|
|
54
58
|
// ALB-based alarms only when a target group is provided
|
|
55
59
|
if (targetGroup) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
60
|
+
if (config.http5xxThreshold !== false) {
|
|
61
|
+
const http5xxThreshold = config.http5xxThreshold ?? ALARM_DEFAULTS.ALB.HTTP_5XX_PERCENT;
|
|
62
|
+
const http5xxAlarm = new Alarm(scope, `${serviceName}Http5xxAlarm`, {
|
|
63
|
+
alarmDescription: buildAlarmDescription(`ECS service ${serviceName} 5xx error rate exceeds ${http5xxThreshold}%`, applicationId),
|
|
64
|
+
metric: new MathExpression({
|
|
65
|
+
expression: "(errors / requests) * 100",
|
|
66
|
+
label: "5xx Error Rate %",
|
|
67
|
+
period: ALARM_DEFAULTS.EVALUATION_PERIOD,
|
|
68
|
+
usingMetrics: {
|
|
69
|
+
errors: targetGroup.metrics.httpCodeTarget(HttpCodeTarget.TARGET_5XX_COUNT, { statistic: "Sum" }),
|
|
70
|
+
requests: targetGroup.metrics.requestCount({ statistic: "Sum" })
|
|
71
|
+
}
|
|
72
|
+
}),
|
|
73
|
+
threshold: http5xxThreshold,
|
|
74
|
+
evaluationPeriods: 2,
|
|
75
|
+
datapointsToAlarm: 2,
|
|
76
|
+
comparisonOperator: ComparisonOperator.GREATER_THAN_THRESHOLD,
|
|
77
|
+
treatMissingData: TreatMissingData.NOT_BREACHING
|
|
78
|
+
});
|
|
79
|
+
registerAlarm(http5xxAlarm, snsAction, alarms);
|
|
80
|
+
}
|
|
75
81
|
const p99Alarm = new Alarm(scope, `${serviceName}P99ResponseTimeAlarm`, {
|
|
76
82
|
alarmDescription: buildAlarmDescription(`ECS service ${serviceName} p99 response time exceeds threshold`, applicationId),
|
|
77
83
|
metric: targetGroup.metrics.targetResponseTime({
|
|
@@ -4,8 +4,8 @@ export { createRdsAlarms, type RdsAlarmThresholds, type RdsAlarmsProps } from ".
|
|
|
4
4
|
export { createLambdaAlarms, type LambdaAlarmThresholds, type LambdaAlarmsProps } from "./lambdaAlarms.js";
|
|
5
5
|
export { createScheduleAlarms, type ScheduleAlarmThresholds, type CreateScheduleAlarmsProps } from "./scheduleAlarms.js";
|
|
6
6
|
export { createSqsDlqAlarms, type SqsAlarmThresholds, type SqsDlqAlarmsProps } from "./sqsAlarms.js";
|
|
7
|
-
export { createClickHouseAlarms, type ClickHouseAlarmThresholds, type ClickHouseAlarmsProps } from "./clickhouseAlarms.js";
|
|
7
|
+
export { createClickHouseAlarms, validateClickHouseAlarmThresholds, BACKUP_HEARTBEAT_DEFAULT_WINDOW_HOURS, BACKUP_HEARTBEAT_MAX_WINDOW_HOURS, type ClickHouseAlarmThresholds, type ClickHouseHostAlarmThresholds, type ClickHouseAlarmsProps } from "./clickhouseAlarms.js";
|
|
8
8
|
export { createBuildkiteAlarms, type BuildkiteAlarmsProps } from "./buildkiteAlarms.js";
|
|
9
9
|
export { createLogPatternAlarms, type LogPatternAlarmSpec, type LogPatternAlarmsProps } from "./logPatternAlarms.js";
|
|
10
10
|
export { createEcsTaskStopWatchdog, ABNORMAL_TASK_STOP_CODES, TASK_STOP_METRIC_NAME, type EcsTaskStopWatchdog, type EcsTaskStopWatchdogProps } from "./ecsTaskStopWatchdog.js";
|
|
11
|
-
export { METRIC_NAMESPACE, type MetricNamespace } from "./metricNamespaces.js";
|
|
11
|
+
export { METRIC_NAMESPACE, stackScopedMetricNamespace, type MetricNamespace } from "./metricNamespaces.js";
|
|
@@ -4,8 +4,8 @@ export { createRdsAlarms } from "./rdsAlarms.js";
|
|
|
4
4
|
export { createLambdaAlarms } from "./lambdaAlarms.js";
|
|
5
5
|
export { createScheduleAlarms } from "./scheduleAlarms.js";
|
|
6
6
|
export { createSqsDlqAlarms } from "./sqsAlarms.js";
|
|
7
|
-
export { createClickHouseAlarms } from "./clickhouseAlarms.js";
|
|
7
|
+
export { createClickHouseAlarms, validateClickHouseAlarmThresholds, BACKUP_HEARTBEAT_DEFAULT_WINDOW_HOURS, BACKUP_HEARTBEAT_MAX_WINDOW_HOURS } from "./clickhouseAlarms.js";
|
|
8
8
|
export { createBuildkiteAlarms } from "./buildkiteAlarms.js";
|
|
9
9
|
export { createLogPatternAlarms } from "./logPatternAlarms.js";
|
|
10
10
|
export { createEcsTaskStopWatchdog, ABNORMAL_TASK_STOP_CODES, TASK_STOP_METRIC_NAME } from "./ecsTaskStopWatchdog.js";
|
|
11
|
-
export { METRIC_NAMESPACE } from "./metricNamespaces.js";
|
|
11
|
+
export { METRIC_NAMESPACE, stackScopedMetricNamespace } from "./metricNamespaces.js";
|
|
@@ -12,3 +12,16 @@ export declare const METRIC_NAMESPACE: {
|
|
|
12
12
|
readonly WEBAPP: "Fjall/WebApp";
|
|
13
13
|
};
|
|
14
14
|
export type MetricNamespace = (typeof METRIC_NAMESPACE)[keyof typeof METRIC_NAMESPACE];
|
|
15
|
+
/**
|
|
16
|
+
* Scope a metric namespace to the deploying stack (`<base>/<stack name>`).
|
|
17
|
+
*
|
|
18
|
+
* Log-pattern metric streams carry no dimensions — CloudWatch metric-filter
|
|
19
|
+
* dimensions can only reference fields extracted from the log event, and
|
|
20
|
+
* non-JSON logs (ClickHouse server logs) have no field carrying stack
|
|
21
|
+
* identity — so two stacks in one account+region publishing under a bare
|
|
22
|
+
* namespace+name share ONE metric stream and each stack's alarm fires on the
|
|
23
|
+
* union of both stacks' log lines. The stack name is the per-instance
|
|
24
|
+
* discriminator (two databases in one stack is already a synth error via the
|
|
25
|
+
* EC2 capacity slot).
|
|
26
|
+
*/
|
|
27
|
+
export declare function stackScopedMetricNamespace(base: MetricNamespace): string;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Aws } from "aws-cdk-lib";
|
|
1
2
|
/**
|
|
2
3
|
* Single source of truth for Fjall CloudWatch metric namespaces.
|
|
3
4
|
*
|
|
@@ -11,3 +12,18 @@ export const METRIC_NAMESPACE = {
|
|
|
11
12
|
ECS: "Fjall/ECS",
|
|
12
13
|
WEBAPP: "Fjall/WebApp"
|
|
13
14
|
};
|
|
15
|
+
/**
|
|
16
|
+
* Scope a metric namespace to the deploying stack (`<base>/<stack name>`).
|
|
17
|
+
*
|
|
18
|
+
* Log-pattern metric streams carry no dimensions — CloudWatch metric-filter
|
|
19
|
+
* dimensions can only reference fields extracted from the log event, and
|
|
20
|
+
* non-JSON logs (ClickHouse server logs) have no field carrying stack
|
|
21
|
+
* identity — so two stacks in one account+region publishing under a bare
|
|
22
|
+
* namespace+name share ONE metric stream and each stack's alarm fires on the
|
|
23
|
+
* union of both stacks' log lines. The stack name is the per-instance
|
|
24
|
+
* discriminator (two databases in one stack is already a synth error via the
|
|
25
|
+
* EC2 capacity slot).
|
|
26
|
+
*/
|
|
27
|
+
export function stackScopedMetricNamespace(base) {
|
|
28
|
+
return `${base}/${Aws.STACK_NAME}`;
|
|
29
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fjall/components-infrastructure",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.1.1",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/fjall-tech/fjall.git",
|
|
@@ -80,8 +80,8 @@
|
|
|
80
80
|
},
|
|
81
81
|
"dependencies": {
|
|
82
82
|
"@aws-sdk/client-organizations": "^3.1098.0",
|
|
83
|
-
"@fjall/generator": "^12.
|
|
84
|
-
"@fjall/util": "^12.
|
|
83
|
+
"@fjall/generator": "^12.1.1",
|
|
84
|
+
"@fjall/util": "^12.1.1",
|
|
85
85
|
"constructs": "^10.7.2"
|
|
86
86
|
},
|
|
87
87
|
"overrides": {
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
},
|
|
90
90
|
"peerDependencies": {
|
|
91
91
|
"aws-cdk": "^2.1134.0",
|
|
92
|
-
"aws-cdk-lib": "^2.
|
|
92
|
+
"aws-cdk-lib": "^2.265.0",
|
|
93
93
|
"constructs": "^10.7.2"
|
|
94
94
|
},
|
|
95
95
|
"engines": {
|