Skip to content

Activity Logs for All Events

1. Background

This feature adds a unified audit trail for write operations in Global Service Status.

The goal is to answer a simple operational question:

Who did what to which entity, and when?

The implementation is intentionally focused on human-triggered API write operations such as create, update, and delete. It does not try to audit every internal side effect, scheduled task, cache refresh, or sync event.


2. Design Goals

The feature was designed with the following principles:

  • One common audit mechanism for different business entities
  • Minimal intrusion into business code by using annotation + AOP
  • Best-effort logging: business flow must not fail because audit logging fails
  • Queryable history by entity, service, operator, action, and time range
  • Extensible coverage so more entities can be added later with low cost
  • Explicit handling for cascade deletes where AOP on the controller is not enough

3. High-Level Architecture

The feature is built from four parts:

  1. Controller annotations
    • Write endpoints are marked with @AuditLog
  2. Aspect interception
    • ActivityLogAspect runs after the controller method succeeds
    • It extracts action, entity type, entity id, entity name, service id, and operator
  3. Async persistence service
    • ActivityLogServiceImpl.record(...) persists the row asynchronously
  4. Admin query API
    • ActivityLogController exposes search and entity-history endpoints

Core files:

  • server/src/main/java/com/cisco/gss/common/annotation/AuditLog.java
  • server/src/main/java/com/cisco/gss/aspect/ActivityLogAspect.java
  • server/src/main/java/com/cisco/gss/common/domain/audit/ActivityLog.java
  • server/src/main/java/com/cisco/gss/service/impl/ActivityLogServiceImpl.java
  • server/src/main/java/com/cisco/gss/repository/ActivityLogRepository.java
  • server/src/main/java/com/cisco/gss/controller/ActivityLogController.java
  • migration/postgres/scripts/v47__create_activity_log_table.sql

4. Database Model

Audit data is stored in table status_activity_log.

Main columns:

Column Meaning
log_id unique id for the log row
operator authenticated operator/user name
action CREATE, UPDATE, DELETE
entity_type audited business entity type
entity_id id of the target entity
entity_name display name captured at operation time
service_id service scope when available
description human-readable summary
created_time audit timestamp

Indexes:

  • (entity_type, entity_id) for entity history lookup
  • (service_id) for service-scoped audit queries
  • (operator) for user-based queries
  • (created_time) for time-range queries

Notes:

  • There is only created_time; there is no updated_time
  • Audit rows are append-only; they are not updated in place

5. Operator Resolution

Operator information is carried through UserContextHolder.

Flow:

  1. AuthAspect / DevAuthorization verifies the caller
  2. The operator name is extracted from auth verification result
  3. UserContextHolder.setOperatorName(...) stores it in a ThreadLocal
  4. ActivityLogAspect reads it when building the audit row
  5. AuthAspect clears the ThreadLocal in finally

Relevant files:

  • server/src/main/java/com/cisco/gss/Auth/AuthAspect.java
  • server/src/main/java/com/cisco/gss/common/audit/UserContextHolder.java

Fallback behavior:

  • If no operator can be resolved, the aspect stores "unknown"

6. Annotation Contract

@AuditLog is the main extension point.

Current attributes:

Attribute Purpose
action default action to record
entityType business entity type
idParamName use a specific method argument as entity id
serviceIdParamName use a specific method argument as service id
detectActionByHttpMethod infer action from actual HTTP method

6.1 idParamName

Use this when the controller returns void or when the target id should come from a path variable.

Typical example:

@AuditLog(action = ActivityAction.DELETE, entityType = ActivityEntityType.COMPONENT, idParamName = "componentId", serviceIdParamName = "")

6.2 serviceIdParamName

Default value is "serviceId".

Use cases:

  • keep default when the controller method already has serviceId
  • set to "" when service id is not available from a direct parameter and should be resolved from return value or left null

6.3 detectActionByHttpMethod

This is used for endpoints that share one handler for both POST and PUT.

Runtime mapping in the aspect:

  • POST -> CREATE
  • PUT / PATCH -> UPDATE
  • DELETE -> DELETE
  • fallback -> annotation action

Current notable usage:

  • MaintenanceController.saveMaintenanceEvent(...)

This avoids wrong CREATE logs for PUT requests on combined endpoints.


7. Aspect Behavior

ActivityLogAspect intercepts methods annotated with @AuditLog.

7.1 When a row is created

A log row is created only after the controller method completes successfully.

This means:

  • validation failure -> no audit row
  • exception in business logic -> no audit row
  • successful method -> aspect attempts to log

7.2 Best-effort behavior

Audit logging must never break the business request.

If audit persistence fails:

  • the business response still succeeds
  • the aspect logs a warning/error internally
  • no exception is propagated back to the caller

7.3 Extraction rules

The aspect extracts fields using the following rules.

Action

  • from @AuditLog.action()
  • or, if enabled, dynamically from HTTP method using detectActionByHttpMethod

Entity ID

Priority:

  1. explicit method parameter defined by idParamName
  2. fallback getter lookup on return value

Getter fallback order:

  • getMessageId
  • getIncidentId
  • getComponentId
  • getUuid
  • getId
  • getServiceId

This order was intentionally tuned to avoid wrong IDs, e.g.:

  • IncidentMessageDto should use messageId instead of incidentId
  • TempNotice should use id instead of serviceId

Entity Name

The aspect tries a list of getters on the returned object, including:

  • getIncidentName
  • getComponentName
  • getServiceName
  • getEventName
  • getClusterName
  • getDcName
  • getTempNoticeName
  • getApiName
  • getVersion
  • getTitle
  • getName

Service ID

Priority:

  1. explicit parameter named by serviceIdParamName
  2. fallback getter getServiceId() on the return value

Description

Generated as:

  • ACTION ENTITY_TYPE
  • or ACTION ENTITY_TYPE: entityName

Examples:

  • DELETE COMPONENT: Messaging
  • CREATE MAINTENANCE_EVENT: Webex Calendar Maintenance

8. Asynchronous Persistence

ActivityLogServiceImpl.record(...) is annotated with @Async.

This means:

  • the controller response is not blocked by audit insert latency
  • the main request does not wait for database write completion
  • audit rows are still best-effort and isolated from business success

Important consequence:

  • the system guarantees that logging is attempted after success, but it does not guarantee transactional coupling between the API response and the async insert

This is acceptable because audit logging here is observability/audit support, not a hard business invariant.


9. Covered Entity Types

Current enum values in ActivityEntityType:

  • SERVICE
  • COMPONENT
  • INCIDENT
  • INCIDENT_MESSAGE
  • ANNOUNCEMENT
  • MAINTENANCE_EVENT
  • CLUSTER
  • DATA_CENTER
  • RELEASE_NOTE
  • TEMP_NOTICE
  • STATUS_API_DOC

Note: the SQL migration comment still lists only an older subset. The Java enum is the current source of truth for feature coverage.


10. Endpoint Coverage Matrix

10.1 Direct controller coverage via @AuditLog

Controller Entity type Covered writes
ServiceController SERVICE create / update / delete
ComponentController COMPONENT create / update / delete, group create, icon upload/delete
IncidentController INCIDENT, INCIDENT_MESSAGE incident create / update / delete / recover, message create / update / delete
MaintenanceController MAINTENANCE_EVENT create/update combined endpoint, delete
maintenance announcement controller ANNOUNCEMENT create / update / delete
ClusterController CLUSTER create / update / delete
DataCenterController DATA_CENTER create / update / delete
ReleaseNoteController RELEASE_NOTE create / update / delete
TempNoticeController TEMP_NOTICE create / update / delete
StatusApiDocController STATUS_API_DOC create / update / delete

10.2 Deliberate exclusions

Some write endpoints are intentionally not audited, because they are bulk utility operations, migration helpers, or not meaningful for operator history:

  • bulk ordering endpoints such as modifyComponents, modifyDataCenters, modifyClusters
  • UUID backfill / history migration APIs
  • cache clear / internal helper endpoints
  • system scheduled tasks

If these endpoints become audit-relevant later, they should be added explicitly rather than assumed to be covered.


11. Special Cases That Required Manual Logging

AOP on controllers logs the main API target, but it cannot automatically see all entities deleted as side effects inside service-layer cascade logic.

Three places required explicit post-commit logging.

11.1 Deleting a parent component also deletes child components

File:

  • server/src/main/java/com/cisco/gss/service/impl/ComponentServiceImpl.java

Behavior:

  • controller audit logs the parent component delete
  • service fetches child components before deletion
  • after transaction commit, it inserts one extra DELETE COMPONENT log per child component

Why:

  • otherwise only the parent delete would appear in history

11.2 Deleting an incident also deletes its incident messages

File:

  • server/src/main/java/com/cisco/gss/service/impl/IncidentServiceImpl.java

Behavior:

  • controller audit logs the incident delete
  • service fetches existing incident messages before deletion
  • after transaction commit, it inserts one extra DELETE INCIDENT_MESSAGE log per deleted message

Why:

  • otherwise incident message deletion history would be invisible

11.3 Deleting a data center also deletes its clusters

File:

  • server/src/main/java/com/cisco/gss/service/impl/DataCenterServiceImpl.java

Behavior:

  • controller audit logs the data center delete
  • service fetches clusters before deletion
  • after transaction commit, it inserts one extra DELETE CLUSTER log per deleted cluster

Why:

  • otherwise cluster deletions triggered by data center removal would not be audited

11.4 Why post-commit

These manual logs are written in afterCommit() instead of inline.

Reason:

  • if the transaction rolls back, we must not leave fake delete audit rows behind

This matches the same success-only principle used by the main controller-based AOP logging.


12. Important Entity-Specific Extraction Decisions

12.1 IncidentMessageDto

When creating an incident message, the returned DTO originally did not carry enough data for audit extraction.

To support correct logging, the service enriches the returned DTO with:

  • incidentName
  • serviceId

This allows the aspect to populate:

  • entity_name
  • service_id

for INCIDENT_MESSAGE rows.

12.2 TempNotice

TempNotice contains both id and serviceId.

The ID getter priority had to be tuned so that:

  • entity_id = temp notice id
  • not serviceId

12.3 Incident vs Maintenance in status_incident

Incident and incident-style maintenance share the same status_incident table and are distinguished by incident_type.

Current design choice:

  • activity log keeps entity_type as INCIDENT / INCIDENT_MESSAGE
  • if later analysis needs to distinguish incident vs maintenance, it should join using entity_id and the authoritative business table

Reason:

  • avoid duplicating business classification logic in the audit layer
  • keep status_incident as the single source of truth

This is different from calendar maintenance, which is a separate concept and uses MAINTENANCE_EVENT.


13. Query APIs

Admin query endpoints are exposed from ActivityLogController.

13.1 Search API

GET /status/admin/activity-logs

Supported filters:

  • entityType
  • action
  • serviceId
  • operatorName
  • entityId
  • startTime
  • endTime
  • page
  • size

Characteristics:

  • paged response
  • size is capped to 200
  • results ordered by createdTime DESC

13.2 Entity history API

GET /status/admin/activity-logs/entity?entityType=...&entityId=...

Use this when the caller wants the full audit history for one entity.

Authorization:

  • both endpoints require @DevAuthorization

14. What Is Intentionally Not Logged

The feature intentionally does not log the following by default:

14.1 Scheduled/system jobs

Examples:

  • auto status update tasks
  • recovery jobs
  • background sync jobs

Reason:

  • activity log is mainly for human operator actions
  • system-driven high-frequency events would pollute the audit stream
  • normal application logs are usually more appropriate for those flows

14.2 Pure technical side effects

Examples:

  • cache invalidation
  • delayed sync message dispatch
  • redis refresh
  • downstream file regeneration

Reason:

  • these are implementation details of a write operation, not the operator intent itself

15. Extension Guide

To add activity logging for a new entity:

Step 1. Add a new enum value

File:

  • ActivityEntityType.java

Step 2. Annotate the controller write endpoints

Use @AuditLog(...) on create/update/delete methods.

Typical examples:

@AuditLog(action = ActivityAction.CREATE, entityType = ActivityEntityType.NEW_ENTITY)
@AuditLog(action = ActivityAction.DELETE, entityType = ActivityEntityType.NEW_ENTITY, idParamName = "id", serviceIdParamName = "")

Step 3. Ensure the return type exposes enough data

The aspect relies on getters.

If the return object does not expose the needed fields, enrich it so that at least these can be resolved when relevant:

  • entity id
  • entity name
  • service id

Step 4. Add name getter support if needed

If the new entity uses a non-standard display-name getter, add it to NAME_GETTERS in ActivityLogAspect.

Step 5. Consider cascade side effects

If one API deletes or mutates related child entities, decide whether those child operations also need audit rows.

If yes:

  • fetch affected children before deletion
  • register afterCommit() logging
  • insert one row per child side effect

Step 6. Decide whether the endpoint is really audit-worthy

Do not automatically log:

  • migration helpers
  • batch fix utilities
  • scheduled jobs
  • noisy internal maintenance operations

16 Trade-offs and Rationale

Why annotation + AOP

Pros:

  • low boilerplate in controllers
  • uniform behavior
  • easy to extend

Trade-off:

  • some complex side effects still require manual service-level logging

Why async persistence

Pros:

  • does not slow down the main API path significantly
  • audit failure does not break business flow

Trade-off:

  • audit insert is not strictly part of the request transaction outcome

Why not put every business classification into entity_type

Pros of current approach:

  • keep the audit layer generic
  • avoid duplicating core business truth in audit tables

Trade-off:

  • some deep analysis may still need joins to business tables

Why not log system jobs by default

Pros:

  • keeps activity log useful for operator history
  • avoids high-volume noise

Trade-off:

  • system automation tracing remains in app logs or separate mechanisms

17. Current Limitations

  1. The migration SQL comment is outdated and does not list all currently covered entity types.
  2. Some bulk update endpoints are intentionally not audited.
  3. description is generic today; it records action + entity name, not a full field diff.
  4. The feature tracks that an operation happened, but not a before/after payload snapshot.

18. Summary

This feature introduces a single, extensible audit mechanism for write operations across major status-page entities.

The core model is:

  • mark write APIs with @AuditLog
  • let ActivityLogAspect extract context automatically
  • persist asynchronously into status_activity_log
  • fill controller-AOP blind spots with post-commit manual logs for cascade deletes

The result is a practical operator audit trail that is:

  • centralized
  • queryable
  • low-touch to extend
  • resilient to failure
  • intentionally focused on meaningful human actions rather than every internal system event