Skip to content

Commit cd13fb0

Browse files
authored
chore(cloud_hub): Prep for production (#315)
- Update fly config to use DB volume for database - Add Sentry configuration - Fix integration with CLI
1 parent e564770 commit cd13fb0

File tree

15 files changed

+215
-97
lines changed

15 files changed

+215
-97
lines changed

apps/cli/lib/src/context.dart

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import 'package:celest_cli/src/project/project_paths.dart';
1515
import 'package:celest_cli/src/serialization/json_generator.dart';
1616
import 'package:celest_cli/src/storage/storage.dart';
1717
import 'package:celest_cli/src/types/type_helper.dart';
18+
import 'package:celest_cli/src/version.dart';
1819
import 'package:celest_cloud/celest_cloud.dart';
1920
import 'package:celest_core/_internal.dart';
2021
import 'package:file/file.dart';
@@ -169,10 +170,11 @@ bool kCelestTest = false;
169170
http.Client httpClient = http.RetryClient(
170171
http.IOClient(
171172
HttpClient()
172-
..userAgent = 'Celest/CLI'
173+
..userAgent = 'Celest-CLI/$packageVersion'
174+
..idleTimeout = const Duration(seconds: 60)
173175
..connectionTimeout = const Duration(seconds: 4),
174176
),
175-
whenError: (e, _) => e is SocketException,
177+
whenError: (e, _) => e is SocketException || e is http.ClientException,
176178
);
177179

178180
/// Global analytics instance.

apps/cli/lib/src/frontend/celest_frontend.dart

Lines changed: 42 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -856,7 +856,7 @@ final class CelestFrontend {
856856
case ast.SdkType.flutter:
857857
final bundleRes = await processManager.run(
858858
[
859-
'flutter',
859+
Sdk.current.flutter ?? 'flutter',
860860
'build',
861861
'bundle',
862862
'--packages=${projectPaths.packagesConfig}',
@@ -977,68 +977,48 @@ final class CelestFrontend {
977977
required ast.ResolvedProject resolvedProject,
978978
}) =>
979979
performance.trace('CelestFrontend', 'deployProject', () async {
980-
try {
981-
final entrypointCompiler = EntrypointCompiler(
982-
logger: logger,
983-
verbose: verbose,
984-
enabledExperiments:
985-
celestProject.analysisOptions.enabledExperiments,
986-
);
987-
final kernel = await entrypointCompiler.compile(
988-
resolvedProject: resolvedProject,
989-
entrypointPath: projectPaths.localApiEntrypoint,
990-
);
991-
final operation = cloud.projects.environments.deploy(
992-
environmentName,
993-
resolvedProject: resolvedProject.toProto(),
994-
assets: [
995-
pb.ProjectAsset(
996-
type: pb.ProjectAsset_Type.DART_KERNEL,
997-
etag: kernel.outputDillDigest.toString(),
998-
filename: p.basename(kernel.outputDillPath),
999-
inline: kernel.outputDill,
1000-
),
1001-
],
1002-
);
1003-
final waiter = CloudCliOperation(
1004-
operation,
1005-
resourceType: 'project',
1006-
logger: logger,
1007-
);
1008-
final deployment = await waiter.run(
1009-
verbs: const (
1010-
run: 'deploy',
1011-
running: 'Deploying',
1012-
completed: 'deployed',
1013-
),
1014-
cancelTrigger: _stopSignal.future,
1015-
resource: pb.DeployProjectEnvironmentResponse(),
1016-
);
1017-
logger.fine('Deployed project: $deployment');
1018-
return (
1019-
ast.ResolvedProject.fromProto(
1020-
pb.ResolvedProject.fromBuffer(
1021-
deployment.project.writeToBuffer(),
1022-
),
1023-
),
1024-
Uri.parse(deployment.uri),
1025-
);
1026-
} on Exception catch (e, st) {
1027-
if (e case CancellationException() || CliException()) {
1028-
rethrow;
1029-
}
1030-
Error.throwWithStackTrace(
1031-
CliException(
1032-
'Failed to deploy project. Please contact the Celest team and '
1033-
'reference environment: $environmentName',
1034-
additionalContext: {
1035-
'environment_name': environmentName,
1036-
'error': '$e',
1037-
},
980+
final entrypointCompiler = EntrypointCompiler(
981+
logger: logger,
982+
verbose: verbose,
983+
enabledExperiments: celestProject.analysisOptions.enabledExperiments,
984+
);
985+
final kernel = await entrypointCompiler.compile(
986+
resolvedProject: resolvedProject,
987+
entrypointPath: projectPaths.localApiEntrypoint,
988+
);
989+
final operation = cloud.projects.environments.deploy(
990+
environmentName,
991+
resolvedProject: resolvedProject.toProto(),
992+
assets: [
993+
pb.ProjectAsset(
994+
type: pb.ProjectAsset_Type.DART_KERNEL,
995+
etag: kernel.outputDillDigest.toString(),
996+
filename: p.basename(kernel.outputDillPath),
997+
inline: kernel.outputDill,
1038998
),
1039-
st,
1040-
);
1041-
}
999+
],
1000+
);
1001+
final waiter = CloudCliOperation(
1002+
operation,
1003+
resourceType: 'project',
1004+
logger: logger,
1005+
);
1006+
final deployment = await waiter.run(
1007+
verbs: const (
1008+
run: 'deploy',
1009+
running: 'Deploying',
1010+
completed: 'deployed',
1011+
),
1012+
cancelTrigger: _stopSignal.future,
1013+
resource: pb.DeployProjectEnvironmentResponse(),
1014+
);
1015+
final deployedProject =
1016+
deployment.project.unpackInto(pb.ResolvedProject());
1017+
logger.fine('Deployed project to ${deployment.uri}: $deployedProject');
1018+
return (
1019+
ast.ResolvedProject.fromProto(deployedProject),
1020+
Uri.parse(deployment.uri),
1021+
);
10421022
});
10431023

10441024
Future<void> _generateClientCode({

examples/tasks/celest/client/lib/tasks_client.dart

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,14 @@ export 'package:celest_backend/src/functions/tasks.dart' show ServerException;
2222
final Celest celest = Celest();
2323

2424
enum CelestEnvironment {
25-
local;
25+
local,
26+
production;
2627

2728
Uri get baseUri => switch (this) {
2829
local => _$celest.kIsWeb || !Platform.isAndroid
29-
? Uri.parse('http://localhost:53358')
30-
: Uri.parse('http://10.0.2.2:53358'),
30+
? Uri.parse('http://localhost:7777')
31+
: Uri.parse('http://10.0.2.2:7777'),
32+
production => Uri.parse('https://tasks-694b15.fly.dev'),
3133
};
3234
}
3335

@@ -67,11 +69,16 @@ class Celest with _$celest.CelestBase {
6769
CelestEnvironment environment = CelestEnvironment.local,
6870
_$celest.Serializers? serializers,
6971
}) {
72+
if (_initialized) {
73+
_reset();
74+
}
7075
_currentEnvironment = environment;
7176
_baseUri = environment.baseUri;
72-
if (!_initialized) {
73-
initSerializers(serializers: serializers);
74-
}
77+
initSerializers(serializers: serializers);
7578
_initialized = true;
7679
}
80+
81+
void _reset() {
82+
_initialized = false;
83+
}
7784
}

packages/celest/lib/src/runtime/http/cloud_middleware.dart

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,14 +75,23 @@ final class CorsMiddleware implements Middleware {
7575
/// {@endtemplate}
7676
final class CloudExceptionMiddleware implements Middleware {
7777
/// {@macro celest.runtime.cloud_exception_middleware}
78-
const CloudExceptionMiddleware();
78+
const CloudExceptionMiddleware({
79+
this.onException,
80+
});
81+
82+
/// Optional function to be called when an exception occurs.
83+
///
84+
/// This can be used to react to exceptions but does not affect the default
85+
/// handling for responses.
86+
final void Function(Object, StackTrace)? onException;
7987

8088
@override
8189
Handler call(Handler inner) {
8290
return (request) async {
8391
try {
8492
return await inner(request);
8593
} on CloudException catch (e, st) {
94+
onException?.call(e, st);
8695
context.logger.severe(e.message, e, st);
8796
return Response(
8897
e.code,
@@ -109,6 +118,7 @@ final class CloudExceptionMiddleware implements Middleware {
109118
);
110119
} on Exception catch (e, st) {
111120
if (e is HijackException) rethrow;
121+
onException?.call(e, st);
112122
context.logger.severe('An unexpected exception occurred', e, st);
113123
return Response.badRequest(
114124
headers: const {
@@ -133,6 +143,7 @@ final class CloudExceptionMiddleware implements Middleware {
133143
}),
134144
);
135145
} on Error catch (e, st) {
146+
onException?.call(e, st);
136147
context.logger.shout('An unexpected error occurred', e, st);
137148
return Response.internalServerError(
138149
headers: const {

packages/celest_cloud/lib/src/cloud/cloud.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
/// @docImport 'dart:io';
22
library;
33

4+
import 'package:celest_ast/src/proto/celest/ast/v1/resolved_ast.pb.dart'
5+
as astpb;
46
import 'package:celest_cloud/src/cloud/authentication/authentication.dart';
57
import 'package:celest_cloud/src/cloud/cloud_protocol.dart';
68
import 'package:celest_cloud/src/cloud/cloud_protocol.grpc.dart';
@@ -133,6 +135,9 @@ class CelestCloud {
133135
ProjectEnvironment(),
134136
DeployProjectEnvironmentResponse(),
135137

138+
// AST
139+
astpb.ResolvedProject(),
140+
136141
// RPC
137142
pb.Status(),
138143

services/celest_cloud_auth/lib/src/authorization/authorization_middleware.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ extension type AuthorizationMiddleware._(_Deps _deps) implements Object {
7272
requestPath,
7373
);
7474
if (result == null) {
75-
throw core.InternalServerError(
75+
throw core.NotFoundException(
7676
'Route not found: ${request.method} $requestPath',
7777
);
7878
}

services/celest_cloud_hub/Dockerfile

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,35 @@
1+
# Builds SQLite from Source
2+
#
3+
# The version shipped with apt is very outdated (3.40.1). This is the only way
4+
# it seems to get anything newer.
5+
FROM debian:bookworm-slim AS sqlite
6+
7+
# Install build dependencies
8+
RUN apt-get update && apt-get install -y \
9+
build-essential \
10+
tcl-dev \
11+
libreadline-dev \
12+
wget \
13+
unzip \
14+
&& rm -rf /var/lib/apt/lists/*
15+
16+
# Set up build directory
17+
WORKDIR /sqlite_build
18+
19+
# Download the latest SQLite source
20+
ARG SQLITE_VERSION=3490100
21+
RUN wget https://www.sqlite.org/2025/sqlite-autoconf-${SQLITE_VERSION}.tar.gz \
22+
&& tar xzf sqlite-autoconf-${SQLITE_VERSION}.tar.gz \
23+
&& cd sqlite-autoconf-${SQLITE_VERSION}
24+
25+
# Build SQLite with default options
26+
WORKDIR /sqlite_build/sqlite-autoconf-${SQLITE_VERSION}
27+
RUN CFLAGS='-DSQLITE_ENABLE_FTS5 -DSQLITE_ENABLE_JSON1' \
28+
./configure --prefix=/usr \
29+
--enable-threadsafe \
30+
&& make -j$(nproc) \
31+
&& make install DESTDIR=/sqlite_install
32+
133
FROM dart:stable AS build
234

335
# Set the dependency versions
@@ -22,11 +54,6 @@ RUN arch=$(uname -m) && \
2254
chmod +x /usr/local/bin/yq && \
2355
rm -rf flyctl.tar.gz /tmp/flyctl
2456

25-
# Install sqlite3
26-
WORKDIR /app
27-
RUN apt update && apt install -y libsqlite3-0
28-
RUN cp $(find / -name libsqlite3.so* -type f | head -n1) /app/libsqlite3.so
29-
3057
# Fix pub cache
3158
WORKDIR /app
3259
COPY tool tool
@@ -52,11 +79,24 @@ WORKDIR /app/services/celest_cloud_hub
5279
RUN dart pub get
5380
RUN dart compile exe bin/cloud_hub.dart -o /app/cloud_hub
5481

55-
FROM scratch
56-
COPY --from=build /runtime/ /
57-
COPY --from=build /app/cloud_hub /app/cloud_hub
58-
COPY --from=build /app/libsqlite3.so /app/libsqlite3.so
82+
FROM debian:bookworm-slim
83+
84+
# Configure SQLite
85+
COPY --from=sqlite /sqlite_install /
86+
87+
# Install dependencies
88+
RUN apt-get update && apt-get install -y \
89+
ca-certificates \
90+
libreadline8 \
91+
&& rm -rf /var/lib/apt/lists/*
92+
93+
# Verify SQLite installation
94+
RUN sqlite3 --version && \
95+
ldconfig && \
96+
ldd $(which sqlite3)
97+
5998
COPY --from=build /usr/local/bin/flyctl /usr/local/bin/flyctl
99+
COPY --from=build /app/cloud_hub /app/cloud_hub
60100

61101
ENV PORT=8080
62102
EXPOSE $PORT

services/celest_cloud_hub/bin/cloud_hub.dart

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,18 @@ import 'package:celest_cloud_hub/src/services/projects_service.dart';
2323
import 'package:celest_core/_internal.dart';
2424
import 'package:grpc/grpc.dart' as grpc;
2525
import 'package:logging/logging.dart';
26+
import 'package:sentry/sentry.dart';
27+
import 'package:sentry_logging/sentry_logging.dart';
2628
import 'package:stack_trace/stack_trace.dart';
2729

2830
Future<void> main() async {
29-
context.logger.level = Level.INFO;
31+
context.logger.level = switch (Platform.environment['LOG_LEVEL']) {
32+
final level? => Level.LEVELS.firstWhere(
33+
(l) => l.name.toLowerCase() == level.toLowerCase(),
34+
orElse: () => throw StateError('Invalid log level: $level'),
35+
),
36+
_ => Level.INFO,
37+
};
3038
context.logger.onRecord.listen((record) {
3139
print('${record.level.name}: ${record.time}: ${record.message}');
3240
if (record.error != null) {
@@ -44,6 +52,40 @@ Future<void> main() async {
4452

4553
context.logger.config('Starting Cloud Hub');
4654

55+
final sentryDsn = Platform.environment['SENTRY_DSN'];
56+
if (sentryDsn == null) {
57+
return _run();
58+
}
59+
return Sentry.init(
60+
(options) {
61+
options
62+
..dsn = sentryDsn
63+
..environment = kDebugMode ? 'dev' : 'prod'
64+
..debug = context.logger.level > Level.INFO
65+
..tracesSampleRate = 1
66+
..sampleRate = 1
67+
..attachStacktrace = true
68+
..sendDefaultPii = true
69+
..attachThreads = true
70+
..captureFailedRequests = true
71+
..httpClient = context.httpClient
72+
..markAutomaticallyCollectedErrorsAsFatal = true
73+
..enableDeduplication = true
74+
..addIntegration(
75+
// Only using for breadcrumbs.
76+
LoggingIntegration(
77+
minBreadcrumbLevel: Level.ALL,
78+
minEventLevel: const Level('EVENT', 1500),
79+
),
80+
);
81+
},
82+
appRunner: _run,
83+
// ignore: invalid_use_of_internal_member
84+
callAppRunnerInRunZonedGuarded: false,
85+
);
86+
}
87+
88+
Future<void> _run() async {
4789
context.logger.config('Configuring Cloud Hub database');
4890
final db = await connect(
4991
Context.current,
@@ -120,8 +162,10 @@ Future<void> main() async {
120162
await gateway.start();
121163
},
122164
onError: (error, stackTrace) {
123-
context.logger.severe('Unexpected error', error, stackTrace);
124-
exit(1);
165+
context.logger.shout('Unexpected error', error, stackTrace);
166+
if (Sentry.isEnabled) {
167+
Sentry.captureException(error, stackTrace: stackTrace);
168+
}
125169
},
126170
);
127171

0 commit comments

Comments
 (0)