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:
- Controller annotations
- Write endpoints are marked with
@AuditLog
- Write endpoints are marked with
- Aspect interception
ActivityLogAspectruns after the controller method succeeds- It extracts action, entity type, entity id, entity name, service id, and operator
- Async persistence service
ActivityLogServiceImpl.record(...)persists the row asynchronously
- Admin query API
ActivityLogControllerexposes search and entity-history endpoints
Core files:
server/src/main/java/com/cisco/gss/common/annotation/AuditLog.javaserver/src/main/java/com/cisco/gss/aspect/ActivityLogAspect.javaserver/src/main/java/com/cisco/gss/common/domain/audit/ActivityLog.javaserver/src/main/java/com/cisco/gss/service/impl/ActivityLogServiceImpl.javaserver/src/main/java/com/cisco/gss/repository/ActivityLogRepository.javaserver/src/main/java/com/cisco/gss/controller/ActivityLogController.javamigration/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 noupdated_time - Audit rows are append-only; they are not updated in place
5. Operator Resolution ¶
Operator information is carried through UserContextHolder.
Flow:
AuthAspect/DevAuthorizationverifies the caller- The operator name is extracted from auth verification result
UserContextHolder.setOperatorName(...)stores it in aThreadLocalActivityLogAspectreads it when building the audit rowAuthAspectclears theThreadLocalinfinally
Relevant files:
server/src/main/java/com/cisco/gss/Auth/AuthAspect.javaserver/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->CREATEPUT/PATCH->UPDATEDELETE->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:
- explicit method parameter defined by
idParamName - fallback getter lookup on return value
Getter fallback order:
getMessageIdgetIncidentIdgetComponentIdgetUuidgetIdgetServiceId
This order was intentionally tuned to avoid wrong IDs, e.g.:
IncidentMessageDtoshould usemessageIdinstead ofincidentIdTempNoticeshould useidinstead ofserviceId
Entity Name ¶
The aspect tries a list of getters on the returned object, including:
getIncidentNamegetComponentNamegetServiceNamegetEventNamegetClusterNamegetDcNamegetTempNoticeNamegetApiNamegetVersiongetTitlegetName
Service ID ¶
Priority:
- explicit parameter named by
serviceIdParamName - fallback getter
getServiceId()on the return value
Description ¶
Generated as:
ACTION ENTITY_TYPE- or
ACTION ENTITY_TYPE: entityName
Examples:
DELETE COMPONENT: MessagingCREATE 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:
SERVICECOMPONENTINCIDENTINCIDENT_MESSAGEANNOUNCEMENTMAINTENANCE_EVENTCLUSTERDATA_CENTERRELEASE_NOTETEMP_NOTICESTATUS_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 COMPONENTlog 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_MESSAGElog 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 CLUSTERlog 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:
incidentNameserviceId
This allows the aspect to populate:
entity_nameservice_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 noticeid- 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_typeasINCIDENT/INCIDENT_MESSAGE - if later analysis needs to distinguish incident vs maintenance, it should join using
entity_idand the authoritative business table
Reason:
- avoid duplicating business classification logic in the audit layer
- keep
status_incidentas 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:
entityTypeactionserviceIdoperatorNameentityIdstartTimeendTimepagesize
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 ¶
- The migration SQL comment is outdated and does not list all currently covered entity types.
- Some bulk update endpoints are intentionally not audited.
descriptionis generic today; it records action + entity name, not a full field diff.- 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
ActivityLogAspectextract 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