Improve mcp and cli#368
Conversation
- KQL: `attributes.exception.{type,message,stacktrace,escaped}` now
COALESCEs the flattened column with a jsonb_path probe over span events
(`event_name=="exception"`). Previously every search returned 0 because
hs-opentelemetry records exceptions as span events, not flat attributes.
- API: `apiIssueGet` synthesises a span-chain "logical stack" when the
source `exception.stacktrace` is empty (true for 100% of monoscope's
prod exceptions — GHC backtraces aren't captured by default).
- Pages.Telemetry: guard `renderSpanRecordRow` integer divisions against
divide-by-zero (empty service group / all-instant trace) — root cause
of the recurring `ArithException` in the issue detail page.
- CLI: bump HTTP response timeout 30s -> 5min so long KQL searches
don't surface as `ResponseTimeout` errors.
- events get: add --at TIMESTAMP flag for direct O(1) lookup via new
GET /api/v1/events/{id}/time/{ts} endpoint (uses timeseries partition
key instead of 90d range scan)
- events get: extend default lookback from 24H to 90d so older spans
resolve without --at
- KQL: map span_name → name, service → resource___service___name,
trace_id → context___trace_id in WHERE clauses (these are SELECT
aliases, not real columns; querying them returned HTTP 400)
- CLI: add client-side KQL validation via validateQueryOrDie; bad queries
now show Megaparsec's precise error instead of an opaque server 400
- queryMetrics: fix error body (was "Invalid signature; \"...\""
instead of "Invalid query: ...")
Code ReviewOverviewThis PR does three things: bumps the CLI HTTP timeout to 5 min for long-running KQL searches, fixes two divide-by-zero bugs in
|
| Priority | Item |
|---|---|
| High | Replace manual resource case with Telemetry.spanServiceName |
| High | Replace storedStack/patchStack JSON surgery with typed RuntimeExceptionData round-trip |
| Medium | spanId — use fromMaybe "?" (s.context >>= (.span_id)) |
| Medium | Cache synthesised stack to avoid N DB calls per issue read |
| Low | Remove or wire up outputFieldAliases doctests |
| Low | Trim multi-line comment blocks to single-line WHY comments |
Code ReviewOverviewThis PR adds: (1) O(1) point-in-time event lookup via a new Bug:
|
- synthStackFromSpans: replace wrong nested resource["service"]["name"] lookup with Telemetry.spanServiceName (uses flat "service.name" key, matching the SQL and the existing helper). Service labels were always empty before this fix. - Remove now-unused AesonText import from ApiHandlers - enrichIssue: add TODO comment noting the N+1 query cost and the correct long-term fix (store synthetic stack at ingestion time) - apiEventGet: move to after apiFacets with proper haddock; fix orphaned apiFacets comment that was running into apiEventGet's doc - Expr.hs: document outputFieldAliases SELECT/WHERE trade-off and the exceptionFlattenedFields/flattenedOtelAttributes overlap invariant - Commands.hs: clarify why validateQueryOrDie is not needed for tail/ context (those commands have no user-supplied KQL field)
Code ReviewOverviewThis PR adds: (1) O(1) point-in-time event lookup via a new Bug: silent fallback on invalid
|
- cli: validate --at ISO-8601 timestamp eagerly; an unparseable value now prints an error and exits non-zero instead of silently falling back to a 90d range scan that hides the typo. - cli: fix Haddock — collapse two stacked '-- |' markers on validateEventsOpts into one block so haddock no longer drops the second. - api: enrichIssue decodes via the typed RuntimeExceptionData record instead of pattern-matching raw JSON, so a renamed/removed field is a compile error rather than a silent "" default. - parser: derive exceptionFlattenedFields from flattenedOtelAttributes so the "every entry must also live under attributes.exception.* there" invariant is enforced automatically. - api: hoist the empty-spans guard to the top of synthStackFromSpans so the sortOn/map allocation is skipped on the empty case; replace 'maybe "" (\\v -> …)' with foldMap to match codebase style. - tests: add Web.ApiHandlersSpec covering synthStackFromSpans (ordering, error marker, missing service/name/span_id, empty list); extend ParserSpec with output-field-alias and exception-COALESCE rewriting cases.
Code ReviewOverviewThis PR adds: (1) a fast O(1) point-in-time event lookup ( Bugs / CorrectnessNon-UUID event IDs rejected silently on the The new route uses -- Routes.hs — current
:> Capture "event_id" UUID.UUID -- rejects non-UUID IDs silently
-- CLI/Commands.hs — current (no pre-flight check)
let path = "/api/v1/events/" <> opts.eventId <> "/time/" <> ...Performance / Risk
The fallback was Two extra queries per The acknowledged TODO is fine, but worth noting: 5-minute timeout applied globally (
Code Succinctness / Package Usage
-- current (4 lines, extra toList/fromList)
exceptionFlattenedFields =
S.fromList
. mapMaybe (T.stripPrefix "attributes.exception.")
. S.toList
$ flattenedOtelAttributes
-- simpler
exceptionFlattenedFields = S.mapMaybe (T.stripPrefix "attributes.exception.") flattenedOtelAttributesDefensive / Error Handling
_ -> pure issue -- AE.Error case: no log, no signalIf Minor Style Notes
What's Good
|
Relude already re-exports sortOn so the explicit List. qualifier is redundant. The hlint check on the previous commit was already flagging this; my edit kept the same call shape and inherited the warning.
Code ReviewOverview: This PR adds a fast O(1) event point-in-time lookup via Bugs fixed correctly
Reuse existing helpers
-- current
apiEventGet pid eid ts = do
mItem <- Telemetry.logRecordByProjectAndId pid ts eid
case mItem of
Nothing -> throwError err404{errBody = encodeUtf8 ("event not found" :: Text)}
Just item -> pure (AE.toJSON item)
-- simpler, uses existing notFoundOr
apiEventGet pid eid ts =
AE.toJSON <$> (notFoundOr "event not found" =<< Telemetry.logRecordByProjectAndId pid ts eid)
validateQueryOrDie q
| T.null (T.strip q) = pure ()
| otherwise =
validateOrDie $ first ("invalid query: " <>) $ void (parseQueryToAST q)Succinctness:
|
Code ReviewOverviewThis PR adds three main things: (1) an O(1) Bug: test fixture uses wrong resource structureIn resource = (\svc -> AesonText (Map.singleton "service" (AE.Object (AEKM.singleton "name" (AE.String svc))))) <$> serviceBut Fix: use Conciseness opportunities
-- current
spanId = maybe "?" (fromMaybe "?" . (.span_id)) s.context
-- shorter, uses Monad Maybe
spanId = fromMaybe "?" (s.context >>= (.span_id))
The 4-line TODO block is the kind of thing that belongs in a ticket/PR description, not inline. A single-line
svcTxt = foldMap (\v -> " [" <> v <> "]") svc
Performance
Potential path injection (low risk)In let path = "/api/v1/events/" <> opts.eventId <> "/time/" <> toText (iso8601Show t)
Minor
What's good
|
…ormat - Update isnotnull(trace_id) test to expect context___trace_id in WHERE - Fix resource field format in ApiHandlersSpec to use service.name flat key - Add bracketed service assertion to synthStackFromSpans test - Simplify span context access and trim verbose comments in ApiHandlers
Code ReviewOverviewThis PR adds MCP server documentation, a new Issues1. Global 5-minute timeout ( 2. transformFlattenedAttribute entire
| entire `S.member` flattenedOtelAttributes
, Just field <- T.stripPrefix "attributes.exception." entire =
"COALESCE(..." <> field <> ...
| entire `S.member` flattenedOtelAttributes = T.replace "." "___" entire
...3. 4. 5. Path concatenation safety ( | T.any (== '/') opts.eventId -> printError "event id must not contain '/'" >> liftIO exitFailure
Minor
What's good
|
getNestedValue now tries the literal joined key before splitting on '.', so resource maps with flat 'service.name' keys resolve correctly. Also inlines exceptionFlattenedFields, tightens CLI event-id validation, sets a 5m wreq timeout for long aggregates, and refactors the span-order test to compare line indices.
Code ReviewOverviewThis PR adds MCP server documentation, a fast Issues & Suggestions1.
|
Code ReviewOverviewThis PR adds an MCP server documentation layer, a Bug: false-positive test in
|
Code Review — PR #368 (Improve MCP and CLI)OverviewThis PR adds: (1) an O(1) Bugs / Correctness
-- test/unit/Web/ApiHandlersSpec.hs
idx needle = viaNonEmpty head [i | (i, l) <- zip [0 :: Int ..] ls, needle `T.isInfixOf` l]
shouldSatisfy (\e -> e < idx "later")
Performance
Succinctness / Style
| otherwise = case AE.fromJSON (getAeson issue.issueData) of
AE.Success (rd :: Issues.RuntimeExceptionData)
| T.null rd.stackTrace -> do ...
_ -> pure issueThe outer guard + inner pattern match + inner guard can be flattened with a local
svcTxt = foldMap (\v -> " [" <> v <> "]") svc
Long optional (strOption (long "at" <> metavar "TIMESTAMP" <> help "ISO-8601 timestamp for a fast point-in-time lookup (avoids 90d range scan)"))The parenthetical implementation detail ("avoids 90d range scan") is user-visible in Minor / Nits
SummaryThe core logic is sound. The two actionable bugs are the |
Closes #
How to test
Checklist