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 status, previous_status
REMOVED App deleted (empty)

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 identify pushes by this sentinel URI and the absence of a matching pending request UUID.

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)
  - onTimerSpawn        │                  │──▶ DeliveryCallback(WSS)
  - onExitUpdate        │  subscribe()     │
  - health(bool)        │  unsubscribe()   │
  - enable/disable      │  removeByConn()  │
  - Configuration::     │  removeByApp()   │
    removeApp           └──────────────────┘
                               │
                    ┌──────────┼──────────┐
                    │ StdoutWatcher (1s)  │
                    │ per-app timer poll  │
                    └─────────────────────┘

15.4.1. Thread Safety

  • EventDispatcher uses std::recursive_mutex for all operations

  • dispatch() delivers events and cleans dead subscriptions in a single lock scope (no TOCTOU)

  • StdoutWatcher timer callback uses weak_ptr capture to prevent use-after-free

  • Connection cleanup (removeByConnection) is called from SocketServer::onClose and WebSocketService::destroySession after releasing transport-specific locks

15.4.2. Ownership Enforcement

  • unsubscribe() verifies the requesting user matches the subscription owner

  • Connection disconnect auto-removes all subscriptions for that connection

  • App deletion dispatches REMOVED event then purges all subscriptions

15.5. SDK Usage

Note: Event subscription requires a persistent connection (TCP or WebSocket). The C++ SDK (ClientHttp) is HTTP-only and does not support subscriptions.

15.5.1. Go

// Subscribe to events
client, _ := appmesh.NewTCPClient(appmesh.Option{})
client.Login("admin", "admin123", "", 0, "")

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
app, _ := client.AddApp(appmesh.Application{
    Name:    "myapp",
    Command: ptr("ping github.com"),
}, "START", "EXIT", "STDOUT")
fmt.Println("subscription_id:", app.SubscriptionID)

// Unsubscribe
client.Unsubscribe(result.SubscriptionID)

15.5.2. Python

from appmesh import AppMeshClientTCP, App

client = AppMeshClientTCP()
client.login("admin", "admin123")

# 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'

const client = new AppMeshClientTCP()
await client.login('admin', 'admin123')

// 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.login("admin", "admin123");

// 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.login("admin", "admin123", None, None, None).await?;

// Subscribe to events
let result = client.subscribe("myapp", Some(&["START", "EXIT"])).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 not supported — stdout polling requires a specific app name.

  • Server restart clears all subscriptions (in-memory only). Clients must re-subscribe after reconnecting.

  • Event ordering is guaranteed per-subscription via a monotonic sequence counter. Cross-subscription ordering is not guaranteed.