stats.ts 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. import { lakeAthenaWorkgroup, lakeCatalog, lakeCluster, lakeQueryPermissions, lakeRegion, tableBucket } from "./lake"
  2. import { EMAILOCTOPUS_API_KEY } from "./app"
  3. import { domain } from "./stage"
  4. ////////////////
  5. // LAKE
  6. ////////////////
  7. const inferenceNamespace = new aws.s3tables.Namespace("LakeInferenceNamespace", {
  8. namespace: "inference",
  9. tableBucketArn: tableBucket.arn,
  10. })
  11. const inferenceEventTable = new aws.s3tables.Table(
  12. "LakeInferenceEventTable",
  13. {
  14. name: "event",
  15. namespace: inferenceNamespace.namespace,
  16. tableBucketArn: inferenceNamespace.tableBucketArn,
  17. format: "ICEBERG",
  18. metadata: {
  19. iceberg: {
  20. schema: {
  21. fields: [
  22. { name: "event_timestamp", type: "string", required: false },
  23. { name: "event_date", type: "string", required: false },
  24. { name: "event_type", type: "string", required: false },
  25. { name: "dataset", type: "string", required: false },
  26. { name: "cf_continent", type: "string", required: false },
  27. { name: "cf_country", type: "string", required: false },
  28. { name: "cf_city", type: "string", required: false },
  29. { name: "cf_region", type: "string", required: false },
  30. { name: "cf_latitude", type: "double", required: false },
  31. { name: "cf_longitude", type: "double", required: false },
  32. { name: "cf_timezone", type: "string", required: false },
  33. { name: "duration", type: "double", required: false },
  34. { name: "request_length", type: "long", required: false },
  35. { name: "status", type: "int", required: false },
  36. { name: "ip", type: "string", required: false },
  37. { name: "is_stream", type: "boolean", required: false },
  38. { name: "session", type: "string", required: false },
  39. { name: "request", type: "string", required: false },
  40. { name: "client", type: "string", required: false },
  41. { name: "user_agent", type: "string", required: false },
  42. { name: "model", type: "string", required: false },
  43. { name: "model_tier", type: "string", required: false },
  44. { name: "model_variant", type: "string", required: false },
  45. { name: "source", type: "string", required: false },
  46. { name: "provider", type: "string", required: false },
  47. { name: "provider_model", type: "string", required: false },
  48. { name: "llm_error_code", type: "int", required: false },
  49. { name: "llm_error_message", type: "string", required: false },
  50. { name: "error_response", type: "string", required: false },
  51. { name: "error_type", type: "string", required: false },
  52. { name: "error_message", type: "string", required: false },
  53. { name: "error_cause", type: "string", required: false },
  54. { name: "error_cause2", type: "string", required: false },
  55. { name: "api_key", type: "string", required: false },
  56. { name: "workspace", type: "string", required: false },
  57. { name: "user_id", type: "string", required: false },
  58. { name: "is_subscription", type: "boolean", required: false },
  59. { name: "subscription", type: "string", required: false },
  60. { name: "response_length", type: "long", required: false },
  61. { name: "time_to_first_byte", type: "long", required: false },
  62. { name: "timestamp_first_byte", type: "long", required: false },
  63. { name: "timestamp_last_byte", type: "long", required: false },
  64. { name: "tokens_input", type: "long", required: false },
  65. { name: "tokens_output", type: "long", required: false },
  66. { name: "tokens_reasoning", type: "long", required: false },
  67. { name: "tokens_cache_read", type: "long", required: false },
  68. { name: "tokens_cache_write_5m", type: "long", required: false },
  69. { name: "tokens_cache_write_1h", type: "long", required: false },
  70. { name: "cost_input_microcents", type: "long", required: false },
  71. { name: "cost_output_microcents", type: "long", required: false },
  72. { name: "cost_cache_read_microcents", type: "long", required: false },
  73. { name: "cost_cache_write_microcents", type: "long", required: false },
  74. { name: "cost_total_microcents", type: "long", required: false },
  75. { name: "cost_input", type: "long", required: false },
  76. { name: "cost_output", type: "long", required: false },
  77. { name: "cost_cache_read", type: "long", required: false },
  78. { name: "cost_cache_write_5m", type: "long", required: false },
  79. { name: "cost_cache_write_1h", type: "long", required: false },
  80. { name: "cost_total", type: "long", required: false },
  81. ],
  82. },
  83. },
  84. },
  85. },
  86. { deleteBeforeReplace: $app.stage !== "production", ignoreChanges: ["metadata"] },
  87. )
  88. export const inferenceEvent = new sst.Linkable("InferenceEvent", {
  89. properties: {
  90. region: lakeRegion,
  91. catalog: lakeCatalog,
  92. database: inferenceNamespace.namespace,
  93. table: inferenceEventTable.name,
  94. tableBucket: tableBucket.name,
  95. workgroup: lakeAthenaWorkgroup.name,
  96. },
  97. })
  98. ////////////////
  99. // DATABASE
  100. ////////////////
  101. const cluster = planetscale.getDatabaseOutput({
  102. name: "opencode-stats",
  103. organization: "anomalyco",
  104. })
  105. const branch =
  106. $app.stage === "production"
  107. ? planetscale.getBranchOutput({
  108. name: "production",
  109. organization: cluster.organization,
  110. database: cluster.name,
  111. })
  112. : new planetscale.Branch("StatsDatabaseBranch", {
  113. database: cluster.name,
  114. organization: cluster.organization,
  115. name: $app.stage,
  116. parentBranch: "production",
  117. })
  118. const password = new planetscale.Password("StatsDatabasePassword", {
  119. name: $app.stage,
  120. database: cluster.name,
  121. organization: cluster.organization,
  122. branch: branch.name,
  123. })
  124. const databaseUrl = $interpolate`mysql://${password.username.apply(encodeURIComponent)}:${password.plaintext.apply(
  125. encodeURIComponent,
  126. )}@${password.accessHostUrl}/${cluster.name}`
  127. export const database = new sst.Linkable("StatsDatabase", {
  128. properties: {
  129. host: password.accessHostUrl,
  130. database: cluster.name,
  131. username: password.username,
  132. password: password.plaintext,
  133. port: 3306,
  134. url: databaseUrl,
  135. },
  136. })
  137. new sst.x.DevCommand("StatsStudio", {
  138. link: [database],
  139. environment: {
  140. DATABASE_URL: databaseUrl,
  141. },
  142. dev: {
  143. command: "bun db:studio",
  144. directory: "packages/stats/core",
  145. autostart: false,
  146. },
  147. })
  148. ////////////////
  149. // APP
  150. ////////////////
  151. export const app = new sst.cloudflare.x.SolidStart("Stats", {
  152. path: "packages/stats/app",
  153. buildCommand: "bun run build",
  154. domain: `stats.${domain}`,
  155. link: [database, EMAILOCTOPUS_API_KEY],
  156. environment: {
  157. PUBLIC_URL: `https://${domain}/data`,
  158. },
  159. })
  160. ////////////////
  161. // SERVICES
  162. ////////////////
  163. const statsSyncConfig = new sst.Linkable("StatsSyncConfig", {
  164. properties: {
  165. dataset: "zen",
  166. },
  167. })
  168. export const statSync = new sst.aws.Service("StatsSyncService", {
  169. cluster: lakeCluster,
  170. architecture: "arm64",
  171. cpu: "0.25 vCPU",
  172. // 0.5 GB caused an OOM crash loop: every restart immediately re-ran the 4 Athena
  173. // stats queries (~$5/pass) every ~5 minutes instead of hourly.
  174. memory: "2 GB",
  175. image: {
  176. context: ".",
  177. dockerfile: "packages/stats/server/Dockerfile",
  178. },
  179. command: ["bun", "src/stat-sync.ts"],
  180. link: [database, inferenceEvent, statsSyncConfig],
  181. permissions: lakeQueryPermissions,
  182. scaling: {
  183. min: 1,
  184. max: 1,
  185. },
  186. dev: {
  187. command: "bun src/stat-sync.ts",
  188. directory: "packages/stats/server",
  189. autostart: false,
  190. },
  191. })