blogwright-analytics 0.4.0-beta.2 → 0.4.0-beta.4
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 +103 -10
- package/dist/adapters/transform-diagnostics.d.ts +4 -0
- package/dist/adapters/transform-diagnostics.js +4 -0
- package/dist/app/_app/immutable/assets/0.BfwUqNJ-.css +1 -0
- package/dist/app/_app/immutable/assets/2.lkfjnM1s.css +1 -0
- package/dist/app/_app/immutable/assets/DefaultTooltip.CXdtNw-m.css +1 -0
- package/dist/app/_app/immutable/assets/Tooltip.BsTJVNOF.css +1 -0
- package/dist/app/_app/immutable/chunks/4DYKm5b2.js +1 -0
- package/dist/app/_app/immutable/chunks/B6Dt35mA.js +1 -0
- package/dist/app/_app/immutable/chunks/BAneOF1f.js +1 -0
- package/dist/app/_app/immutable/chunks/BQHoVMm0.js +1 -0
- package/dist/app/_app/immutable/chunks/{Doz7YX1W.js → BUku_rSO.js} +1 -1
- package/dist/app/_app/immutable/chunks/BoX17NrM.js +1 -0
- package/dist/app/_app/immutable/chunks/{vDgBJUjM.js → BpoiI2_d.js} +1 -1
- package/dist/app/_app/immutable/chunks/Bsj_2e-_.js +1 -0
- package/dist/app/_app/immutable/chunks/{-aNGNaBT.js → BumHYZmq.js} +1 -1
- package/dist/app/_app/immutable/chunks/{BJngAqxk.js → C0tbX_qQ.js} +1 -1
- package/dist/app/_app/immutable/chunks/C7qhOD3U.js +1 -0
- package/dist/app/_app/immutable/chunks/C8TrOru6.js +1 -0
- package/dist/app/_app/immutable/chunks/{DJc8C0NK.js → CAgm0Ruv.js} +1 -1
- package/dist/app/_app/immutable/chunks/CSrT-PUe.js +1 -0
- package/dist/app/_app/immutable/chunks/CU11V7_a.js +1 -0
- package/dist/app/_app/immutable/chunks/CdRe9NtS.js +1 -0
- package/dist/app/_app/immutable/chunks/Ch6MextH.js +1 -0
- package/dist/app/_app/immutable/chunks/{DCkHk5n_.js → Cr8mHXFf.js} +1 -1
- package/dist/app/_app/immutable/chunks/CuOP1eLc.js +2 -0
- package/dist/app/_app/immutable/chunks/{Bc-53cml.js → CwPKxnpy.js} +1 -1
- package/dist/app/_app/immutable/chunks/{FH_3TS2H.js → Cz3xtWLM.js} +1 -1
- package/dist/app/_app/immutable/chunks/D26njVw7.js +5 -0
- package/dist/app/_app/immutable/chunks/D6rPBWFd.js +1 -0
- package/dist/app/_app/immutable/chunks/D8dq3MNd.js +1 -0
- package/dist/app/_app/immutable/chunks/DOqi3Wfa.js +1 -0
- package/dist/app/_app/immutable/chunks/DRQZHPoe.js +1 -0
- package/dist/app/_app/immutable/chunks/DUJFqVVs.js +1 -0
- package/dist/app/_app/immutable/chunks/DVCZGvXq.js +3 -0
- package/dist/app/_app/immutable/chunks/Dco3y1p7.js +1 -0
- package/dist/app/_app/immutable/chunks/DmKBckYv.js +1 -0
- package/dist/app/_app/immutable/chunks/DnQEoGed.js +1 -0
- package/dist/app/_app/immutable/chunks/DoNmKjPy.js +1 -0
- package/dist/app/_app/immutable/chunks/DodcabfQ.js +1 -0
- package/dist/app/_app/immutable/chunks/DugTr35P.js +1 -0
- package/dist/app/_app/immutable/chunks/{xIq_fFFM.js → RBkNRIpl.js} +1 -1
- package/dist/app/_app/immutable/chunks/{DKMlMI4a.js → Sb-KFJGv.js} +1 -1
- package/dist/app/_app/immutable/chunks/XoU76ysy.js +1 -0
- package/dist/app/_app/immutable/chunks/imr7FUlI.js +15 -0
- package/dist/app/_app/immutable/chunks/{Dh958be7.js → xefVn_UF.js} +1 -1
- package/dist/app/_app/immutable/entry/app.w1HyN1Pe.js +2 -0
- package/dist/app/_app/immutable/entry/start.Iz8smxW0.js +1 -0
- package/dist/app/_app/immutable/nodes/0.BjQeslHk.js +1 -0
- package/dist/app/_app/immutable/nodes/1.DfqY5csg.js +1 -0
- package/dist/app/_app/immutable/nodes/2.CByBWAMF.js +10 -0
- package/dist/app/_app/version.json +1 -1
- package/dist/app/index.html +40 -36
- package/dist/aws/firehose.d.ts +32 -1
- package/dist/aws/firehose.js +11 -0
- package/dist/commands.js +1 -1
- package/dist/nodes.d.ts +65 -18
- package/dist/nodes.js +378 -58
- package/dist/path-filter.d.ts +4 -0
- package/dist/path-filter.js +34 -0
- package/dist/plugin.d.ts +2 -2
- package/dist/plugin.js +2 -2
- package/dist/queries.d.ts +16 -7
- package/dist/queries.js +82 -12
- package/dist/reporting-range.d.ts +53 -0
- package/dist/reporting-range.js +57 -0
- package/dist/server.js +47 -5
- package/dist/traffic-breakdown.d.ts +2 -0
- package/dist/traffic-breakdown.js +15 -0
- package/dist/transform/diagnostics.d.ts +17 -0
- package/dist/transform/diagnostics.js +1 -0
- package/dist/transform/handler.d.ts +6 -5
- package/dist/transform/handler.js +34 -11
- package/dist/transform-bundle/index.mjs +53 -13
- package/dist/transform-bundle/transform-manifest.json +2 -2
- package/dist/view-granularity.d.ts +27 -0
- package/dist/view-granularity.js +18 -0
- package/package.json +12 -3
- package/dist/app/_app/immutable/assets/0.BTQrrh5B.css +0 -1
- package/dist/app/_app/immutable/assets/2.DoCaRy3T.css +0 -1
- package/dist/app/_app/immutable/assets/DefaultTooltip.BXd66MzC.css +0 -1
- package/dist/app/_app/immutable/chunks/1aEdY044.js +0 -15
- package/dist/app/_app/immutable/chunks/B3vZDoek.js +0 -1
- package/dist/app/_app/immutable/chunks/B5KRA4hC.js +0 -1
- package/dist/app/_app/immutable/chunks/BC0iYSWx.js +0 -2
- package/dist/app/_app/immutable/chunks/BZrG8lR6.js +0 -5
- package/dist/app/_app/immutable/chunks/Bd1gDe3Y.js +0 -1
- package/dist/app/_app/immutable/chunks/Bkszif4s.js +0 -1
- package/dist/app/_app/immutable/chunks/BnKf2MvA.js +0 -1
- package/dist/app/_app/immutable/chunks/CKcppAud.js +0 -1
- package/dist/app/_app/immutable/chunks/CRTcXoMo.js +0 -1
- package/dist/app/_app/immutable/chunks/CvyVA_jC.js +0 -1
- package/dist/app/_app/immutable/chunks/CxBzrV8P.js +0 -1
- package/dist/app/_app/immutable/chunks/CyCGwyzf.js +0 -1
- package/dist/app/_app/immutable/chunks/D-mRKj3n.js +0 -1
- package/dist/app/_app/immutable/chunks/D0Ty6LN0.js +0 -1
- package/dist/app/_app/immutable/chunks/D2BnX0Uk.js +0 -3
- package/dist/app/_app/immutable/chunks/DBwYG1VY.js +0 -1
- package/dist/app/_app/immutable/chunks/DMoYijy0.js +0 -1
- package/dist/app/_app/immutable/chunks/DRmJpyQa.js +0 -1
- package/dist/app/_app/immutable/chunks/DVXZkpbf.js +0 -1
- package/dist/app/_app/immutable/chunks/DorUkkYD.js +0 -1
- package/dist/app/_app/immutable/chunks/DtuTIrAM.js +0 -1
- package/dist/app/_app/immutable/chunks/V9ZjaxiY.js +0 -1
- package/dist/app/_app/immutable/chunks/ZvRY-WVy.js +0 -1
- package/dist/app/_app/immutable/chunks/caXkbKD3.js +0 -1
- package/dist/app/_app/immutable/chunks/oNmA_rTI.js +0 -1
- package/dist/app/_app/immutable/chunks/yT96EWBw.js +0 -1
- package/dist/app/_app/immutable/entry/app.cy9eKSTf.js +0 -2
- package/dist/app/_app/immutable/entry/start.B2ycr8Ji.js +0 -1
- package/dist/app/_app/immutable/nodes/0.UTMEigHJ.js +0 -1
- package/dist/app/_app/immutable/nodes/1.CMX4YrAG.js +0 -1
- package/dist/app/_app/immutable/nodes/2.CQMIFodk.js +0 -6
package/dist/nodes.js
CHANGED
|
@@ -3,24 +3,29 @@
|
|
|
3
3
|
* CloudFront-logs-to-Iceberg pipeline is built from and nothing else: the
|
|
4
4
|
* site's own bucket, distribution and log group stay in the CLI's graph
|
|
5
5
|
* (`packages/cli/src/nodes.ts`) and are never touched from here. This module
|
|
6
|
-
* carries all
|
|
6
|
+
* carries all fourteen of them, in four chains. The table chain - the S3 Tables bucket,
|
|
7
7
|
* the namespace inside it, the `page_views` table, and the Glue federation
|
|
8
8
|
* Firehose reads that table through - runs `analytics-table-bucket` ->
|
|
9
9
|
* `analytics-namespace` -> `analytics-table` ->
|
|
10
10
|
* `analytics-catalog-integration`. The transform chain - the long-lived
|
|
11
|
-
* `visitor_key` salt, the
|
|
12
|
-
* secret's ARN, and the
|
|
13
|
-
*
|
|
14
|
-
* `analytics-transform-
|
|
15
|
-
*
|
|
16
|
-
* -
|
|
17
|
-
*
|
|
18
|
-
*
|
|
11
|
+
* `visitor_key` salt, the log group the function's own output lands in, the
|
|
12
|
+
* Lambda execution role whose policy names that secret's ARN, and the
|
|
13
|
+
* record-transform function itself - runs `analytics-salt-secret` ->
|
|
14
|
+
* `analytics-transform-role` -> `analytics-transform-function`, with
|
|
15
|
+
* `analytics-transform-log-group` a second edge into that function. The
|
|
16
|
+
* delivery chain - the bucket every record Firehose cannot deliver lands in,
|
|
17
|
+
* the log group Firehose's delivery errors land in, the role it assumes, and
|
|
18
|
+
* the stream itself - runs `analytics-error-bucket` ->
|
|
19
|
+
* `analytics-firehose-role` -> `analytics-firehose-stream`, with
|
|
20
|
+
* `analytics-firehose-log-group` a second edge into that stream, and joins the
|
|
21
|
+
* other two chains through the role's five grants and the stream's
|
|
22
|
+
* destination. The vended-delivery chain -
|
|
19
23
|
* the CloudWatch delivery destination pointing at that stream and the delivery
|
|
20
24
|
* joining it to the site's log source - runs `analytics-log-destination` ->
|
|
21
25
|
* `analytics-log-delivery` and hangs off the stream. All four are wired through
|
|
22
26
|
* `dependsOn`, and a node depends on every node whose recorded ARN it
|
|
23
|
-
* interpolates
|
|
27
|
+
* interpolates - plus the two writers whose log group has to exist before the
|
|
28
|
+
* first line they write, which is an ordering with nothing read back. {@link buildAnalyticsNodes} at the foot of this module returns
|
|
24
29
|
* the assembled set, and `plugin.ts` hands it to the SPI's `Plugin.nodes`;
|
|
25
30
|
* assembling an array is all it does - nothing here reconciles anything.
|
|
26
31
|
*
|
|
@@ -41,8 +46,8 @@
|
|
|
41
46
|
* over the host's `signingUsEast1` signer; no node here picks a region for a
|
|
42
47
|
* request. {@link ANALYTICS_REGION} below is the same region as *text*, needed
|
|
43
48
|
* only because an ARN spells its region out and because every node `title`
|
|
44
|
-
* states the pin, so the bootstrap output an operator reads carries it.
|
|
45
|
-
* the
|
|
49
|
+
* states the pin, so the bootstrap output an operator reads carries it. Twelve
|
|
50
|
+
* of the fourteen titles state it as the region they are created in; the two IAM
|
|
46
51
|
* role nodes state it as the pipeline they serve, because IAM is global and
|
|
47
52
|
* "created in us-east-1" is not a property a role has (§Region pinning says so
|
|
48
53
|
* in as many words) - a title claiming otherwise would be the pin stated
|
|
@@ -85,16 +90,30 @@ const TABLE_NODE = 'analytics-table';
|
|
|
85
90
|
const CATALOG_NODE = 'analytics-catalog-integration';
|
|
86
91
|
/** The `analytics-salt-secret` node id, shared by its `id`, its state key and the edge into it. */
|
|
87
92
|
const SALT_SECRET_NODE = 'analytics-salt-secret';
|
|
93
|
+
/**
|
|
94
|
+
* The `analytics-transform-log-group` node id. The group the transform Lambda
|
|
95
|
+
* writes its own output to, owned here rather than left to the implicit
|
|
96
|
+
* creation Lambda would otherwise do on first invocation - which is what makes
|
|
97
|
+
* its retention a property at all.
|
|
98
|
+
*/
|
|
99
|
+
const TRANSFORM_LOG_GROUP_NODE = 'analytics-transform-log-group';
|
|
88
100
|
/** The `analytics-transform-role` node id. */
|
|
89
101
|
const TRANSFORM_ROLE_NODE = 'analytics-transform-role';
|
|
90
102
|
/** The `analytics-transform-function` node id. */
|
|
91
103
|
const TRANSFORM_FUNCTION_NODE = 'analytics-transform-function';
|
|
92
104
|
/** The `analytics-error-bucket` node id, shared by its `id`, its state key and the edge into it. */
|
|
93
105
|
const ERROR_BUCKET_NODE = 'analytics-error-bucket';
|
|
106
|
+
/**
|
|
107
|
+
* The `analytics-firehose-log-group` node id. The group Firehose writes its
|
|
108
|
+
* delivery errors to, and the one node in this module that owns a log stream as
|
|
109
|
+
* well as a group: enabling error logging through the API requires both to
|
|
110
|
+
* exist in advance.
|
|
111
|
+
*/
|
|
112
|
+
const FIREHOSE_LOG_GROUP_NODE = 'analytics-firehose-log-group';
|
|
94
113
|
/** The `analytics-firehose-role` node id. */
|
|
95
114
|
const FIREHOSE_ROLE_NODE = 'analytics-firehose-role';
|
|
96
115
|
/**
|
|
97
|
-
* The `analytics-firehose-stream` node id. Exported, alone among the
|
|
116
|
+
* The `analytics-firehose-stream` node id. Exported, alone among the fourteen,
|
|
98
117
|
* because `analytics status` reads this node's recorded outputs back out of
|
|
99
118
|
* the scoped state its `read` hydrated - the stream's delivery health - and a
|
|
100
119
|
* second copy of the string in `commands.ts` would be a state key with two
|
|
@@ -649,9 +668,12 @@ const ZIP_MTIME = REPRODUCIBLE_ZIP_MTIME;
|
|
|
649
668
|
/** The deflate level `packageAndUploadAgent` uses. */
|
|
650
669
|
const ZIP_LEVEL = 6;
|
|
651
670
|
/**
|
|
652
|
-
* The prefix Lambda derives a function's log group from
|
|
653
|
-
*
|
|
654
|
-
*
|
|
671
|
+
* The prefix Lambda derives a function's log group from, and therefore the
|
|
672
|
+
* prefix {@link transformLogGroupName} builds the one group this plugin owns
|
|
673
|
+
* for its function from. `analytics-transform-log-group` creates that group; it
|
|
674
|
+
* is not left to the implicit creation the Lambda service performs on a
|
|
675
|
+
* function's first invocation, because a group created that way is retained
|
|
676
|
+
* forever and carries none of the environment's tags.
|
|
655
677
|
*/
|
|
656
678
|
const LAMBDA_LOG_GROUP_PREFIX = '/aws/lambda/';
|
|
657
679
|
/** The name of the inline policy this plugin puts on its own transform role. */
|
|
@@ -763,10 +785,25 @@ function transformFunctionName(ctx) {
|
|
|
763
785
|
function transformRoleName(ctx) {
|
|
764
786
|
return boundedName(`${ctx.names.prefix}${TRANSFORM_ROLE_SUFFIX}`, ROLE_NAME_MAX_LENGTH, 'transform role');
|
|
765
787
|
}
|
|
788
|
+
/**
|
|
789
|
+
* The transform Lambda's log group name - **the one home this string has.**
|
|
790
|
+
*
|
|
791
|
+
* `analytics-transform-log-group` creates the group under this name and
|
|
792
|
+
* {@link transformLogGroupArn} scopes the role's grant to the ARN built from
|
|
793
|
+
* it, so the group that exists and the group the function is allowed to write
|
|
794
|
+
* to are one string by construction rather than two literals that happen to
|
|
795
|
+
* agree. Derived from {@link transformFunctionName} rather than re-derived from
|
|
796
|
+
* `ctx.names.prefix` for the same reason: Lambda's own group name is that
|
|
797
|
+
* function's name under {@link LAMBDA_LOG_GROUP_PREFIX}, so a second derivation
|
|
798
|
+
* could name a group Lambda never writes to.
|
|
799
|
+
*/
|
|
800
|
+
function transformLogGroupName(ctx) {
|
|
801
|
+
return `${LAMBDA_LOG_GROUP_PREFIX}${transformFunctionName(ctx)}`;
|
|
802
|
+
}
|
|
766
803
|
/**
|
|
767
804
|
* The log group ARN the role's `logs:` grant is scoped to - the function's
|
|
768
|
-
* **own** group and nothing else, the scoping
|
|
769
|
-
* `packages/cli/src/nodes.ts
|
|
805
|
+
* **own** group and nothing else, the scoping the site's exec role applies
|
|
806
|
+
* (`packages/cli/src/nodes.ts`).
|
|
770
807
|
*
|
|
771
808
|
* The region is {@link ANALYTICS_REGION} and not `ctx.config.region`, which is
|
|
772
809
|
* the one place this ARN differs from the CLI's `logGroupArn` helper (whose
|
|
@@ -775,18 +812,31 @@ function transformRoleName(ctx) {
|
|
|
775
812
|
* log group is too, and a grant naming the primary region would be a grant on a
|
|
776
813
|
* group that never exists.
|
|
777
814
|
*
|
|
778
|
-
*
|
|
779
|
-
*
|
|
780
|
-
*
|
|
781
|
-
*
|
|
782
|
-
* that implicit creation
|
|
783
|
-
*
|
|
784
|
-
*
|
|
785
|
-
*
|
|
786
|
-
*
|
|
815
|
+
* **`analytics-transform-log-group` creates this group**, which is why the
|
|
816
|
+
* policy below grants `logs:CreateLogStream` and `logs:PutLogEvents` and *not*
|
|
817
|
+
* `logs:CreateLogGroup`: the role has nothing to create, the shape the site's
|
|
818
|
+
* exec role has. An earlier version of this comment said no node creates the
|
|
819
|
+
* group and that Lambda's implicit creation on the function's first invocation
|
|
820
|
+
* was enough; production disproved it - the group never appeared, the transform
|
|
821
|
+
* ran and reported nothing, and the pipeline's record-level signals (a record
|
|
822
|
+
* the transform cannot map goes to Firehose's error prefix,
|
|
823
|
+
* `transform/handler.ts`, and a batch that throws raises Firehose's own error
|
|
824
|
+
* metric) answered *which* and never *why*.
|
|
787
825
|
*/
|
|
788
826
|
function transformLogGroupArn(ctx) {
|
|
789
|
-
|
|
827
|
+
return analyticsLogGroupArn(ctx, transformLogGroupName(ctx));
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* A log group's ARN in {@link ANALYTICS_REGION}, in the `:*` form both an IAM
|
|
831
|
+
* grant and this plugin's recorded outputs use.
|
|
832
|
+
*
|
|
833
|
+
* Takes no region parameter, which is the one way it differs from the CLI's own
|
|
834
|
+
* `logGroupArn` (`packages/cli/src/nodes.ts`) - that helper defaults to
|
|
835
|
+
* `ctx.config.region`, correctly, because the site's groups live there. Every
|
|
836
|
+
* group this plugin names lives in us-east-1 with the rest of the pipeline, so
|
|
837
|
+
* there is no region for a caller to choose and no default for one to forget.
|
|
838
|
+
*/
|
|
839
|
+
function analyticsLogGroupArn(ctx, group) {
|
|
790
840
|
return `arn:aws:logs:${ANALYTICS_REGION}:${ctx.accountId}:log-group:${group}:*`;
|
|
791
841
|
}
|
|
792
842
|
/**
|
|
@@ -856,8 +906,13 @@ function requireTransformRoleArn(ctx) {
|
|
|
856
906
|
* without a teardown.
|
|
857
907
|
*
|
|
858
908
|
* **Two statements, two concrete resources, no `*` anywhere.** The `logs`
|
|
859
|
-
* statement names the function's own log group ({@link transformLogGroupArn})
|
|
860
|
-
*
|
|
909
|
+
* statement names the function's own log group ({@link transformLogGroupArn})
|
|
910
|
+
* and carries two actions rather than three: `logs:CreateLogGroup` is
|
|
911
|
+
* deliberately absent, because `analytics-transform-log-group` creates that
|
|
912
|
+
* group and a role granted the creation of a group it never has to create is a
|
|
913
|
+
* grant with no call behind it - the same shape the site's exec role has. What
|
|
914
|
+
* Lambda needs at runtime is the stream inside that group and the events in it.
|
|
915
|
+
* The `secretsmanager` statement names the one secret this pipeline owns and
|
|
861
916
|
* nothing else. A `*` in the second would hand every secret in the account -
|
|
862
917
|
* every other environment's salt, and `blogwright-pds`'s OAuth client key and
|
|
863
918
|
* live session - to a role whose only job is to read one value, and nothing in
|
|
@@ -1024,6 +1079,100 @@ export function analyticsSaltSecretNode() {
|
|
|
1024
1079
|
},
|
|
1025
1080
|
};
|
|
1026
1081
|
}
|
|
1082
|
+
/**
|
|
1083
|
+
* A CloudWatch log group this plugin owns, on the read/create/update/delete
|
|
1084
|
+
* contract the site's own groups have (`logGroupNode`,
|
|
1085
|
+
* `packages/cli/src/nodes.ts`): `read` reports presence and records the ARN,
|
|
1086
|
+
* `create` ensures the group with the environment's tags and then applies
|
|
1087
|
+
* {@link LOG_RETENTION_DAYS}, `update` re-applies that retention on every
|
|
1088
|
+
* apply, and `delete` removes the group.
|
|
1089
|
+
*
|
|
1090
|
+
* Owning the group is what makes retention a property at all. A group a service
|
|
1091
|
+
* creates for itself is retained **forever** and no reconcile ever notices;
|
|
1092
|
+
* re-applying the policy on every `update` also converts a group that already
|
|
1093
|
+
* exists in that state - an environment provisioned before these two nodes
|
|
1094
|
+
* existed - without a teardown.
|
|
1095
|
+
*
|
|
1096
|
+
* `stream`, when given, is a log stream the group must also hold. It is ensured
|
|
1097
|
+
* on `create` **and re-ensured on every `update`**, which is the one place these
|
|
1098
|
+
* nodes depart from the site's `logGroupNode` and is the
|
|
1099
|
+
* {@link applyErrorBucketConfiguration} reconcile-on-every-apply pattern rather
|
|
1100
|
+
* than the site's narrower update. The reason is concrete: `read()` answers on
|
|
1101
|
+
* the group alone, so a group left by a run that stopped between
|
|
1102
|
+
* `CreateLogGroup` and `CreateLogStream` reports present forever while an
|
|
1103
|
+
* `update` that only re-applied retention would do nothing about the stream it
|
|
1104
|
+
* is missing. Deleting a group deletes the streams inside it, so `delete` needs
|
|
1105
|
+
* no counterpart.
|
|
1106
|
+
*
|
|
1107
|
+
* `dependsOn: []` on both: a group is the head of the chain that writes to it
|
|
1108
|
+
* and reads no other node's output.
|
|
1109
|
+
*/
|
|
1110
|
+
function analyticsLogGroupNode(spec) {
|
|
1111
|
+
const { id, title, name, stream } = spec;
|
|
1112
|
+
const record = (ctx, group) => {
|
|
1113
|
+
const out = output(ctx, id);
|
|
1114
|
+
out.name = group;
|
|
1115
|
+
out.arn = analyticsLogGroupArn(ctx, group);
|
|
1116
|
+
};
|
|
1117
|
+
return {
|
|
1118
|
+
id,
|
|
1119
|
+
dependsOn: [],
|
|
1120
|
+
title,
|
|
1121
|
+
async read(ctx) {
|
|
1122
|
+
const group = name(ctx);
|
|
1123
|
+
if (!(await logs(ctx).logGroupExists(group)))
|
|
1124
|
+
return false;
|
|
1125
|
+
record(ctx, group);
|
|
1126
|
+
return true;
|
|
1127
|
+
},
|
|
1128
|
+
async create(ctx) {
|
|
1129
|
+
const group = name(ctx);
|
|
1130
|
+
const client = logs(ctx);
|
|
1131
|
+
await client.ensureLogGroup(group, ctx.tags);
|
|
1132
|
+
// Identity before the secondary calls, the ordering `analytics-error-bucket`
|
|
1133
|
+
// and the site's own `bucketNode` follow: a crash between CreateLogGroup and
|
|
1134
|
+
// the retention or the stream must still leave the group recorded in state.
|
|
1135
|
+
record(ctx, group);
|
|
1136
|
+
await client.putRetentionPolicy(group, LOG_RETENTION_DAYS);
|
|
1137
|
+
if (stream !== undefined)
|
|
1138
|
+
await client.ensureLogStream(group, stream);
|
|
1139
|
+
},
|
|
1140
|
+
async update(ctx) {
|
|
1141
|
+
const group = name(ctx);
|
|
1142
|
+
const client = logs(ctx);
|
|
1143
|
+
await client.putRetentionPolicy(group, LOG_RETENTION_DAYS);
|
|
1144
|
+
if (stream !== undefined)
|
|
1145
|
+
await client.ensureLogStream(group, stream);
|
|
1146
|
+
},
|
|
1147
|
+
async delete(ctx) {
|
|
1148
|
+
// `deleteLogGroup` swallows its own not-found, so a re-run after a
|
|
1149
|
+
// completed teardown is a no-op rather than a failure.
|
|
1150
|
+
await logs(ctx).deleteLogGroup(name(ctx));
|
|
1151
|
+
},
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
/**
|
|
1155
|
+
* The transform Lambda's own log group: the mapping decisions, the drop path,
|
|
1156
|
+
* and the cold-start read of the salt secret - the only place this pipeline
|
|
1157
|
+
* says *why* a record went where it did.
|
|
1158
|
+
*
|
|
1159
|
+
* Lambda writes into it under the execution role's existing
|
|
1160
|
+
* `logs:CreateLogStream` and `logs:PutLogEvents`, scoped to this group and no
|
|
1161
|
+
* other ({@link transformLogGroupArn}). The role is deliberately not granted
|
|
1162
|
+
* `logs:CreateLogGroup`, because this node is what creates the group.
|
|
1163
|
+
*
|
|
1164
|
+
* It is at the head of the chain that writes to it -
|
|
1165
|
+
* `analytics-transform-function` declares the edge - and the role does not,
|
|
1166
|
+
* because the role's policy *derives* this group's ARN from the function's name
|
|
1167
|
+
* rather than reading a recorded one, so there is no output to wait for.
|
|
1168
|
+
*/
|
|
1169
|
+
export function analyticsTransformLogGroupNode() {
|
|
1170
|
+
return analyticsLogGroupNode({
|
|
1171
|
+
id: TRANSFORM_LOG_GROUP_NODE,
|
|
1172
|
+
title: `Transform Lambda log group (${ANALYTICS_REGION})`,
|
|
1173
|
+
name: transformLogGroupName,
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1027
1176
|
/**
|
|
1028
1177
|
* The transform Lambda's execution role: permission to write its own logs and
|
|
1029
1178
|
* to read the one secret it needs, and nothing else.
|
|
@@ -1310,12 +1459,16 @@ function whileRoleIsPropagating(call) {
|
|
|
1310
1459
|
* artifact's name, even though the zip travels inline rather than through a
|
|
1311
1460
|
* bucket (see {@link MAX_INLINE_ZIP_BYTES} for why inline).
|
|
1312
1461
|
*
|
|
1313
|
-
* It depends on `analytics-transform-role`, whose recorded ARN it runs as
|
|
1462
|
+
* It depends on `analytics-transform-role`, whose recorded ARN it runs as, and
|
|
1463
|
+
* on `analytics-transform-log-group`, whose ARN it reads nothing of: a group
|
|
1464
|
+
* that does not exist when the function first runs is a log line lost with
|
|
1465
|
+
* nothing raised, and on teardown the reverse order the engine walks removes
|
|
1466
|
+
* the function before the group holding the evidence of what it did.
|
|
1314
1467
|
*/
|
|
1315
1468
|
export function analyticsTransformFunctionNode() {
|
|
1316
1469
|
return {
|
|
1317
1470
|
id: TRANSFORM_FUNCTION_NODE,
|
|
1318
|
-
dependsOn: [TRANSFORM_ROLE_NODE],
|
|
1471
|
+
dependsOn: [TRANSFORM_ROLE_NODE, TRANSFORM_LOG_GROUP_NODE],
|
|
1319
1472
|
title: `Record-transform Lambda (${ANALYTICS_REGION})`,
|
|
1320
1473
|
async read(ctx) {
|
|
1321
1474
|
const name = transformFunctionName(ctx);
|
|
@@ -1474,6 +1627,41 @@ const FIREHOSE_TRUST = {
|
|
|
1474
1627
|
* that has no second case on this side of it.
|
|
1475
1628
|
*/
|
|
1476
1629
|
const ERROR_OUTPUT_PREFIX = 'firehose-errors/';
|
|
1630
|
+
/**
|
|
1631
|
+
* Days both of this plugin's log groups retain what is written to them,
|
|
1632
|
+
* re-applied on every `update` the way the site's own groups re-apply theirs.
|
|
1633
|
+
*
|
|
1634
|
+
* 365, matching the site's `retention.microvmDays` default - a year of the
|
|
1635
|
+
* build's own output, and a year of this pipeline's. It is a plugin-owned
|
|
1636
|
+
* constant and **not** read from `ctx.config.retention`, deliberately: that
|
|
1637
|
+
* block holds exactly two keys, `microvmDays` and `cloudfrontDays`, and each
|
|
1638
|
+
* one names one of the site's own two log groups. A third and fourth consumer
|
|
1639
|
+
* reading either would make an operator's setting for a group they named
|
|
1640
|
+
* silently govern two resources it was never named for, so that an environment
|
|
1641
|
+
* shortening its CloudFront retention would shorten the transform's diagnostics
|
|
1642
|
+
* with it and nothing would say so. Making retention configurable per plugin is
|
|
1643
|
+
* a config change with a name of its own, not a key borrowed here.
|
|
1644
|
+
*/
|
|
1645
|
+
const LOG_RETENTION_DAYS = 365;
|
|
1646
|
+
/**
|
|
1647
|
+
* The log stream inside {@link firehoseLogGroupName} that Firehose writes its
|
|
1648
|
+
* delivery errors to. Firehose's own name for that stream - enabling error
|
|
1649
|
+
* logging through the API names it explicitly, and the service creates neither
|
|
1650
|
+
* the group nor the stream.
|
|
1651
|
+
*
|
|
1652
|
+
* `BackupDelivery`, the stream Firehose uses for a destination configured with
|
|
1653
|
+
* S3 backup, is deliberately not created: the Iceberg destination this pipeline
|
|
1654
|
+
* builds configures none, so a second stream would be an empty one forever.
|
|
1655
|
+
*/
|
|
1656
|
+
const DESTINATION_DELIVERY_STREAM = 'DestinationDelivery';
|
|
1657
|
+
/**
|
|
1658
|
+
* The prefix a Firehose delivery stream's log group is conventionally named
|
|
1659
|
+
* under, the counterpart of {@link LAMBDA_LOG_GROUP_PREFIX}. Firehose derives
|
|
1660
|
+
* nothing from it - it writes to whatever group the stream's
|
|
1661
|
+
* `CloudWatchLoggingOptions` names - so the convention is this plugin's to keep,
|
|
1662
|
+
* and {@link firehoseLogGroupName} is where it is kept.
|
|
1663
|
+
*/
|
|
1664
|
+
const FIREHOSE_LOG_GROUP_PREFIX = '/aws/kinesisfirehose/';
|
|
1477
1665
|
/**
|
|
1478
1666
|
* Seconds Firehose buffers records before writing a file, and the size in MiB
|
|
1479
1667
|
* that would flush one sooner. Both are sent, because the service requires the
|
|
@@ -1525,6 +1713,39 @@ function firehoseRoleName(ctx) {
|
|
|
1525
1713
|
function streamName(ctx) {
|
|
1526
1714
|
return boundedName(`${ctx.names.prefix}${FIREHOSE_STREAM_SUFFIX}`, STREAM_NAME_MAX_LENGTH, 'delivery stream');
|
|
1527
1715
|
}
|
|
1716
|
+
/**
|
|
1717
|
+
* The log group Firehose writes its delivery errors to - **the one home this
|
|
1718
|
+
* string has**, for {@link transformLogGroupName}'s reason and with more riding
|
|
1719
|
+
* on it. `analytics-firehose-log-group` creates the group under this name
|
|
1720
|
+
* today; the delivery role's grant on the stream inside it and the stream's own
|
|
1721
|
+
* `CloudWatchLoggingOptions` are the two readers that follow, and each is meant
|
|
1722
|
+
* to reach the name through this helper rather than spell a third literal of
|
|
1723
|
+
* it. Firehose writes to whatever group its logging options name, so three
|
|
1724
|
+
* spellings would fail as an empty group rather than as an error.
|
|
1725
|
+
*
|
|
1726
|
+
* Derived from {@link streamName} rather than from `ctx.names.prefix`, so the
|
|
1727
|
+
* group is named after the stream whose errors it holds even if that stream's
|
|
1728
|
+
* suffix ever changes.
|
|
1729
|
+
*/
|
|
1730
|
+
function firehoseLogGroupName(ctx) {
|
|
1731
|
+
return `${FIREHOSE_LOG_GROUP_PREFIX}${streamName(ctx)}`;
|
|
1732
|
+
}
|
|
1733
|
+
/**
|
|
1734
|
+
* The ARN of the one log stream Firehose writes its delivery errors to, which
|
|
1735
|
+
* the delivery role's `logs:PutLogEvents` grant is scoped to.
|
|
1736
|
+
*
|
|
1737
|
+
* Not {@link analyticsLogGroupArn}'s `:*` form, and the difference is the whole
|
|
1738
|
+
* point of a separate helper: the group ARN's trailing wildcard grants every
|
|
1739
|
+
* stream the group will ever hold, while `PutLogEvents` authorises against a
|
|
1740
|
+
* stream ARN and this role writes to exactly one. Both halves come from the
|
|
1741
|
+
* same two helpers the log-group node and the stream's own
|
|
1742
|
+
* `CloudWatchLoggingOptions` reach for, so the grant cannot name a stream that
|
|
1743
|
+
* is not the one being written to.
|
|
1744
|
+
*/
|
|
1745
|
+
function firehoseLogStreamArn(ctx) {
|
|
1746
|
+
const group = firehoseLogGroupName(ctx);
|
|
1747
|
+
return `arn:aws:logs:${ANALYTICS_REGION}:${ctx.accountId}:log-group:${group}:log-stream:${DESTINATION_DELIVERY_STREAM}`;
|
|
1748
|
+
}
|
|
1528
1749
|
/**
|
|
1529
1750
|
* The error bucket's ARN as `analytics-error-bucket` recorded it. See
|
|
1530
1751
|
* {@link requireRecordedArn}.
|
|
@@ -1637,13 +1858,13 @@ function glueGrantResources(ctx) {
|
|
|
1637
1858
|
* and a table recreated under a new generated ARN reaches the policy without a
|
|
1638
1859
|
* teardown.
|
|
1639
1860
|
*
|
|
1640
|
-
* **Exactly
|
|
1861
|
+
* **Exactly five statements, one per capability the change spec names, every
|
|
1641
1862
|
* `Resource` a concrete ARN and none of them `*`.** The action lists are AWS's
|
|
1642
1863
|
* own, from the "Grant Firehose access to Amazon S3 Tables" policy under IAM
|
|
1643
1864
|
* access control; what is narrowed is the resources, which that policy writes
|
|
1644
1865
|
* with wildcards over the whole account.
|
|
1645
1866
|
*
|
|
1646
|
-
*
|
|
1867
|
+
* Three of the five are easy to get subtly wrong and are worth stating:
|
|
1647
1868
|
*
|
|
1648
1869
|
* - the error-bucket statement names the bucket **and** `<bucket>/*`. Bucket
|
|
1649
1870
|
* actions (`s3:ListBucket`, `s3:GetBucketLocation`) authorise against the
|
|
@@ -1658,11 +1879,19 @@ function glueGrantResources(ctx) {
|
|
|
1658
1879
|
* unqualified invoke, so copying it would deny every transform call and send
|
|
1659
1880
|
* every record to the error bucket.
|
|
1660
1881
|
*
|
|
1661
|
-
*
|
|
1662
|
-
*
|
|
1663
|
-
*
|
|
1664
|
-
*
|
|
1665
|
-
*
|
|
1882
|
+
* - the CloudWatch Logs statement names the **one log stream** Firehose writes
|
|
1883
|
+
* its delivery errors to, `<firehose log group>:log-stream:DestinationDelivery`,
|
|
1884
|
+
* and not the group's `:*` form. `logs:PutLogEvents` authorises against the
|
|
1885
|
+
* stream ARN, and this role writes to exactly one stream, so the wildcard the
|
|
1886
|
+
* group ARN carries would grant every stream a future group ever holds. Only
|
|
1887
|
+
* `PutLogEvents`: `analytics-firehose-log-group` creates the group *and* the
|
|
1888
|
+
* stream, so the role has nothing to create - the shape
|
|
1889
|
+
* `analytics-transform-role`'s grant has, one action shorter.
|
|
1890
|
+
*
|
|
1891
|
+
* That fifth statement is the only one of the three AWS's own policy adds that
|
|
1892
|
+
* this pipeline needs. The other two stay out: Kinesis (this stream is
|
|
1893
|
+
* `DirectPut`) and KMS (no customer-managed key is configured anywhere in this
|
|
1894
|
+
* pipeline), each conditional on a feature this pipeline does not use.
|
|
1666
1895
|
*/
|
|
1667
1896
|
async function applyFirehoseRolePolicy(ctx) {
|
|
1668
1897
|
const errorBucketArn = requireErrorBucketArn(ctx, FIREHOSE_ROLE_NODE);
|
|
@@ -1720,6 +1949,14 @@ async function applyFirehoseRolePolicy(ctx) {
|
|
|
1720
1949
|
],
|
|
1721
1950
|
Resource: [errorBucketArn, `${errorBucketArn}/*`],
|
|
1722
1951
|
},
|
|
1952
|
+
{
|
|
1953
|
+
// Without this the destination's `CloudWatchLoggingOptions` are inert:
|
|
1954
|
+
// Firehose is told where to write its delivery errors and is not allowed
|
|
1955
|
+
// to, so the failure it was meant to explain stays as silent as before.
|
|
1956
|
+
Effect: 'Allow',
|
|
1957
|
+
Action: ['logs:PutLogEvents'],
|
|
1958
|
+
Resource: firehoseLogStreamArn(ctx),
|
|
1959
|
+
},
|
|
1723
1960
|
],
|
|
1724
1961
|
});
|
|
1725
1962
|
}
|
|
@@ -1754,6 +1991,12 @@ function firehoseDestination(ctx) {
|
|
|
1754
1991
|
bufferIntervalSeconds: STREAM_BUFFER_INTERVAL_SECONDS,
|
|
1755
1992
|
bufferSizeMb: STREAM_BUFFER_SIZE_MB,
|
|
1756
1993
|
transformLambdaArn: requireTransformFunctionArn(ctx, FIREHOSE_STREAM_NODE),
|
|
1994
|
+
// Both through the helpers `analytics-firehose-log-group` creates the group and
|
|
1995
|
+
// the stream under, and the delivery role grants on - one string each. Firehose
|
|
1996
|
+
// writes to whatever group its logging options name and creates nothing, so a
|
|
1997
|
+
// second spelling of either would fail as an empty group rather than as an error.
|
|
1998
|
+
logGroupName: firehoseLogGroupName(ctx),
|
|
1999
|
+
logStreamName: DESTINATION_DELIVERY_STREAM,
|
|
1757
2000
|
};
|
|
1758
2001
|
}
|
|
1759
2002
|
/**
|
|
@@ -1775,14 +2018,39 @@ function recordOptional(out, key, value) {
|
|
|
1775
2018
|
else
|
|
1776
2019
|
out[key] = value;
|
|
1777
2020
|
}
|
|
2021
|
+
/**
|
|
2022
|
+
* What the reconcile is about to change, named for the operator reading the log
|
|
2023
|
+
* line, and only what actually differs.
|
|
2024
|
+
*
|
|
2025
|
+
* A single "AppendOnly <recorded> -> <desired>" would report a transition on
|
|
2026
|
+
* every reconcile, including the logging-only one where `AppendOnly` is `true`
|
|
2027
|
+
* on both sides - "AppendOnly true -> true", an operator told the reason for a
|
|
2028
|
+
* call is a field that did not move. The caller reaches this only past a guard
|
|
2029
|
+
* that returned on both flags matching, so at least one clause always fires and
|
|
2030
|
+
* the result is never empty.
|
|
2031
|
+
*/
|
|
2032
|
+
function destinationDrift(appendOnly, loggingEnabled) {
|
|
2033
|
+
const parts = [];
|
|
2034
|
+
if (appendOnly !== STREAM_APPEND_ONLY) {
|
|
2035
|
+
parts.push(`AppendOnly ${String(appendOnly)} -> ${String(STREAM_APPEND_ONLY)}`);
|
|
2036
|
+
}
|
|
2037
|
+
if (loggingEnabled !== true) {
|
|
2038
|
+
parts.push(`error logging ${loggingEnabled === false ? 'off' : 'unrecorded'} -> on`);
|
|
2039
|
+
}
|
|
2040
|
+
return parts.join(', ');
|
|
2041
|
+
}
|
|
1778
2042
|
/**
|
|
1779
2043
|
* Record the delivery stream's identity and health from a `DescribeDeliveryStream`.
|
|
1780
2044
|
*
|
|
1781
2045
|
* `state` and `failure` are what `analytics status` reports (task 55), so the
|
|
1782
2046
|
* stream's health is hydrated by the same `read` the reconcile runs and there is
|
|
1783
2047
|
* no second describe path. `versionId` and `destinationId` are what
|
|
1784
|
-
* `UpdateDestination` cannot be called without, and `appendOnly`
|
|
1785
|
-
*
|
|
2048
|
+
* `UpdateDestination` cannot be called without, and `appendOnly` and
|
|
2049
|
+
* `loggingEnabled` are the two live flags the reconcile compares - the first
|
|
2050
|
+
* against {@link STREAM_APPEND_ONLY}, the second against error logging simply
|
|
2051
|
+
* being on. Both go through {@link recordOptional}, so a describe that stops
|
|
2052
|
+
* reporting one clears it rather than leaving a stale `true` that would make the
|
|
2053
|
+
* reconcile skip work it should do.
|
|
1786
2054
|
*
|
|
1787
2055
|
* The ARN is guarded on its value, the guard `analytics-table` and
|
|
1788
2056
|
* `analytics-catalog-integration` both put on theirs: `describeDeliveryStream`
|
|
@@ -1797,6 +2065,7 @@ function recordStream(ctx, status) {
|
|
|
1797
2065
|
recordOptional(out, 'versionId', status.versionId);
|
|
1798
2066
|
recordOptional(out, 'destinationId', status.destinationId);
|
|
1799
2067
|
recordOptional(out, 'appendOnly', status.appendOnly);
|
|
2068
|
+
recordOptional(out, 'loggingEnabled', status.loggingEnabled);
|
|
1800
2069
|
recordOptional(out, 'failure', status.failure);
|
|
1801
2070
|
}
|
|
1802
2071
|
/**
|
|
@@ -1945,10 +2214,39 @@ async function applyErrorBucketConfiguration(ctx, name) {
|
|
|
1945
2214
|
await client.putBucketTagging(name, ctx.tags ?? {});
|
|
1946
2215
|
await client.putPublicAccessBlock(name);
|
|
1947
2216
|
}
|
|
2217
|
+
/**
|
|
2218
|
+
* The log group Firehose writes its delivery errors to, and the log stream
|
|
2219
|
+
* inside it that they are written to.
|
|
2220
|
+
*
|
|
2221
|
+
* **Firehose creates neither.** Enabling error logging through the API rather
|
|
2222
|
+
* than the console requires the group *and* the stream to exist in advance, so
|
|
2223
|
+
* this node creates both - and re-ensures the stream on every `update` beside
|
|
2224
|
+
* the retention, which is why {@link analyticsLogGroupNode} takes a stream at
|
|
2225
|
+
* all. Without that, a group left by a run that stopped between
|
|
2226
|
+
* `CreateLogGroup` and `CreateLogStream` is permanently one call short, with
|
|
2227
|
+
* `read()` reporting it present and `update()` doing nothing about it, and the
|
|
2228
|
+
* symptom is a delivery failure with nowhere to be explained.
|
|
2229
|
+
*
|
|
2230
|
+
* {@link DESTINATION_DELIVERY_STREAM} is the only stream created:
|
|
2231
|
+
* `BackupDelivery` belongs to a destination configured with S3 backup, and the
|
|
2232
|
+
* Iceberg destination this pipeline builds configures none.
|
|
2233
|
+
*
|
|
2234
|
+
* It is at the head of the chain that writes to it -
|
|
2235
|
+
* `analytics-firehose-stream` declares the edge - while the delivery role does
|
|
2236
|
+
* not, for the reason `analytics-transform-log-group`'s counterpart does not.
|
|
2237
|
+
*/
|
|
2238
|
+
export function analyticsFirehoseLogGroupNode() {
|
|
2239
|
+
return analyticsLogGroupNode({
|
|
2240
|
+
id: FIREHOSE_LOG_GROUP_NODE,
|
|
2241
|
+
title: `Firehose delivery-error log group (${ANALYTICS_REGION})`,
|
|
2242
|
+
name: firehoseLogGroupName,
|
|
2243
|
+
stream: DESTINATION_DELIVERY_STREAM,
|
|
2244
|
+
});
|
|
2245
|
+
}
|
|
1948
2246
|
/**
|
|
1949
2247
|
* The role Firehose assumes to read the catalog, write the table, invoke the
|
|
1950
|
-
* transform
|
|
1951
|
-
* resources, no `*`.
|
|
2248
|
+
* transform, store what it could not deliver and say so in one log stream -
|
|
2249
|
+
* five grants, five concrete resources, no `*`.
|
|
1952
2250
|
*
|
|
1953
2251
|
* It declares `dependsOn` on the three nodes whose recorded ARNs those grants
|
|
1954
2252
|
* interpolate. `topoSort` drains zero-indegree nodes alphabetically
|
|
@@ -2038,7 +2336,16 @@ export function analyticsFirehoseRoleNode() {
|
|
|
2038
2336
|
export function analyticsFirehoseStreamNode() {
|
|
2039
2337
|
return {
|
|
2040
2338
|
id: FIREHOSE_STREAM_NODE,
|
|
2041
|
-
dependsOn: [
|
|
2339
|
+
dependsOn: [
|
|
2340
|
+
FIREHOSE_ROLE_NODE,
|
|
2341
|
+
TABLE_NODE,
|
|
2342
|
+
CATALOG_NODE,
|
|
2343
|
+
TRANSFORM_FUNCTION_NODE,
|
|
2344
|
+
// Not an ARN this node interpolates: the group has to exist before the
|
|
2345
|
+
// stream can report a delivery failure into it, and on teardown the
|
|
2346
|
+
// stream goes before the group that holds its errors.
|
|
2347
|
+
FIREHOSE_LOG_GROUP_NODE,
|
|
2348
|
+
],
|
|
2042
2349
|
title: `Firehose delivery stream (${ANALYTICS_REGION})`,
|
|
2043
2350
|
async read(ctx) {
|
|
2044
2351
|
const status = await firehose(ctx).describeDeliveryStream(streamName(ctx));
|
|
@@ -2049,10 +2356,11 @@ export function analyticsFirehoseStreamNode() {
|
|
|
2049
2356
|
// "already exists", and the reconcile would go green over a stream that
|
|
2050
2357
|
// accepts nothing.
|
|
2051
2358
|
//
|
|
2052
|
-
// What `update` then does with such a stream
|
|
2053
|
-
// the recorded
|
|
2054
|
-
// `DELETING` stream
|
|
2055
|
-
// calls and reported done
|
|
2359
|
+
// What `update` then does with such a stream may be *nothing*: it branches
|
|
2360
|
+
// on the two recorded flags and nothing else, so a `CREATING_FAILED` or
|
|
2361
|
+
// `DELETING` stream that is already append-only with error logging on is
|
|
2362
|
+
// reconciled with zero AWS calls and reported done - the state is not part
|
|
2363
|
+
// of the comparison. That is stated rather than guarded because this
|
|
2056
2364
|
// `read` is the hydration path - `recordStream` puts `state` and `failure`
|
|
2057
2365
|
// into the plugin's scoped state, and reporting an unusable stream from
|
|
2058
2366
|
// them is `analytics status`' job (task 55). Do not read this comment as a
|
|
@@ -2070,12 +2378,21 @@ export function analyticsFirehoseStreamNode() {
|
|
|
2070
2378
|
async update(ctx) {
|
|
2071
2379
|
const recorded = ctx.state.resources[FIREHOSE_STREAM_NODE];
|
|
2072
2380
|
const appendOnly = typeof recorded?.['appendOnly'] === 'boolean' ? recorded['appendOnly'] : undefined;
|
|
2073
|
-
|
|
2074
|
-
//
|
|
2075
|
-
//
|
|
2076
|
-
//
|
|
2077
|
-
//
|
|
2078
|
-
|
|
2381
|
+
const loggingEnabled = typeof recorded?.['loggingEnabled'] === 'boolean' ? recorded['loggingEnabled'] : undefined;
|
|
2382
|
+
// Both live flags already match what this pipeline wants, so there is
|
|
2383
|
+
// nothing to reconcile and no AWS call at all. `undefined` does NOT match
|
|
2384
|
+
// either: a stream whose destination reported no flag, or a state file that
|
|
2385
|
+
// lost one, is a stream this node cannot claim is configured, and pushing
|
|
2386
|
+
// the desired configuration is the safe direction.
|
|
2387
|
+
//
|
|
2388
|
+
// **The logging half is what reaches the installed base.** Every stream
|
|
2389
|
+
// this plugin has created is append-only, so on `appendOnly` alone this
|
|
2390
|
+
// return fires for all of them and no already-deployed stream would ever
|
|
2391
|
+
// be switched from silent delivery failures to logged ones. The second
|
|
2392
|
+
// condition is what makes an existing stream reconcile exactly once: the
|
|
2393
|
+
// update sends `CloudWatchLoggingOptions`, the re-read records
|
|
2394
|
+
// `loggingEnabled: true`, and the next apply returns here.
|
|
2395
|
+
if (appendOnly === STREAM_APPEND_ONLY && loggingEnabled === true)
|
|
2079
2396
|
return;
|
|
2080
2397
|
const name = streamName(ctx);
|
|
2081
2398
|
const client = firehose(ctx);
|
|
@@ -2098,7 +2415,7 @@ export function analyticsFirehoseStreamNode() {
|
|
|
2098
2415
|
// operator told the update was rejected when it succeeded.
|
|
2099
2416
|
let refusal;
|
|
2100
2417
|
try {
|
|
2101
|
-
ctx.logger.step(`updating the analytics delivery stream "${name}" in place (
|
|
2418
|
+
ctx.logger.step(`updating the analytics delivery stream "${name}" in place (${destinationDrift(appendOnly, loggingEnabled)}) - UpdateDestination keeps the stream's ARN, so the CloudFront log delivery pointed at it is untouched`);
|
|
2102
2419
|
await whileRoleIsPropagating(() => client.updateDestination(name, destination, { versionId, destinationId }));
|
|
2103
2420
|
}
|
|
2104
2421
|
catch (err) {
|
|
@@ -2637,6 +2954,7 @@ export function analyticsLogDeliveryNode() {
|
|
|
2637
2954
|
// missing destination each fail with nothing sent.
|
|
2638
2955
|
const site = requireSiteDeliverySource(ctx);
|
|
2639
2956
|
const destinationArn = requireLogDestinationArn(ctx);
|
|
2957
|
+
const requestDay = utcDay(new Date());
|
|
2640
2958
|
await logs(ctx).createDelivery(site.source, destinationArn, {
|
|
2641
2959
|
// `CLOUDFRONT_RECORD_FIELDS`, never a list restated here: `schema.ts`
|
|
2642
2960
|
// owns which CloudFront fields exist, which ones fill a column, and
|
|
@@ -2669,7 +2987,7 @@ export function analyticsLogDeliveryNode() {
|
|
|
2669
2987
|
// {@link CREATED_DAY_KEY} for why moving it later is the one direction
|
|
2670
2988
|
// that corrupts data rather than merely losing some.
|
|
2671
2989
|
if (typeof out[CREATED_DAY_KEY] !== 'string')
|
|
2672
|
-
out[CREATED_DAY_KEY] =
|
|
2990
|
+
out[CREATED_DAY_KEY] = requestDay;
|
|
2673
2991
|
},
|
|
2674
2992
|
async delete(ctx) {
|
|
2675
2993
|
// This plugin's own deliveries and nothing else - **never
|
|
@@ -2690,7 +3008,7 @@ export function analyticsLogDeliveryNode() {
|
|
|
2690
3008
|
};
|
|
2691
3009
|
}
|
|
2692
3010
|
/**
|
|
2693
|
-
* The plugin's
|
|
3011
|
+
* The plugin's fourteen resource nodes, assembled in the order the change spec's
|
|
2694
3012
|
* §Analytics pipeline → Resource nodes table lists them. This is what
|
|
2695
3013
|
* `Plugin.nodes` (`plugin.ts`) hands the CLI's generic `analytics bootstrap`
|
|
2696
3014
|
* and `analytics destroy` verbs, and it is the whole of what this package
|
|
@@ -2711,7 +3029,7 @@ export function analyticsLogDeliveryNode() {
|
|
|
2711
3029
|
*
|
|
2712
3030
|
* **No `ctx` parameter, deliberately.** The SPI declares `nodes?(ctx)` and the
|
|
2713
3031
|
* CLI calls it with one, so this function is assignable to it as written - a
|
|
2714
|
-
* zero-argument function satisfies a one-argument signature. None of the
|
|
3032
|
+
* zero-argument function satisfies a one-argument signature. None of the fourteen
|
|
2715
3033
|
* factories needs a context to be *built*: each reads `ctx` inside `read`,
|
|
2716
3034
|
* `create`, `update` and `delete`, when the reconcile is actually running. A
|
|
2717
3035
|
* parameter accepted and ignored here would be an unused binding and, worse, a
|
|
@@ -2733,10 +3051,12 @@ export function buildAnalyticsNodes() {
|
|
|
2733
3051
|
analyticsCatalogIntegrationNode(),
|
|
2734
3052
|
// The transform chain.
|
|
2735
3053
|
analyticsSaltSecretNode(),
|
|
3054
|
+
analyticsTransformLogGroupNode(),
|
|
2736
3055
|
analyticsTransformRoleNode(),
|
|
2737
3056
|
analyticsTransformFunctionNode(),
|
|
2738
3057
|
// The delivery chain.
|
|
2739
3058
|
analyticsErrorBucketNode(),
|
|
3059
|
+
analyticsFirehoseLogGroupNode(),
|
|
2740
3060
|
analyticsFirehoseRoleNode(),
|
|
2741
3061
|
analyticsFirehoseStreamNode(),
|
|
2742
3062
|
// The vended-delivery chain.
|