LandfallDocs
Reference · Source SDK

Source SDK

How to write a telemetry source Landfall can read from without a Landfall code change: implement the Landfall Source Protocol in any language, host it, run the conformance suite against it, and connect it by URL. The same suite is what every bundled source passes, so a service that passes it behaves inside the platform exactly as a bundled one does. The design is on Data sources architecture; the admin-facing setup is on Remote source.

Implementing the protocol

The protocol is version 1, defined once as a schema and rendered as an OpenAPI document for HTTP and a protobuf contract for gRPC. The two carry identical operations and fields. HTTP is the transport a service must implement; gRPC is offered from the same schema and the conformance suite covers both. This guide leads with HTTP.

HTTPgRPC rpcRequestResponse
GET /v1/capabilitiesCapabilities{ protocolVersion: "1", source, kinds[], operations: OperationDescriptor[] }
GET /v1/healthHealth{ ok: boolean, detail?: string }
POST /v1/invokeInvoke{ operation, params, tenant }RawResult
POST /v1/metrics/listMetricsList{ namespace?, match?, tenant }RawResult
POST /v1/metrics/rangeMetricsRange{ expr, window, step?, tenant }RawResult plus optional series
POST /v1/logs/streamsLogsStreams{ prefix?, label?, tenant }RawResult
POST /v1/logs/rangeLogsRange{ query, window, limit?, tenant }RawResult plus optional entries

Rules the suite enforces:

  • capabilities.operations must list every operation invoke accepts. An unknown operation is 404 (HTTP) or NOT_FOUND (gRPC), which Landfall records as a partial result.
  • A service that declares kinds: ["metrics"] must implement metrics/list and metrics/range; the same for logs with logs/streams and logs/range. A service may declare both kinds, or neither (invoke only).
  • Responses larger than 4 MiB are truncated by Landfall into a partial result with error: "response too large"; reads follow the same timeout budget as bundled sources, so answer within seconds, not minutes.
  • tenant is an opaque per-connection reference Landfall sends on every call ({ ref: string }). It is never an organization id; use it to key any per-connection state you keep.

Authentication

Every call carries the credential the admin supplied at connection time. On HTTP it is Authorization: Bearer <token>; on gRPC the same token arrives as authorization metadata. For mutual TLS, Landfall presents the client certificate stored on the connection and your service verifies it against the CA it trusts. A service must answer 401 or 403 (HTTP) or UNAUTHENTICATED / PERMISSION_DENIED (gRPC) for a bad credential; Landfall maps both to a partial result with error: "credentials rejected" and the connection shows failed until the credential is updated.

The response envelope

Every read returns one shape. The raw field is your provider's payload, verbatim; on gRPC it is a JSON string, because google.protobuf.Struct is lossy.

RawResult
{
  "source": "acme-metrics",              // your own tag, echoed
  "operation": "metrics/range",
  "params": { "expr": "cpu", "window": { "fromMs": 1757181600000, "toMs": 1757185200000 } },
  "fetchedAt": "2026-09-06T18:00:00Z",   // RFC 3339
  "raw": { ... },                        // verbatim provider payload
  "partial": false,                      // true when the read is incomplete
  "error": null,                         // the reason, when partial
  "series": [ ... ],                     // optional shaped form, metrics
  "entries": [ ... ]                     // optional shaped form, logs
}

Two supporting shapes describe what you advertise. Every operation declares one kind and, when it takes a time range, a window declaration that tells Landfall how you name and encode the range parameters:

OperationDescriptor and WindowSpec
OperationDescriptor: {
  operation: string,                 // your own name for it
  description: string,
  params: { [name]: string },        // parameter hints, for the agent
  kind: "metrics" | "logs" | "changes" | "chat",
  canonical?: "list" | "range" | "streams",   // which canonical call this backs
  window?: WindowSpec
}
WindowSpec: {
  start: string, end: string, step?: string,  // your parameter names
  format: "date" | "iso" | "epoch-s" | "epoch-ms" | "epoch-ns" | "grafana",
  stepFormat?: string, minStepSeconds?: number
}
WindowBounds: { fromMs: integer, toMs: integer }   // what Landfall sends on canonical calls

Optional series and entries

A canvas chart, the incident watcher and the live-metrics poller need series; a log widget needs entries. If your service can produce them cheaply, return them beside the raw payload and Landfall uses them directly. If you leave them out, or return an empty array, Landfall falls back to the raw payload and the read is not an error. Points and entries must be sorted ascending by time, times are ISO 8601, and values are finite numbers; the suite checks this on fixtures.

RawSeries and LogEntry
RawSeries: { label: string, unit?: string, points: [{ t: string, v: number }] }
LogEntry:  { t: string, line: string, labels?: { [name]: string }, stream?: string }

Running the reference server

The SDK ships a reference implementation over both transports, backed by a deterministic fake metrics and logs store. It is the documented example for protocol authors, the fixture the conformance suite runs against in the monorepo, and the quickest way to see a remote source in the catalog. It accepts the bearer token dev-token.

Terminal
node dist/libs/signals/protocol/reference-server.js --http 8787 --grpc 8788

curl -s -H "Authorization: Bearer dev-token" http://localhost:8787/v1/health
curl -s -H "Authorization: Bearer dev-token" http://localhost:8787/v1/capabilities
curl -s -X POST -H "Authorization: Bearer dev-token" -H "Content-Type: application/json" \
  http://localhost:8787/v1/metrics/range \
  -d '{"expr":"cpu","window":{"fromMs":1757181600000,"toMs":1757185200000},"tenant":{"ref":"local"}}'

Then connect it from Settings → Integrations → Remote source with base URL http://localhost:8787, transport HTTP and token dev-token, following Remote source. The source of the reference server (libs/signals/src/protocol/reference-server.ts) is the shortest complete example of every endpoint.

Running the conformance suite against your service

The suite is the contract. It runs against any source, bundled or remote, and reports a named violation rather than a vague failure. It is a Jest helper, sourceConformance(name, factory, fixtures), that produces a full describe block; the bundled plugins each run it, and the remote plugin runs it against the reference server. To run it against your own service, write one spec in a checkout of the monorepo that builds the remote plugin's client at your base URL:

libs/signals/test/my-service.conformance.spec.ts
import { sourceConformance } from '../src/testing/conformance';
import { RemoteSourcePlugin } from '../src/plugins/remote/remote.plugin';

sourceConformance(
  'my-service',
  () => RemoteSourcePlugin.connect({
    baseUrl: process.env.SOURCE_URL ?? 'http://localhost:8787',
    transport: 'http',
    bearerToken: process.env.SOURCE_TOKEN ?? 'dev-token',
  }),
  { metricsExpr: 'cpu', logsQuery: '{app="checkout"}' },
);
Terminal
SOURCE_URL=http://localhost:8787 SOURCE_TOKEN=dev-token \
  pnpm nx test signals --testFile my-service.conformance

It checks that:

  • every advertised operation invokes, or fails with a typed error;
  • an operation that is not advertised fails;
  • every windowed operation declares an applicable window;
  • each declared kind's canonical operations are wired and answer;
  • series and entries, when returned, are well ordered and well typed on fixture reads;
  • the health check fails closed: an empty or wrong credential is refused, never accepted by default.

The integrations wizard runs the same checks at Test connection before marking a remote source connected, so passing the suite locally means the connection will verify.

Opening an incident from your service

The protocol defines a trigger payload your service posts to the organization's ingest door, POST /triggers, with the organization's ingest token. Each remote connection has its own trigger identity, and the same dedup and recovery handling as every other automated trigger. The step-by-step guide, field table and refusal cases are on Triggering Integrations → Remote source.

SourceTriggerPayload
{
  "landfallSource": "remote",            // required; how Landfall recognizes the shape
  "connectionId": "acme-metrics",        // required; the remote connection this alert belongs to
  "title": "checkout p99 > 2s",          // required
  "severity": "sev2",                    // required: sev1 | sev2 | sev3 | sev4
  "occurredAt": "2026-09-06T18:00:00Z",
  "entityHints": ["service:checkout"],
  "dedupKey": "checkout-p99-2026-09-06", // stable per alert; a recovery reuses it
  "summary": "...",
  "url": "https://...",
  "kind": "trigger"                      // or "recovery"
}

Versioning

protocolVersion is a string your capabilities listing reports. Version 1 is additive-only: fields and operations may be added, never removed or renamed, so a service written against today's contract keeps working. A service reporting a major version Landfall does not speak is refused at Test connection with a clear message rather than connected and broken. The OpenAPI document and the protobuf contract are generated from the one schema and checked into the monorepo (libs/signals/protocol/source-protocol.openapi.yaml and libs/signals/proto/source.proto); a test fails the build if either drifts from the schema.

Later: submitting a package to the marketplace

Hosting a service is the path for anyone outside Landfall today. Some transports cannot be hosted sensibly (a vendor SDK that must run in-process); those are built as packages, by Landfall now and, later, by screened third parties. The marketplace's data model is designed for that path so it can be added without changing what an admin sees: submission of a package in the same shape as a bundled source, an isolated build, the conformance suite plus a security screen, signing, and a package index bundled into a release image. None of that path exists yet, and this section is a design note, not a promise of timing. Until it does, write a service.

Reference

Protocol version1
TransportsHTTP (required, JSON) and gRPC (optional, same schema, raw as a JSON string)
AuthBearer token, or mutual TLS with a client certificate Landfall presents
Schemalibs/signals/src/protocol/schema.ts
OpenAPIlibs/signals/protocol/source-protocol.openapi.yaml
Protobuflibs/signals/proto/source.proto
Reference serverlibs/signals/src/protocol/reference-server.ts
Conformance suitelibs/signals/src/testing/conformance.ts
Size and time budget4 MiB per response; the bundled-source timeout budget