monitoring.ts 7.8 KB

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