Audit Logging

LightMesh records audit data at two levels:

  1. API activity — every GraphQL operation your user or API key performs (stored in ApiActivities, queried with activities).
  2. Data changes — create/update/delete events on IPAM records (stored in Changes per organization schema, queried with changes and userChanges).

Use the UI for day-to-day review, or call the GraphQL queries below from Postman, scripts, or automation. For API authentication, see LightMesh API and New API Key.


Where to view audit data in the UI

Subnet Activity tab

Open a subnet and select the Activity tab to see change history scoped to that subnet’s network address.

Subnet Activity tab

My History (account)

Signed-in users can open My History under their account settings to see their own API operations (the activities query for the current user).

My History page


Authentication

All audit queries use the same GraphQL endpoint as the rest of LightMesh:

POST https://next.lightmesh.com/graphql

Browser session: use your normal logged-in session (cookies).

API key or CLI token: send the token header, as described in LightMesh API:

Content-Type: application/json
token: <your-api-key>

Optional CLI headers (cli_request, installed_version) are recorded on API activity rows as source and version when present.


Query overview

Query Purpose Typical use
activities GraphQL API usage for a user Compliance, “who called what,” debugging integrations
changes Changes within a subnet CIDR Subnet Activity tab, subnet-scoped audits
userChanges All changes made by a user in the org User-level audit export, investigations

Shared list arguments (where supported):

Argument Type Notes
limit Int Page size; capped at 500
offset Int Pagination offset (default 0)
sort_by String Whitelisted per query (see below)
sort_dir String asc, desc, ascend, or descend
since String Start of range — ISO-8601 (e.g. 2026-05-14T00:00:00.000Z)
until String End of range — ISO-8601
hoursBack Int Relative window; used when since is omitted

Invalid since / until values return a validation error instead of silently returning empty results.


1. activities — API usage log

Returns rows from ApiActivities: timestamp, operation name, optional detail JSON (sanitized variables and notes), and userId.

Access rules

  • Signed-in user: defaults to your own activity. Pass userId only if you are a superuser or are allowed to view that user in your organization.
  • API key: you must pass userId for a user who belongs to the same organization as the key.

Noise operations (activities, getMeasurement, CurrentUserQuery, etc.) are filtered out of results.

Example — recent activity for a user

{
  "query": "query ($userId: Int, $limit: Int, $offset: Int, $since: String, $until: String) { activities(userId: $userId, limit: $limit, offset: $offset, since: $since, until: $until, sort_by: \"timestamp\", sort_dir: \"DESC\") { total count results { timestamp operation detail } } }",
  "variables": {
    "userId": 42,
    "limit": 50,
    "offset": 0,
    "since": "2026-05-01T00:00:00.000Z",
    "until": "2026-05-31T23:59:59.999Z"
  }
}

Example — search by operation name

{
  "query": "query ($search: String, $limit: Int) { activities(search: $search, limit: $limit, sort_by: \"timestamp\", sort_dir: \"DESC\") { total results { timestamp operation } } }",
  "variables": {
    "search": "createSubnet",
    "limit": 25
  }
}

Example — last 24 hours (hoursBack)

{
  "query": "query ($userId: Int!, $hoursBack: Int) { activities(userId: $userId, hoursBack: $hoursBack, sort_by: \"timestamp\", sort_dir: \"DESC\") { total results { timestamp operation } } }",
  "variables": {
    "userId": 42,
    "hoursBack": 24
  }
}

Sort columns: timestamp, operation (default: timestamp DESC).

detail field

When present, detail is JSON written at request time. Sensitive variable names (password, token, api key, etc.) are redacted. Read-only queries include a note that no before/after exists; mutations may reference corresponding Change rows for tracked models.

activities query in Postman

activities query response in Postman


2. changes — subnet-scoped change history

Returns Change rows for addresses contained in the given subnet (networkAddress is required).

Matches what the subnet Activity tab loads: changes whose networkAddress falls within the subnet CIDR, optionally filtered by changeableType, changeableId, and zoneId.

Example — subnet activity (Subnet model)

{
  "query": "query ($model: String, $id: Int, $zoneId: Int, $networkAddress: String!, $limit: Int) { changes(changeableType: $model, changeableId: $id, zoneId: $zoneId, networkAddress: $networkAddress, limit: $limit, sort_by: \"createdAt\", sort_dir: \"DESC\") { count results { createdAt changeableType changeableId changeType user { firstName lastName } auditDescription auditBefore auditAfter } } }",
  "variables": {
    "model": "Subnet",
    "id": 1,
    "zoneId": 1,
    "networkAddress": "10.0.0.0/16",
    "limit": 50
  }
}
Argument Required Description
networkAddress Yes Subnet CIDR (e.g. 10.0.0.0/16)
changeableType No Model name filter (e.g. Subnet, IPAssignment)
changeableId No Record id filter
zoneId No Zone id filter

Sort columns: createdAt, changeableType (default: createdAt DESC).

Presentation fields

  • auditDescription — human-readable summary
  • auditBefore / auditAfter — JSON snapshots when available
  • previousValue / currentValue — raw change payloads
  • document — legacy text description where present

3. userChanges — per-user change history

Returns Change rows for a single user across the organization schema. Use this for “everything user X changed” reports.

Access rules

Same as activities: API keys must pass userId for a member of the key’s org; normal users see themselves unless they are superusers.

Example — user changes in a date range

{
  "query": "query ($userId: Int!, $since: String!, $until: String!, $limit: Int, $offset: Int) { userChanges(userId: $userId, since: $since, until: $until, limit: $limit, offset: $offset, sort_by: \"createdAt\", sort_dir: \"DESC\") { count results { createdAt changeableType changeableId changeType previousValue currentValue auditDescription } } }",
  "variables": {
    "userId": 42,
    "since": "2026-05-01T00:00:00.000Z",
    "until": "2026-05-31T23:59:59.999Z",
    "limit": 100,
    "offset": 0
  }
}

Example — filter by entity type

{
  "query": "query ($userId: Int!, $changeableType: String, $hoursBack: Int) { userChanges(userId: $userId, changeableType: $changeableType, hoursBack: $hoursBack, sort_by: \"createdAt\", sort_dir: \"DESC\") { count results { createdAt changeableType changeableId changeType } } }",
  "variables": {
    "userId": 42,
    "changeableType": "IPAssignment",
    "hoursBack": 168
  }
}

Sort columns: createdAt, changeableType (default: createdAt DESC).

userChanges query in Postman

userChanges query in Postman

userChanges query in Postman


Pagination pattern

Both list types return count (rows in the current page) and, for activities, total (full match count).

Request the next page by increasing offset by the previous limit:

{
  "variables": {
    "userId": 42,
    "limit": 50,
    "offset": 50,
    "since": "2026-05-01T00:00:00.000Z",
    "until": "2026-05-31T23:59:59.999Z"
  }
}

Common errors

Message Cause Fix
userId is required when authenticating with an API key activities or userChanges without userId on API key auth Pass a valid userId in the same org
networkAddress is required for subnet-scoped changes() changes called without CIDR Pass networkAddress (e.g. subnet’s networkAddress from subnet query)
Invalid "since" timestamp... Bad date string Use ISO-8601 UTC timestamps
Forbidden Viewing another user’s audit without permission Use your own userId or an admin account
Variable type String vs String! for networkAddress Client query out of date Declare $networkAddress: String! in the client query

For superuser analytics (activitySummary, activeUsersByMonth, etc.), those queries are restricted to internal admin use and are not part of standard tenant audit workflows.