Skip to content

Connecting to GLog

How any service (SaaS/PaaS in the gStack ecosystem, or an external system) sends audit events to GLog and reads them back. GLog is the centralized, tamper-evident audit/logging microservice: you POST an event, GLog stores it immutably (SHA-256 hash chain per (tenant, service)) and makes it searchable.

TL;DR — one HTTP call per event: POST https://glog.gstack.esempla.systems/api/v1/app/audit-events with a small JSON body. No SDK required to start; official SDKs (Java on Maven, PHP on Composer) are available (§7, sdk.md).

Related docs: api.md (full endpoint/field reference), the runnable glog.postman_collection.json, and live OpenAPI at /api/v1/openapi.json (Swagger UI /swagger-ui.html).


Demo (live)https://glog.gstack.esempla.systems
Internal (on the shared box, container→container)http://glog-frontend (nginx → glog-backend)
IngestPOST /api/v1/app/audit-events
Get oneGET /api/v1/app/audit-events/{id}
SearchGET /api/v1/app/audit-events?tenantId&from&to&objectType&page&size
IntegrityGET /api/v1/admin/audit-events/integrity
Health / metricsGET /health/ready, /health/live, /metrics

All bodies and responses are application/json.


GLog is an OAuth2 resource server. Two modes:

  • Demo deployment — auth is bypassed; send no token. Every request is tenant demo, so use "tenantId": "demo". This is the mode the live URL above runs in.
  • Production — send Authorization: Bearer <jwt> from Keycloak (realm interdictii). The token must carry:
    • a scope: audit:write (ingest), audit:read (search/get), audit:admin (integrity/replay);
    • a tenant claim that equals the tenantId in your request — this enforces tenant isolation (you can only write/read your own tenant).

A service authenticates with the client-credentials grant (machine-to-machine): it gets a token from Keycloak once, caches it, and refreshes before expiry.


Minimum body — five required fields:

{
"tenantId": "demo",
"eventTimestamp": "2026-07-24T09:01:05Z",
"service": "gpay",
"objectType": "PAYMENT",
"actionType": "CREATE"
}

Everything else is optional but recommended: operation, status (SUCCESS/FAILURE), actorType, userDetails, ipAddress, objectAffected, idnp, correlationId, and a free-form details object. Full field table in api.md.

Response: 201 Created, Location: …/{id}, and the stored event (with server-computed id, receivedAt, entryHash, prevHash).

  1. Set a correlationId shared across every service touching one business flow (a request id, a payment id, …). GLog then stitches the whole cross-service timeline together — the “why don’t I see X?” investigation.
  2. Send an Idempotency-Key header on events you might retry — a repeated key returns the first-stored event instead of creating a duplicate.

curl

Terminal window
curl -X POST https://glog.gstack.esempla.systems/api/v1/app/audit-events \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: pay-5521-created' \
-d '{
"tenantId":"demo","eventTimestamp":"2026-07-24T09:01:05Z",
"service":"gpay","objectType":"PAYMENT","actionType":"CREATE",
"operation":"PAYMENT_PROCESS","status":"SUCCESS","objectAffected":"pay-5521",
"correlationId":"corr-1001","details":{"amount":1250,"currency":"MDL"}
}'

Java (JDK 11+ HttpClient, no dependencies)

var body = """
{"tenantId":"demo","eventTimestamp":"%s","service":"gpay",
"objectType":"PAYMENT","actionType":"CREATE","status":"SUCCESS",
"correlationId":"corr-1001","details":{"amount":1250}}
""".formatted(java.time.Instant.now());
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("https://glog.gstack.esempla.systems/api/v1/app/audit-events"))
.header("Content-Type", "application/json")
// .header("Authorization", "Bearer " + token) // production
.POST(java.net.http.HttpRequest.BodyPublishers.ofString(body))
.build();
java.net.http.HttpClient.newHttpClient()
.sendAsync(req, java.net.http.HttpResponse.BodyHandlers.ofString()); // fire-and-forget

PHP (Guzzle)

$client = new GuzzleHttp\Client(['base_uri' => 'https://glog.gstack.esempla.systems']);
$client->postAsync('/api/v1/app/audit-events', [
// 'headers' => ['Authorization' => "Bearer $token"], // production
'json' => [
'tenantId' => 'demo',
'eventTimestamp' => gmdate('Y-m-d\TH:i:s\Z'),
'service' => 'gpay',
'objectType' => 'PAYMENT',
'actionType' => 'CREATE',
'status' => 'SUCCESS',
'correlationId' => 'corr-1001',
'details' => ['amount' => 1250],
],
]); // async promise — don't block the request

Python (requests)

import requests, datetime
requests.post("https://glog.gstack.esempla.systems/api/v1/app/audit-events", json={
"tenantId": "demo",
"eventTimestamp": datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"),
"service": "gpay", "objectType": "PAYMENT", "actionType": "CREATE",
"status": "SUCCESS", "correlationId": "corr-1001", "details": {"amount": 1250},
}, timeout=3) # short timeout; swallow errors

Node (fetch)

fetch('https://glog.gstack.esempla.systems/api/v1/app/audit-events', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
tenantId: 'demo', eventTimestamp: new Date().toISOString(),
service: 'gpay', objectType: 'PAYMENT', actionType: 'CREATE',
status: 'SUCCESS', correlationId: 'corr-1001', details: { amount: 1250 },
}),
}).catch(() => {}); // best-effort

Terminal window
# one event
curl https://glog.gstack.esempla.systems/api/v1/app/audit-events/<id>
# search a tenant/time window (returns { items, page, size, total })
curl 'https://glog.gstack.esempla.systems/api/v1/app/audit-events\
?tenantId=demo&from=2026-07-01T00:00:00Z&to=2026-08-01T00:00:00Z&page=0&size=20'
# verify the hash chain wasn't tampered with (audit:admin)
curl 'https://glog.gstack.esempla.systems/api/v1/admin/audit-events/integrity\
?tenantId=demo&from=2026-07-01T00:00:00Z&to=2026-08-01T00:00:00Z'

Or use the GLog web UI (dashboard, search with filters, correlated view, archive).


  • Never let auditing break the business path. Send asynchronously and best-effort: fire-and-forget, catch every error, use a short timeout. If GLog is down, your service must keep working. (This is exactly how the gNotify reference integration behaves.)
  • Buffer if you need guarantees. For events you cannot lose, enqueue locally (in-memory queue, outbox table, or a broker) and drain to GLog with retry — don’t call GLog inline.
  • Bind service and tenantId to identity, not user input — in production they must match the authenticated principal, or the chain partition and correlation become spoofable.
  • Put queryable values in top-level fields, bulky context in details. Frequently filtered values (service, status, objectType, correlationId, ipAddress) are indexed columns; everything else goes in the JSON details.
  • Timestamps in UTC ISO-8601 (2026-07-24T09:01:05Z). eventTimestamp = when the action happened; GLog stamps receivedAt itself.
  • Mind PII. GLog is immutable (WORM) — you cannot delete an event later. Log identifiers (idnp, ids) deliberately; avoid dumping secrets/tokens into details.

gNotify already integrates this way — see saas_gnotify: service/audit/GlogAuditClient.java posts an event on every delivery (operation=<CHANNEL>_SENT|_FAILED, correlationId = the notification tracking id), driven by gnotify.glog.* config, @Async + try/catch so it never blocks a send. On the shared box it reaches GLog over the internal http://glog-frontend address. Copy that pattern for the next producer.


7. Official SDKs (GitLab package registry)

Section titled “7. Official SDKs (GitLab package registry)”

Besides raw HTTP (above), there are official SDKs that hide the transport — auth token acquisition/refresh, request building, and typed helpers — so a service emits an event in one call. They live in this repo under sdk/ and are published to the GitLab Package Registry of this project (git.esempla.systems/govtech/gstack/glog).

➡ Install & usage: sdk.md. Quick reference below.

7.1 Java SDK — systems.esempla.glog:glog-sdk-java (Maven)

Section titled “7.1 Java SDK — systems.esempla.glog:glog-sdk-java (Maven)”

Intended API:

GLogClient glog = GLogClient.builder()
.baseUrl("https://glog.gstack.esempla.systems")
.tenant("demo")
// .keycloak(issuer, clientId, clientSecret) // production: auto token + refresh
.async(true) // fire-and-forget by default
.build();
glog.audit(AuditEvent.of("gpay", "PAYMENT", "CREATE")
.operation("PAYMENT_PROCESS").status(Status.SUCCESS)
.objectAffected("pay-5521").correlationId("corr-1001")
.detail("amount", 1250));
Page<AuditEvent> page = glog.search().tenant("demo")
.from(Instant.now().minus(Duration.ofDays(1))).to(Instant.now()).fetch();

Consuming it (GitLab Maven registry):

<repository>
<id>gitlab-glog</id>
<url>https://git.esempla.systems/api/v4/projects/574/packages/maven</url>
</repository>
<dependency>
<groupId>systems.esempla.glog</groupId>
<artifactId>glog-sdk-java</artifactId>
<version>0.1.0</version>
</dependency>

(A GitLab Deploy Token / PAT goes in ~/.m2/settings.xml for auth. A Spring Boot auto-configuration starter — glog-spring-boot-starter — is a natural companion so a @GlogAudit bean is injected from application.yml.)

7.2 PHP SDK — esempla/glog-sdk (Composer)

Section titled “7.2 PHP SDK — esempla/glog-sdk (Composer)”
$glog = new Esempla\GLog\Client(baseUrl: 'https://glog.gstack.esempla.systems', tenant: 'demo');
$glog->audit(
service: 'gpay', objectType: 'PAYMENT', actionType: 'CREATE',
operation: 'PAYMENT_PROCESS', status: 'SUCCESS',
correlationId: 'corr-1001', details: ['amount' => 1250],
);

Consuming it (GitLab Composer registry):

{
"repositories": [
{ "type": "composer", "url": "https://git.esempla.systems/api/v4/group/1087/-/packages/composer/packages.json" }
],
"require": { "esempla/glog-sdk": "^0.1" }
}

The SDKs live in this repo (sdk/java, sdk/php). Releases are published manually by a maintainer with a GitLab token (write_package_registry) — no CI/CD:

  • Java — mvn -f sdk/java/pom.xml deploy to the project’s Maven registry.
  • PHP — register the tag via the GitLab Composer registry API.

Exact commands are in sdk.md → Versioning & publishing.

Versioning follows the GLog API (0.x while the API is pre-1.0). The SDKs are thin wrappers over the documented HTTP contract, so anything an SDK does can always be done with a plain request.

The SDKs are built and tested (sdk/java, sdk/php); see sdk.md for install & publish.