Skip to content

Commit 4759f2e

Browse files
Request tracing support across core and connectors (#489)
* add OpenTelemetry tracing package for context propagation * Propagate request context across event bus so subscribers inherit trace IDs * Add trace_id, span_id and parent_span_id columns * Include tracing for AMIE packet handlers * Include tracing for COmanage subscriber * Include tracing for SLURM subscribers * source-column * Add audit-driven trace store that reads audit_events and amie_audit_log * expose audit endpoints * Remove cross-connector tracing test from AMIE pipeline package * Updated the baseline amie integration tests to reflect the additional tracing events * Renumber tracing migrations * Rename TraceEvent fields and COmanage subscriber audit event for clarity * Add entity_type column to audit_events * Replace amie_audit_log with amie_audit_extras table * Route AMIE audit writes through core audit_events * Drop UNION in audit_trace_store * Add AMIE endpoints * Update AMIE baseline integration tests to align with the new audit modeling * Move AMIE HTTP endpoints into the connector via mux passthrough * Consolidate AMIE migrations and switch trace/span IDs to hex strings * Renumber audit migrations for consistency
1 parent ec14c2e commit 4759f2e

110 files changed

Lines changed: 4675 additions & 426 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,16 @@ airavata-custos/
4747
| `extensions/` | Independent services that run alongside Custos to extend HPC node behavior | `CILogon-SSH-PAM`, `SSH-Certificate-Signer` |
4848
| `dev-ops/` | Local dev stack and deployment automation | `compose/`, `terraform/`, `account-provisioning/` |
4949

50+
## Audit conventions
51+
52+
Every audit row in the system lives in the core `audit_events` table. Core, every connector, and any future extension write to it via the same shape: `event_type`, `entity_type`, `entity_id`, `details`, `source`, and the OpenTelemetry trace columns (`trace_id`, `span_id`, `parent_span_id`). The trace-view API at `/audit/traces*` reads from this one table.
53+
54+
When a connector needs to attach connector-specific references to an audit row (for example AMIE keeps `packet_id` and `event_id` so it can fetch from a packet down to its audits), those references go in a separate `<connector>_audit_extras` table owned by the connector. The extras table has `audit_event_id` as its primary key with a `ON DELETE CASCADE` foreign key to `audit_events(id)`, plus whatever connector-specific columns it needs. The connector writes the row into its own extras table, inside the same transaction as the `audit_events` insert.
55+
56+
Connector-specific endpoints live under `/connectors/{name}/...` and join `audit_events` with the connector's extras table. The unified trace view never reads the extras tables.
57+
58+
The shape generalizes: a new connector that needs to record connector-specific references creates its own `<connector>_audit_extras` table and follows the same pattern. The core `audit_events` table stays neutral and never grows connector-shaped columns.
59+
5060
## Prerequisites
5161

5262
* Go 1.24+

cmd/server/main.go

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,14 @@ import (
3333
"github.com/apache/airavata-custos/internal/connectors"
3434
"github.com/apache/airavata-custos/internal/db"
3535
"github.com/apache/airavata-custos/internal/server"
36+
"github.com/apache/airavata-custos/internal/store"
37+
"github.com/apache/airavata-custos/internal/tracing"
3638
"github.com/apache/airavata-custos/pkg/events"
3739
"github.com/apache/airavata-custos/pkg/service"
3840
)
3941

4042
func main() {
41-
slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, nil)))
43+
slog.SetDefault(slog.New(tracing.SlogHandler(slog.NewJSONHandler(os.Stdout, nil))))
4244

4345
if err := run(); err != nil {
4446
slog.Error("server exited with error", "error", err)
@@ -73,6 +75,26 @@ func run() error {
7375
return err
7476
}
7577

78+
tracingMode := tracing.ModeProduction
79+
if os.Getenv("CUSTOS_TRACING_MODE") == "noop" {
80+
tracingMode = tracing.ModeNoop
81+
}
82+
tracingShutdown, err := tracing.Init(tracing.InitConfig{
83+
Mode: tracingMode,
84+
Logger: slog.Default(),
85+
ServiceName: "custos",
86+
})
87+
if err != nil {
88+
return err
89+
}
90+
defer func() {
91+
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
92+
defer cancel()
93+
if err := tracingShutdown(shutdownCtx); err != nil {
94+
slog.Warn("tracing shutdown returned error", "error", err)
95+
}
96+
}()
97+
7698
// Create a new event bus instance to async messaging between service and connectors
7799
eventBus := events.New()
78100
svc := service.New(database, eventBus)
@@ -82,14 +104,19 @@ func run() error {
82104

83105
tryBootstrap(ctx, svc)
84106

107+
adminDeps := &server.AdminDeps{
108+
AuditTraces: store.NewAuditTraceStore(database),
109+
}
110+
srv := server.New(svc, adminDeps)
111+
85112
// Tracks every background goroutine spawned by connectors so we can wait
86113
// for them to drain on shutdown instead of killing them mid-flight.
87114
var connectorsWG sync.WaitGroup
88-
if err := connectors.LoadConnectors(ctx, database, eventBus, svc, &connectorsWG); err != nil {
115+
if err := connectors.LoadConnectors(ctx, database, eventBus, svc, &connectorsWG, srv.Mux()); err != nil {
89116
return err
90117
}
91118

92-
handler := server.LoggingMiddleware(server.New(svc))
119+
handler := server.LoggingMiddleware(tracing.Middleware(srv))
93120

94121
httpServer := &http.Server{
95122
Addr: addr,

connectors/ACCESS/AMIE-Processor/README.md

Lines changed: 71 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -61,28 +61,93 @@ curl http://localhost:8083/metrics # Prometheus metrics
6161

6262
## Testing
6363

64+
Three layers, used for different things:
65+
66+
### 1. Unit tests (correctness, no external services)
67+
68+
```bash
69+
make test # all tests, verbose
70+
make test-short # short mode
71+
```
72+
73+
Every package under this connector ships unit tests against `testify/mock`. No DB, no AMIE server, no network. Run on every commit.
74+
75+
### 2. Integration tests (real DB + mock AMIE server)
76+
77+
Run the per-handler suite (~11 handler tests) plus the pipeline suite (`baseline_integration_test`, `comanage_trace_chain_test`, `tracing_clean_slate_test`). All gated by build tag `integration` and require these env vars set:
78+
79+
- `DATABASE_DSN`, `AMIE_BASE_URL`, `AMIE_SITE_CODE`, `AMIE_API_KEY`, `AMIE_CLUSTER_ID`
80+
81+
The canonical runner brings up an isolated DB on `:3307` + mock AMIE on `:8181`, applies all migrations, fires every integration test, then tears down:
82+
83+
```bash
84+
# From repo root
85+
make integration-test-amie
86+
```
87+
88+
Equivalent script: `scripts/run-amie-integration-tests.sh`.
89+
90+
#### What the pipeline tests fire
91+
92+
Only one fixture exists today: `testdata/scenarios/baseline.yaml` — a deterministic 9-packet flow covering all major handler paths:
93+
94+
| Packet | Asserts |
95+
|---|---|
96+
| `request_project_create` | PI user + project + allocation + PI cluster account |
97+
| `request_account_create` | user + cluster account + membership |
98+
| `data_project_create` / `data_account_create` | PERSIST_DNS rows |
99+
| `request_user_modify` | user + DN updates |
100+
| `request_person_merge` | survivor / retiree consolidation |
101+
| `request_account_inactivate` / `_reactivate` | membership status flips |
102+
| `request_project_inactivate` / `_reactivate` | project + all-member flips |
103+
| `inform_transaction_complete` | TRANSACTION_COMPLETE row |
104+
105+
The three pipeline tests reuse the same fixture but assert different properties:
106+
- `baseline_integration_test` — DB shape + idempotency on rerun.
107+
- `comanage_trace_chain_test` — proves AMIE and COmanage audit rows share a `trace_id` over the unified `audit_events` table (uses mock COmanage REST; never hits the real registry).
108+
- `tracing_clean_slate_test` — clean-slate DB + full audit coverage + `/audit/*` endpoint smoke against an in-process httptest server.
109+
110+
To add a new scenario: drop a YAML next to `baseline.yaml` and call `pipe.fireScenario(t, "<name>")` from a new `*_integration_test.go` file.
111+
112+
### 3. Mock-server REST scenarios + k6 (load / soak / observability)
113+
114+
For traffic generation against a **running** server (NOT a test — no assertions). Used to play in the admin UI, watch Grafana, or stress the worker.
115+
116+
Start the mock AMIE server standalone:
117+
64118
```bash
65-
make test # Run all tests with verbose output
66-
make test-short # Run tests in short mode
119+
cd connectors/ACCESS/AMIE-Processor/mock-server
120+
source venv/bin/activate
121+
python3 mock-amie-server.py # :8180
67122
```
68123

69-
All tests use mocks (testify/mock) and require no external services.
124+
Then queue packets via REST:
125+
126+
```bash
127+
curl -X POST 'http://localhost:8180/test/TESTSITE/scenarios?type=mixed' # success + failure mix
128+
curl -X POST 'http://localhost:8180/test/TESTSITE/scenarios?type=success_only'
129+
curl -X POST 'http://localhost:8180/test/TESTSITE/scenarios?type=failures_only'
130+
curl -X POST 'http://localhost:8180/test/TESTSITE/scenarios?type=heavy' # large batch
131+
curl -X POST 'http://localhost:8180/test/TESTSITE/scenarios?type=dev_email' # needs DEV_EMAIL env
132+
```
70133

71-
**93 test functions, 160 total test cases (including subtests), 0 failures.**
134+
For sustained traffic at a configurable rate, use k6 with `mock-server/amie-traffic.js` — see `mock-server/README.md` for stage configuration.
72135

73136
## Observability
74137

75138
The service exports Prometheus metrics at `/metrics`. A pre-built Grafana dashboard is available at `compose/grafana/dashboards/amie-service.json` showing packet processing stats, failures, retries, and processing duration percentiles.
76139

77-
To run the full observability stack:
140+
To run the metrics stack:
78141

79142
```bash
80143
# From the repo root
81-
docker compose -f compose/docker-compose.yml up db prometheus grafana -d
144+
docker compose -f dev-ops/compose/docker-compose.yml up db prometheus grafana -d
82145
```
83146

84147
Then open Grafana at `http://localhost:3000` (admin/admin). The AMIE dashboard loads automatically.
85148

149+
For request-flow tracing, every AMIE audit row carries `trace_id` / `span_id` / `parent_span_id` and lives in the core `audit_events` table (source=`amie`). The connector-specific `packet_id` / `event_id` references live in `amie_audit_extras` joined on `audit_event_id`. The admin trace view at `/audit/traces*` queries `audit_events` directly and renders the hierarchy from `parent_span_id`. For per-packet drill-down, see `GET /connectors/amie/packets/{packet_id}/audits`.
150+
86151
## Architecture
87152

88153
### Packet Processing Pipeline

connectors/ACCESS/AMIE-Processor/db/migrations/000001_initial_schema.down.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
-- under the License.
1717

1818
DROP TABLE IF EXISTS amie_user_dns;
19-
DROP TABLE IF EXISTS amie_audit_log;
19+
DROP TABLE IF EXISTS amie_audit_extras;
2020
DROP TABLE IF EXISTS amie_processing_errors;
2121
DROP TABLE IF EXISTS amie_processing_events;
2222
DROP TABLE IF EXISTS amie_packets;

connectors/ACCESS/AMIE-Processor/db/migrations/000001_initial_schema.up.sql

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -74,22 +74,20 @@ CREATE TABLE IF NOT EXISTS amie_processing_errors
7474
KEY idx_amie_errors_occurred_at (occurred_at)
7575
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
7676

77-
CREATE TABLE IF NOT EXISTS amie_audit_log
77+
-- AMIE writes audit rows to core's audit_events table; this side table
78+
-- carries the connector-specific references (packet_id, event_id) joined on
79+
-- audit_event_id.
80+
CREATE TABLE IF NOT EXISTS amie_audit_extras
7881
(
79-
id BIGINT NOT NULL AUTO_INCREMENT,
80-
packet_id VARCHAR(255) NOT NULL,
81-
event_id VARCHAR(255) NULL,
82-
action VARCHAR(64) NOT NULL,
83-
entity_type VARCHAR(64) NOT NULL,
84-
entity_id VARCHAR(255) NULL,
85-
summary TEXT NULL,
86-
created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
87-
PRIMARY KEY (id),
88-
CONSTRAINT fk_amie_audit_packet FOREIGN KEY (packet_id) REFERENCES amie_packets (id) ON DELETE CASCADE,
89-
CONSTRAINT fk_amie_audit_event FOREIGN KEY (event_id) REFERENCES amie_processing_events (id) ON DELETE SET NULL,
90-
KEY idx_amie_audit_packet_id (packet_id),
91-
KEY idx_amie_audit_action (action),
92-
KEY idx_amie_audit_created_at (created_at)
82+
audit_event_id VARCHAR(255) NOT NULL,
83+
packet_id VARCHAR(255) NOT NULL,
84+
event_id VARCHAR(255) NULL,
85+
PRIMARY KEY (audit_event_id),
86+
CONSTRAINT fk_amie_audit_extras_event FOREIGN KEY (audit_event_id) REFERENCES audit_events(id) ON DELETE CASCADE,
87+
CONSTRAINT fk_amie_audit_extras_packet FOREIGN KEY (packet_id) REFERENCES amie_packets(id) ON DELETE CASCADE,
88+
CONSTRAINT fk_amie_audit_extras_procev FOREIGN KEY (event_id) REFERENCES amie_processing_events(id) ON DELETE SET NULL,
89+
KEY idx_amie_audit_extras_packet_id (packet_id),
90+
KEY idx_amie_audit_extras_event_id (event_id)
9391
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
9492

9593
-- AMIE-side DN registry. AMIE delivers DnList fields that contain DNs across

connectors/ACCESS/AMIE-Processor/handler/data_account_create.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,11 @@ import (
2424
"fmt"
2525
"log/slog"
2626

27+
"go.opentelemetry.io/otel/codes"
28+
2729
"github.com/apache/airavata-custos/connectors/ACCESS/AMIE-Processor/model"
2830
"github.com/apache/airavata-custos/connectors/ACCESS/AMIE-Processor/store"
31+
"github.com/apache/airavata-custos/internal/tracing"
2932
"github.com/apache/airavata-custos/pkg/service"
3033
)
3134

@@ -42,7 +45,16 @@ func NewDataAccountCreateHandler(svc *service.Service, userDNStore store.UserDNS
4245

4346
func (h *DataAccountCreateHandler) SupportsType() string { return "data_account_create" }
4447

45-
func (h *DataAccountCreateHandler) Handle(ctx context.Context, tx *sql.Tx, packetJSON map[string]any, packet *model.Packet, eventID string) error {
48+
func (h *DataAccountCreateHandler) Handle(ctx context.Context, tx *sql.Tx, packetJSON map[string]any, packet *model.Packet, eventID string) (err error) {
49+
ctx, span := tracing.Start(ctx, "amie.handle:"+packet.Type)
50+
defer span.End()
51+
defer func() {
52+
if err != nil {
53+
span.RecordError(err)
54+
span.SetStatus(codes.Error, err.Error())
55+
}
56+
}()
57+
4658
body, err := getBody(packetJSON)
4759
if err != nil {
4860
return err

connectors/ACCESS/AMIE-Processor/handler/data_project_create.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,11 @@ import (
2424
"fmt"
2525
"log/slog"
2626

27+
"go.opentelemetry.io/otel/codes"
28+
2729
"github.com/apache/airavata-custos/connectors/ACCESS/AMIE-Processor/model"
2830
"github.com/apache/airavata-custos/connectors/ACCESS/AMIE-Processor/store"
31+
"github.com/apache/airavata-custos/internal/tracing"
2932
"github.com/apache/airavata-custos/pkg/service"
3033
)
3134

@@ -42,7 +45,16 @@ func NewDataProjectCreateHandler(svc *service.Service, userDNStore store.UserDNS
4245

4346
func (h *DataProjectCreateHandler) SupportsType() string { return "data_project_create" }
4447

45-
func (h *DataProjectCreateHandler) Handle(ctx context.Context, tx *sql.Tx, packetJSON map[string]any, packet *model.Packet, eventID string) error {
48+
func (h *DataProjectCreateHandler) Handle(ctx context.Context, tx *sql.Tx, packetJSON map[string]any, packet *model.Packet, eventID string) (err error) {
49+
ctx, span := tracing.Start(ctx, "amie.handle:"+packet.Type)
50+
defer span.End()
51+
defer func() {
52+
if err != nil {
53+
span.RecordError(err)
54+
span.SetStatus(codes.Error, err.Error())
55+
}
56+
}()
57+
4658
body, err := getBody(packetJSON)
4759
if err != nil {
4860
return err

connectors/ACCESS/AMIE-Processor/handler/handler.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ type AmieClient interface {
4141
ReplyToPacket(ctx context.Context, packetRecID int64, reply map[string]any) error
4242
}
4343

44-
// AuditService writes to amie_audit_log. The audit log is AMIE-local.
44+
// AuditService writes one audit_events row (source='amie') plus the matching
45+
// amie_audit_extras row carrying (packet_id, event_id).
4546
type AuditService interface {
4647
Log(ctx context.Context, tx *sql.Tx, packetID, eventID string, action model.AuditAction, entityType, entityID, summary string) error
4748
}

connectors/ACCESS/AMIE-Processor/handler/inform_transaction_complete.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@ import (
2323
"fmt"
2424
"log/slog"
2525

26+
"go.opentelemetry.io/otel/codes"
27+
2628
"github.com/apache/airavata-custos/connectors/ACCESS/AMIE-Processor/model"
29+
"github.com/apache/airavata-custos/internal/tracing"
2730
)
2831

2932
type InformTransactionCompleteHandler struct {
@@ -38,7 +41,16 @@ func (h *InformTransactionCompleteHandler) SupportsType() string {
3841
return "inform_transaction_complete"
3942
}
4043

41-
func (h *InformTransactionCompleteHandler) Handle(ctx context.Context, tx *sql.Tx, packetJSON map[string]any, packet *model.Packet, eventID string) error {
44+
func (h *InformTransactionCompleteHandler) Handle(ctx context.Context, tx *sql.Tx, packetJSON map[string]any, packet *model.Packet, eventID string) (err error) {
45+
ctx, span := tracing.Start(ctx, "amie.handle:"+packet.Type)
46+
defer span.End()
47+
defer func() {
48+
if err != nil {
49+
span.RecordError(err)
50+
span.SetStatus(codes.Error, err.Error())
51+
}
52+
}()
53+
4254
body, err := getBody(packetJSON)
4355
if err != nil {
4456
return err

connectors/ACCESS/AMIE-Processor/handler/integration_common.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import (
3636
amieservice "github.com/apache/airavata-custos/connectors/ACCESS/AMIE-Processor/service"
3737
"github.com/apache/airavata-custos/connectors/ACCESS/AMIE-Processor/store"
3838
"github.com/apache/airavata-custos/internal/db"
39+
corestore "github.com/apache/airavata-custos/internal/store"
3940
"github.com/apache/airavata-custos/pkg/events"
4041
coreservice "github.com/apache/airavata-custos/pkg/service"
4142
)
@@ -102,7 +103,8 @@ func setupTestDB(t *testing.T) *sqlx.DB {
102103
func truncateAll(t *testing.T, database *sqlx.DB) {
103104
t.Helper()
104105
tables := []string{
105-
"amie_audit_log",
106+
"amie_audit_extras",
107+
"audit_events",
106108
"amie_processing_errors",
107109
"amie_processing_events",
108110
"amie_packets",
@@ -155,7 +157,7 @@ func newTestCoreService(database *sqlx.DB) *coreservice.Service {
155157
}
156158

157159
func newTestAuditService(database *sqlx.DB) *amieservice.AuditService {
158-
return amieservice.NewAuditService(store.NewAuditStore(database))
160+
return amieservice.NewAuditService(corestore.NewAuditEventStore(database), store.NewAuditExtrasStore(database))
159161
}
160162

161163
type fakeReply struct {
@@ -258,7 +260,9 @@ func countAuditActions(t *testing.T, database *sqlx.DB, packetID string, action
258260
t.Helper()
259261
var n int
260262
if err := database.Get(&n,
261-
"SELECT COUNT(*) FROM amie_audit_log WHERE packet_id = ? AND action = ?",
263+
`SELECT COUNT(*) FROM audit_events ae
264+
JOIN amie_audit_extras x ON x.audit_event_id = ae.id
265+
WHERE x.packet_id = ? AND ae.event_type = ?`,
262266
packetID, string(action),
263267
); err != nil {
264268
t.Fatalf("count audit %s: %v", action, err)

0 commit comments

Comments
 (0)