Skip to content

Commit 3a2efdf

Browse files
committed
chore(rivetkit): remove legacy metrics & prometheus auth
1 parent 8f5aeb9 commit 3a2efdf

11 files changed

Lines changed: 129 additions & 107 deletions

File tree

.agent/notes/inspector-security-audit.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Audited the native Rust inspector HTTP/WebSocket surface in `rivetkit-rust/packa
1313
- Rust HTTP `/inspector/*` routes call `InspectorAuth::verify(...)` before route dispatch.
1414
- Rust inspector WebSocket `/inspector/connect` verifies the `rivet_inspector_token.*` websocket protocol token first, then falls back to `Authorization: Bearer ...`.
1515
- TypeScript native HTTP `/inspector/*` routes call `ctx.verifyInspectorAuth(...)`, which delegates to the same core `InspectorAuth`.
16-
- `InspectorAuth` prefers `RIVET_INSPECTOR_TOKEN` when configured. If absent, it falls back to the per-actor KV token at key `[3]`.
16+
- `InspectorAuth` prefers `_RIVET_TEST_INSPECTOR_TOKEN` (test-only override, not public) when configured. If absent, it falls back to the per-actor KV token at key `[3]`, which is the production auth path.
1717
- Fixed in US-094: Rust bearer parsing now matches TS more closely by accepting case-insensitive `Bearer` and arbitrary whitespace after the scheme.
1818

1919
## HTTP Endpoint Matrix

frontend/src/components/actors/guard-connectable-inspector.tsx

Lines changed: 6 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rivetkit-rust/packages/rivetkit-core/src/actor/task.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1101,6 +1101,8 @@ impl ActorTask {
11011101
}
11021102

11031103
async fn start_actor(&mut self) -> Result<()> {
1104+
let startup_started_at = Instant::now();
1105+
let actor_id = self.ctx.actor_id().to_owned();
11041106
if !self.ctx.started() {
11051107
self.ctx.configure_sleep(self.factory.config().clone());
11061108
self.ctx
@@ -1110,7 +1112,13 @@ impl ActorTask {
11101112
self.ctx.configure_actor_events(self.actor_event_tx.clone());
11111113
self.ctx.configure_queue_preload(self.preloaded_kv.clone());
11121114

1115+
let load_state_started_at = Instant::now();
11131116
let persisted = self.load_persisted_startup().await?;
1117+
tracing::debug!(
1118+
actor_id = %actor_id,
1119+
duration_ms = duration_ms_f64(load_state_started_at.elapsed()),
1120+
"perf internal: loadStateMs"
1121+
);
11141122
let is_new = !persisted.actor.has_initialized;
11151123
self.ctx.load_persisted_actor(persisted.actor);
11161124
self.ctx.load_last_pushed_alarm(persisted.last_pushed_alarm);
@@ -1119,9 +1127,15 @@ impl ActorTask {
11191127
.persist_state(SaveStateOpts { immediate: true })
11201128
.await
11211129
.context("persist actor initialization")?;
1130+
let init_inspector_token_started_at = Instant::now();
11221131
crate::inspector::init_inspector_token(&self.ctx)
11231132
.await
11241133
.context("initialize inspector token")?;
1134+
tracing::debug!(
1135+
actor_id = %actor_id,
1136+
duration_ms = duration_ms_f64(init_inspector_token_started_at.elapsed()),
1137+
"perf internal: initInspectorTokenMs"
1138+
);
11251139
self.ctx
11261140
.restore_hibernatable_connections_with_preload(self.preloaded_kv.as_ref())
11271141
.await
@@ -1135,6 +1149,12 @@ impl ActorTask {
11351149
self.spawn_run_handle(is_new).await?;
11361150
self.reset_sleep_deadline().await;
11371151
self.ctx.drain_overdue_scheduled_events().await?;
1152+
tracing::debug!(
1153+
actor_id = %actor_id,
1154+
duration_ms = duration_ms_f64(startup_started_at.elapsed()),
1155+
is_new,
1156+
"perf internal: startupTotalMs"
1157+
);
11381158
Ok(())
11391159
}
11401160

@@ -2041,3 +2061,7 @@ fn result_outcome<T>(result: &Result<T>) -> &'static str {
20412061
fn state_delta_payload_bytes(deltas: &[StateDelta]) -> usize {
20422062
deltas.iter().map(StateDelta::payload_len).sum()
20432063
}
2064+
2065+
fn duration_ms_f64(duration: Duration) -> f64 {
2066+
duration.as_secs_f64() * 1000.0
2067+
}

rivetkit-rust/packages/rivetkit-core/src/inspector/auth.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ use serde::{Deserialize, Serialize};
77
use crate::ActorContext;
88

99
const INSPECTOR_TOKEN_KEY: [u8; 1] = [3];
10-
const INSPECTOR_TOKEN_ENV: &str = "RIVET_INSPECTOR_TOKEN";
10+
/// Test-only override. Not a public/production auth mechanism; production
11+
/// inspector auth goes through the per-actor KV token at key [3].
12+
const INSPECTOR_TOKEN_ENV: &str = "_RIVET_TEST_INSPECTOR_TOKEN";
1113
const INSPECTOR_TOKEN_BYTES: usize = 32;
1214

1315
#[derive(Clone, Copy, Debug, Default)]
@@ -53,9 +55,9 @@ impl InspectorAuth {
5355
/// Ensures the actor has an inspector token persisted in KV at `[3]` so the
5456
/// engine-facing KV API can serve the token to the dashboard inspector.
5557
/// Skips the write when the token already exists. No-ops when the
56-
/// `RIVET_INSPECTOR_TOKEN` env override is set, since that takes precedence
57-
/// over any KV-stored token and we do not want to pin a per-actor token that
58-
/// will never be consulted.
58+
/// `_RIVET_TEST_INSPECTOR_TOKEN` env override is set, since that takes
59+
/// precedence over any KV-stored token and we do not want to pin a per-actor
60+
/// token that will never be consulted.
5961
pub async fn init_inspector_token(ctx: &ActorContext) -> Result<()> {
6062
if std::env::var(INSPECTOR_TOKEN_ENV)
6163
.ok()

rivetkit-rust/packages/rivetkit-core/src/registry/http.rs

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ impl RegistryDispatcher {
7777
FrameworkHttpRoute::Queue(name) => {
7878
self.handle_queue_fetch(instance, request, name).await
7979
}
80+
FrameworkHttpRoute::Metadata => handle_metadata_fetch(&request),
81+
FrameworkHttpRoute::Health => handle_health_fetch(&request),
82+
FrameworkHttpRoute::Root => handle_root_fetch(&request),
8083
}
8184
}
8285

@@ -330,7 +333,7 @@ impl RegistryDispatcher {
330333
instance: &ActorTaskHandle,
331334
request: &HttpRequest,
332335
) -> Result<HttpResponse> {
333-
if !request_has_bearer_token(request, self.inspector_token.as_deref()) {
336+
if !request_has_bearer_token(request, self.metrics_token.as_deref()) {
334337
return Ok(unauthorized_response());
335338
}
336339

@@ -358,6 +361,9 @@ impl RegistryDispatcher {
358361
pub(super) enum FrameworkHttpRoute {
359362
Action(String),
360363
Queue(String),
364+
Metadata,
365+
Health,
366+
Root,
361367
}
362368

363369
pub(super) struct DecodedHttpQueueRequest {
@@ -377,7 +383,75 @@ pub(super) fn framework_http_route(path: &str) -> Result<Option<FrameworkHttpRou
377383
percent_decode_path_segment(segment)?,
378384
)));
379385
}
380-
Ok(None)
386+
match path {
387+
"/metadata" => Ok(Some(FrameworkHttpRoute::Metadata)),
388+
"/health" => Ok(Some(FrameworkHttpRoute::Health)),
389+
"/" => Ok(Some(FrameworkHttpRoute::Root)),
390+
_ => Ok(None),
391+
}
392+
}
393+
394+
fn handle_metadata_fetch(request: &Request) -> Result<HttpResponse> {
395+
if request.method() != http::Method::GET {
396+
return method_not_allowed_response(request);
397+
}
398+
let runtime_type = if std::env::var("NODE_ENV").as_deref() == Ok("production") {
399+
"deployed"
400+
} else {
401+
"local"
402+
};
403+
json_http_response(
404+
StatusCode::OK,
405+
&serde_json::json!({
406+
"runtime": "rivetkit",
407+
"version": env!("CARGO_PKG_VERSION"),
408+
"type": runtime_type,
409+
}),
410+
)
411+
}
412+
413+
fn handle_health_fetch(request: &Request) -> Result<HttpResponse> {
414+
if request.method() != http::Method::GET {
415+
return method_not_allowed_response(request);
416+
}
417+
text_response(StatusCode::OK, "ok")
418+
}
419+
420+
fn handle_root_fetch(request: &Request) -> Result<HttpResponse> {
421+
if request.method() != http::Method::GET {
422+
return method_not_allowed_response(request);
423+
}
424+
text_response(
425+
StatusCode::OK,
426+
"This is an RivetKit actor.\n\nLearn more at https://rivetkit.org",
427+
)
428+
}
429+
430+
fn text_response(status: StatusCode, body: &str) -> Result<HttpResponse> {
431+
let mut headers = HashMap::new();
432+
headers.insert(
433+
http::header::CONTENT_TYPE.to_string(),
434+
"text/plain; charset=utf-8".to_owned(),
435+
);
436+
Ok(HttpResponse {
437+
status: status.as_u16(),
438+
headers,
439+
body: Some(body.as_bytes().to_vec()),
440+
body_stream: None,
441+
})
442+
}
443+
444+
fn method_not_allowed_response(request: &Request) -> Result<HttpResponse> {
445+
let encoding = request_encoding(request.headers());
446+
message_boundary_error_response(
447+
encoding,
448+
framework_error_status("actor", "method_not_allowed"),
449+
MethodNotAllowed {
450+
method: request.method().to_string(),
451+
path: request.uri().path().to_owned(),
452+
}
453+
.build(),
454+
)
381455
}
382456

383457
pub(super) fn single_path_segment<'a>(path: &'a str, prefix: &str) -> Option<&'a str> {

rivetkit-rust/packages/rivetkit-core/src/registry/mod.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,9 @@ pub(crate) struct RegistryDispatcher {
121121
starting_instances: SccHashMap<String, Arc<Notify>>,
122122
pending_stops: SccHashMap<String, PendingStop>,
123123
region: String,
124-
inspector_token: Option<String>,
124+
/// Shared secret gating the Prometheus `/metrics` actor endpoint. When
125+
/// unset, the endpoint fails closed and is effectively disabled.
126+
metrics_token: Option<String>,
125127
handle_inspector_http_in_runtime: bool,
126128
}
127129

@@ -479,7 +481,7 @@ impl RegistryDispatcher {
479481
starting_instances: SccHashMap::new(),
480482
pending_stops: SccHashMap::new(),
481483
region: env::var("RIVET_REGION").unwrap_or_default(),
482-
inspector_token: env::var("RIVET_INSPECTOR_TOKEN")
484+
metrics_token: env::var("_RIVET_METRICS_TOKEN")
483485
.ok()
484486
.filter(|token| !token.is_empty()),
485487
handle_inspector_http_in_runtime,

rivetkit-typescript/packages/rivetkit/src/utils/env-vars.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,6 @@ export const getRivetPublicToken = (): string | undefined =>
3838
// use different namespaces
3939

4040
// RivetKit configuration
41-
export const getRivetkitInspectorToken = (): string | undefined =>
42-
getEnvUniversal("RIVET_INSPECTOR_TOKEN");
4341
export const getRivetkitInspectorDisable = (): boolean =>
4442
getEnvUniversal("RIVET_INSPECTOR_DISABLE") === "1";
4543
export const getRivetkitStoragePath = (): string | undefined =>

rivetkit-typescript/packages/rivetkit/tests/driver/actor-inspector.test.ts

Lines changed: 0 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -802,89 +802,5 @@ describeDriverMatrix("Actor Inspector", (driverTestConfig) => {
802802
expect(response.status).toBe(401);
803803
});
804804

805-
test("GET /inspector/metrics returns startup metrics", async (c) => {
806-
const { client } = await setupDriverTest(c, driverTestConfig);
807-
const handle = client.counter.getOrCreate(["inspector-metrics"]);
808-
809-
// Ensure actor exists
810-
await handle.increment(0);
811-
812-
const gatewayUrl = await handle.getGatewayUrl();
813-
814-
const response = await fetch(
815-
buildInspectorUrl(gatewayUrl, "/inspector/metrics"),
816-
{
817-
headers: { Authorization: "Bearer token" },
818-
},
819-
);
820-
expect(response.status).toBe(200);
821-
const data: any = await response.json();
822-
823-
// Verify startup metrics are present and have reasonable values
824-
expect(data.startup_total_ms).toBeDefined();
825-
expect(data.startup_total_ms.type).toBe("gauge");
826-
expect(data.startup_total_ms.value).toBeGreaterThan(0);
827-
828-
expect(data.startup_kv_round_trips).toBeDefined();
829-
expect(data.startup_kv_round_trips.type).toBe("gauge");
830-
expect(data.startup_kv_round_trips.value).toBeGreaterThanOrEqual(0);
831-
832-
expect(data.startup_is_new).toBeDefined();
833-
expect(data.startup_is_new.type).toBe("gauge");
834-
835-
// Verify internal metrics exist
836-
expect(data.startup_internal_load_state_ms).toBeDefined();
837-
expect(
838-
data.startup_internal_load_state_ms.value,
839-
).toBeGreaterThanOrEqual(0);
840-
expect(data.startup_internal_init_queue_ms).toBeDefined();
841-
expect(data.startup_internal_init_inspector_token_ms).toBeDefined();
842-
843-
// Verify user metrics exist
844-
expect(data.startup_user_create_vars_ms).toBeDefined();
845-
expect(data.startup_user_on_wake_ms).toBeDefined();
846-
expect(data.startup_user_create_state_ms).toBeDefined();
847-
848-
// Verify existing KV metrics still present
849-
expect(data.kv_operations).toBeDefined();
850-
expect(data.kv_operations.type).toBe("labeled_timing");
851-
});
852-
853-
test("GET /inspector/metrics returns sqlite commit phase metrics", async (c) => {
854-
const { client } = await setupDriverTest(c, driverTestConfig);
855-
const handle = client.dbActorRaw.getOrCreate([
856-
`inspector-sqlite-metrics-${crypto.randomUUID()}`,
857-
]);
858-
859-
await handle.insertValue("hello");
860-
861-
const gatewayUrl = await handle.getGatewayUrl();
862-
const response = await fetch(
863-
buildInspectorUrl(gatewayUrl, "/inspector/metrics"),
864-
{
865-
headers: { Authorization: "Bearer token" },
866-
},
867-
);
868-
expect(response.status).toBe(200);
869-
const data: any = await response.json();
870-
871-
expect(data.sqlite_commit_phases).toBeDefined();
872-
expect(data.sqlite_commit_phases.type).toBe("labeled_timing");
873-
expect(
874-
data.sqlite_commit_phases.values.request_build.calls,
875-
).toBeGreaterThan(0);
876-
expect(
877-
data.sqlite_commit_phases.values.request_build.totalMs,
878-
).toBeGreaterThan(0);
879-
expect(
880-
data.sqlite_commit_phases.values.serialize.totalMs,
881-
).toBeGreaterThan(0);
882-
expect(
883-
data.sqlite_commit_phases.values.transport.totalMs,
884-
).toBeGreaterThan(0);
885-
expect(
886-
data.sqlite_commit_phases.values.state_update.totalMs,
887-
).toBeGreaterThan(0);
888-
});
889805
});
890806
});

website/src/content/docs/actors/debugging.mdx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -269,11 +269,13 @@ Standard actor endpoints (health, actions, requests) and inspector endpoints hav
269269

270270
#### Inspector Endpoints
271271

272+
Each actor generates a unique inspector token on first start and persists it in its internal KV store at key `0x03` (base64 `Aw==`). Pass it as a bearer token in the `Authorization` header.
273+
272274
| Environment | Authentication |
273275
|---|---|
274-
| **Local development** | No authentication required if `RIVET_INSPECTOR_TOKEN` is not set. A warning is logged. |
275-
| **Self-hosted engine** | Set the `RIVET_INSPECTOR_TOKEN` environment variable. Pass it as a bearer token in the `Authorization` header. The actor-specific inspector token used by the standalone Inspector UI is also accepted. |
276-
| **Rivet Cloud** | Token is required. Pass it as a bearer token in the `Authorization` header. The actor-specific inspector token used by the standalone Inspector UI is also accepted. |
276+
| **Local development** | No authentication required. |
277+
| **Self-hosted engine** | Bearer the actor's inspector token in the `Authorization` header. The Rivet dashboard fetches it automatically; for direct API access, fetch it through the management KV endpoint (see below). |
278+
| **Rivet Cloud** | Bearer the actor's inspector token in the `Authorization` header. The Rivet dashboard fetches it automatically; for direct API access, fetch it through the management KV endpoint (see below). |
277279

278280
```bash
279281
curl "$RIVET_API/gateway/{actor_id}/inspector/summary" \

website/src/content/docs/general/environment-variables.mdx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,14 @@ These variables configure how clients connect to your actors.
4141

4242
| Environment Variable | Description |
4343
|---------------------|-------------|
44-
| `RIVET_INSPECTOR_TOKEN` | Token for accessing the Rivet Inspector |
4544
| `RIVET_INSPECTOR_DISABLE` | Set to `1` to disable the inspector |
4645

46+
## Metrics
47+
48+
| Environment Variable | Description |
49+
|---------------------|-------------|
50+
| `_RIVET_METRICS_TOKEN` | Bearer token that gates the per-actor Prometheus `/metrics` endpoint. When unset, `/metrics` is disabled. |
51+
4752
## Experimental
4853

4954
| Environment Variable | Description |

0 commit comments

Comments
 (0)