15. Event Subscription
App Mesh supports real-time event subscription over persistent connections (TCP and WebSocket). Clients can subscribe to specific application events and receive server-push notifications without polling.
15.1. Event Types
| Event | Description | Data Fields |
|---|---|---|
START |
Process spawned | pid, process_uuid |
EXIT |
Process exited | pid, exit_code, last_error |
STDOUT |
Stdout output available | output, position, finished |
HEALTH |
Health status changed | health (0=healthy, 1=unhealthy), previous_health |
STATUS |
App enabled/disabled | enabled, previous_enabled |
REMOVED |
App deleted | (empty) |
The events value accepts the event names above and ALL. An empty events
value also selects all events.
15.2. REST API
15.2.1. Subscribe (per-app)
POST /appmesh/app/{app_name}/subscribe?events=START,EXIT,STDOUT
Authorization: Bearer <JWT>
Response:
{
"subscription_id": "cqk8g7l4d",
"app_name": "myapp",
"events": ["START", "EXIT", "STDOUT"]
}
15.2.2. Subscribe (all apps)
POST /appmesh/subscribe?events=START,EXIT
15.2.3. Unsubscribe
DELETE /appmesh/app/{app_name}/subscribe?subscription_id=cqk8g7l4d
15.2.4. Subscribe at Registration
Register an app and subscribe atomically (no events missed):
PUT /appmesh/app/{app_name}?subscribe_events=START,EXIT,STDOUT
Content-Type: application/json
{ "name": "myapp", "command": "python3 server.py" }
The response includes subscription_id alongside the normal app JSON when subscription is active.
Note: Subscribe requires a persistent connection (TCP or WebSocket). REST/HTTP returns
405 Method Not Allowed.
15.3. Event Push Message Format
Events are delivered as standard Response messages with request_uri = "/appmesh/event". Clients route a push by this sentinel URI alone.
Response {
uuid: "<event-uuid>"
request_uri: "/appmesh/event"
http_status: 200
body: {
"subscription_id": "cqk8g7l4d",
"event_type": "EXIT",
"app_name": "myapp",
"timestamp": 1714000000,
"sequence": 42,
"data": { "pid": 12345, "exit_code": 1 }
}
headers: {
"X-Subscription-Id": "cqk8g7l4d",
"X-Event-Type": "EXIT",
"X-App-Name": "myapp"
}
}
15.4. Architecture
┌───────────────────────┐
│ EventDispatcher │ (singleton)
│ │
Application hooks ──▶ │ dispatch() │──▶ DeliveryCallback(TCP)
- onStartAccepted │ │──▶ DeliveryCallback(WSS)
- recordProcessExit │ subscribe() │
- health(bool) │ unsubscribe() │
- enable/disable │ removeByConnection() │
- Configuration:: │ removeByApp() │
removeApp └───────────────────────┘
│
┌──────────┼──────────┐
│ StdoutStrategy │
│ pipe/timer fallback │
└─────────────────────┘
15.4.1. Thread Safety
EventDispatcheruses a non-recursivestd::mutexonly to snapshot or mutate subscription indexesdispatch()uses a separate delivery mutex to assign sequence numbers and invoke transport delivery callbacks serially; this preserves per-subscription order without holding the subscription-index mutex in a callbackthe delivery mutex is intentionally global to avoid per-subscription queue/lock state; transport callbacks must remain bounded and must not re-enter
EventDispatcher, because one slow delivery delays later server-side emissionsfailed subscriptions are removed in a second short index critical section; unsubscribe may race an already-snapshotted delivery, whose copied callback remains valid for that one in-flight call
stdout strategies are activated only after
STARTpublication and are torn down outside the process resource lock, so final drain/delivery cannot re-enter a held process locktimer-based stdout callbacks capture shared state and use
weak_ptr<Application>rather than a raw application ownerConnection cleanup (
removeByConnection) is called fromSocketServer::onCloseandWebSocketService::destroySessionafter releasing transport-specific locks
15.4.2. Ownership Enforcement
unsubscribe()verifies the requesting user matches the subscription ownerConnection disconnect auto-removes all subscriptions for that connection
App deletion dispatches
REMOVEDevent then purges all subscriptions
15.5. SDK Usage
Note: Event subscription requires a persistent connection (TCP or WebSocket). The C++ SDK (
AppMeshClient) is HTTP-only and does not support subscriptions.Client-side delivery guarantees (event ordering, the synthetic
__disconnected__event, pre-registration buffering, timeout and cleanup policy) are normatively defined in SDKContract.md.
15.5.1. Go
// Subscribe to events
client, _ := appmesh.NewTCPClient(appmesh.Option{})
client.SetToken(os.Getenv("APPMESH_BEARER_TOKEN"))
result, _ := client.Subscribe(appmesh.SubscribeOption{
AppName: "myapp",
Events: []string{"START", "EXIT"},
}, func(event appmesh.AppEvent) {
fmt.Printf("Event: %s app=%s\n", event.EventType, event.AppName)
})
// Register app with atomic subscribe
command := "ping github.com"
app, _ := client.AddApp(appmesh.Application{
Name: "myapp",
Command: &command,
}, "START", "EXIT", "STDOUT")
fmt.Println("subscription_id:", app.SubscriptionID)
// Unsubscribe
client.Unsubscribe(result.SubscriptionID)
15.5.2. Python
import os
from appmesh import AppMeshClientTCP, App
client = AppMeshClientTCP(bearer_token=os.environ["APPMESH_BEARER_TOKEN"])
# Subscribe to events
def on_event(event):
print(f"Event: {event.event_type} app={event.app_name}")
result = client.subscribe("myapp", ["START", "EXIT"], on_event)
# Register app with atomic subscribe
app = client.add_app(
App({"name": "myapp", "command": "ping github.com"}),
subscribe_events=["START", "EXIT", "STDOUT"]
)
# Unsubscribe
client.unsubscribe(result.subscription_id)
15.5.3. JavaScript
import { AppMeshClientTCP } from 'appmesh/tcp'
const client = new AppMeshClientTCP()
client.set_bearer_token(process.env.APPMESH_BEARER_TOKEN)
// Subscribe
const result = await client.subscribe('myapp', ['START', 'EXIT'], (event) => {
console.log(`Event: ${event.event_type} app=${event.app_name}`)
})
// Unsubscribe
await client.unsubscribe(result.subscription_id)
15.5.4. Java
AppMeshClientTCP client = new AppMeshClientTCP.Builder().disableSSLVerify().build();
client.setBearerToken(System.getenv("APPMESH_BEARER_TOKEN"));
// Subscribe to events
JSONObject result = client.subscribe("myapp", "START", "EXIT");
String subId = result.getString("subscription_id");
// Register app with atomic subscribe
JSONObject app = new JSONObject().put("name", "myapp").put("command", "ping github.com");
JSONObject appResult = client.addApp("myapp", app, "START", "EXIT", "STDOUT");
// appResult has "subscription_id" when active
// Unsubscribe
client.unsubscribe(subId);
15.5.5. Rust
let client = ClientBuilderTCP::new().danger_accept_invalid_certs(true).build()?;
client.client().set_token(&std::env::var("APPMESH_BEARER_TOKEN")?);
// Subscribe to events
let result = client.subscribe("myapp", Some(&["START", "EXIT"]), None).await?;
// Register app with atomic subscribe
let app = Application::builder("myapp").command("ping github.com").build();
let created = client.add_app(&app, Some(&["START", "STDOUT"])).await?;
println!("subscription_id: {:?}", created.subscription_id);
// Unsubscribe
client.unsubscribe(&result.subscription_id).await?;
15.6. Limitations
REST/HTTP does not support subscriptions (no persistent connection). Use TCP or WebSocket.
Wildcard stdout (
/appmesh/subscribe?events=STDOUT) is supported; it participates in live delivery and the final stdout drain for every matching application.Server restart clears all subscriptions (in-memory only). Clients must re-subscribe after reconnecting.
Event ordering is guaranteed per-subscription via a monotonic
sequencecounter. Cross-subscription ordering is not guaranteed.