Skip to main content

Registering Event Types

To deploy statements that read event types X, you must first register X.

Unregistered statements silently match nothing

Statements over unregistered types compile, deploy, run, and never match anything. Green jobs, green pipelines, idle alarms—and nothing later catches them. So the system names and rejects them at deploy time.

What to Register​

ItemRules
name^[A-Za-z_][A-Za-z0-9_]*$. Must be identical to the name the statement uses as FROM X
fields[]At least one. An empty list is rejected — "nothing to choose from"
Field typeFive scalars STRING · BOOLEAN · INT · LONG · DOUBLE, a timestamp TIMESTAMP, five maps MAP_STRING · MAP_BOOLEAN · MAP_INT · MAP_LONG · MAP_DOUBLE
Field nameSame pattern. event_time is not allowed, nor are duplicates
event_time_fieldRequired. No default. Either the literal @envelope, or the name of a declared LONG field (epoch ms)
event_time_field having no default is intentional

A type that guesses its event timestamp produces statements that run and match nothing.

The only thing you can reference is a LONG field (epoch ms). TIMESTAMP can be a column, but it is not a valid source for a timestamp basis.

TIMESTAMP and MAP_*​

Back when there were only five scalars, the platform's own event classes could not be registered — because Point · PointMap · Alarm · AssetData · AssetEvent each carried their own dates or map members. The engine could carry them; the issue was they had no name in the registry.

MAP_* carries the value type in its name.

{ "name": "data_map", "type": "MAP_STRING" },
{ "name": "timestamp_map", "type": "MAP_LONG" }

A single asset event can carry both a string map and an epoch map. If you collapse everything into strings, the epochs become strings, comparisons become lexicographic, and statements still run.

  • Keys are always STRING — JSON object keys have no other kind.
  • Value types are only one of the five scalars. A map whose values are themselves objects is rejected by member name. If you flatten them to strings, statements compare values that are meaningless.
  • MAP_* converts per item and rejects any item it cannot convert by key (timestamp_map[bad]) — if you only name the column, you can't find which of fifty items failed.
TIMESTAMP is an "instant," not a "wall clock"

The TIMESTAMP shapes the projection accepts are only two — a number (epoch ms), and a date object holding that number.

ISO strings are rejected by name. Such a string is rendered in the writer's default timezone and carries no offset, so when the reader parses it, TaskManager's timezone is applied. The window shifts by that difference and nothing turns red.

⚠ The wire shape hasn't changed. The field is still {"name": …, "type": …} and the schema version is unchanged — because the value type lives "inside the type name."

API​

MethodPathBehavior
GET/api/v1/event-typesList, sorted by name
GET/api/v1/event-types/{name}Get one. 404 if missing
POST/api/v1/event-typesRegister. Returns 409 if the name already exists
PUT/api/v1/event-types/{name}?redeploy=falseUpdate. Returns 409 if statements use it; if redeploy=true, redeploys in the same call
DELETE/api/v1/event-types/{name}Delete. Returns 409 if statements use it
# 등록
curl -fsS -X POST http://127.0.0.1:7400/api/v1/event-types \
-H "X-API-Key: ${PP_CEP_API_KEY}" -H 'Content-Type: application/json' \
-d '{
"name": "AlarmEvent",
"event_time_field": "occurred_at",
"fields": [
{ "name": "machineId", "type": "STRING" },
{ "name": "severity", "type": "INT" },
{ "name": "occurred_at","type": "LONG" }
]
}'

# 목록
curl -fsS -H "X-API-Key: ${PP_CEP_API_KEY}" http://127.0.0.1:7400/api/v1/event-types

# 스키마를 바꾸고 그 타입을 쓰는 문장까지 같은 호출에서 재배포
curl -fsS -X PUT "http://127.0.0.1:7400/api/v1/event-types/AlarmEvent?redeploy=true" \
-H "X-API-Key: ${PP_CEP_API_KEY}" -H 'Content-Type: application/json' -d @alarm-event.json
  • The default for redeploy is false.
  • If a redeploy via redeploy=true fails even once, it rolls back to the previous schema.
  • If the name in the path differs from the name in the body, you get 400 — this endpoint changes a type, not renames it.
  • If the registry isn't initialized yet, you get 503 Event type registry is not initialized.. It doesn't return an empty list, because that would be read as "no registered types," and then the engine rejects every statement.

Storage​

The PostgreSQL datalake DB's cep_event_type table.

CREATE TABLE IF NOT EXISTS cep_event_type (
name TEXT NOT NULL,
event_time_field TEXT NOT NULL,
fields_json TEXT NOT NULL,
version BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
updated_by TEXT,
CONSTRAINT cep_event_type_pkey PRIMARY KEY (name)
);

The table is created by the CEP at startup. It migrates its own set of tables (cep_event_type · cep_statement · cep_api_key) — all "create if not exists" and none with drop statements, so it applies cleanly to installations that already have data.

A read failure is an exception, not an empty result. If it returned an empty result, the engine would reject every statement for a properly registered type, and that rejection would name that type as if the operator had typed it wrong.

If the table is missing, health check drops with a name

If the table is entirely missing, the registry fails in place, and those statements are rejected as if they were unregistered types — so the operator goes hunting for a typo.

That's why /api/health checks the schema separately. It reports a missing table by name via DEGRADED. If the DB comes up late during startup ordering, it recovers on its own.

How a Type Becomes an Engine Stream​

Each registered type becomes one input stream for the backend — stream name = event type name.

There is exactly one place where a type is moved to an engine type, and that branch has no default fallback — if a type is added, the compile breaks on purpose. If it silently fell through as "unknown type," a column would become meaningless.

Timestamps are treated as "instants"

Epoch milliseconds are instants without a timezone. Putting an instant into a timezone-less "wall clock" type is like choosing a timezone but not saying so, and every comparison against another timestamp column silently shifts by the session offset.

You don't need to deploy event classes

Payloads are read as JSON and projected to the registered schema. No producer Java class is loaded on the engine side, so a new event type is just "a row in a table."

  • A value of the wrong type is rejected by name. Putting 3.5 into INT is rejected, not truncated.
  • A missing field is left as null — intended asymmetry.

Rejection Messages for Unregistered Types​

Deploy request (POST /api/v1/eql/deploy, 400, EQL deploy failed: prefix):

event type(s) not registered: X, Y. A statement over an unregistered type deploys, runs
and matches nothing, so it is refused here. Registered types are: A, B

When the registry is empty, the last statement is No event type is registered at all..

Engine side:

event type [X] is not registered. Registered types are: A, B, C
event type [X] is not registered - no event type is registered at all. Register it before deploying a statement that reads it
event type [X] has no input table in this environment after <how> - tables present: [...]

Control plane:

the engine refused this rule and will refuse it again: <위 메시지>

Check Points — Two, Both Before Job Submission​

WhenWhy there
Deploy request (/api/v1/eql/deploy)An unregistered type won't be caught later, so it's named and caught here
Engine submissionBefore the query compiles, not after the job runs
The deploy gate "opens noisily"

If the registry can't be read or a statement can't be parsed, deployment is allowed but the success message includes the reason. That means an operator reading "deployed" also reads "event type was not checked."