|
| 1 | +# CSAPI-Go Server Integration Report |
| 2 | + |
| 3 | +**Date:** 2026-04-17 |
| 4 | +**Status:** In Progress |
| 5 | +**Scope:** Dual-publish the entire OS4CSAPI publisher fleet to the connected-systems-go server |
| 6 | + |
| 7 | +--- |
| 8 | + |
| 9 | +## 1 Executive Summary |
| 10 | + |
| 11 | +OS4CSAPI has deployed a second Connected Systems API server — [connected-systems-go](https://github.com/OS4CSAPI/connected-systems-go) — alongside the existing OSH SensorHub. This report documents the integration effort: server architecture, behavioral differences discovered during live testing, workarounds applied, publishers migrated to date, and the plan to complete the remaining fleet. |
| 12 | + |
| 13 | +**Current state:** 2 of 10 publishers dual-publishing. 6 GitHub issues filed against the Go server. |
| 14 | + |
| 15 | +--- |
| 16 | + |
| 17 | +## 2 Server Architecture |
| 18 | + |
| 19 | +### 2.1 Deployment Topology |
| 20 | + |
| 21 | +| Component | SensorHub | connected-systems-go | |
| 22 | +|---|---|---| |
| 23 | +| **URL** | `https://129-80-248-53.sslip.io/sensorhub/api` | `https://129-80-248-53.sslip.io/csapi-go/` | |
| 24 | +| **Host** | Oracle Cloud VM (same host) | Oracle Cloud VM (same host) | |
| 25 | +| **Port** | 8181 (behind Caddy) | 8282 (behind Caddy) | |
| 26 | +| **Runtime** | Java / OSH SensorHub | Go binary / Docker | |
| 27 | +| **Storage** | H2 embedded DB | PostgreSQL + PostGIS | |
| 28 | +| **Auth** | HTTP Basic (`os4csapi` / `ogc134mm`) | None (tolerates auth headers) | |
| 29 | +| **IDs** | Short numeric (e.g., `0520`) | UUIDs (e.g., `a1b2c3d4-...`) | |
| 30 | +| **Reverse proxy** | Caddy route `/sensorhub/*` | Caddy route `/csapi-go/*` | |
| 31 | +| **Explorer preset** | "OSH SensorHub" | "CSAPI-Go" | |
| 32 | + |
| 33 | +### 2.2 Docker Deployment |
| 34 | + |
| 35 | +The Go server runs as a Docker container on the Oracle VM: |
| 36 | + |
| 37 | +``` |
| 38 | +docker run -d \ |
| 39 | + --name csapi-go \ |
| 40 | + --restart unless-stopped \ |
| 41 | + -p 8282:8282 \ |
| 42 | + -e DATABASE_URL=postgres://... \ |
| 43 | + connected-systems-go:latest |
| 44 | +``` |
| 45 | + |
| 46 | +Caddy reverse-proxies `/csapi-go/*` to `localhost:8282`. |
| 47 | + |
| 48 | +### 2.3 Explorer Integration |
| 49 | + |
| 50 | +The [ogc-csapi-explorer](https://github.com/OS4CSAPI/ogc-csapi-explorer) demo app includes: |
| 51 | + |
| 52 | +- **CF Pages proxy:** `demo/functions/api/csapi-go/[[path]].ts` → forwards to Go server |
| 53 | +- **Vite dev proxy:** `demo/vite.config.ts` → local dev routing |
| 54 | +- **Server selector:** "CSAPI-Go" option on the connection page (no auth required) |
| 55 | + |
| 56 | +--- |
| 57 | + |
| 58 | +## 3 Behavioral Differences Discovered |
| 59 | + |
| 60 | +During live integration testing, we identified 6 differences between connected-systems-go and OSH SensorHub. These have been filed as GitHub issues on [OS4CSAPI/connected-systems-go](https://github.com/OS4CSAPI/connected-systems-go/issues). |
| 61 | + |
| 62 | +### 3.1 Bugs (P1–P2) |
| 63 | + |
| 64 | +#### Issue #1 — Datastream `uid` empty-string unique constraint violation |
| 65 | + |
| 66 | +- **GitHub:** [OS4CSAPI/connected-systems-go#1](https://github.com/OS4CSAPI/connected-systems-go/issues/1) |
| 67 | +- **Severity:** P1-Critical |
| 68 | +- **Problem:** POST a datastream without `uid` → server stores `uid = ""`. Second POST (also without `uid`) fails with a PostgreSQL unique constraint violation. |
| 69 | +- **Workaround:** Always provide an explicit `uid` on datastream creation. |
| 70 | +- **Code change:** All `ensure_datastream()` calls now include `"uid": "urn:os4csapi:datastream:..."`. |
| 71 | + |
| 72 | +#### Issue #2 — DELETE does not cascade FK constraints |
| 73 | + |
| 74 | +- **GitHub:** [OS4CSAPI/connected-systems-go#2](https://github.com/OS4CSAPI/connected-systems-go/issues/2) |
| 75 | +- **Severity:** P1-Critical |
| 76 | +- **Problem:** DELETE on a parent resource (deployment, system) fails with a raw PostgreSQL FK violation instead of cascading or returning a structured `409` error. |
| 77 | +- **Workaround:** Delete child resources bottom-up (observations → datastreams → systems → deployments). |
| 78 | +- **Impact:** Bootstrap `--clean` operations must be carefully ordered. |
| 79 | + |
| 80 | +### 3.2 Research Spikes (P3–P4) |
| 81 | + |
| 82 | +#### Issue #3 — Time fields must be ISO 8601 strings |
| 83 | + |
| 84 | +- **GitHub:** [OS4CSAPI/connected-systems-go#3](https://github.com/OS4CSAPI/connected-systems-go/issues/3) |
| 85 | +- **Severity:** P3-Minor |
| 86 | +- **Comparison:** SensorHub accepts both numeric epoch values and ISO strings. Go server rejects numerics. |
| 87 | +- **Workaround:** Publishers detect `"csapi-go"` in the base URL and coerce time fields to strings. |
| 88 | +- **Code change (USGS EQ):** |
| 89 | + ```python |
| 90 | + self._coerce_time_to_str = "csapi-go" in self._base_url |
| 91 | + # In _post_observation(): |
| 92 | + if self._coerce_time_to_str: |
| 93 | + for key in ("eventTime", "updatedTime"): |
| 94 | + if key in r and not isinstance(r[key], str): |
| 95 | + r[key] = str(r[key]) |
| 96 | + ``` |
| 97 | + |
| 98 | +#### Issue #4 — Rejects `"NaN"` strings for numeric fields |
| 99 | + |
| 100 | +- **GitHub:** [OS4CSAPI/connected-systems-go#4](https://github.com/OS4CSAPI/connected-systems-go/issues/4) |
| 101 | +- **Severity:** P3-Minor |
| 102 | +- **Comparison:** SensorHub accepts `"NaN"` as a Gson-compatible token. Go server rejects it for schema-declared numeric fields. |
| 103 | +- **Workaround:** Publishers detect the Go server and replace `"NaN"` with `0.0`. |
| 104 | +- **Code change (OpenSky):** |
| 105 | + ```python |
| 106 | + self._is_go_server = "csapi-go" in self._base_url |
| 107 | + # In _post_observation(): |
| 108 | + if self._is_go_server: |
| 109 | + for key, val in r.items(): |
| 110 | + if val == "NaN": |
| 111 | + r[key] = 0.0 |
| 112 | + ``` |
| 113 | +- **Trade-off:** `0.0` is semantically different from "no data" — for altitude, 0 means sea level. |
| 114 | + |
| 115 | +#### Issue #5 — Strict schema validation (all declared fields required) |
| 116 | + |
| 117 | +- **GitHub:** [OS4CSAPI/connected-systems-go#5](https://github.com/OS4CSAPI/connected-systems-go/issues/5) |
| 118 | +- **Severity:** P3-Minor |
| 119 | +- **Comparison:** SensorHub accepts observations with a subset of schema-declared fields. Go server requires ALL fields present. |
| 120 | +- **Workaround:** Publishers include every declared field in every observation (e.g., duplicating `resultTime` as `result.timestamp`). |
| 121 | +- **Code change (OpenSky):** Keep `timestamp` field in the observation result even though it duplicates `resultTime`. |
| 122 | + |
| 123 | +#### Issue #6 — Returns `@link` objects only, not flat `@id` strings |
| 124 | + |
| 125 | +- **GitHub:** [OS4CSAPI/connected-systems-go#6](https://github.com/OS4CSAPI/connected-systems-go/issues/6) |
| 126 | +- **Severity:** P4-Informational |
| 127 | +- **Comparison:** SensorHub returns `"system@id": "0520"`. Go server returns `"system@link": { "href": "systems/<uuid>" }`. |
| 128 | +- **Impact:** Broke the Explorer's map view (0 datastreams shown) and the library's parent-system resolution. |
| 129 | +- **Workaround (Explorer):** `extractSystemId()` helper with `system@link.href` fallback. Pushed as commit `2f0869a`. |
| 130 | +- **Workaround (Library):** `parseBaseStream()` in `part2.ts` checks `system@link.href` → `system@id` chain. Filed as [ogc-client-CSAPI_2#166](https://github.com/OS4CSAPI/ogc-client-CSAPI_2/issues/166). |
| 131 | + |
| 132 | +### 3.3 Other Go Server Characteristics |
| 133 | + |
| 134 | +| Behavior | SensorHub | connected-systems-go | |
| 135 | +|---|---|---| |
| 136 | +| Collection key for geo-resources | `items` | `features` (GeoJSON) | |
| 137 | +| Collection key for datastreams | `items` | `items` | |
| 138 | +| ID format | Short numeric | UUID | |
| 139 | +| Auth requirement | Required (HTTP Basic) | None (headers tolerated) | |
| 140 | +| Datastream UID on create | Optional (auto-generated) | Effectively required (see #1) | |
| 141 | + |
| 142 | +--- |
| 143 | + |
| 144 | +## 4 Code Changes Applied |
| 145 | + |
| 146 | +### 4.1 Shared Infrastructure — `bootstrap_helpers.py` |
| 147 | + |
| 148 | +**`find_by_uid()`** — Updated to check both `items` and `features` keys in collection responses, since the Go server wraps geo-resources in GeoJSON `features` arrays: |
| 149 | + |
| 150 | +```python |
| 151 | +# Support both GeoJSON (features) and flat JSON (items) collections |
| 152 | +items = result.get("items", []) or result.get("features", []) |
| 153 | +``` |
| 154 | + |
| 155 | +### 4.2 USGS Earthquake Publisher |
| 156 | + |
| 157 | +**Files changed:** `bootstrap_usgs_eq.py`, `usgs_eq_publisher.py` |
| 158 | + |
| 159 | +| Change | Detail | |
| 160 | +|---|---| |
| 161 | +| `uid` on datastream | Added to bootstrap: `"uid": "urn:os4csapi:datastream:usgs-eq-feed:earthquakeEvent:v1"` | |
| 162 | +| `OSH_BASE_URL` override | Publisher reads `OSH_BASE_URL` env var to target Go server | |
| 163 | +| Time coercion | `_coerce_time_to_str` flag — converts `eventTime`/`updatedTime` to strings when targeting Go | |
| 164 | + |
| 165 | +**Systemd services:** |
| 166 | +- `usgs-eq-publisher.service` → SensorHub |
| 167 | +- `usgs-eq-publisher-go.service` → Go server (env: `OSH_BASE_URL=https://129-80-248-53.sslip.io/csapi-go`) |
| 168 | + |
| 169 | +**Confirmed:** 300 published, 0 errors per cycle on both servers. |
| 170 | + |
| 171 | +### 4.3 OpenSky ADS-B Publisher |
| 172 | + |
| 173 | +**Files changed:** `bootstrap_opensky.py`, `opensky_publisher.py` |
| 174 | + |
| 175 | +| Change | Detail | |
| 176 | +|---|---| |
| 177 | +| `uid` on datastream | Added to bootstrap: `"uid": "urn:os4csapi:datastream:opensky-feed:adsbState:v1"` | |
| 178 | +| `OSH_BASE_URL` override | Publisher reads `OSH_BASE_URL` env var | |
| 179 | +| `_is_go_server` flag | Detects Go server from URL | |
| 180 | +| NaN → 0.0 | Replaces `"NaN"` strings with `0.0` for numeric fields | |
| 181 | +| Keep `timestamp` field | Retains the field in observation results (Go requires all schema fields) | |
| 182 | + |
| 183 | +**Systemd services:** |
| 184 | +- `opensky-publisher.service` → SensorHub |
| 185 | +- `opensky-publisher-go.service` → Go server |
| 186 | + |
| 187 | +**Confirmed:** ~170 published, 0 errors per cycle on both servers. |
| 188 | + |
| 189 | +--- |
| 190 | + |
| 191 | +## 5 Go Server Resource Inventory |
| 192 | + |
| 193 | +After bootstrapping USGS EQ and OpenSky on the Go server: |
| 194 | + |
| 195 | +| Resource Type | Count | Examples | |
| 196 | +|---|---|---| |
| 197 | +| Systems | 2 | USGS Earthquake Feed, OpenSky ADS-B Feed | |
| 198 | +| Datastreams | 2 | earthquakeEvent, adsbState | |
| 199 | +| Deployments | 2 | USGS EQ deployment, OpenSky deployment | |
| 200 | +| Procedures | 2 | USGS EQ procedure, OpenSky procedure | |
| 201 | +| Observations | Growing | ~300 EQ events/cycle, ~170 aircraft states/cycle | |
| 202 | + |
| 203 | +--- |
| 204 | + |
| 205 | +## 6 Dual-Publish Pattern |
| 206 | + |
| 207 | +The established pattern for adding Go server support to any publisher: |
| 208 | + |
| 209 | +### 6.1 Bootstrap Script Changes |
| 210 | + |
| 211 | +1. Add explicit `uid` to all `ensure_datastream()` calls: |
| 212 | + ```python |
| 213 | + ensure_datastream(base_url, auth, system_id, { |
| 214 | + "uid": "urn:os4csapi:datastream:<publisher>:<output>:v1", |
| 215 | + "outputName": "<output>", |
| 216 | + ... |
| 217 | + }) |
| 218 | + ``` |
| 219 | + |
| 220 | +### 6.2 Publisher Code Changes |
| 221 | + |
| 222 | +1. **`OSH_BASE_URL` override** — read from env to target a different server: |
| 223 | + ```python |
| 224 | + self._base_url = os.environ.get( |
| 225 | + "OSH_BASE_URL", |
| 226 | + f"https://{self.osh_address}/{self.osh_root}/api", |
| 227 | + ) |
| 228 | + ``` |
| 229 | + |
| 230 | +2. **Go server detection** — set a flag for conditional behavior: |
| 231 | + ```python |
| 232 | + self._is_go_server = "csapi-go" in self._base_url |
| 233 | + ``` |
| 234 | + |
| 235 | +3. **Time coercion** — convert numeric time fields to strings: |
| 236 | + ```python |
| 237 | + if self._is_go_server: |
| 238 | + for key in TIME_FIELDS: |
| 239 | + if key in result and not isinstance(result[key], str): |
| 240 | + result[key] = str(result[key]) |
| 241 | + ``` |
| 242 | + |
| 243 | +4. **NaN replacement** — substitute `0.0` for `"NaN"` strings: |
| 244 | + ```python |
| 245 | + if self._is_go_server: |
| 246 | + for key, val in result.items(): |
| 247 | + if val == "NaN": |
| 248 | + result[key] = 0.0 |
| 249 | + ``` |
| 250 | + |
| 251 | +5. **Include all schema fields** — ensure every declared field is present in every observation. |
| 252 | + |
| 253 | +### 6.3 VM Deployment |
| 254 | + |
| 255 | +1. Run bootstrap against Go server: |
| 256 | + ```bash |
| 257 | + OSH_BASE_URL="https://129-80-248-53.sslip.io/csapi-go" \ |
| 258 | + OSH_USER=dummy OSH_PASS=dummy \ |
| 259 | + python -m publishers.<name>.bootstrap_<name> |
| 260 | + ``` |
| 261 | + |
| 262 | +2. Create systemd service (copy existing, change env): |
| 263 | + ```ini |
| 264 | + [Service] |
| 265 | + Environment="OSH_ADDRESS=129-80-248-53.sslip.io" |
| 266 | + Environment="OSH_BASE_URL=https://129-80-248-53.sslip.io/csapi-go" |
| 267 | + Environment="OSH_USER=dummy" |
| 268 | + Environment="OSH_PASS=dummy" |
| 269 | + ExecStart=/usr/bin/python3 -m publishers.<name>.<name>_publisher |
| 270 | + ``` |
| 271 | + |
| 272 | +3. Enable and start: `sudo systemctl enable --now <name>-publisher-go.service` |
| 273 | + |
| 274 | +--- |
| 275 | + |
| 276 | +## 7 Publisher Fleet Status |
| 277 | + |
| 278 | +| # | Publisher | SensorHub | Go Server | Notes | |
| 279 | +|---|-----------|:---------:|:---------:|-------| |
| 280 | +| 1 | USGS Earthquake | ✅ Running | ✅ Running | First dual-publish. 300 obs/cycle. | |
| 281 | +| 2 | OpenSky ADS-B | ✅ Running | ✅ Running | ~170 obs/cycle. NaN→0.0 workaround. | |
| 282 | +| 3 | ISS | ✅ Running | ❌ Not started | 1 system, 2 DS. Low complexity. | |
| 283 | +| 4 | NWS | ✅ Running | ❌ Not started | 10 stations. Medium complexity. | |
| 284 | +| 5 | NDBC | ✅ Running | ❌ Not started | 5 buoys. Medium complexity. | |
| 285 | +| 6 | NDBC BuoyCAM | ✅ Running | ❌ Not started | 5 cameras. Image observations — may need special handling. | |
| 286 | +| 7 | CO-OPS | ✅ Running | ❌ Not started | 5 tide stations. Medium complexity. | |
| 287 | +| 8 | AviationWeather | ✅ Running | ❌ Not started | 5 METAR stations. Medium complexity. | |
| 288 | +| 9 | USGS Water | ✅ Running | ❌ Not started | 8 stations. Medium complexity. | |
| 289 | +| 10 | USGS NIMS | ✅ Running | ❌ Not started | 8 cameras. Image observations. | |
| 290 | + |
| 291 | +**Excluded from dual-publish:** |
| 292 | +- **UAS Simulator** — FastAPI service, not a publisher in the same sense; separate integration path. |
| 293 | +- **Localizer** — Depends on UAS simulator data; separate integration path. |
| 294 | + |
| 295 | +--- |
| 296 | + |
| 297 | +## 8 GitHub Issues Filed |
| 298 | + |
| 299 | +All filed on [OS4CSAPI/connected-systems-go](https://github.com/OS4CSAPI/connected-systems-go/issues): |
| 300 | + |
| 301 | +| # | Label | Title | |
| 302 | +|---|-------|-------| |
| 303 | +| [#1](https://github.com/OS4CSAPI/connected-systems-go/issues/1) | bug | Datastream creation without explicit `uid` stores empty string, violates unique constraint | |
| 304 | +| [#2](https://github.com/OS4CSAPI/connected-systems-go/issues/2) | bug | DELETE on parent resources fails with raw PostgreSQL FK constraint error | |
| 305 | +| [#3](https://github.com/OS4CSAPI/connected-systems-go/issues/3) | enhancement | Research: Time field encoding — strict ISO 8601 vs. numeric timestamps | |
| 306 | +| [#4](https://github.com/OS4CSAPI/connected-systems-go/issues/4) | enhancement | Research: NaN handling for numeric observation fields | |
| 307 | +| [#5](https://github.com/OS4CSAPI/connected-systems-go/issues/5) | enhancement | Research: Strict schema validation — requiring ALL declared fields | |
| 308 | +| [#6](https://github.com/OS4CSAPI/connected-systems-go/issues/6) | enhancement | Research: Cross-resource references — `@link` objects only vs. flat `@id` strings | |
| 309 | + |
| 310 | +Related library issue: [ogc-client-CSAPI_2#166](https://github.com/OS4CSAPI/ogc-client-CSAPI_2/issues/166) — Library parsers need `@link.href` fallback. |
| 311 | + |
| 312 | +--- |
| 313 | + |
| 314 | +## 9 Git Status |
| 315 | + |
| 316 | +### OSHConnect-Python (this repo) |
| 317 | + |
| 318 | +6 unpushed commits (`e022ef2`..`eed2e4d`) on `main`: |
| 319 | + |
| 320 | +| Commit | Description | |
| 321 | +|--------|-------------| |
| 322 | +| `e022ef2` | Go CSAPI compat: uid on datastreams, str Time fields, GeoJSON features lookup, OSH_BASE_URL override | |
| 323 | +| `cfa4395` | Fix display URL to use actual base_url | |
| 324 | +| `1cda1d5` | Dual-publish compat: coerce Time fields to strings only for Go server | |
| 325 | +| `63c5201` | OpenSky: add OSH_BASE_URL override, uid on datastream, fix display URL | |
| 326 | +| `d331433` | OpenSky: keep timestamp field for Go server (schema validation requires it) | |
| 327 | +| `eed2e4d` | OpenSky: replace NaN strings with 0.0 for Go server (strict JSON validation) | |
| 328 | + |
| 329 | +**Action needed:** Push to origin before continuing. |
| 330 | + |
| 331 | +### ogc-csapi-explorer |
| 332 | + |
| 333 | +Commit `2f0869a` (Go server `@link.href` compat) already pushed. |
| 334 | + |
| 335 | +--- |
| 336 | + |
| 337 | +## 10 Next Steps — Remaining Publisher Migration |
| 338 | + |
| 339 | +### Phase 1: Push existing work |
| 340 | +1. Push 6 unpushed OSHConnect-Python commits to GitHub |
| 341 | + |
| 342 | +### Phase 2: Migrate remaining publishers (8) |
| 343 | +For each publisher, apply the dual-publish pattern (§6): |
| 344 | + |
| 345 | +| Priority | Publisher | Rationale | |
| 346 | +|----------|-----------|-----------| |
| 347 | +| 1 | ISS | Simplest (1 system, 2 DS). Quick win to validate pattern. | |
| 348 | +| 2 | NWS | 10 stations, well-tested bootstrap. Good stress test. | |
| 349 | +| 3 | NDBC | 5 buoys, similar structure to NWS. | |
| 350 | +| 4 | CO-OPS | 5 tide stations, similar structure. | |
| 351 | +| 5 | AviationWeather | 5 METAR stations, similar structure. | |
| 352 | +| 6 | USGS Water | 8 stations, similar structure. | |
| 353 | +| 7 | NDBC BuoyCAM | Image observations — may require Go server testing for binary/URL payloads. | |
| 354 | +| 8 | USGS NIMS | Image observations — same considerations as BuoyCAM. | |
| 355 | + |
| 356 | +### Phase 3: Verification |
| 357 | +- Confirm all Go server services running with 0 errors |
| 358 | +- Run Explorer smoke test against Go server |
| 359 | +- Update health-check dashboard to monitor Go server publishers |
| 360 | + |
| 361 | +### Phase 4: Documentation |
| 362 | +- Update this report with final status |
| 363 | +- Update `Publisher_Fleet_Portability_Plan.md` to reference dual-publish capability |
| 364 | +- Update `README.md` with Go server instructions |
| 365 | + |
| 366 | +--- |
| 367 | + |
| 368 | +## Appendix A — VM Service Inventory |
| 369 | + |
| 370 | +Services running on `129.80.248.53` as of 2026-04-17: |
| 371 | + |
| 372 | +``` |
| 373 | +# SensorHub publishers (12) |
| 374 | +iss-publisher.service |
| 375 | +nws-publisher.service |
| 376 | +ndbc-publisher.service |
| 377 | +ndbc-buoycam-publisher.service |
| 378 | +coops-publisher.service |
| 379 | +aviation-wx-publisher.service |
| 380 | +opensky-publisher.service |
| 381 | +usgs-eq-publisher.service |
| 382 | +usgs-water-publisher.service |
| 383 | +usgs-nims-publisher.service |
| 384 | +
|
| 385 | +# Go server publishers (2) |
| 386 | +usgs-eq-publisher-go.service |
| 387 | +opensky-publisher-go.service |
| 388 | +
|
| 389 | +# Other |
| 390 | +simulator.service |
| 391 | +``` |
0 commit comments