@afora/otel 0.0.0-stage → 0.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,58 @@
1
- # Temporary Holding Version
1
+ # @afora/otel
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [OpenTelemetry](https://opentelemetry.io) tracing for [`@afora/core`](../../README.md) applications: a span per command operation, use case and repository call made through a use case, nested as the calls are, with the exception layer and name on failure and no input, response or sensitive value in any span. What the spans are and carry is in [docs/tracing.md](../../docs/tracing.md).
4
+
5
+ ```bash
6
+ bun add @afora/core @afora/otel @opentelemetry/api
7
+ ```
8
+
9
+ `@afora/core` and `@opentelemetry/api` (`^1.9.0`) are peer dependencies. Set up an OpenTelemetry SDK as usual (`@opentelemetry/sdk-node`, an exporter); the package itself uses only the API, so it runs on Bun, Node >= 22 and Workers.
10
+
11
+ ## Example
12
+
13
+ <!-- docs:code skip: Post, Posts, Blog and Api are the declarations of your application -->
14
+ ```ts
15
+ import { applicationOf } from "@afora/core";
16
+ import { otelTracer } from "@afora/otel";
17
+
18
+ export const App = applicationOf({
19
+ name: "blog",
20
+ entities: { Post },
21
+ repositories: { Posts },
22
+ commands: { Blog },
23
+ outputs: { Api },
24
+ trace: otelTracer(),
25
+ });
26
+ ```
27
+
28
+ A request to `App.outputs.Api.build()`, on Hono or any HTTP sink, gives one trace:
29
+
30
+ ```
31
+ command Blog.publish afora.command=Blog.publish afora.operation=publish afora.outcome=ok
32
+ └─ use-case Publish afora.use_case=Publish afora.outcome=ok
33
+ └─ repository Post.create afora.repository=Post afora.method=create afora.use_case=Publish
34
+ ```
35
+
36
+ ## API
37
+
38
+ - **`otelTracer(tracer?)`**: the `ITracer` of `@afora/core` over an OpenTelemetry `Tracer`, by default `trace.getTracer(AFORA_TRACER_NAME)` of the global tracer provider (it works before the SDK registers, as OpenTelemetry tracers do).
39
+ - A command span is a child of the active span, such as the server span of your HTTP instrumentation; the spans below it are children of their caller's span whether or not a context manager is registered.
40
+ - With a context manager, each call runs with its span active, so the spans your code and instrumented drivers start inside a use case or a repository call nest in it.
41
+ - A failed span has the status `ERROR`, with the exception name as its message, and the attributes `afora.exception.layer`, `afora.exception.name` and `afora.exception.unexpected`; no exception event is recorded, since the message and stack of an error can hold values. A span that succeeded keeps the status unset.
42
+ - **`AFORA_TRACER_NAME`**: `"@afora/otel"`, the instrumentation scope of the default tracer.
43
+
44
+ ## In an afora.json
45
+
46
+ ```json
47
+ { "version": 1, "name": "blog", "trace": { "import": "../trace.ts#tracer" } }
48
+ ```
49
+
50
+ with `export const tracer = otelTracer();` in `trace.ts` (see [afora.json](../../docs/afora-json.md#tracing)).
51
+
52
+ ## Tests
53
+
54
+ ```bash
55
+ bun run test:otel # from the repository root
56
+ ```
57
+
58
+ The tests serve an application through [`@afora/hono`](../hono/README.md) with an in-memory span exporter, and check the trace of a request (command, use cases, repository calls), the attributes of a failure, and that no password, hash, input or id reaches any span.
@@ -0,0 +1,19 @@
1
+ import type { ITracer } from "@afora/core";
2
+ import { type Tracer } from "@opentelemetry/api";
3
+ /** The instrumentation scope of the default tracer. */
4
+ export declare const AFORA_TRACER_NAME = "@afora/otel";
5
+ /**
6
+ * The `trace` of an application, over an OpenTelemetry `Tracer` (by default
7
+ * `trace.getTracer("@afora/otel")`, of the global tracer provider):
8
+ *
9
+ * ```ts
10
+ * applicationOf({ name: "blog", trace: otelTracer(), ... });
11
+ * ```
12
+ *
13
+ * A command span is a child of the active span (an HTTP server span of your
14
+ * instrumentation, say); a use case span a child of its command or of the use
15
+ * case that ran it; a repository span a child of its use case, whether or not
16
+ * a context manager is registered. With one, each call also runs with its
17
+ * span active, so the spans of your code and of instrumented drivers nest in it.
18
+ */
19
+ export declare function otelTracer(tracer?: Tracer): ITracer;
package/dist/index.js ADDED
@@ -0,0 +1,40 @@
1
+ // src/index.ts
2
+ import {
3
+ context,
4
+ SpanStatusCode,
5
+ trace
6
+ } from "@opentelemetry/api";
7
+ var AFORA_TRACER_NAME = "@afora/otel";
8
+
9
+ class OtelSpan {
10
+ span;
11
+ constructor(span) {
12
+ this.span = span;
13
+ }
14
+ end(status, attributes) {
15
+ this.span.setAttributes(attributes);
16
+ if (status === "error") {
17
+ const name = attributes["afora.exception.name"];
18
+ this.span.setStatus({
19
+ code: SpanStatusCode.ERROR,
20
+ ...typeof name === "string" ? { message: name } : {}
21
+ });
22
+ }
23
+ this.span.end();
24
+ }
25
+ }
26
+ function otelTracer(tracer = trace.getTracer(AFORA_TRACER_NAME)) {
27
+ return Object.freeze({
28
+ startSpan(name, attributes, parent) {
29
+ const active = context.active();
30
+ return new OtelSpan(tracer.startSpan(name, { attributes }, parent instanceof OtelSpan ? trace.setSpan(active, parent.span) : active));
31
+ },
32
+ withSpan(span, run) {
33
+ return span instanceof OtelSpan ? context.with(trace.setSpan(context.active(), span.span), run) : run();
34
+ }
35
+ });
36
+ }
37
+ export {
38
+ AFORA_TRACER_NAME,
39
+ otelTracer
40
+ };
package/package.json CHANGED
@@ -1,6 +1,60 @@
1
1
  {
2
- "name": "@afora/otel",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "@afora/otel",
3
+ "version": "0.0.11",
4
+ "description": "OpenTelemetry tracing for afora applications: a span per command, use case and repository call, nested as the calls are, with no input, response or sensitive value in any span.",
5
+ "keywords": [
6
+ "afora",
7
+ "opentelemetry",
8
+ "otel",
9
+ "tracing",
10
+ "observability"
11
+ ],
12
+ "author": "Alan Reis <alanreisanjo@gmail.com> (https://github.com/Hoyasumii)",
13
+ "license": "MIT",
14
+ "homepage": "https://github.com/afora-framework/core/tree/main/packages/otel#readme",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/afora-framework/core.git",
18
+ "directory": "packages/otel"
19
+ },
20
+ "type": "module",
21
+ "main": "./dist/index.js",
22
+ "module": "./dist/index.js",
23
+ "types": "./dist/index.d.ts",
24
+ "exports": {
25
+ ".": {
26
+ "types": "./dist/index.d.ts",
27
+ "import": "./dist/index.js",
28
+ "default": "./dist/index.js"
29
+ },
30
+ "./package.json": "./package.json"
31
+ },
32
+ "files": [
33
+ "dist"
34
+ ],
35
+ "engines": {
36
+ "bun": ">=1.4.4",
37
+ "node": ">=22"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public",
41
+ "provenance": true
42
+ },
43
+ "scripts": {
44
+ "typecheck": "bun check",
45
+ "build": "rm -rf dist && bun build ./src/index.ts --outdir ./dist --target node --format esm --packages external --tsconfig-override tsconfig.build.json && tsc -p tsconfig.build.json && bun ../../scripts/add-dts-extensions.ts dist",
46
+ "test": "bun test"
47
+ },
48
+ "devDependencies": {
49
+ "@afora/hono": "workspace:*",
50
+ "@opentelemetry/api": "^1.9.0",
51
+ "@opentelemetry/context-async-hooks": "^2.2.0",
52
+ "@opentelemetry/sdk-trace-base": "^2.2.0",
53
+ "@types/bun": "^1.4.4",
54
+ "hono": "^4.13.14"
55
+ },
56
+ "peerDependencies": {
57
+ "@afora/core": ">=0.0.11",
58
+ "@opentelemetry/api": "^1.9.0"
59
+ }
60
+ }