monitoring.ts 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. import { SECRET } from "./secret"
  2. import { domain } from "./stage"
  3. const description = "Managed by SST (Don't edit in Honeycomb UI)"
  4. const webhookRecipient = new honeycomb.WebhookRecipient("DiscordAlerts", {
  5. name: $app.stage === "production" ? "Discord Alerts" : `Discord Alerts (${$app.stage})`,
  6. url: `https://${domain}/honeycomb/webhook`,
  7. secret: SECRET.HoneycombWebhookSecret.result,
  8. templates: [
  9. {
  10. type: "trigger",
  11. body: `{
  12. "url": {{ .Result.URL | quote }},
  13. "type": {{ .Vars.type | quote }},
  14. "name": {{ .Name | quote }},
  15. "status": {{ .Alert.Status | quote }},
  16. "isTest": {{ .Alert.IsTest }},
  17. "groups": {{ .Result.GroupsTriggered | toJson }}
  18. }`,
  19. },
  20. ],
  21. variables: [
  22. {
  23. name: "type",
  24. },
  25. ],
  26. })
  27. // Honeycomb can keep stale query-local calculated fields when the name is unchanged,
  28. // so tie the field name to the expression while avoiding deploy-to-deploy churn.
  29. // https://github.com/honeycombio/terraform-provider-honeycombio/issues/852
  30. const calculatedField = (field: { name: string; expression: string }) => ({
  31. ...field,
  32. name: `${field.name}_${(
  33. Array.from(field.expression).reduce((result, char) => Math.imul(31, result) + char.charCodeAt(0), 0) >>> 0
  34. ).toString(36)}`,
  35. })
  36. const modelHttpErrorsQuery = (product: "go" | "zen") => {
  37. const filters = [
  38. { column: "model", op: "exists" },
  39. { column: "event_type", op: "=", value: "completions" },
  40. { column: "user_agent", op: "contains", value: "opencode" },
  41. { column: "isGoTier", op: "=", value: product === "go" ? "true" : "false" },
  42. ]
  43. const failedHttpStatus = calculatedField({
  44. name: "is_failed_http_status",
  45. expression:
  46. product === "go"
  47. ? `IF(AND(GTE($status, "400"), NOT(EQUALS($status, "401")), NOT(EQUALS($status, "429"))), 1, 0)`
  48. : `IF(AND(EQUALS($status, "429"), $isFreeTier), 0, AND(GTE($status, "400"), NOT(EQUALS($status, "401"))), 1, 0)`,
  49. })
  50. return honeycomb.getQuerySpecificationOutput({
  51. breakdowns: ["model"],
  52. calculatedFields: [failedHttpStatus],
  53. calculations: [
  54. { op: "COUNT", name: "TOTAL", filterCombination: "AND", filters },
  55. {
  56. op: "SUM",
  57. name: "FAILED",
  58. column: failedHttpStatus.name,
  59. filterCombination: "AND",
  60. filters,
  61. },
  62. ],
  63. formulas: [{ name: "ERROR", expression: "IF(GTE($TOTAL, 100), DIV($FAILED, $TOTAL), 0)" }],
  64. timeRange: 900,
  65. }).json
  66. }
  67. const providerHttpErrorsQuery = (product: "go" | "zen") => {
  68. const filters = [
  69. { column: "provider", op: "exists" },
  70. { column: "provider", op: "!=", value: "fireworks-go-glm-5.1" },
  71. { column: "user_agent", op: "contains", value: "opencode" },
  72. { column: "isGoTier", op: "=", value: product === "go" ? "true" : "false" },
  73. ]
  74. const successHttpStatus = calculatedField({
  75. name: "is_success_http_status",
  76. expression: `IF(AND(GTE($status, "200"), LT($status, "400")), 1, 0)`,
  77. })
  78. const failedProviderHttpStatus = calculatedField({
  79. name: "is_failed_provider_http_status",
  80. expression: `IF(GT($llm.error.code, "400"), 1, 0)`,
  81. })
  82. return honeycomb.getQuerySpecificationOutput({
  83. breakdowns: ["provider"],
  84. calculatedFields: [successHttpStatus, failedProviderHttpStatus],
  85. calculations: [
  86. {
  87. op: "SUM",
  88. name: "SUCCESS",
  89. column: successHttpStatus.name,
  90. filterCombination: "AND",
  91. filters: [...filters, { column: "event_type", op: "=", value: "completions" }],
  92. },
  93. {
  94. op: "SUM",
  95. name: "FAILED",
  96. column: failedProviderHttpStatus.name,
  97. filterCombination: "AND",
  98. filters: [...filters, { column: "event_type", op: "=", value: "llm.error" }],
  99. },
  100. ],
  101. formulas: [
  102. { name: "ERROR", expression: "IF(GTE(SUM($SUCCESS, $FAILED), 50), DIV($FAILED, SUM($SUCCESS, $FAILED)), 0)" },
  103. ],
  104. timeRange: 900,
  105. }).json
  106. }
  107. const modelLowTpsQuery = (product: "go" | "zen") => {
  108. const filters = [
  109. { column: "model", op: "exists" },
  110. { column: "event_type", op: "=", value: "completions" },
  111. { column: "user_agent", op: "contains", value: "opencode" },
  112. { column: "isGoTier", op: "=", value: product === "go" ? "true" : "false" },
  113. { column: "status", op: ">=", value: "200" },
  114. { column: "status", op: "<", value: "400" },
  115. { column: "tps.output", op: "exists" },
  116. ]
  117. return honeycomb.getQuerySpecificationOutput({
  118. breakdowns: ["model"],
  119. calculations: [
  120. { op: "COUNT", name: "TOTAL", filterCombination: "AND", filters },
  121. {
  122. op: "P50",
  123. name: "TPS",
  124. column: "tps.output",
  125. filterCombination: "AND",
  126. filters,
  127. },
  128. ],
  129. formulas: [{ name: "LOW_TPS", expression: "IF(GTE($TOTAL, 100), $TPS, 999)" }],
  130. timeRange: 1800,
  131. }).json
  132. }
  133. new honeycomb.Trigger("IncreasedModelHttpErrorsGo", {
  134. name: "Increased Model HTTP Errors [Go]",
  135. description,
  136. queryJson: modelHttpErrorsQuery("go"),
  137. alertType: "on_change",
  138. frequency: 300,
  139. thresholds: [{ op: ">=", value: 0.7, exceededLimit: 1 }],
  140. recipients: [
  141. {
  142. id: webhookRecipient.id,
  143. notificationDetails: [
  144. {
  145. variables: [{ name: "type", value: "model_http_errors" }],
  146. },
  147. ],
  148. },
  149. ],
  150. })
  151. new honeycomb.Trigger("IncreasedModelHttpErrorsZen", {
  152. name: "Increased Model HTTP Errors [Zen]",
  153. description,
  154. queryJson: modelHttpErrorsQuery("zen"),
  155. alertType: "on_change",
  156. frequency: 300,
  157. thresholds: [{ op: ">=", value: 0.7, exceededLimit: 1 }],
  158. recipients: [
  159. {
  160. id: webhookRecipient.id,
  161. notificationDetails: [
  162. {
  163. variables: [{ name: "type", value: "model_http_errors" }],
  164. },
  165. ],
  166. },
  167. ],
  168. })
  169. new honeycomb.Trigger("LowModelTpsGo", {
  170. name: "Low Model TPS [Go]",
  171. description,
  172. queryJson: modelLowTpsQuery("go"),
  173. alertType: "on_change",
  174. frequency: 600,
  175. thresholds: [{ op: "<=", value: 10, exceededLimit: 1 }],
  176. recipients: [
  177. {
  178. id: webhookRecipient.id,
  179. notificationDetails: [
  180. {
  181. variables: [{ name: "type", value: "model_low_tps" }],
  182. },
  183. ],
  184. },
  185. ],
  186. })
  187. new honeycomb.Trigger("LowModelTpsZen", {
  188. name: "Low Model TPS [Zen]",
  189. description,
  190. queryJson: modelLowTpsQuery("zen"),
  191. alertType: "on_change",
  192. frequency: 600,
  193. thresholds: [{ op: "<=", value: 10, exceededLimit: 1 }],
  194. recipients: [
  195. {
  196. id: webhookRecipient.id,
  197. notificationDetails: [
  198. {
  199. variables: [{ name: "type", value: "model_low_tps" }],
  200. },
  201. ],
  202. },
  203. ],
  204. })
  205. new honeycomb.Trigger("IncreasedProviderHttpErrorsGo", {
  206. name: "Increased Provider HTTP Errors [Go]",
  207. description,
  208. queryJson: providerHttpErrorsQuery("go"),
  209. alertType: "on_change",
  210. frequency: 300,
  211. thresholds: [{ op: ">=", value: 0.7, exceededLimit: 1 }],
  212. recipients: [
  213. {
  214. id: webhookRecipient.id,
  215. notificationDetails: [
  216. {
  217. variables: [{ name: "type", value: "provider_http_errors" }],
  218. },
  219. ],
  220. },
  221. ],
  222. })
  223. new honeycomb.Trigger("IncreasedProviderHttpErrorsZen", {
  224. name: "Increased Provider HTTP Errors [Zen]",
  225. description,
  226. queryJson: providerHttpErrorsQuery("zen"),
  227. alertType: "on_change",
  228. frequency: 300,
  229. thresholds: [{ op: ">=", value: 0.7, exceededLimit: 1 }],
  230. recipients: [
  231. {
  232. id: webhookRecipient.id,
  233. notificationDetails: [
  234. {
  235. variables: [{ name: "type", value: "provider_http_errors" }],
  236. },
  237. ],
  238. },
  239. ],
  240. })
  241. new honeycomb.Trigger("IncreasedFreeTierRequests", {
  242. name: "Increased Free Tier Requests",
  243. description,
  244. queryJson: honeycomb.getQuerySpecificationOutput({
  245. calculations: [{ op: "COUNT" }],
  246. filters: [
  247. { column: "event_type", op: "=", value: "completions" },
  248. { column: "user_agent", op: "contains", value: "opencode" },
  249. { column: "isFreeTier", op: "=", value: "true" },
  250. ],
  251. timeRange: 3600,
  252. }).json,
  253. alertType: "on_change",
  254. frequency: 900,
  255. thresholds: [{ op: ">=", value: 50, exceededLimit: 1 }],
  256. baselineDetails: [{ type: "percentage", offsetMinutes: 1440 }],
  257. recipients: [
  258. {
  259. id: webhookRecipient.id,
  260. notificationDetails: [
  261. {
  262. variables: [{ name: "type", value: "custom" }],
  263. },
  264. ],
  265. },
  266. ],
  267. })