SPHERE Stats Needed

This is a post to list the needed stats/history for SPHERE.

For each user - creation and deletion events

  • timestamp, username, created or deleted

For each user login attempt

  • timestamp, username, succeded or failed

For each organization - creation and deletion

  • timestamp, org, username, created or deleted, org category, org description

For each project - creation and deletion

  • timestamp, project, username, created or deleted, project category, project description

For each membership change

  • timestamp, project or org name, username originating the change, username whose membership is changing, new membership role

For each experiment - creation or deletion or realization/materialization/revision

  • timestamp, experiment name, project name, username originating the event, type of event (created, deleted, pushed new model, realization, materialization), succeeded or failed, for push/realize/materialize include model file, for failures include error message

For realizations and materializations

  • timestamp, experiment, project, user, list of physical resources involved, list of virtual nodes and lans, allocate or deallocate, success or failure

If some of these cannot be achieved, let’s document what we can and cannot do and keep moving towards the ideal where we have all these

After dev discussion we decided that a database to track these stats is the way to go.

We can still use the existing stats service (which publishes stats to https://grafana.sphere-testbed.net), but those stats are time-series and it sounds like you want straight up stats - which don’t really fit well in time-series format.

Do you have a database preference? MongoDB was one suggestion that was raised…

Once this is done, we also need a page that shows relevant stats, number of users, projects, etc.

And a page that shows “status” of every facility, possibly overlaid on a map. Click facility to show status, etc.

And support for exporting this data.

For “For each X - creation and deletion events” - we can note creation, activation, decline, and delete. Or just activation, freeze, decline. Not sure which we want.

Went with postgres as SQL seemed to make more sense based on the queries we may do. The setup is a single “event” table with fields for expected query parameters and JSON for “other stuff” related to the entry. For example, realization, project, experiment in a realization-created entry.

Sample data:

portal-audit=> SELECT * from events;
              event_uuid              |           timestamp           |     event_type     | caller  | subject | subject_type |   target    | success | error_message |                            payload                             
--------------------------------------+-------------------------------+--------------------+---------+---------+--------------+-------------+---------+---------------+----------------------------------------------------------------
 2e2812be-3e7a-4a30-8c64-4ec09018f1ab | 2026-04-23 20:50:23.597554+00 | org_member_added   | glawler | orgtest | organization | testuser    | t       |               | {"kind": "usermember", "role": "member", "state": "active"}
 5e46bd28-2982-40e6-b39f-770fe69f7137 | 2026-04-23 20:50:28.16609+00  | org_member_added   | glawler | orgtest | organization | testproject | t       |               | {"kind": "projectmember", "role": "member", "state": "active"}
 bb539f70-7571-4f37-b6e1-e9f2ee40ea5c | 2026-04-23 20:50:41.026119+00 | org_member_deleted | glawler | orgtest | organization | testproject | f       |               | {"kind": "projectmember"}
 307025f7-fa9f-43f8-8979-883788a272fe | 2026-04-23 20:50:47.453077+00 | org_member_deleted | glawler | orgtest | organization | testuser    | f       |               | {"kind": "usermember"}
 4e1c56c3-4456-4bce-b208-5f073ccff081 | 2026-04-23 21:17:46.79238+00  | experiment_created |         | testexp | experiment   |             | t       |               | {"project": "glawler"}
 e0bfb051-70d2-425e-92c9-6a4cd2c9dfaf | 2026-04-23 21:17:55.933064+00 | experiment_deleted | glawler | testexp | experiment   |             | t       |               | {"project": "glawler"}
 e018cb7c-8f94-47a4-ba03-e317fba25d61 | 2026-04-23 21:18:45.443554+00 | experiment_created | glawler | testexp | experiment   |             | t       |               | {"project": "glawler"}
 dd64b054-0334-41c4-a7af-f021232938b3 | 2026-04-23 21:18:48.265956+00 | experiment_deleted | glawler | testexp | experiment   |             | t       |               | {"project": "glawler"}
(8 rows)

The table:

CREATE TABLE IF NOT EXISTS events (                   
    event_uuid    UUID PRIMARY KEY,                   
    timestamp     TIMESTAMPTZ NOT NULL,               
    event_type    TEXT NOT NULL,                      
                                                      
    caller        TEXT NOT NULL,                      
    subject       TEXT NOT NULL,                      
    subject_type  TEXT NOT NULL,                      
    target        TEXT,                               
                                                      
    success       BOOLEAN NOT NULL,                   
    error_message TEXT,                               
                                                      
    payload       JSONB NOT NULL DEFAULT '{}'::jsonb  
);                                                    
                                                      
CREATE INDEX IF NOT EXISTS idx_events_timestamp       
    ON events (timestamp DESC);                       
                                                      
CREATE INDEX IF NOT EXISTS idx_events_event_type      
    ON events (event_type);                           
                                                      
CREATE INDEX IF NOT EXISTS idx_events_caller          
    ON events (caller);                               
                                                      
CREATE INDEX IF NOT EXISTS idx_events_subject         
    ON events (subject_type, subject);                
                                                      
CREATE INDEX IF NOT EXISTS idx_events_target          
    ON events (target);                               

Note that with postgres you can use BJSON to query into the json payload field.

SELECT *
FROM events
WHERE event_type = 'org_member_added'
  AND success = true
  AND payload->>'kind' = 'usermember';

I’m playing around with metabase, a GUI frontend for postgres databases. Seems ok. You can write SQL and generate tables. The install is easy so may be worthwhile to install if you think it would be used.

Example output:

The SQL: