- Go 70.5%
- Nix 29.5%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| flake.nix | ||
| go.mod | ||
| main.go | ||
| README.md | ||
Education Ticketing Mock Server
A lightweight, single-file, in-memory dummy Education Ticketing API server written in Go. Designed for hands-on Python/API workshops, live coding demonstrations, and testing incident management workflows without needing an external ticketing platform or database.
Table of Contents
- Overview
- Key Features
- Configuration Items (CIs)
- Getting Started
- API Reference &
curlExamples- 1. List Configuration Items (
GET /api/ci) - 2. List All Alerts (
GET /api/alerts) - 3. List All Issues / Incidents (
GET /api/issues) - 4. Get Alerts for a Specific Incident (
GET /api/issue?id=<INC_ID>) - 5. Create or Update an Incident (
POST /api/issue) - 6. Clear Alert and Close Incident (
POST /api/issuewithseverity: 0)
- 1. List Configuration Items (
- HTTP Status Codes & Error Handling
- Python Hands-on Client Example
- Incident Lifecycle Rules
Overview
This mock server simulates a simplified Education Ticketing Incident Management endpoint. It maintains all data in-memory (no external database or configuration files needed). It allows workshop attendees to interact via HTTP clients (such as Python's requests module, curl, or Postman) to:
- Query pre-defined system Configuration Items (CIs).
- Send incoming system monitoring alerts.
- Group alerts automatically under active incidents based on unique message keys.
- Close incidents and clear alerts when resolution events (
severity = 0) are received.
Students accessing the environment remotely (e.g., via VS Code Remote SSH) can bind to 0.0.0.0:8080 and interact via http://localhost:8080.
Key Features
- Zero External Dependencies: Built purely with Go's standard library (
net/http,encoding/json,sync). - Thread-Safe In-Memory State: Concurrent
POSTandGETrequests are safely managed with a mutex lock. - Automatic Fallback: Unrecognized or missing CIs default gracefully to
OPS(System Operations team). - Incident Lifecycle Management: Groups repeated alerts under the same active incident; closes incidents on clear alerts (
severity = 0); opens fresh incidents for subsequent alerts with the same message key. - REST-compliant HTTP Status Codes: Returns structured JSON error messages alongside appropriate codes (
400 Bad Request,405 Method Not Allowed).
Configuration Items (CIs)
The server contains a read-only catalog of system Configuration Items:
| 3PID | Description | Assignment Group |
|---|---|---|
JES |
Job Entry Subsystem | Mainframe OS |
AOC |
IBM System Automation | Mainframe OS |
WLP |
IBM WebSphere Liberty Profile | Mainframe WAS |
OPS |
System Operations team | Mainframe Operations (Default) |
Note: Students cannot modify this list; they can only reference these CIs in incident requests.
Getting Started
Prerequisites
- Go (1.18 or later) installed on your machine.
Server Source Code (main.go)
Save the following code to main.go:
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strings"
"sync"
)
// Data Structures
type ConfigurationItem struct {
PID string `json:"3PID"`
Description string `json:"Description"`
Assignment string `json:"Assignment"`
}
type Alert struct {
ID string `json:"id"`
MessageKey string `json:"message_key"`
CI string `json:"ci"`
Severity int `json:"severity"`
IncidentID string `json:"incident_id"`
}
type Incident struct {
ID string `json:"id"`
MessageKey string `json:"message_key"`
Description string `json:"description"`
CI string `json:"ci"`
Status string `json:"status"` // "OPEN" or "CLOSED"
}
type IssueRequest struct {
Description string `json:"description"`
MessageKey string `json:"message_key"`
CI string `json:"ci"`
Severity int `json:"severity"`
}
type ErrorResponse struct {
Error string `json:"error"`
}
// In-Memory Storage & Thread Safety
var (
mu sync.Mutex
alertSeq = 1
incidentSeq = 1
alerts = []Alert{}
incidents = []*Incident{}
// Pre-populated product list (Read-Only)
validCIs = map[string]ConfigurationItem{
"JES": {PID: "JES", Description: "Job Entry Subsystem", Assignment: "Mainframe OS"},
"AOC": {PID: "AOC", Description: "IBM System Automation", Assignment: "Mainframe OS"},
"WLP": {PID: "WLP", Description: "IBM WebSphere Liberty Profile", Assignment: "Mainframe WAS"},
"OPS": {PID: "OPS", Description: "System Operations team", Assignment: "Mainframe Operations"},
}
)
func main() {
http.HandleFunc("/api/ci", handleCIs)
http.HandleFunc("/api/alerts", handleAlerts)
http.HandleFunc("/api/issues", handleIssues)
http.HandleFunc("/api/issue", handleIssue)
port := ":8080"
fmt.Printf("Dummy Education Ticketing Mock running at http://localhost%s
", port)
log.Fatal(http.ListenAndServe(port, nil))
}
// Helper: Find an OPEN incident by message key
func findOpenIncident(messageKey string) *Incident {
for _, inc := range incidents {
if inc.MessageKey == messageKey && inc.Status == "OPEN" {
return inc
}
}
return nil
}
// Helper: Write JSON error response with specific HTTP status code
func writeJSONError(w http.ResponseWriter, message string, statusCode int) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
json.NewEncoder(w).Encode(ErrorResponse{Error: message})
}
// Helper: Write standard JSON response
func writeJSON(w http.ResponseWriter, data interface{}, statusCode int) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
json.NewEncoder(w).Encode(data)
}
// Handler: GET /api/ci
func handleCIs(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeJSONError(w, "Method not allowed. Use GET.", http.StatusMethodNotAllowed)
return
}
ciList := make([]ConfigurationItem, 0, len(validCIs))
for _, ci := range validCIs {
ciList = append(ciList, ci)
}
writeJSON(w, ciList, http.StatusOK)
}
// Handler: GET /api/alerts
func handleAlerts(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeJSONError(w, "Method not allowed. Use GET.", http.StatusMethodNotAllowed)
return
}
mu.Lock()
defer mu.Unlock()
writeJSON(w, alerts, http.StatusOK)
}
// Handler: GET /api/issues
func handleIssues(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeJSONError(w, "Method not allowed. Use GET.", http.StatusMethodNotAllowed)
return
}
mu.Lock()
defer mu.Unlock()
statusFilter := strings.ToUpper(r.URL.Query().Get("status"))
if statusFilter == "" {
writeJSON(w, incidents, http.StatusOK)
return
}
filtered := []*Incident{}
for _, inc := range incidents {
if inc.Status == statusFilter {
filtered = append(filtered, inc)
}
}
writeJSON(w, filtered, http.StatusOK)
}
// Handler: POST /api/issue or GET /api/issue?id=xxxxxx
func handleIssue(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
getAlertsByIncident(w, r)
case http.MethodPost:
createOrUpdateIssue(w, r)
default:
writeJSONError(w, "Method not allowed. Use GET or POST.", http.StatusMethodNotAllowed)
}
}
// GET /api/issue?id=xxxxxx
func getAlertsByIncident(w http.ResponseWriter, r *http.Request) {
incID := r.URL.Query().Get("id")
if incID == "" {
writeJSONError(w, "Query parameter 'id' is required", http.StatusBadRequest)
return
}
mu.Lock()
defer mu.Unlock()
relatedAlerts := []Alert{}
for _, alert := range alerts {
if alert.IncidentID == incID {
relatedAlerts = append(relatedAlerts, alert)
}
}
writeJSON(w, relatedAlerts, http.StatusOK)
}
// POST /api/issue
func createOrUpdateIssue(w http.ResponseWriter, r *http.Request) {
var req IssueRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSONError(w, "Invalid JSON request body", http.StatusBadRequest)
return
}
if strings.TrimSpace(req.MessageKey) == "" {
writeJSONError(w, "Field 'message_key' is required", http.StatusBadRequest)
return
}
// Validate CI, fallback to OPS if invalid or empty
ci := req.CI
if _, exists := validCIs[ci]; !exists {
ci = "OPS"
}
mu.Lock()
defer mu.Unlock()
activeIncident := findOpenIncident(req.MessageKey)
// Handle Severity 0 -> Clear alert & close active incident
if req.Severity == 0 {
if activeIncident == nil {
writeJSONError(w, fmt.Sprintf("No open incident found for message_key '%s' to clear", req.MessageKey), http.StatusBadRequest)
return
}
activeIncident.Status = "CLOSED"
alertID := fmt.Sprintf("ALT-%05d", alertSeq)
alertSeq++
clearingAlert := Alert{
ID: alertID,
MessageKey: req.MessageKey,
CI: ci,
Severity: 0,
IncidentID: activeIncident.ID,
}
alerts = append(alerts, clearingAlert)
writeJSON(w, map[string]interface{}{
"status": "cleared",
"incident_id": activeIncident.ID,
"alert_id": alertID,
"message": "Incident closed and alert cleared.",
}, http.StatusOK)
return
}
// Create a new incident if none is open for this message_key
if activeIncident == nil {
incID := fmt.Sprintf("INC-%05d", incidentSeq)
incidentSeq++
newInc := &Incident{
ID: incID,
MessageKey: req.MessageKey,
Description: req.Description,
CI: ci,
Status: "OPEN",
}
incidents = append(incidents, newInc)
activeIncident = newInc
}
// Attach new alert to current open incident
alertID := fmt.Sprintf("ALT-%05d", alertSeq)
alertSeq++
newAlert := Alert{
ID: alertID,
MessageKey: req.MessageKey,
CI: ci,
Severity: req.Severity,
IncidentID: activeIncident.ID,
}
alerts = append(alerts, newAlert)
writeJSON(w, map[string]interface{}{
"status": "processed",
"incident_id": activeIncident.ID,
"alert_id": alertID,
"ci_used": ci,
}, http.StatusOK)
}
Running the Server
Start the Go server by running:
go run main.go
By default, the server listens on http://localhost:8080.
API Reference & curl Examples
1. List Configuration Items (GET /api/ci)
Retrieve the list of valid CIs available in the system.
curl -i -X GET http://localhost:8080/api/ci
Sample Response (200 OK):
[
{
"3PID": "JES",
"Description": "Job Entry Subsystem",
"Assignment": "Mainframe OS"
},
{
"3PID": "AOC",
"Description": "IBM System Automation",
"Assignment": "Mainframe OS"
},
{
"3PID": "WLP",
"Description": "IBM WebSphere Liberty Profile",
"Assignment": "Mainframe WAS"
},
{
"3PID": "OPS",
"Description": "System Operations team",
"Assignment": "Mainframe Operations"
}
]
2. List All Alerts (GET /api/alerts)
Retrieve all historical alerts received by the server.
curl -i -X GET http://localhost:8080/api/alerts
Sample Response (200 OK):
[
{
"id": "ALT-00001",
"message_key": "job_failure_99",
"ci": "JES",
"severity": 3,
"incident_id": "INC-00001"
}
]
3. List All Issues / Incidents (GET /api/issues)
Retrieve all incidents (OPEN and CLOSED). Optional filtering by status is available via query parameter.
Get All Incidents
curl -i -X GET http://localhost:8080/api/issues
Filter Open Incidents Only
curl -i -X GET "http://localhost:8080/api/issues?status=OPEN"
Filter Closed Incidents Only
curl -i -X GET "http://localhost:8080/api/issues?status=CLOSED"
Sample Response (200 OK):
[
{
"id": "INC-00001",
"message_key": "job_failure_99",
"description": "Mainframe Batch Job Failed",
"ci": "JES",
"status": "OPEN"
}
]
4. Get Alerts for a Specific Incident (GET /api/issue?id=<INC_ID>)
Fetch all alerts linked to a specific incident ID.
curl -i -X GET "http://localhost:8080/api/issue?id=INC-00001"
Sample Response (200 OK):
[
{
"id": "ALT-00001",
"message_key": "job_failure_99",
"ci": "JES",
"severity": 3,
"incident_id": "INC-00001"
},
{
"id": "ALT-00002",
"message_key": "job_failure_99",
"ci": "JES",
"severity": 4,
"incident_id": "INC-00001"
}
]
5. Create or Update an Incident (POST /api/issue)
Send an alert payload.
- If no open incident exists for the given
message_key, a new incident (INC-xxxxx) is created and an alert (ALT-xxxxx) is attached. - If an open incident already exists for the given
message_key, no new incident is created; the new alert is linked to the existing open incident. - If the specified
ciis invalid or missing, it defaults toOPS.
Example A: Trigger initial alert (Creates INC-00001)
curl -i -X POST http://localhost:8080/api/issue \
-H "Content-Type: application/json" \
-d '{
"description": "Job JES001 abended with RC=12",
"message_key": "jes_job_01",
"ci": "JES",
"severity": 3
}'
Sample Response (200 OK):
{
"alert_id": "ALT-00001",
"ci_used": "JES",
"incident_id": "INC-00001",
"status": "processed"
}
Example B: Subsequent alert with same message_key (Attaches to existing INC-00001)
curl -i -X POST http://localhost:8080/api/issue \
-H "Content-Type: application/json" \
-d '{
"description": "Job JES001 retry failed",
"message_key": "jes_job_01",
"ci": "JES",
"severity": 4
}'
Sample Response (200 OK):
{
"alert_id": "ALT-00002",
"ci_used": "JES",
"incident_id": "INC-00001",
"status": "processed"
}
Example C: Alert with invalid CI (Falls back to OPS)
curl -i -X POST http://localhost:8080/api/issue \
-H "Content-Type: application/json" \
-d '{
"description": "Unknown system glitch",
"message_key": "unk_001",
"ci": "UNKNOWN_SYSTEM",
"severity": 2
}'
Sample Response (200 OK):
{
"alert_id": "ALT-00003",
"ci_used": "OPS",
"incident_id": "INC-00002",
"status": "processed"
}
6. Clear Alert and Close Incident (POST /api/issue with severity: 0)
Sending an alert with severity: 0 clears the alert state and transitions the matching open incident to CLOSED.
curl -i -X POST http://localhost:8080/api/issue \
-H "Content-Type: application/json" \
-d '{
"description": "Job completed successfully after manual restart",
"message_key": "jes_job_01",
"ci": "JES",
"severity": 0
}'
Sample Response (200 OK):
{
"alert_id": "ALT-00004",
"incident_id": "INC-00001",
"message": "Incident closed and alert cleared.",
"status": "cleared"
}
HTTP Status Codes & Error Handling
The API uses standard HTTP response codes and returns structured JSON error messages in the format {"error": "message"}.
1. 400 Bad Request — Missing message_key
curl -i -X POST http://localhost:8080/api/issue \
-H "Content-Type: application/json" \
-d '{"description": "Missing key test", "severity": 1}'
Response (400 Bad Request):
{
"error": "Field 'message_key' is required"
}
2. 400 Bad Request — Clearing a Non-existent/Closed Incident
Attempting to send severity: 0 for a message_key that has no currently OPEN incident:
curl -i -X POST http://localhost:8080/api/issue \
-H "Content-Type: application/json" \
-d '{
"description": "Clear non-existent job",
"message_key": "non_existent_key",
"severity": 0
}'
Response (400 Bad Request):
{
"error": "No open incident found for message_key 'non_existent_key' to clear"
}
3. 400 Bad Request — Missing Query Parameter
curl -i -X GET http://localhost:8080/api/issue
Response (400 Bad Request):
{
"error": "Query parameter 'id' is required"
}
4. 405 Method Not Allowed — Invalid HTTP Verb
Attempting to send a DELETE or POST request to a GET-only endpoint:
curl -i -X DELETE http://localhost:8080/api/ci
Response (405 Method Not Allowed):
{
"error": "Method not allowed. Use GET."
}
Python Hands-on Client Example
Here is a Python client script using requests that demonstrates interaction with the Education Ticketing endpoints during workshop exercises:
import requests
BASE_URL = "http://localhost:8080/api"
# 1. Fetch products / CIs
ci_res = requests.get(f"{BASE_URL}/ci").json()
print("Available CIs:", ci_res)
# 2. Trigger an Incident (Valid CI)
payload = {
"description": "Mainframe Batch Job Failed",
"message_key": "job_failure_99",
"ci": "JES",
"severity": 3,
}
res1 = requests.post(f"{BASE_URL}/issue", json=payload).json()
print("
First Alert Sent:", res1)
# 3. Trigger another alert for same Message Key (Attaches to same INC)
res2 = requests.post(f"{BASE_URL}/issue", json=payload).json()
print("Second Alert Sent:", res2)
# 4. Trigger with invalid CI (Falls back to OPS)
invalid_ci_payload = {
"description": "Unknown system issue",
"message_key": "unknown_01",
"ci": "INVALID_CI",
"severity": 2,
}
res3 = requests.post(f"{BASE_URL}/issue", json=invalid_ci_payload).json()
print("
Invalid CI Fallback Test:", res3)
# 5. Clear Incident (Severity = 0)
clear_payload = {
"description": "Resolved",
"message_key": "job_failure_99",
"ci": "JES",
"severity": 0,
}
res4 = requests.post(f"{BASE_URL}/issue", json=clear_payload).json()
print("
Clear Incident Test:", res4)
# 6. Fetch all incidents (Open & Closed)
incidents = requests.get(f"{BASE_URL}/issues").json()
print("
All Incidents:", incidents)
Incident Lifecycle Rules
POST /api/issue (severity > 0)
│
Does OPEN incident exist
for this message_key?
/ \
/ \
NO YES
/ \
v v
Create NEW Incident Attach Alert to
(Status: "OPEN") EXISTING Incident
│ │
└────────┬────────┘
│
POST /api/issue (severity = 0)
│
Set Incident Status = "CLOSED"
│
Next POST with same message_key
creates a BRAND NEW Incident