Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions .agents/skills/fprofiler/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
name: fprofiler
description: Use whenever generating or reviewing F# server-side code that records request diagnostics or builds a debug toolbar with Alma.Profiler — calling Queries.add, Errors.add, Resources.add, Profiler.init, or constructing a Profiler.Toolbar. Trigger also on mentions of web app profiler, HTTP query history, error tracking, resource availability, ApplicationValues, Target/Response/Query, or rendering a Symfony-style debug toolbar consumed by fable-profiler.
---

# F-Profiler

Library: [alma-oss/fprofiler](https://github.com/alma-oss/fprofiler)
NuGet: `Alma.Profiler`

## Purpose

`Alma.Profiler` is an F# library that collects server-side runtime diagnostics — application metadata, git info, resource availability, HTTP query history, and tracked errors — and assembles them into a `Profiler.Toolbar` data model. It is the server-side counterpart of the client-side `fable-profiler`, which renders the toolbar as a Symfony-style debug bar.

## When to Use

- Recording HTTP queries, errors, or resource endpoints observed during request handling.
- Assembling a profiler toolbar to send to a client for display.
- Reviewing F# code that consumes any of the `Queries`, `Errors`, `Resources`, or `Profiler` modules.

## When NOT to Use

- Pure client-side rendering of the toolbar (that is `fable-profiler`).
- Metrics aggregation, alerting, or persistent storage — this library keeps only short, in-memory history.
- Non-F# consumers, or scenarios needing durable/queryable diagnostics history.

## Main Concepts

- **`Profiler.init`** — entry point; takes the current application, `ApplicationValues`, environment, and a debug string, and returns a `Profiler.Toolbar`.
- **`Profiler.Toolbar`** — the assembled output model (from `Alma.Profiler.Common`); a list of items, each with detail panels.
- **`ApplicationValues`** — wrapper over a `(Label * Value) list` of arbitrary application metadata; entries whose label starts with `git ` are routed into the Git toolbar item.
- **`Queries`** — module with global mutable, capped (last 10) history of recorded HTTP queries; exposes `add`, `values`, `count`.
- **`Target` / `Response` / `Query`** — query value types; `Target` pairs an `HTTPMethod` (Get/Post/Put/Delete) with a `Url`; `Response` wraps `Result<string,string>`; `Query` is the recorded Ok/Error outcome.
- **`Errors`** — module with global mutable, capped (last 10) history of error messages; exposes `add`, `values`, `count`. `ErrorMessage` is a plain `string`.
- **`Resources`** — module tracking service resource availability per `Instance`; exposes `add` and `values` (not capped).
- **`count` vs `values`** — `count` returns the running total of all items ever added; `values` returns only the retained recent items.
- **`List` (Utils)** — augments F# `List` with `filterNotIn`, `filterNotInBy`, `filterInBy`, and `takeUpTo`.

## Related Libraries

- `Alma.Profiler.Common` — shared toolbar types (`Profiler.Toolbar`, `Profiler.Item`, `Profiler.DetailItem`, `Label`, `Value`, `Color`, `Detail`).
- `Alma.Metrics` — `ResourceAvailability`, `ResourceType`, `ResourceLocation`, `Audience`.
- `Alma.ServiceIdentification` — `Instance`, `Service`, `Box`.
- `Alma.EnvironmentModel` — `Environment`.
- `Alma.State` (`ConcurrentStorage`) — thread-safe mutable `State` backing the capped collections.

## Keywords for Search

Alma.Profiler, fprofiler, web app profiler, debug toolbar, Profiler.init, Profiler.Toolbar, ApplicationValues, Queries.add, Errors.add, Resources.add, HTTP query history, error tracking, resource availability, Target, Response, Query, HTTPMethod, ResourceAvailability, git branch, fable-profiler, F# diagnostics

## Reference Files

For composition principles and recommended API usage, read `references/preferred-patterns.md`. For known pitfalls and incorrect assumptions, read `references/anti-patterns.md`. For worked code examples, read `references/examples.md`.
49 changes: 49 additions & 0 deletions .agents/skills/fprofiler/references/anti-patterns.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Anti-Patterns

Each entry is **mistake → why → fix**.

## State & Concurrency

- **Treating the recording modules as request-scoped or isolated.**
Why: `Queries`, `Errors`, and `Resources` hold process-wide global mutable state shared across all requests and threads.
Fix: Assume entries from concurrent or prior requests may be present; record promptly and call `Profiler.init` to snapshot, rather than assuming the store reflects only the current request.

- **Trying to construct, reset, or thread the internal `State` value yourself.**
Why: The `State` backing each module is private and managed internally via `Alma.State.ConcurrentStorage`.
Fix: Use only the public functions (`add`, `values`, `count`); there is no public reset, so design tests and assertions around accumulating state.

## Counts vs Retained Values

- **Assuming `values` returns every item ever added.**
Why: Retained history for `Queries` and `Errors` is capped at the last 10 entries, while `count` keeps the true running total.
Fix: Use `count` for totals and `values` for the (up to) 10 most recent entries; never derive a total from the length of `values`.

- **Expecting more than 10 entries in a toolbar detail panel.**
Why: `Profiler.init` caps each detail panel to the 10 most recent entries.
Fix: Treat detail panels as a recent-activity preview, not a full log; surface complete history elsewhere if needed.

## Queries

- **Building `Query` values directly or hand-rolling Ok/Error wrapping.**
Why: The Ok/Error classification is derived from the `Response`'s `Result` by the library.
Fix: Wrap the outcome with `Response.create` over a `Result<string,string>` and call `Queries.add`; let the library classify success vs failure.

- **Expecting arbitrary HTTP verbs.**
Why: `HTTPMethod` is a closed union of `Get`, `Post`, `Put`, and `Delete` only.
Fix: Map other verbs onto the available cases or extend the library; do not pass a raw method string.

## Resources

- **Assuming resource history is capped or de-duplicated by endpoint.**
Why: `Resources` is keyed by `Instance` and is not capped; a new registration for an existing instance overwrites the previous one.
Fix: Register one canonical resource per instance; if you need multiple endpoints, key them by distinct instances.

- **Relying on resources being pushed into a metrics system.**
Why: Forwarding registered resources to metrics is an open, unimplemented `todo` in the library.
Fix: Do not assume `Resources.add` emits metrics; if you need metrics, publish them separately via `Alma.Metrics`.

## General

- **Reading the library source to infer the API instead of this skill.**
Why: The public surface is small and stable; source spelunking wastes context and risks coupling to internals.
Fix: Use the documented modules and the worked code in `examples.md`.
126 changes: 126 additions & 0 deletions .agents/skills/fprofiler/references/examples.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# Examples

All code for this skill lives here, ordered from simplest to most complete. Each example is self-contained. Names like `ServiceA`, `WebApi`, and `CacheInstance` are neutral placeholders.

## Tracking an Error

```fsharp
open Alma.Profiler

Errors.add "Upstream returned 503"

let recentErrors = Errors.values () // up to 10 newest (ErrorMessage * DateTime), newest first
let totalErrors = Errors.count () // running total ever added
```

## Recording a Query

```fsharp
open Alma.Profiler
open Alma.Profiler.Queries

// A successful call to WebApi
let okTarget = Target (Get, Url "https://example.test/web-api/items")
let okResponse = Response.create (Ok "200 OK, 12 items")
Queries.add okTarget okResponse

// A failed call
let failTarget = Target (Post, Url "https://example.test/web-api/items")
let failResponse = Response.create (Error "500 Internal Server Error")
Queries.add failTarget failResponse

let recentQueries = Queries.values () // up to 10 newest Query values
let totalQueries = Queries.count ()
```

## Registering a Resource

```fsharp
open Alma.Profiler
open Alma.ServiceIdentification

// instance: an Alma.ServiceIdentification.Instance identifying e.g. CacheInstance
let registerCache (instance: Instance) =
Resources.add
"cache" // resource type
"redis://cache.internal.test:6379" // resource location
instance

let knownResources = Resources.values ()
```

## Assembling the Toolbar

```fsharp
open Alma.Profiler
open Alma.Profiler.Common
open Alma.ServiceIdentification
open Alma.EnvironmentModel

// currentApplication: Alma.ServiceIdentification.Box
// currentEnvironment: Alma.EnvironmentModel.Environment
let buildToolbar (currentApplication: Box) (currentEnvironment: Environment) =
// Arbitrary metadata; "Git ..." labels are routed into the Git item.
let applicationValues =
Profiler.ApplicationValues [
Profiler.Label "Git Branch", Profiler.Value "main"
Profiler.Label "Git Commit", Profiler.Value "a1b2c3d"
Profiler.Label "Version", Profiler.Value "9.0.0"
]

// The toolbar snapshots whatever has been recorded via
// Queries.add / Errors.add / Resources.add so far.
Profiler.init
currentApplication
applicationValues
currentEnvironment
"Prod" // debug string; containing "Dev" colors the Debug entry yellow
```

## Request-Lifecycle Integration

```fsharp
open Alma.Profiler
open Alma.Profiler.Queries
open Alma.ServiceIdentification
open Alma.EnvironmentModel

// Record diagnostics while handling a request, then assemble at the end.
let handleRequest
(currentApplication: Box)
(currentEnvironment: Environment)
(cacheInstance: Instance) =

// 1. Register a downstream resource ServiceA depends on.
Resources.add "cache" "redis://cache.internal.test:6379" cacheInstance

// 2. Record an outgoing call and its outcome.
let target = Target (Get, Url "https://example.test/service-a/status")
match (* perform the call *) Ok "200 OK" with
| Ok body -> Queries.add target (Response.create (Ok body))
| Error e ->
Queries.add target (Response.create (Error e))
Errors.add (sprintf "ServiceA call failed: %s" e)

// 3. Produce the toolbar to return to the client.
Profiler.init
currentApplication
(Profiler.ApplicationValues [ Profiler.Label "Git Branch", Profiler.Value "main" ])
currentEnvironment
"Prod"
```

## Testing Against Accumulating State

```fsharp
open Alma.Profiler

// Global mutable state is shared across tests; assert on relative change.
let ``adding an error increments the total`` () =
let before = Errors.count ()
Errors.add "boom"
let after = Errors.count ()
assert (after = before + 1)
// values() is capped at the last 10, so it may not contain every added message
assert (Errors.values () |> List.isEmpty |> not)
```
40 changes: 40 additions & 0 deletions .agents/skills/fprofiler/references/preferred-patterns.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Preferred Patterns

## Core Principles

- **Record during the request, assemble at the end.** Call `Queries.add`, `Errors.add`, and `Resources.add` as events occur while handling a request, then call `Profiler.init` once at response time to produce the `Profiler.Toolbar`.
- **The recording modules own their state.** `Queries`, `Errors`, and `Resources` hold module-level state internally — you push facts in and read them back; you never construct or thread a state value yourself.
- **Let labels drive grouping.** Application metadata passed through `ApplicationValues` is split by label: any entry whose label starts with `git ` (case-insensitive) is surfaced in the Git toolbar item; everything else appears under the Application item.

## Recommended API Usage

- **Errors** — `Errors.add` takes a message string; `Errors.values` returns retained `(ErrorMessage * DateTime)` entries newest-first; `Errors.count` returns the running total.
- **Queries** — build a `Target` from an `HTTPMethod` and `Url`, wrap the outcome with `Response.create` over a `Result<string,string>`, then call `Queries.add target response`. `Query.ofResponse` is the internal bridge that turns a `Response` into an Ok/Error `Query`; prefer `Queries.add` rather than building `Query` values by hand. See `examples.md` → Recording a Query.
- **Resources** — `Resources.add resourceType resourceLocation instance` registers an endpoint for an `Instance`; later registrations for the same instance overwrite earlier ones. See `examples.md` → Registering a Resource.
- **Profiler** — `Profiler.init currentApplication applicationValues currentEnvironment debug` returns the toolbar. The Queries and Errors items are emitted only when their count is greater than zero; detail panels show at most the 10 most recent entries. See `examples.md` → Assembling the Toolbar.

## Error Handling

- A query is recorded as a failure by passing an `Error` case inside the `Response`'s `Result`; this colors its toolbar entry red. A successful query uses the `Ok` case and is colored green.
- `Errors.add` is for application-level error messages surfaced in the toolbar's Errors item; it is independent of failed queries.

## Composition

- The output is a plain immutable `Profiler.Toolbar` value (a list of items). Treat it as data: serialize it and hand it to the client; do not mutate it after `Profiler.init`.
- Toolbar items map to fixed ids (`Application`, `Git`, `Resources`, `Queries`, `Errors`); rely on these stable ids rather than item ordering when consuming the toolbar downstream.

## Integration with Other Libraries

- `Profiler.init` reads identity from `Alma.ServiceIdentification` (`Box.instance`, `Instance`, `Service`) and the environment from `Alma.EnvironmentModel` (`Environment.value`).
- `Resources` builds `Alma.Metrics` `ResourceAvailability` values; `Profiler.init` pattern-matches on `Service` / `Common` / `MultiTenantService` resource shapes, treating any `ResourceType` containing `router` as a yellow entry.
- The toolbar model itself comes from `Alma.Profiler.Common`; reuse its `Label`, `Value`, `Color`, and `Detail` helpers when extending application values.

## Naming Conventions

- All public modules use `[<RequireQualifiedAccess>]`, so always call qualified: `Queries.add`, `Errors.values`, `Resources.add`, `Profiler.init`.
- Git-related application values follow the `Git <name>` label convention (e.g. `Git Branch`, `Git Commit`) so they group correctly.

## Testing Recommendations

- The recording modules use global mutable state, so tests are not isolated by default. Account for residual entries from earlier tests, and prefer asserting on relative changes in `count` rather than absolute values.
- Because retained history is capped at the last 10 entries while `count` keeps the true total, assert these two independently.
1 change: 1 addition & 0 deletions .claude/skills
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# AGENTS.md — Alma.Profiler

This repo ships Agent Skill for the `Alma.Profiler` library. Compatible agents discover it automatically; see `.agents/skills/fprofiler/SKILL.md`.

## Project Purpose

`Alma.Profiler` is an F# NuGet library that provides server-side profiler functionality for web applications. It collects and presents runtime diagnostics — application metadata, git info, resource availability, HTTP query history, and error tracking — as a structured `Profiler.Toolbar` data model that the client-side `fable-profiler` renders as a Symfony-style debug toolbar.
Expand Down
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<!-- Imports repo agent instructions so Claude Code uses the same guidance as other agents. -->
@AGENTS.md