From 48dee8228d880e21c97ad0e940e5210bf8eca894 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Tue, 26 May 2026 10:18:50 +0300 Subject: [PATCH 01/11] docs: add migration design spec for Nuxt 4 + Vuetify 4 upgrade Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...6-05-26-nuxt4-vuetify4-migration-design.md | 385 ++++++++++++++++++ 1 file changed, 385 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-26-nuxt4-vuetify4-migration-design.md diff --git a/docs/superpowers/specs/2026-05-26-nuxt4-vuetify4-migration-design.md b/docs/superpowers/specs/2026-05-26-nuxt4-vuetify4-migration-design.md new file mode 100644 index 00000000..bc982b57 --- /dev/null +++ b/docs/superpowers/specs/2026-05-26-nuxt4-vuetify4-migration-design.md @@ -0,0 +1,385 @@ +# httpSMS Frontend Migration: Nuxt 2 + Vuetify 2 → Nuxt 4 + Vuetify 4 + +## Summary + +Migrate the `web/` frontend from Nuxt 2 (Vue 2, Vuetify 2, Vuex, class-based components) to Nuxt 4 (Vue 3, Vuetify 4, Pinia, ` +``` + +**After (Vue 3):** +```vue + +``` + +#### Vuetify Breakpoints: `$vuetify.breakpoint` → `useDisplay()` + +**Before:** +```vue + +``` + +**After:** +```vue + + +``` + +#### State: Vuex → Pinia + +**Before:** +```ts +this.$store.dispatch('loadPhones', true) +this.$store.getters.getAuthUser +``` + +**After:** +```ts +const phonesStore = usePhonesStore() +await phonesStore.loadPhones(true) +phonesStore.authUser +``` + +#### Firebase: `this.$fire.auth` → VueFire composables + +**Before:** +```ts +await this.$fire.auth.currentUser?.getIdToken() +``` + +**After:** +```ts +import { useCurrentUser } from 'vuefire' +const user = useCurrentUser() +const token = await user.value?.getIdToken() +``` + +#### Dynamic Routes: `_id` → `[id]` + +- `pages/threads/_id/index.vue` → `pages/threads/[id]/index.vue` +- `pages/heartbeats/_id.vue` → `pages/heartbeats/[id].vue` + +### Vuetify 4 Breaking Changes to Address + +Using the Vuetify MCP for each component, the key changes are: + +1. **CSS Layers** — mandatory in v4; adjust any custom style overrides +2. **Theme** — default is now "system" (we want dark, configure explicitly) +3. **Typography** — MD2 → MD3 type scale (text-h1 → text-display-large, etc.) +4. **Breakpoints** — reduced default sizes (restore v3 values via config) +5. **Elevation** — 25 levels → 6 levels (MD3) +6. **VBtn** — no default uppercase, grid → flex layout +7. **VSnackbar** — removed multi-line prop +8. **VSelect** — "item" slot → "internalItem" +9. **Grid** — v-row/v-col overhauled +10. **CSS Reset** — mostly removed, add selective resets + +### Vuetify MCP Usage Per Component + +For EVERY component/page being migrated, the implementation must: +1. Call `vuetify-mcp-get_component_api_by_version` for each Vuetify component used +2. Call `vuetify-mcp-get_v4_breaking_changes` filtered by relevant category +3. Apply the correct v4 API (props, slots, events) based on MCP output +4. Verify no deprecated props/events remain + +### Pinia Store Design + +Split the monolithic Vuex store into domain stores: + +| Store | Responsibility | +|-------|---------------| +| `auth.ts` | Firebase auth state, user profile, onAuthStateChanged | +| `messages.ts` | Messages CRUD, search | +| `threads.ts` | Message threads, current thread | +| `phones.ts` | Phone list, heartbeats, polling | +| `billing.ts` | Usage, subscription, payments | +| `notifications.ts` | Toast/snackbar queue | +| `app.ts` | App metadata, polling state, runtime config | + +### Plugin Migrations + +| Old Plugin | New Approach | +|-----------|-------------| +| `plugins/axios.ts` | `composables/useApi.ts` using `$fetch` with auth header | +| `plugins/filters.ts` | `utils/filters.ts` (import explicitly or app.config globalProperties) | +| `plugins/vue-glow.ts` | `plugins/vue-glow.client.ts` (client-only plugin) | +| `plugins/chart.ts` | `plugins/chart.client.ts` (client-only plugin) | +| `plugins/errors.ts` | `utils/errors.ts` | +| `plugins/bag.ts` | `utils/bag.ts` | +| `plugins/capitalize.ts` | `utils/capitalize.ts` | +| `plugins/veutify.ts` | `plugins/vuetify.ts` (createVuetify setup) | + +## Migration Order (Tasks) + +### Phase 1: Scaffold & Configuration +1. Initialize fresh Nuxt 4 project in `web/` (backup old code) +2. Install dependencies (vuetify, pinia, nuxt-vuefire, sass, @mdi/js, pusher-js, etc.) +3. Configure `nuxt.config.ts` (SSG, runtime config, modules) +4. Set up Vuetify plugin with dark theme, restored breakpoints, MDI SVG icons +5. Set up nuxt-vuefire with Firebase config +6. Configure TypeScript strictly + +### Phase 2: Foundation +7. Port `shared/types/` (API models — mostly copy) +8. Port `utils/` (errors, filters, bag, capitalize) +9. Create `composables/useApi.ts` (replace Axios plugin) +10. Create `composables/useAuth.ts` (Firebase auth helpers) + +### Phase 3: State Management +11. Create Pinia store: `stores/auth.ts` +12. Create Pinia store: `stores/notifications.ts` +13. Create Pinia store: `stores/app.ts` +14. Create Pinia store: `stores/phones.ts` +15. Create Pinia store: `stores/messages.ts` +16. Create Pinia store: `stores/threads.ts` +17. Create Pinia store: `stores/billing.ts` + +### Phase 4: Layouts & Middleware +18. Port `middleware/auth.ts` +19. Port `middleware/guest.ts` +20. Port `layouts/default.vue` (with Vuetify MCP) +21. Port `layouts/website.vue` (with Vuetify MCP) +22. Port `layouts/error.vue` (with Vuetify MCP) +23. Create `app.vue` + +### Phase 5: Components (use Vuetify MCP for each) +24. Port `components/Toast.vue` +25. Port `components/LoadingDashboard.vue` +26. Port `components/LoadingButton.vue` +27. Port `components/BackButton.vue` +28. Port `components/CopyButton.vue` +29. Port `components/FixedHeader.vue` +30. Port `components/BlogAuthorBio.vue` +31. Port `components/BlogInfo.vue` +32. Port `components/NuxtLogo.vue` +33. Port `components/FirebaseAuth.vue` +34. Port `components/MessageThread.vue` +35. Port `components/MessageThreadHeader.vue` + +### Phase 6: Pages (use Vuetify MCP for each) +36. Port `pages/index.vue` (homepage) +37. Port `pages/login.vue` +38. Port `pages/threads/index.vue` +39. Port `pages/threads/[id]/index.vue` +40. Port `pages/messages/index.vue` +41. Port `pages/search-messages/index.vue` +42. Port `pages/bulk-messages/index.vue` +43. Port `pages/settings/index.vue` +44. Port `pages/billing/index.vue` +45. Port `pages/heartbeats/[id].vue` +46. Port `pages/phone-api-keys/index.vue` +47. Port `pages/privacy-policy/index.vue` +48. Port `pages/terms-and-conditions/index.vue` +49. Port `pages/blog/index.vue` +50. Port `pages/blog/how-to-send-sms-messages-from-excel.vue` +51. Port `pages/blog/grant-send-and-read-sms-permissions-on-android.vue` +52. Port `pages/blog/forward-incoming-sms-from-phone-to-webhook.vue` +53. Port `pages/blog/end-to-end-encryption-to-sms-messages.vue` +54. Port `pages/blog/send-bulk-sms-from-csv-file-with-no-code.vue` +55. Port `pages/blog/send-sms-from-android-phone-with-python.vue` +56. Port `pages/blog/send-sms-when-new-row-is-added-to-google-sheets-using-zapier.vue` + +### Phase 7: Final Setup +57. Port static assets (`public/`) +58. Port environment files (`.env`, `.env.production`) +59. Update Dockerfile and nginx.conf +60. Update sitemap configuration +61. Configure highlight.js (nuxt-highlightjs or manual) + +### Phase 8: Verification (EVERY component and page) +62. Verify `app.vue` renders +63. Verify `layouts/default.vue` renders correctly +64. Verify `layouts/website.vue` renders correctly +65. Verify `layouts/error.vue` renders correctly +66. Verify `components/Toast.vue` renders correctly +67. Verify `components/LoadingDashboard.vue` renders correctly +68. Verify `components/LoadingButton.vue` renders correctly +69. Verify `components/BackButton.vue` renders correctly +70. Verify `components/CopyButton.vue` renders correctly +71. Verify `components/FixedHeader.vue` renders correctly +72. Verify `components/BlogAuthorBio.vue` renders correctly +73. Verify `components/BlogInfo.vue` renders correctly +74. Verify `components/NuxtLogo.vue` renders correctly +75. Verify `components/FirebaseAuth.vue` renders correctly +76. Verify `components/MessageThread.vue` renders correctly +77. Verify `components/MessageThreadHeader.vue` renders correctly +78. Verify `pages/index.vue` renders correctly +79. Verify `pages/login.vue` renders correctly +80. Verify `pages/threads/index.vue` renders correctly +81. Verify `pages/threads/[id]/index.vue` renders correctly +82. Verify `pages/messages/index.vue` renders correctly +83. Verify `pages/search-messages/index.vue` renders correctly +84. Verify `pages/bulk-messages/index.vue` renders correctly +85. Verify `pages/settings/index.vue` renders correctly +86. Verify `pages/billing/index.vue` renders correctly +87. Verify `pages/heartbeats/[id].vue` renders correctly +88. Verify `pages/phone-api-keys/index.vue` renders correctly +89. Verify `pages/privacy-policy/index.vue` renders correctly +90. Verify `pages/terms-and-conditions/index.vue` renders correctly +91. Verify `pages/blog/index.vue` renders correctly +92. Verify all blog subpages render correctly +93. Run `pnpm build` (static generation) successfully +94. Verify no TypeScript errors (`pnpm typecheck`) +95. Verify lint passes (`pnpm lint`) + +## Verification Strategy + +Each verification task in Phase 8 means: +1. Start the dev server (`pnpm dev`) +2. Navigate to the page/route in question +3. Confirm no console errors, no hydration mismatches +4. Confirm visual layout matches intent (Vuetify components render, dark theme active, responsive breakpoints work) +5. For interactive components (forms, modals, auth), confirm basic interactions work + +The build verification (`pnpm build`) confirms all pages can be statically generated without errors. + +## Risk Mitigations + +- **Backup old code**: Keep old `web/` contents in a branch before starting +- **Incremental porting**: Each file is ported and verified before moving to the next +- **Vuetify MCP**: Use for every Vuetify component to catch breaking changes +- **Restored breakpoints**: Keep v2/v3 breakpoint values to minimize layout drift +- **CSS Reset compatibility**: Add selective reset CSS to maintain existing spacing behavior From 7f5074f8e986d20d61e17052e5defd7f729a5875 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Fri, 29 May 2026 11:40:36 +0300 Subject: [PATCH 02/11] chore: add axiom-go and OTLP exporter dependencies Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/go.mod | 27 +++++++++++++------------ api/go.sum | 58 ++++++++++++++++++++++++++++-------------------------- 2 files changed, 44 insertions(+), 41 deletions(-) diff --git a/api/go.mod b/api/go.mod index 1fe7dca1..85c440d5 100644 --- a/api/go.mod +++ b/api/go.mod @@ -48,11 +48,11 @@ require ( github.com/xuri/excelize/v2 v2.10.1 go.mongodb.org/mongo-driver/v2 v2.6.0 go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/v2/mongo/otelmongo v0.0.0-20260513205827-ba143fc95a5e - go.opentelemetry.io/otel v1.43.0 - go.opentelemetry.io/otel/metric v1.43.0 - go.opentelemetry.io/otel/sdk v1.43.0 - go.opentelemetry.io/otel/sdk/metric v1.43.0 - go.opentelemetry.io/otel/trace v1.43.0 + go.opentelemetry.io/otel v1.44.0 + go.opentelemetry.io/otel/metric v1.44.0 + go.opentelemetry.io/otel/sdk v1.44.0 + go.opentelemetry.io/otel/sdk/metric v1.44.0 + go.opentelemetry.io/otel/trace v1.44.0 golang.org/x/sync v0.20.0 google.golang.org/api v0.277.0 google.golang.org/protobuf v1.36.11 @@ -121,6 +121,7 @@ require ( github.com/go-openapi/swag/yamlutils v0.26.0 // indirect github.com/go-sql-driver/mysql v1.10.0 // indirect github.com/goccy/go-json v0.10.6 // indirect + github.com/gofrs/flock v0.13.0 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.15 // indirect @@ -177,9 +178,9 @@ require ( go.opentelemetry.io/contrib/instrumentation/runtime v0.68.0 // indirect go.opentelemetry.io/contrib/processors/minsev v0.16.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.44.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0 // indirect go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0 // indirect go.opentelemetry.io/otel/log v0.19.0 // indirect go.opentelemetry.io/otel/sdk/log v0.19.0 // indirect @@ -190,17 +191,17 @@ require ( go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.51.0 // indirect golang.org/x/mod v0.35.0 // indirect - golang.org/x/net v0.53.0 // indirect + golang.org/x/net v0.55.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect - golang.org/x/sys v0.44.0 // indirect + golang.org/x/sys v0.45.0 // indirect golang.org/x/text v0.37.0 // indirect golang.org/x/time v0.15.0 // indirect golang.org/x/tools v0.44.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto v0.0.0-20260504160031-60b97b32f348 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260504160031-60b97b32f348 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260504160031-60b97b32f348 // indirect - google.golang.org/grpc v1.81.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect + google.golang.org/grpc v1.81.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gorm.io/driver/clickhouse v0.7.0 // indirect gorm.io/driver/mysql v1.6.0 // indirect diff --git a/api/go.sum b/api/go.sum index fa1163a2..3e522e0b 100644 --- a/api/go.sum +++ b/api/go.sum @@ -164,8 +164,8 @@ github.com/gofiber/fiber/v2 v2.52.13 h1:TOKP64iqC9b5P49VrBW5tHhUOvDyrtJ0xePEfzJb github.com/gofiber/fiber/v2 v2.52.13/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= github.com/gofiber/swagger v1.1.1 h1:FZVhVQQ9s1ZKLHL/O0loLh49bYB5l1HEAgxDlcTtkRA= github.com/gofiber/swagger v1.1.1/go.mod h1:vtvY/sQAMc/lGTUCg0lqmBL7Ht9O7uzChpbvJeJQINw= -github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= -github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= +github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= +github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= @@ -371,36 +371,38 @@ go.opentelemetry.io/contrib/processors/minsev v0.16.0 h1:bjTZkvAKnG1mqWgCjU7RkOk go.opentelemetry.io/contrib/processors/minsev v0.16.0/go.mod h1:R2mmaDsqsWb+Y0mQkPifiCwifdotrG4fFoD4z0tim+g= go.opentelemetry.io/contrib/propagators/b3 v1.19.0 h1:ulz44cpm6V5oAeg5Aw9HyqGFMS6XM7untlMEhD7YzzA= go.opentelemetry.io/contrib/propagators/b3 v1.19.0/go.mod h1:OzCmE2IVS+asTI+odXQstRGVfXQ4bXv9nMBRK0nNyqQ= -go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= -go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 h1:HIBTQ3VO5aupLKjC90JgMqpezVXwFuq6Ryjn0/izoag= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0/go.mod h1:ji9vId85hMxqfvICA0Jt8JqEdrXaAkcpkI9HPXya0ro= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 h1:w1K+pCJoPpQifuVpsKamUdn9U0zM3xUziVOqsGksUrY= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0/go.mod h1:HBy4BjzgVE8139ieRI75oXm3EcDN+6GhD88JT1Kjvxg= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.44.0 h1:RuynHbfU8JUEw7DyONgkVYg2SVtsoF28y0LGIr69jgA= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.44.0/go.mod h1:qZF+/lBs71APw8mlnEZcqZHMzqrYrsFiJOv83lX1OGo= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 h1:4YsVu3B8+3qtWYYrsUYgn0OG78pN0rnNPRGX4SbokQI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0/go.mod h1:+wnlSn0mD1ADVMe3v9Z/WIaiz6q6gL2J/ejaAmdmv80= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0 h1:lgh3PiVrRUWMLOVSkQicxzZll5NjF1r+AtsX1XRIHw0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0/go.mod h1:5Cnhth3m/AgOeTgE3ex12pPmiu/gGtZit03kSzx9X7s= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0 h1:TC+BewnDpeiAmcscXbGMfxkO+mwYUwE/VySwvw88PfA= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0/go.mod h1:J/ZyF4vfPwsSr9xJSPyQ4LqtcTPULFR64KwTikGLe+A= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0 h1:mS47AX77OtFfKG4vtp+84kuGSFZHTyxtXIN269vChY0= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0/go.mod h1:PJnsC41lAGncJlPUniSwM81gc80GkgWJWr3cu2nKEtU= go.opentelemetry.io/otel/log v0.19.0 h1:KUZs/GOsw79TBBMfDWsXS+KZ4g2Ckzksd1ymzsIEbo4= go.opentelemetry.io/otel/log v0.19.0/go.mod h1:5DQYeGmxVIr4n0/BcJvF4upsraHjg6vudJJpnkL6Ipk= -go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= -go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= +go.opentelemetry.io/otel/metric/x v0.66.0 h1:YkCrx1zLOChi9ZcZ6euupOcsgzbVlec7D/xoEU1+cTA= +go.opentelemetry.io/otel/metric/x v0.66.0/go.mod h1:d1+BDj9t96do0/1LoU1ayfCv79ZgNE41qbhBvnMOBZk= go.opentelemetry.io/otel/oteltest v1.0.0-RC3 h1:MjaeegZTaX0Bv9uB9CrdVjOFM/8slRjReoWoV9xDCpY= go.opentelemetry.io/otel/oteltest v1.0.0-RC3/go.mod h1:xpzajI9JBRr7gX63nO6kAmImmYIAtuQblZ36Z+LfCjE= -go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= -go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= +go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= go.opentelemetry.io/otel/sdk/log v0.19.0 h1:scYVLqT22D2gqXItnWiocLUKGH9yvkkeql5dBDiXyko= go.opentelemetry.io/otel/sdk/log v0.19.0/go.mod h1:vFBowwXGLlW9AvpuF7bMgnNI95LiW10szrOdvzBHlAg= go.opentelemetry.io/otel/sdk/log/logtest v0.19.0 h1:BEbF7ZBB6qQloV/Ub1+3NQoOUnVtcGkU3XX4Ws3GQfk= go.opentelemetry.io/otel/sdk/log/logtest v0.19.0/go.mod h1:Lua81/3yM0wOmoHTokLj9y9ADeA02v1naRrVrkAZuKk= -go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= -go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= -go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= -go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= +go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= @@ -441,8 +443,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= -golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -466,8 +468,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= -golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -509,12 +511,12 @@ google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAs google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto v0.0.0-20260504160031-60b97b32f348 h1:JjVGDZYWkJWZcxveJGzfkXC5myDVWAd4dZdgbzrDUv8= google.golang.org/genproto v0.0.0-20260504160031-60b97b32f348/go.mod h1:95PqD4xM+AdOcBGsmgfaofXsiA37uXDtDufVbntT3TU= -google.golang.org/genproto/googleapis/api v0.0.0-20260504160031-60b97b32f348 h1:U8orV30l6KpDsi9dxU0CoJZGbjS8EEpw+6ba+XwGPQA= -google.golang.org/genproto/googleapis/api v0.0.0-20260504160031-60b97b32f348/go.mod h1:Yzdzr5OOZFgSsEV2D/Xi9NL3bszpXFAg0hFJiRohcD8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260504160031-60b97b32f348 h1:pfIbyB44sWzHiCpRqIen67ZQnVXSfIxWrqUMk1qwODE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260504160031-60b97b32f348/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw= -google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8= +google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= +google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= From 8859825f3aad4c0c7537f7de5e84d97e23f21c08 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Fri, 29 May 2026 11:43:35 +0300 Subject: [PATCH 03/11] feat: add Axiom zerolog adapter for production logging Replace GCP Cloud Logging JSON output with Axiom's zerolog writer. Logs are shipped directly to Axiom's ingest API in production. Local dev still uses console logger. jsonLogger() kept for rollback. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/go.mod | 5 ++++- api/go.sum | 16 ++++++++++++++++ api/pkg/di/container.go | 22 +++++++++++++++++++++- 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/api/go.mod b/api/go.mod index 85c440d5..75734091 100644 --- a/api/go.mod +++ b/api/go.mod @@ -12,6 +12,7 @@ require ( github.com/NdoleStudio/lemonsqueezy-go v1.3.1 github.com/NdoleStudio/plunk-go v0.0.2 github.com/avast/retry-go/v5 v5.0.0 + github.com/axiomhq/axiom-go v0.32.0 github.com/carlmjohnson/requests v0.25.1 github.com/cloudevents/sdk-go/v2 v2.16.2 github.com/cockroachdb/cockroach-go/v2 v2.4.3 @@ -95,6 +96,8 @@ require ( github.com/PuerkitoBio/goquery v1.12.0 // indirect github.com/andybalholm/brotli v1.2.1 // indirect github.com/andybalholm/cascadia v1.3.3 // indirect + github.com/buger/jsonparser v1.1.2 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/clipperhouse/displaywidth v0.11.0 // indirect @@ -121,8 +124,8 @@ require ( github.com/go-openapi/swag/yamlutils v0.26.0 // indirect github.com/go-sql-driver/mysql v1.10.0 // indirect github.com/goccy/go-json v0.10.6 // indirect - github.com/gofrs/flock v0.13.0 // indirect github.com/golang/protobuf v1.5.4 // indirect + github.com/google/go-querystring v1.2.0 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.15 // indirect github.com/googleapis/gax-go/v2 v2.22.0 // indirect diff --git a/api/go.sum b/api/go.sum index 3e522e0b..3495f841 100644 --- a/api/go.sum +++ b/api/go.sum @@ -68,12 +68,18 @@ github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kk github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= github.com/avast/retry-go/v5 v5.0.0 h1:kf1Qc2UsTZ4qq8elDymqfbISvkyMuhgRxuJqX2NHP7k= github.com/avast/retry-go/v5 v5.0.0/go.mod h1://d+usmKWio1agtZfS1H/ltTqwtIfBnRq9zEwjc3eH8= +github.com/axiomhq/axiom-go v0.32.0 h1:aRpbqUAn01hY8aJXQftvWHyXfnrNB2KzN5ZquBWvFcE= +github.com/axiomhq/axiom-go v0.32.0/go.mod h1:3Gmr5M4tINm7Ti00GVfzAduO92Uhd0pghr4ZehIhFxc= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk= +github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/carlmjohnson/requests v0.25.1 h1:17zNRLecxtAjhtdEIV+F+wrYfe+AGZUjWJtpndcOUYA= github.com/carlmjohnson/requests v0.25.1/go.mod h1:z3UEf8IE4sZxZ78spW6/tLdqBkfCu1Fn4RaYMnZ8SRM= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= @@ -176,6 +182,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0= +github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= @@ -318,6 +326,14 @@ github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= github.com/thedevsaddam/govalidator v1.9.10 h1:m3dLRbSZ5Hts3VUWYe+vxLMG+FdyQuWOjzTeQRiMCvU= github.com/thedevsaddam/govalidator v1.9.10/go.mod h1:Ilx8u7cg5g3LXbSS943cx5kczyNuUn7LH/cK5MYuE90= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM= +github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/tiendc/go-deepcopy v1.7.2 h1:Ut2yYR7W9tWjTQitganoIue4UGxZwCcJy3orjrrIj44= github.com/tiendc/go-deepcopy v1.7.2/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ= github.com/uptrace/uptrace-go v1.43.0 h1:5QuCdyFJdWUEXx6Fr6sYfezdgO6n6lnkOvUTLlyQO7U= diff --git a/api/pkg/di/container.go b/api/pkg/di/container.go index df2e5d6f..29682aac 100644 --- a/api/pkg/di/container.go +++ b/api/pkg/di/container.go @@ -47,6 +47,7 @@ import ( "go.opentelemetry.io/otel/sdk/resource" semconv "go.opentelemetry.io/otel/semconv/v1.10.0" + axiomzerolog "github.com/axiomhq/axiom-go/adapters/zerolog" "github.com/hirosassa/zerodriver" "github.com/rs/zerolog" "go.opentelemetry.io/otel/sdk/trace" @@ -1877,7 +1878,7 @@ func logDriver(skipFrameCount int) *zerodriver.Logger { if isLocal() { return consoleLogger(skipFrameCount) } - return jsonLogger(skipFrameCount) + return axiomLogger(skipFrameCount) } func jsonLogger(skipFrameCount int) *zerodriver.Logger { @@ -1906,6 +1907,25 @@ func jsonLogger(skipFrameCount int) *zerodriver.Logger { return &zerodriver.Logger{Logger: &zl} } +func axiomLogger(skipFrameCount int) *zerodriver.Logger { + logLevel := zerolog.DebugLevel + zerolog.SetGlobalLevel(logLevel) + zerolog.TimestampFieldName = "time" + zerolog.TimeFieldFormat = time.RFC3339Nano + + axiomWriter, err := axiomzerolog.New( + axiomzerolog.SetDataset(os.Getenv("AXIOM_DATASET")), + ) + if err != nil { + // Fall back to stderr JSON if Axiom is not configured + zl := zerolog.New(os.Stderr).With().Timestamp().CallerWithSkipFrameCount(skipFrameCount).Logger() + return &zerodriver.Logger{Logger: &zl} + } + + zl := zerolog.New(axiomWriter).With().Timestamp().CallerWithSkipFrameCount(skipFrameCount).Logger() + return &zerodriver.Logger{Logger: &zl} +} + func hostName() string { h, err := os.Hostname() if err != nil { From 32eb5d3e6dd1e9fca7ff2e5051df20f8a5383b79 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Fri, 29 May 2026 11:45:42 +0300 Subject: [PATCH 04/11] feat: use standard trace_id/span_id fields for log-trace correlation Replace GCP-specific TraceContext format with standard OTel field names so Axiom can correlate logs with traces. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/pkg/telemetry/zerolog_logger.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/api/pkg/telemetry/zerolog_logger.go b/api/pkg/telemetry/zerolog_logger.go index c6cd8b68..feed1062 100644 --- a/api/pkg/telemetry/zerolog_logger.go +++ b/api/pkg/telemetry/zerolog_logger.go @@ -96,7 +96,9 @@ func (logger *zerologLogger) WithSpan(spanContext trace.SpanContext) Logger { func (logger *zerologLogger) decorateEvent(event *zerodriver.Event) *zerolog.Event { if logger.spanContext != nil { - event.TraceContext(logger.spanContext.TraceID().String(), logger.spanContext.SpanID().String(), logger.spanContext.IsSampled(), logger.projectID) + event.Str("trace_id", logger.spanContext.TraceID().String()) + event.Str("span_id", logger.spanContext.SpanID().String()) + event.Bool("trace_sampled", logger.spanContext.IsSampled()) } for key, value := range logger.fields { event.Str(key, value) From 2f38e287f406550b9b411aa40a7b9597fe7cc6bd Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Fri, 29 May 2026 11:47:56 +0300 Subject: [PATCH 05/11] feat: add Axiom OTLP trace and metric provider Replace Uptrace with standard OTLP HTTP exporters pointed at api.axiom.co for traces and metrics. Old providers kept as dead functions for rollback. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/go.mod | 4 +-- api/pkg/di/container.go | 57 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/api/go.mod b/api/go.mod index 75734091..657a3f86 100644 --- a/api/go.mod +++ b/api/go.mod @@ -50,6 +50,8 @@ require ( go.mongodb.org/mongo-driver/v2 v2.6.0 go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/v2/mongo/otelmongo v0.0.0-20260513205827-ba143fc95a5e go.opentelemetry.io/otel v1.44.0 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.44.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0 go.opentelemetry.io/otel/metric v1.44.0 go.opentelemetry.io/otel/sdk v1.44.0 go.opentelemetry.io/otel/sdk/metric v1.44.0 @@ -181,9 +183,7 @@ require ( go.opentelemetry.io/contrib/instrumentation/runtime v0.68.0 // indirect go.opentelemetry.io/contrib/processors/minsev v0.16.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.44.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0 // indirect go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0 // indirect go.opentelemetry.io/otel/log v0.19.0 // indirect go.opentelemetry.io/otel/sdk/log v0.19.0 // indirect diff --git a/api/pkg/di/container.go b/api/pkg/di/container.go index 29682aac..6ec1b73a 100644 --- a/api/pkg/di/container.go +++ b/api/pkg/di/container.go @@ -44,6 +44,9 @@ import ( cloudtasks "cloud.google.com/go/cloudtasks/apiv2" "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" + "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/sdk/resource" semconv "go.opentelemetry.io/otel/semconv/v1.10.0" @@ -1802,7 +1805,7 @@ func (container *Container) UserRistrettoCache() *ristretto.Cache[string, entiti // InitializeTraceProvider initializes the open telemetry trace provider func (container *Container) InitializeTraceProvider() func() { - return container.initializeUptraceProvider(container.version, container.projectID) + return container.initializeAxiomTraceProvider(container.version, container.projectID) } func (container *Container) initializeGoogleTraceProvider(version string, namespace string) func() { @@ -1841,6 +1844,58 @@ func (container *Container) initializeGoogleTraceProvider(version string, namesp } } +func (container *Container) initializeAxiomTraceProvider(version string, namespace string) func() { + container.logger.Debug("initializing axiom trace provider") + + headers := map[string]string{ + "Authorization": "Bearer " + os.Getenv("AXIOM_TOKEN"), + "X-Axiom-Dataset": os.Getenv("AXIOM_DATASET"), + } + + traceExporter, err := otlptracehttp.New(context.Background(), + otlptracehttp.WithEndpoint("api.axiom.co"), + otlptracehttp.WithHeaders(headers), + ) + if err != nil { + container.logger.Fatal(stacktrace.Propagate(err, "cannot create axiom OTLP trace exporter")) + } + + tp := trace.NewTracerProvider( + trace.WithBatcher(traceExporter), + trace.WithSampler(trace.AlwaysSample()), + trace.WithResource(container.OtelResources(version, namespace)), + ) + otel.SetTracerProvider(tp) + + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator( + propagation.TraceContext{}, + propagation.Baggage{}, + )) + + metricExporter, err := otlpmetrichttp.New(context.Background(), + otlpmetrichttp.WithEndpoint("api.axiom.co"), + otlpmetrichttp.WithHeaders(headers), + ) + if err != nil { + container.logger.Fatal(stacktrace.Propagate(err, "cannot create axiom OTLP metric exporter")) + } + + meterProvider := metric.NewMeterProvider( + metric.WithReader(metric.NewPeriodicReader(metricExporter)), + metric.WithResource(container.OtelResources(version, namespace)), + ) + otel.SetMeterProvider(meterProvider) + + return func() { + if err := tp.Shutdown(context.Background()); err != nil { + container.logger.Error(stacktrace.Propagate(err, "cannot shutdown axiom trace provider")) + } + if err := meterProvider.Shutdown(context.Background()); err != nil { + container.logger.Error(stacktrace.Propagate(err, "cannot shutdown axiom meter provider")) + } + } +} + func (container *Container) initializeUptraceProvider(version string, namespace string) (flush func()) { container.logger.Debug("initializing uptrace provider") // Configure OpenTelemetry with sensible defaults. From 178f273dd6e05666bd9c7b5093263f362b3d28d7 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Fri, 29 May 2026 11:48:30 +0300 Subject: [PATCH 06/11] chore: add Axiom env vars to .env.docker Add AXIOM_TOKEN and AXIOM_DATASET configuration. Mark UPTRACE_DSN as deprecated but keep for rollback. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/.env.docker | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/api/.env.docker b/api/.env.docker index cf8f8cda..0b8b25c3 100644 --- a/api/.env.docker +++ b/api/.env.docker @@ -55,10 +55,15 @@ REDIS_URL=redis://@redis:6379 # Google Cloud Storage bucket for MMS attachments. Leave empty to use in-memory storage. GCS_BUCKET_NAME= -# [optional] If you would like to use uptrace.dev for distributed tracing, you can set the DSN here. -# This is optional and you can leave it empty if you don't want to use uptrace +# [deprecated] Uptrace DSN - kept for rollback. Use AXIOM_TOKEN/AXIOM_DATASET instead. UPTRACE_DSN= +# Axiom observability configuration +# API token for Axiom (required for logging, traces, and metrics in production) +AXIOM_TOKEN= +# Dataset name in Axiom where logs, traces, and metrics are sent +AXIOM_DATASET= + # [optional] Websocket configuration for https://pusher.com if you will like to frontend to update in real time PUSHER_APP_ID= From 370ed81c38e2dceb649e43db2b1b3fc9ddf17e7e Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Fri, 29 May 2026 11:59:56 +0300 Subject: [PATCH 07/11] feat: use Axiom edge endpoint us-east-1.aws.edge.axiom.co Use regional edge endpoint for improved data locality on both OTLP exporters (traces/metrics) and the zerolog log adapter. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/pkg/di/container.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/api/pkg/di/container.go b/api/pkg/di/container.go index 6ec1b73a..9556d6ec 100644 --- a/api/pkg/di/container.go +++ b/api/pkg/di/container.go @@ -51,6 +51,7 @@ import ( semconv "go.opentelemetry.io/otel/semconv/v1.10.0" axiomzerolog "github.com/axiomhq/axiom-go/adapters/zerolog" + "github.com/axiomhq/axiom-go/axiom" "github.com/hirosassa/zerodriver" "github.com/rs/zerolog" "go.opentelemetry.io/otel/sdk/trace" @@ -1853,7 +1854,7 @@ func (container *Container) initializeAxiomTraceProvider(version string, namespa } traceExporter, err := otlptracehttp.New(context.Background(), - otlptracehttp.WithEndpoint("api.axiom.co"), + otlptracehttp.WithEndpoint("us-east-1.aws.edge.axiom.co"), otlptracehttp.WithHeaders(headers), ) if err != nil { @@ -1873,7 +1874,7 @@ func (container *Container) initializeAxiomTraceProvider(version string, namespa )) metricExporter, err := otlpmetrichttp.New(context.Background(), - otlpmetrichttp.WithEndpoint("api.axiom.co"), + otlpmetrichttp.WithEndpoint("us-east-1.aws.edge.axiom.co"), otlpmetrichttp.WithHeaders(headers), ) if err != nil { @@ -1970,6 +1971,7 @@ func axiomLogger(skipFrameCount int) *zerodriver.Logger { axiomWriter, err := axiomzerolog.New( axiomzerolog.SetDataset(os.Getenv("AXIOM_DATASET")), + axiomzerolog.SetClientOptions(axiom.SetEdge("us-east-1.aws.edge.axiom.co")), ) if err != nil { // Fall back to stderr JSON if Axiom is not configured From f6d03a6d99a0f3ff5ae812b5090d2b80550a18b9 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Fri, 29 May 2026 12:00:39 +0300 Subject: [PATCH 08/11] fix: only use edge endpoint for OTLP traces/metrics, not logs Logs use the default Axiom API endpoint. Edge endpoint is only for OTLP trace and metric exporters. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/pkg/di/container.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/api/pkg/di/container.go b/api/pkg/di/container.go index 9556d6ec..076a3437 100644 --- a/api/pkg/di/container.go +++ b/api/pkg/di/container.go @@ -51,7 +51,6 @@ import ( semconv "go.opentelemetry.io/otel/semconv/v1.10.0" axiomzerolog "github.com/axiomhq/axiom-go/adapters/zerolog" - "github.com/axiomhq/axiom-go/axiom" "github.com/hirosassa/zerodriver" "github.com/rs/zerolog" "go.opentelemetry.io/otel/sdk/trace" @@ -1971,7 +1970,6 @@ func axiomLogger(skipFrameCount int) *zerodriver.Logger { axiomWriter, err := axiomzerolog.New( axiomzerolog.SetDataset(os.Getenv("AXIOM_DATASET")), - axiomzerolog.SetClientOptions(axiom.SetEdge("us-east-1.aws.edge.axiom.co")), ) if err != nil { // Fall back to stderr JSON if Axiom is not configured From 2154544ad15cd897a2d3f2912d60ae866febb09a Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Fri, 29 May 2026 12:27:59 +0300 Subject: [PATCH 09/11] feat: use separate Axiom datasets for traces/logs and metrics Traces and logs go to AXIOM_TRACES_DATASET (events), metrics go to AXIOM_METRICS_DATASET (metrics). Replaces single AXIOM_DATASET. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/.env.docker | 6 ++++-- api/pkg/di/container.go | 15 ++++++++++----- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/api/.env.docker b/api/.env.docker index 0b8b25c3..e3489bd4 100644 --- a/api/.env.docker +++ b/api/.env.docker @@ -61,8 +61,10 @@ UPTRACE_DSN= # Axiom observability configuration # API token for Axiom (required for logging, traces, and metrics in production) AXIOM_TOKEN= -# Dataset name in Axiom where logs, traces, and metrics are sent -AXIOM_DATASET= +# Dataset for logs and traces (e.g. "events") +AXIOM_TRACES_DATASET= +# Dataset for metrics (e.g. "metrics") +AXIOM_METRICS_DATASET= # [optional] Websocket configuration for https://pusher.com if you will like to frontend to update in real time diff --git a/api/pkg/di/container.go b/api/pkg/di/container.go index 076a3437..2f8e1554 100644 --- a/api/pkg/di/container.go +++ b/api/pkg/di/container.go @@ -1847,14 +1847,14 @@ func (container *Container) initializeGoogleTraceProvider(version string, namesp func (container *Container) initializeAxiomTraceProvider(version string, namespace string) func() { container.logger.Debug("initializing axiom trace provider") - headers := map[string]string{ + traceHeaders := map[string]string{ "Authorization": "Bearer " + os.Getenv("AXIOM_TOKEN"), - "X-Axiom-Dataset": os.Getenv("AXIOM_DATASET"), + "X-Axiom-Dataset": os.Getenv("AXIOM_TRACES_DATASET"), } traceExporter, err := otlptracehttp.New(context.Background(), otlptracehttp.WithEndpoint("us-east-1.aws.edge.axiom.co"), - otlptracehttp.WithHeaders(headers), + otlptracehttp.WithHeaders(traceHeaders), ) if err != nil { container.logger.Fatal(stacktrace.Propagate(err, "cannot create axiom OTLP trace exporter")) @@ -1872,9 +1872,14 @@ func (container *Container) initializeAxiomTraceProvider(version string, namespa propagation.Baggage{}, )) + metricHeaders := map[string]string{ + "Authorization": "Bearer " + os.Getenv("AXIOM_TOKEN"), + "X-Axiom-Dataset": os.Getenv("AXIOM_METRICS_DATASET"), + } + metricExporter, err := otlpmetrichttp.New(context.Background(), otlpmetrichttp.WithEndpoint("us-east-1.aws.edge.axiom.co"), - otlpmetrichttp.WithHeaders(headers), + otlpmetrichttp.WithHeaders(metricHeaders), ) if err != nil { container.logger.Fatal(stacktrace.Propagate(err, "cannot create axiom OTLP metric exporter")) @@ -1969,7 +1974,7 @@ func axiomLogger(skipFrameCount int) *zerodriver.Logger { zerolog.TimeFieldFormat = time.RFC3339Nano axiomWriter, err := axiomzerolog.New( - axiomzerolog.SetDataset(os.Getenv("AXIOM_DATASET")), + axiomzerolog.SetDataset(os.Getenv("AXIOM_TRACES_DATASET")), ) if err != nil { // Fall back to stderr JSON if Axiom is not configured From 80a8013820e3a9d3fc1f8cf816c8eb93aae57b82 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Fri, 29 May 2026 12:31:05 +0300 Subject: [PATCH 10/11] refactor: rename dataset env vars to AXIOM_DATASET_EVENTS and AXIOM_DATASET_METRICS Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/.env.docker | 4 ++-- api/pkg/di/container.go | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/api/.env.docker b/api/.env.docker index e3489bd4..1d1e16ee 100644 --- a/api/.env.docker +++ b/api/.env.docker @@ -62,9 +62,9 @@ UPTRACE_DSN= # API token for Axiom (required for logging, traces, and metrics in production) AXIOM_TOKEN= # Dataset for logs and traces (e.g. "events") -AXIOM_TRACES_DATASET= +AXIOM_DATASET_EVENTS= # Dataset for metrics (e.g. "metrics") -AXIOM_METRICS_DATASET= +AXIOM_DATASET_METRICS= # [optional] Websocket configuration for https://pusher.com if you will like to frontend to update in real time diff --git a/api/pkg/di/container.go b/api/pkg/di/container.go index 2f8e1554..c9881bf9 100644 --- a/api/pkg/di/container.go +++ b/api/pkg/di/container.go @@ -1849,7 +1849,7 @@ func (container *Container) initializeAxiomTraceProvider(version string, namespa traceHeaders := map[string]string{ "Authorization": "Bearer " + os.Getenv("AXIOM_TOKEN"), - "X-Axiom-Dataset": os.Getenv("AXIOM_TRACES_DATASET"), + "X-Axiom-Dataset": os.Getenv("AXIOM_DATASET_EVENTS"), } traceExporter, err := otlptracehttp.New(context.Background(), @@ -1874,7 +1874,7 @@ func (container *Container) initializeAxiomTraceProvider(version string, namespa metricHeaders := map[string]string{ "Authorization": "Bearer " + os.Getenv("AXIOM_TOKEN"), - "X-Axiom-Dataset": os.Getenv("AXIOM_METRICS_DATASET"), + "X-Axiom-Dataset": os.Getenv("AXIOM_DATASET_METRICS"), } metricExporter, err := otlpmetrichttp.New(context.Background(), @@ -1974,7 +1974,7 @@ func axiomLogger(skipFrameCount int) *zerodriver.Logger { zerolog.TimeFieldFormat = time.RFC3339Nano axiomWriter, err := axiomzerolog.New( - axiomzerolog.SetDataset(os.Getenv("AXIOM_TRACES_DATASET")), + axiomzerolog.SetDataset(os.Getenv("AXIOM_DATASET_EVENTS")), ) if err != nil { // Fall back to stderr JSON if Axiom is not configured From ea0d2ff60079b584fb1a54a02748f012e685ad2d Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Fri, 29 May 2026 13:31:57 +0300 Subject: [PATCH 11/11] Fix falback logger --- .gitignore | 1 + api/pkg/di/container.go | 15 +++++---------- api/pkg/telemetry/gorm_logger.go | 6 +++--- api/pkg/telemetry/zerolog_logger.go | 4 ++-- 4 files changed, 11 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index 0aa9dfff..f43a4040 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ SECURITY_AUDIT_REPORT.md *.exe docs/ +.output diff --git a/api/pkg/di/container.go b/api/pkg/di/container.go index c9881bf9..45cad0cc 100644 --- a/api/pkg/di/container.go +++ b/api/pkg/di/container.go @@ -4,6 +4,7 @@ import ( "context" "crypto/tls" "fmt" + "log" "net/http" "os" "strconv" @@ -1922,8 +1923,8 @@ func (container *Container) initializeUptraceProvider(version string, namespace func logger(skipFrameCount int) telemetry.Logger { fields := map[string]string{ - "pid": strconv.Itoa(os.Getpid()), - "hostname": hostName(), + "hostname": hostName(), + string(semconv.DeploymentEnvironmentKey): os.Getenv("ENV"), } return telemetry.NewZerologLogger( @@ -1968,18 +1969,12 @@ func jsonLogger(skipFrameCount int) *zerodriver.Logger { } func axiomLogger(skipFrameCount int) *zerodriver.Logger { - logLevel := zerolog.DebugLevel - zerolog.SetGlobalLevel(logLevel) - zerolog.TimestampFieldName = "time" - zerolog.TimeFieldFormat = time.RFC3339Nano - axiomWriter, err := axiomzerolog.New( + axiomzerolog.SetLevels([]zerolog.Level{zerolog.TraceLevel, zerolog.DebugLevel, zerolog.InfoLevel, zerolog.WarnLevel, zerolog.ErrorLevel, zerolog.PanicLevel, zerolog.FatalLevel, zerolog.NoLevel}), axiomzerolog.SetDataset(os.Getenv("AXIOM_DATASET_EVENTS")), ) if err != nil { - // Fall back to stderr JSON if Axiom is not configured - zl := zerolog.New(os.Stderr).With().Timestamp().CallerWithSkipFrameCount(skipFrameCount).Logger() - return &zerodriver.Logger{Logger: &zl} + log.Fatal(stacktrace.Propagate(err, "cannot create axiom zerolog writer")) } zl := zerolog.New(axiomWriter).With().Timestamp().CallerWithSkipFrameCount(skipFrameCount).Logger() diff --git a/api/pkg/telemetry/gorm_logger.go b/api/pkg/telemetry/gorm_logger.go index 10ed03c7..3cf02162 100644 --- a/api/pkg/telemetry/gorm_logger.go +++ b/api/pkg/telemetry/gorm_logger.go @@ -27,15 +27,15 @@ func (gorm *gormLogger) LogMode(_ logger.LogLevel) logger.Interface { return gorm } -func (gorm *gormLogger) Info(ctx context.Context, s string, i ...interface{}) { +func (gorm *gormLogger) Info(ctx context.Context, s string, i ...any) { gorm.logger.WithSpan(gorm.tracer.Span(ctx).SpanContext()).Info(fmt.Sprintf(s, i...)) } -func (gorm *gormLogger) Warn(ctx context.Context, s string, i ...interface{}) { +func (gorm *gormLogger) Warn(ctx context.Context, s string, i ...any) { gorm.logger.WithSpan(gorm.tracer.Span(ctx).SpanContext()).Warn(fmt.Errorf(s, i...)) } -func (gorm *gormLogger) Error(ctx context.Context, s string, i ...interface{}) { +func (gorm *gormLogger) Error(ctx context.Context, s string, i ...any) { gorm.logger.WithSpan(gorm.tracer.Span(ctx).SpanContext()).Error(fmt.Errorf(s, i...)) } diff --git a/api/pkg/telemetry/zerolog_logger.go b/api/pkg/telemetry/zerolog_logger.go index feed1062..c5230bc0 100644 --- a/api/pkg/telemetry/zerolog_logger.go +++ b/api/pkg/telemetry/zerolog_logger.go @@ -5,7 +5,7 @@ import ( "github.com/hirosassa/zerodriver" "github.com/rs/zerolog" - semconv "go.opentelemetry.io/otel/semconv/v1.10.0" + semconv "go.opentelemetry.io/otel/semconv/v1.41.0" "go.opentelemetry.io/otel/trace" ) @@ -34,7 +34,7 @@ func NewZerologLogger(projectID string, fields map[string]string, driver *zerodr func (logger *zerologLogger) WithService(service string) Logger { return NewZerologLogger( logger.projectID, - logger.addField(string(semconv.ServiceNameKey), service), + logger.addField(string(semconv.ServiceNamespaceKey), service), logger.zerolog, logger.spanContext, )