Skip to content

Commit 88c62aa

Browse files
committed
merge PR strands-agents#31 dashboards + resolve conflicts with local changes
1 parent c67c506 commit 88c62aa

File tree

4 files changed

+104
-49
lines changed

4 files changed

+104
-49
lines changed

community-dashboard/cdk/lib/community-dashboard-stack.ts

Lines changed: 79 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ import * as cdk from "aws-cdk-lib";
22
import * as ec2 from "aws-cdk-lib/aws-ec2";
33
import * as ecs from "aws-cdk-lib/aws-ecs";
44
import * as efs from "aws-cdk-lib/aws-efs";
5-
import * as elbv2 from "aws-cdk-lib/aws-elasticloadbalancingv2";
5+
import * as apigwv2 from "aws-cdk-lib/aws-apigatewayv2";
66
import * as cloudfront from "aws-cdk-lib/aws-cloudfront";
77
import * as origins from "aws-cdk-lib/aws-cloudfront-origins";
88
import * as logs from "aws-cdk-lib/aws-logs";
99
import * as secretsmanager from "aws-cdk-lib/aws-secretsmanager";
10+
import * as servicediscovery from "aws-cdk-lib/aws-servicediscovery";
1011
import { Construct } from "constructs";
1112
import * as path from "path";
1213

@@ -15,8 +16,6 @@ export class CommunityDashboardStack extends cdk.Stack {
1516
super(scope, id, props);
1617

1718
// ── Secrets Manager ──────────────────────────────────────────────────
18-
// The GitHub PAT must already exist in Secrets Manager as a plain-text
19-
// secret. Pass the ARN via the GITHUB_SECRET_ARN env var or CDK context.
2019
const secretArn =
2120
process.env.GITHUB_SECRET_ARN ??
2221
this.node.tryGetContext("githubSecretArn");
@@ -25,7 +24,7 @@ export class CommunityDashboardStack extends cdk.Stack {
2524
throw new Error(
2625
"GITHUB_SECRET_ARN environment variable or 'githubSecretArn' CDK context must be set.\n" +
2726
"Create the secret first:\n" +
28-
' aws secretsmanager create-secret --name strands-grafana/github-token --secret-string "ghp_xxx" --region us-west-2'
27+
' aws secretsmanager create-secret --name strands-grafana/github-token --secret-string "ghp_xxx" --region us-east-1'
2928
);
3029
}
3130

@@ -63,7 +62,16 @@ export class CommunityDashboardStack extends cdk.Stack {
6362
},
6463
});
6564

66-
// ── ECS Cluster ─────────────────────────────────────────────────────
65+
// ── ECS Cluster + Cloud Map namespace ───────────────────────────────
66+
const namespace = new servicediscovery.PrivateDnsNamespace(
67+
this,
68+
"Namespace",
69+
{
70+
name: "community-dashboard.local",
71+
vpc,
72+
}
73+
);
74+
6775
const cluster = new ecs.Cluster(this, "Cluster", {
6876
vpc,
6977
containerInsights: true,
@@ -75,7 +83,6 @@ export class CommunityDashboardStack extends cdk.Stack {
7583
memoryLimitMiB: 1024,
7684
});
7785

78-
// Mount EFS volume
7986
taskDef.addVolume({
8087
name: "metrics-data",
8188
efsVolumeConfiguration: {
@@ -88,23 +95,18 @@ export class CommunityDashboardStack extends cdk.Stack {
8895
},
8996
});
9097

91-
// Grant EFS access to the task role
9298
fileSystem.grant(
9399
taskDef.taskRole,
94100
"elasticfilesystem:ClientMount",
95101
"elasticfilesystem:ClientWrite",
96102
"elasticfilesystem:ClientRootAccess"
97103
);
98104

99-
// Container definition — built from the unified Dockerfile
100105
const container = taskDef.addContainer("grafana", {
101-
image: ecs.ContainerImage.fromAsset(
102-
path.join(__dirname, "../../"),
103-
{
104-
file: "docker/Dockerfile",
105-
platform: cdk.aws_ecr_assets.Platform.LINUX_AMD64,
106-
}
107-
),
106+
image: ecs.ContainerImage.fromAsset(path.join(__dirname, "../../"), {
107+
file: "docker/Dockerfile",
108+
platform: cdk.aws_ecr_assets.Platform.LINUX_AMD64,
109+
}),
108110
logging: ecs.LogDrivers.awsLogs({
109111
streamPrefix: "community-dashboard",
110112
logRetention: logs.RetentionDays.TWO_WEEKS,
@@ -131,63 +133,97 @@ export class CommunityDashboardStack extends cdk.Stack {
131133
readOnly: false,
132134
});
133135

134-
// ── Fargate Service + ALB ───────────────────────────────────────────
136+
// ── Fargate Service with Cloud Map service discovery ────────────────
135137
const service = new ecs.FargateService(this, "Service", {
136138
cluster,
137139
taskDefinition: taskDef,
138140
desiredCount: 1,
139141
assignPublicIp: false,
140142
platformVersion: ecs.FargatePlatformVersion.LATEST,
143+
enableExecuteCommand: true,
144+
cloudMapOptions: {
145+
cloudMapNamespace: namespace,
146+
name: "grafana",
147+
containerPort: 3000,
148+
dnsRecordType: servicediscovery.DnsRecordType.SRV,
149+
},
141150
});
142151

143-
// Allow the service to reach EFS
144152
service.connections.allowTo(fileSystem, ec2.Port.tcp(2049), "EFS access");
145153

146-
// Application Load Balancer
147-
const alb = new elbv2.ApplicationLoadBalancer(this, "Alb", {
148-
vpc,
149-
internetFacing: true,
154+
// ── API Gateway HTTP API + VPC Link ─────────────────────────────────
155+
// No ALB needed — API Gateway connects directly to ECS via VPC Link.
156+
// This avoids Epoxy/Riddler flagging a public-facing Grafana instance.
157+
const vpcLink = new apigwv2.CfnVpcLink(this, "VpcLink", {
158+
name: "community-dashboard-vpc-link",
159+
subnetIds: vpc.privateSubnets.map((s) => s.subnetId),
160+
securityGroupIds: [service.connections.securityGroups[0].securityGroupId],
150161
});
151162

152-
const listener = alb.addListener("HttpListener", {
153-
port: 80,
163+
const httpApi = new apigwv2.CfnApi(this, "HttpApi", {
164+
name: "community-dashboard-api",
165+
protocolType: "HTTP",
166+
description: "API Gateway for Community Dashboard (Grafana)",
154167
});
155168

156-
listener.addTargets("GrafanaTarget", {
157-
port: 3000,
158-
protocol: elbv2.ApplicationProtocol.HTTP,
159-
targets: [service],
160-
healthCheck: {
161-
path: "/api/health",
162-
interval: cdk.Duration.seconds(30),
163-
healthyThresholdCount: 2,
164-
unhealthyThresholdCount: 3,
165-
},
166-
deregistrationDelay: cdk.Duration.seconds(30),
169+
// Integration: forward all requests to the Cloud Map service via VPC Link
170+
const integration = new apigwv2.CfnIntegration(this, "Integration", {
171+
apiId: httpApi.ref,
172+
integrationType: "HTTP_PROXY",
173+
integrationMethod: "ANY",
174+
connectionType: "VPC_LINK",
175+
connectionId: vpcLink.ref,
176+
integrationUri: service.cloudMapService!.serviceArn,
177+
payloadFormatVersion: "1.0",
167178
});
168179

169-
// ── Outputs ─────────────────────────────────────────────────────────
170-
// CloudFront distribution — provides HTTPS on *.cloudfront.net
180+
new apigwv2.CfnRoute(this, "DefaultRoute", {
181+
apiId: httpApi.ref,
182+
routeKey: "$default",
183+
target: `integrations/${integration.ref}`,
184+
});
185+
186+
const stage = new apigwv2.CfnStage(this, "DefaultStage", {
187+
apiId: httpApi.ref,
188+
stageName: "$default",
189+
autoDeploy: true,
190+
});
191+
192+
// Allow API Gateway VPC Link to reach the ECS service
193+
service.connections.allowFrom(
194+
ec2.Peer.ipv4(vpc.vpcCidrBlock),
195+
ec2.Port.tcp(3000),
196+
"Allow API Gateway VPC Link"
197+
);
198+
199+
// ── CloudFront ──────────────────────────────────────────────────────
200+
// Extract the API Gateway domain from the endpoint URL
201+
const apiDomain = cdk.Fn.select(
202+
2,
203+
cdk.Fn.split("/", httpApi.attrApiEndpoint)
204+
);
205+
171206
const distribution = new cloudfront.Distribution(this, "Distribution", {
172207
defaultBehavior: {
173-
origin: new origins.HttpOrigin(alb.loadBalancerDnsName, {
174-
protocolPolicy: cloudfront.OriginProtocolPolicy.HTTP_ONLY,
208+
origin: new origins.HttpOrigin(apiDomain, {
209+
protocolPolicy: cloudfront.OriginProtocolPolicy.HTTPS_ONLY,
175210
}),
176211
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
177212
cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED,
178-
originRequestPolicy: cloudfront.OriginRequestPolicy.ALL_VIEWER,
213+
originRequestPolicy: cloudfront.OriginRequestPolicy.ALL_VIEWER_EXCEPT_HOST_HEADER,
179214
allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL,
180215
},
181216
});
182217

218+
// ── Outputs ─────────────────────────────────────────────────────────
183219
new cdk.CfnOutput(this, "GrafanaUrl", {
184220
value: `https://${distribution.distributionDomainName}`,
185221
description: "Grafana dashboard URL (HTTPS via CloudFront)",
186222
});
187223

188-
new cdk.CfnOutput(this, "AlbUrl", {
189-
value: `http://${alb.loadBalancerDnsName}`,
190-
description: "Grafana dashboard URL (ALB, HTTP only)",
224+
new cdk.CfnOutput(this, "ApiGatewayUrl", {
225+
value: httpApi.attrApiEndpoint,
226+
description: "API Gateway endpoint URL",
191227
});
192228

193229
new cdk.CfnOutput(this, "EfsFileSystemId", {
@@ -202,7 +238,7 @@ export class CommunityDashboardStack extends cdk.Stack {
202238

203239
new cdk.CfnOutput(this, "CreateSecretCommand", {
204240
value:
205-
'aws secretsmanager create-secret --name strands-grafana/github-token --secret-string "ghp_xxx" --region us-west-2',
241+
'aws secretsmanager create-secret --name strands-grafana/github-token --secret-string "ghp_xxx" --region us-east-1',
206242
description: "Command to create the GitHub token secret (one-time)",
207243
});
208244
}

community-dashboard/docker/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ FROM grafana/grafana:latest
2222
USER root
2323

2424
# Install ca-certificates and curl for downloading supercronic
25-
RUN apk add --no-cache ca-certificates curl
25+
RUN apk add --no-cache ca-certificates curl sqlite
2626

2727
# Install supercronic (cron replacement designed for containers)
2828
ARG SUPERCRONIC_VERSION=v0.2.43

community-dashboard/docker/entrypoint.sh

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,18 @@ echo "[entrypoint] Syncing package downloads..."
4848
strands-metrics --db-path "$DB_PATH" sync-downloads --config-path "$CONFIG_DIR/packages.yaml" || \
4949
echo "[entrypoint] WARNING: Failed to sync downloads."
5050

51+
# ── One-time metrics recompute (set RECOMPUTE_METRICS=true to trigger) ───────
52+
if [ "${RECOMPUTE_METRICS:-false}" = "true" ]; then
53+
echo "[entrypoint] RECOMPUTE_METRICS=true — wiping daily_metrics for full recalculation..."
54+
sqlite3 "$DB_PATH" "DELETE FROM daily_metrics;"
55+
echo "[entrypoint] daily_metrics cleared. Next sync will rebuild all aggregates."
56+
if [ -n "$GITHUB_TOKEN" ]; then
57+
echo "[entrypoint] Running sync + full recompute now..."
58+
strands-metrics --db-path "$DB_PATH" sync || \
59+
echo "[entrypoint] WARNING: Recompute sync failed (will retry on next cron run)."
60+
fi
61+
fi
62+
5163
# ── Cron schedule ───────────────────────────────────────────────────────────
5264
# Sync daily at 06:00 UTC. Output is forwarded to container stdout/stderr
5365
# via /proc/1/fd/1 so it shows up in docker logs / CloudWatch.

community-dashboard/strands-metrics/src/aggregates.rs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use anyhow::Result;
2-
use chrono::{DateTime, Duration, NaiveDate, TimeZone, Utc};
2+
use chrono::{Duration, NaiveDate, NaiveTime, Utc};
33
use rusqlite::{params, Connection};
44

55
pub fn compute_metrics(conn: &Connection) -> Result<()> {
@@ -10,11 +10,17 @@ pub fn compute_metrics(conn: &Connection) -> Result<()> {
1010

1111
let start_date = match last_metric_date {
1212
Some(d) => NaiveDate::parse_from_str(&d, "%Y-%m-%d")
13-
.map(|nd| Utc.from_utc_datetime(&nd.and_hms_opt(0, 0, 0).unwrap()) - Duration::days(3))
13+
.map(|nd| {
14+
nd.and_time(NaiveTime::MIN)
15+
.and_utc()
16+
.checked_sub_signed(Duration::days(3))
17+
.unwrap()
18+
})
1419
.unwrap_or_else(|_| Utc::now()),
15-
None => DateTime::parse_from_rfc3339("2010-01-01T00:00:00Z")
20+
None => NaiveDate::from_ymd_opt(2025, 5, 1)
1621
.unwrap()
17-
.with_timezone(&Utc),
22+
.and_time(NaiveTime::MIN)
23+
.and_utc(),
1824
};
1925

2026
let start_date_str = start_date.format("%Y-%m-%d").to_string();
@@ -56,8 +62,9 @@ pub fn compute_metrics(conn: &Connection) -> Result<()> {
5662
let now = Utc::now();
5763
let num_days = (now - start_date).num_days();
5864

65+
// Iterate newest-first so dashboards show current data ASAP
5966
for i in 0..=num_days {
60-
let date = start_date + Duration::days(i);
67+
let date = now - Duration::days(i);
6168
let date_str = date.format("%Y-%m-%d").to_string();
6269

6370
conn.execute(

0 commit comments

Comments
 (0)