diff --git a/packages/cre-sdk-javy-plugin/dist/javy-chainlink-sdk.plugin.wasm b/packages/cre-sdk-javy-plugin/dist/javy-chainlink-sdk.plugin.wasm index fe41260d..6a07252c 100644 Binary files a/packages/cre-sdk-javy-plugin/dist/javy-chainlink-sdk.plugin.wasm and b/packages/cre-sdk-javy-plugin/dist/javy-chainlink-sdk.plugin.wasm differ diff --git a/packages/cre-sdk-javy-plugin/src/javy_chainlink_sdk/src/lib.rs b/packages/cre-sdk-javy-plugin/src/javy_chainlink_sdk/src/lib.rs index 1c1faefe..0fce1b19 100644 --- a/packages/cre-sdk-javy-plugin/src/javy_chainlink_sdk/src/lib.rs +++ b/packages/cre-sdk-javy-plugin/src/javy_chainlink_sdk/src/lib.rs @@ -51,6 +51,8 @@ unsafe extern "C" { fn random_seed(mode: i32) -> i64; fn now(result_timestamp: *mut u8) -> i32; + + fn emit_metric(data_ptr: *const u8, data_len: i32) -> i32; } import_namespace!("javy_chainlink_sdk"); @@ -226,6 +228,16 @@ pub fn modify_runtime(runtime: Runtime) -> Runtime { }), ); + extend_wasm_exports( + &ctx, + "emitMetric", + Func::from(|_ctx: Ctx<'_>, data: ArgBytes| { + let bytes = data.0; + let rc = unsafe { emit_metric(bytes.as_ptr(), bytes.len() as i32) }; + Ok::(rc) + }), + ); + extend_wasm_exports( &ctx, "sendResponse", diff --git a/packages/cre-sdk/Makefile b/packages/cre-sdk/Makefile index c166c42b..bba1f40d 100644 --- a/packages/cre-sdk/Makefile +++ b/packages/cre-sdk/Makefile @@ -1,4 +1,4 @@ -COMMON_VERSION ?= cre-std-tests@0.6.0 +COMMON_VERSION ?= 8c592883ed580efbf21afd2c15bc0b40727fb071 MODULE := github.com/smartcontractkit/chainlink-common TEST_PATTERN ?= ^TestStandard diff --git a/packages/cre-sdk/buf.gen.yaml b/packages/cre-sdk/buf.gen.yaml index b834e587..da3e69b9 100644 --- a/packages/cre-sdk/buf.gen.yaml +++ b/packages/cre-sdk/buf.gen.yaml @@ -30,6 +30,12 @@ inputs: - ../../submodules/chainlink-protos/cre/sdk/v1alpha - ../../submodules/chainlink-protos/cre/tools/generator/v1alpha - ../../submodules/chainlink-protos/cre/values/v1 + - directory: ../../submodules/chainlink-protos/workflows + # Allowlist of workflow protos exposed in the SDK (e.g. user metrics). + paths: + - ../../submodules/chainlink-protos/workflows/workflows/v2/workflow_user_metric.proto + - ../../submodules/chainlink-protos/workflows/workflows/v2/cre_info.proto + - ../../submodules/chainlink-protos/workflows/workflows/v2/workflow_key.proto plugins: # Modern @bufbuild/protoc-gen-es plugin diff --git a/packages/cre-sdk/src/generated/workflows/v2/cre_info_pb.ts b/packages/cre-sdk/src/generated/workflows/v2/cre_info_pb.ts new file mode 100644 index 00000000..7e4424dc --- /dev/null +++ b/packages/cre-sdk/src/generated/workflows/v2/cre_info_pb.ts @@ -0,0 +1,156 @@ +// @generated by protoc-gen-es v2.6.3 with parameter "target=ts,import_extension=none,json_types=true,keep_empty_files=false" +// @generated from file workflows/v2/cre_info.proto (package workflows.v2, syntax proto3) +/* eslint-disable */ + +import type { Message } from '@bufbuild/protobuf' +import type { GenFile, GenMessage } from '@bufbuild/protobuf/codegenv2' +import { fileDesc, messageDesc } from '@bufbuild/protobuf/codegenv2' + +/** + * Describes the file workflows/v2/cre_info.proto. + */ +export const file_workflows_v2_cre_info: GenFile = + /*@__PURE__*/ + fileDesc( + 'Cht3b3JrZmxvd3MvdjIvY3JlX2luZm8ucHJvdG8SDHdvcmtmbG93cy52MiKMAgoHQ3JlSW5mbxINCgVkb25JRBgBIAEoBRIMCgRkb25GGAIgASgFEgwKBGRvbk4YAyABKAUSDQoFcDJwSUQYBCABKAkSHwoXd29ya2Zsb3dSZWdpc3RyeUFkZHJlc3MYBSABKAkSHwoXd29ya2Zsb3dSZWdpc3RyeVZlcnNpb24YBiABKAkSHQoVd29ya2Zsb3dSZWdpc3RyeUNoYWluGAcgASgJEhUKDWVuZ2luZVZlcnNpb24YCCABKAkSIwobY2FwYWJpbGl0aWVzUmVnaXN0cnlWZXJzaW9uGAkgASgJEhIKCmRvblZlcnNpb24YCiABKAkSFgoOd29ya2Zsb3dTb3VyY2UYCyABKAlCrwEKEGNvbS53b3JrZmxvd3MudjJCDENyZUluZm9Qcm90b1ABWjxnaXRodWIuY29tL3NtYXJ0Y29udHJhY3RraXQvY2hhaW5saW5rLXByb3Rvcy93b3JrZmxvd3MvZ28vdjKiAgNXWFiqAgxXb3JrZmxvd3MuVjLKAgxXb3JrZmxvd3NcVjLiAhhXb3JrZmxvd3NcVjJcR1BCTWV0YWRhdGHqAg1Xb3JrZmxvd3M6OlYyYgZwcm90bzM', + ) + +/** + * @generated from message workflows.v2.CreInfo + */ +export type CreInfo = Message<'workflows.v2.CreInfo'> & { + /** + * @generated from field: int32 donID = 1; + */ + donID: number + + /** + * @generated from field: int32 donF = 2; + */ + donF: number + + /** + * @generated from field: int32 donN = 3; + */ + donN: number + + /** + * @generated from field: string p2pID = 4; + */ + p2pID: string + + /** + * @generated from field: string workflowRegistryAddress = 5; + */ + workflowRegistryAddress: string + + /** + * @generated from field: string workflowRegistryVersion = 6; + */ + workflowRegistryVersion: string + + /** + * @generated from field: string workflowRegistryChain = 7; + */ + workflowRegistryChain: string + + /** + * @generated from field: string engineVersion = 8; + */ + engineVersion: string + + /** + * @generated from field: string capabilitiesRegistryVersion = 9; + */ + capabilitiesRegistryVersion: string + + /** + * @generated from field: string donVersion = 10; + */ + donVersion: string + + /** + * workflowSource identifies where the workflow was deployed from. + * Format varies by source type: + * - Onchain contract: "contract:{chain_selector}:{contract_address}" + * - GRPC source: "grpc:{source_name}:v1" + * - File source: "file:{source_name}:v1" + * + * @generated from field: string workflowSource = 11; + */ + workflowSource: string +} + +/** + * @generated from message workflows.v2.CreInfo + */ +export type CreInfoJson = { + /** + * @generated from field: int32 donID = 1; + */ + donID?: number + + /** + * @generated from field: int32 donF = 2; + */ + donF?: number + + /** + * @generated from field: int32 donN = 3; + */ + donN?: number + + /** + * @generated from field: string p2pID = 4; + */ + p2pID?: string + + /** + * @generated from field: string workflowRegistryAddress = 5; + */ + workflowRegistryAddress?: string + + /** + * @generated from field: string workflowRegistryVersion = 6; + */ + workflowRegistryVersion?: string + + /** + * @generated from field: string workflowRegistryChain = 7; + */ + workflowRegistryChain?: string + + /** + * @generated from field: string engineVersion = 8; + */ + engineVersion?: string + + /** + * @generated from field: string capabilitiesRegistryVersion = 9; + */ + capabilitiesRegistryVersion?: string + + /** + * @generated from field: string donVersion = 10; + */ + donVersion?: string + + /** + * workflowSource identifies where the workflow was deployed from. + * Format varies by source type: + * - Onchain contract: "contract:{chain_selector}:{contract_address}" + * - GRPC source: "grpc:{source_name}:v1" + * - File source: "file:{source_name}:v1" + * + * @generated from field: string workflowSource = 11; + */ + workflowSource?: string +} + +/** + * Describes the message workflows.v2.CreInfo. + * Use `create(CreInfoSchema)` to create a new message. + */ +export const CreInfoSchema: GenMessage = + /*@__PURE__*/ + messageDesc(file_workflows_v2_cre_info, 0) diff --git a/packages/cre-sdk/src/generated/workflows/v2/workflow_key_pb.ts b/packages/cre-sdk/src/generated/workflows/v2/workflow_key_pb.ts new file mode 100644 index 00000000..1d53a513 --- /dev/null +++ b/packages/cre-sdk/src/generated/workflows/v2/workflow_key_pb.ts @@ -0,0 +1,74 @@ +// @generated by protoc-gen-es v2.6.3 with parameter "target=ts,import_extension=none,json_types=true,keep_empty_files=false" +// @generated from file workflows/v2/workflow_key.proto (package workflows.v2, syntax proto3) +/* eslint-disable */ + +import type { Message } from '@bufbuild/protobuf' +import type { GenFile, GenMessage } from '@bufbuild/protobuf/codegenv2' +import { fileDesc, messageDesc } from '@bufbuild/protobuf/codegenv2' + +/** + * Describes the file workflows/v2/workflow_key.proto. + */ +export const file_workflows_v2_workflow_key: GenFile = + /*@__PURE__*/ + fileDesc( + 'Ch93b3JrZmxvd3MvdjIvd29ya2Zsb3dfa2V5LnByb3RvEgx3b3JrZmxvd3MudjIiZgoLV29ya2Zsb3dLZXkSFQoNd29ya2Zsb3dPd25lchgBIAEoCRIUCgx3b3JrZmxvd05hbWUYAiABKAkSEgoKd29ya2Zsb3dJRBgDIAEoCRIWCg5vcmdhbml6YXRpb25JRBgEIAEoCUKzAQoQY29tLndvcmtmbG93cy52MkIQV29ya2Zsb3dLZXlQcm90b1ABWjxnaXRodWIuY29tL3NtYXJ0Y29udHJhY3RraXQvY2hhaW5saW5rLXByb3Rvcy93b3JrZmxvd3MvZ28vdjKiAgNXWFiqAgxXb3JrZmxvd3MuVjLKAgxXb3JrZmxvd3NcVjLiAhhXb3JrZmxvd3NcVjJcR1BCTWV0YWRhdGHqAg1Xb3JrZmxvd3M6OlYyYgZwcm90bzM', + ) + +/** + * @generated from message workflows.v2.WorkflowKey + */ +export type WorkflowKey = Message<'workflows.v2.WorkflowKey'> & { + /** + * @generated from field: string workflowOwner = 1; + */ + workflowOwner: string + + /** + * @generated from field: string workflowName = 2; + */ + workflowName: string + + /** + * @generated from field: string workflowID = 3; + */ + workflowID: string + + /** + * @generated from field: string organizationID = 4; + */ + organizationID: string +} + +/** + * @generated from message workflows.v2.WorkflowKey + */ +export type WorkflowKeyJson = { + /** + * @generated from field: string workflowOwner = 1; + */ + workflowOwner?: string + + /** + * @generated from field: string workflowName = 2; + */ + workflowName?: string + + /** + * @generated from field: string workflowID = 3; + */ + workflowID?: string + + /** + * @generated from field: string organizationID = 4; + */ + organizationID?: string +} + +/** + * Describes the message workflows.v2.WorkflowKey. + * Use `create(WorkflowKeySchema)` to create a new message. + */ +export const WorkflowKeySchema: GenMessage = + /*@__PURE__*/ + messageDesc(file_workflows_v2_workflow_key, 0) diff --git a/packages/cre-sdk/src/generated/workflows/v2/workflow_user_metric_pb.ts b/packages/cre-sdk/src/generated/workflows/v2/workflow_user_metric_pb.ts new file mode 100644 index 00000000..ad22aa12 --- /dev/null +++ b/packages/cre-sdk/src/generated/workflows/v2/workflow_user_metric_pb.ts @@ -0,0 +1,155 @@ +// @generated by protoc-gen-es v2.6.3 with parameter "target=ts,import_extension=none,json_types=true,keep_empty_files=false" +// @generated from file workflows/v2/workflow_user_metric.proto (package workflows.v2, syntax proto3) +/* eslint-disable */ + +import type { Message } from '@bufbuild/protobuf' +import type { GenEnum, GenFile, GenMessage } from '@bufbuild/protobuf/codegenv2' +import { enumDesc, fileDesc, messageDesc } from '@bufbuild/protobuf/codegenv2' +import type { CreInfo, CreInfoJson } from './cre_info_pb' +import { file_workflows_v2_cre_info } from './cre_info_pb' +import type { WorkflowKey, WorkflowKeyJson } from './workflow_key_pb' +import { file_workflows_v2_workflow_key } from './workflow_key_pb' + +/** + * Describes the file workflows/v2/workflow_user_metric.proto. + */ +export const file_workflows_v2_workflow_user_metric: GenFile = + /*@__PURE__*/ + fileDesc( + 'Cid3b3JrZmxvd3MvdjIvd29ya2Zsb3dfdXNlcl9tZXRyaWMucHJvdG8SDHdvcmtmbG93cy52MiLPAgoSV29ya2Zsb3dVc2VyTWV0cmljEiYKB2NyZUluZm8YASABKAsyFS53b3JrZmxvd3MudjIuQ3JlSW5mbxIrCgh3b3JrZmxvdxgCIAEoCzIZLndvcmtmbG93cy52Mi5Xb3JrZmxvd0tleRIbChN3b3JrZmxvd0V4ZWN1dGlvbklEGAMgASgJEhEKCXRpbWVzdGFtcBgEIAEoCRIMCgRuYW1lGAUgASgJEg0KBXZhbHVlGAYgASgBEioKBHR5cGUYByABKA4yHC53b3JrZmxvd3MudjIuVXNlck1ldHJpY1R5cGUSPAoGbGFiZWxzGAggAygLMiwud29ya2Zsb3dzLnYyLldvcmtmbG93VXNlck1ldHJpYy5MYWJlbHNFbnRyeRotCgtMYWJlbHNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBKmwKDlVzZXJNZXRyaWNUeXBlEiAKHFVTRVJfTUVUUklDX1RZUEVfVU5TUEVDSUZJRUQQABIcChhVU0VSX01FVFJJQ19UWVBFX0NPVU5URVIQARIaChZVU0VSX01FVFJJQ19UWVBFX0dBVUdFEAJCugEKEGNvbS53b3JrZmxvd3MudjJCF1dvcmtmbG93VXNlck1ldHJpY1Byb3RvUAFaPGdpdGh1Yi5jb20vc21hcnRjb250cmFjdGtpdC9jaGFpbmxpbmstcHJvdG9zL3dvcmtmbG93cy9nby92MqICA1dYWKoCDFdvcmtmbG93cy5WMsoCDFdvcmtmbG93c1xWMuICGFdvcmtmbG93c1xWMlxHUEJNZXRhZGF0YeoCDVdvcmtmbG93czo6VjJiBnByb3RvMw', + [file_workflows_v2_cre_info, file_workflows_v2_workflow_key], + ) + +/** + * @generated from message workflows.v2.WorkflowUserMetric + */ +export type WorkflowUserMetric = Message<'workflows.v2.WorkflowUserMetric'> & { + /** + * @generated from field: workflows.v2.CreInfo creInfo = 1; + */ + creInfo?: CreInfo + + /** + * @generated from field: workflows.v2.WorkflowKey workflow = 2; + */ + workflow?: WorkflowKey + + /** + * @generated from field: string workflowExecutionID = 3; + */ + workflowExecutionID: string + + /** + * @generated from field: string timestamp = 4; + */ + timestamp: string + + /** + * @generated from field: string name = 5; + */ + name: string + + /** + * @generated from field: double value = 6; + */ + value: number + + /** + * @generated from field: workflows.v2.UserMetricType type = 7; + */ + type: UserMetricType + + /** + * @generated from field: map labels = 8; + */ + labels: { [key: string]: string } +} + +/** + * @generated from message workflows.v2.WorkflowUserMetric + */ +export type WorkflowUserMetricJson = { + /** + * @generated from field: workflows.v2.CreInfo creInfo = 1; + */ + creInfo?: CreInfoJson + + /** + * @generated from field: workflows.v2.WorkflowKey workflow = 2; + */ + workflow?: WorkflowKeyJson + + /** + * @generated from field: string workflowExecutionID = 3; + */ + workflowExecutionID?: string + + /** + * @generated from field: string timestamp = 4; + */ + timestamp?: string + + /** + * @generated from field: string name = 5; + */ + name?: string + + /** + * @generated from field: double value = 6; + */ + value?: number | 'NaN' | 'Infinity' | '-Infinity' + + /** + * @generated from field: workflows.v2.UserMetricType type = 7; + */ + type?: UserMetricTypeJson + + /** + * @generated from field: map labels = 8; + */ + labels?: { [key: string]: string } +} + +/** + * Describes the message workflows.v2.WorkflowUserMetric. + * Use `create(WorkflowUserMetricSchema)` to create a new message. + */ +export const WorkflowUserMetricSchema: GenMessage< + WorkflowUserMetric, + { jsonType: WorkflowUserMetricJson } +> = /*@__PURE__*/ messageDesc(file_workflows_v2_workflow_user_metric, 0) + +/** + * @generated from enum workflows.v2.UserMetricType + */ +export enum UserMetricType { + /** + * @generated from enum value: USER_METRIC_TYPE_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * @generated from enum value: USER_METRIC_TYPE_COUNTER = 1; + */ + COUNTER = 1, + + /** + * @generated from enum value: USER_METRIC_TYPE_GAUGE = 2; + */ + GAUGE = 2, +} + +/** + * @generated from enum workflows.v2.UserMetricType + */ +export type UserMetricTypeJson = + | 'USER_METRIC_TYPE_UNSPECIFIED' + | 'USER_METRIC_TYPE_COUNTER' + | 'USER_METRIC_TYPE_GAUGE' + +/** + * Describes the enum workflows.v2.UserMetricType. + */ +export const UserMetricTypeSchema: GenEnum = + /*@__PURE__*/ + enumDesc(file_workflows_v2_workflow_user_metric, 0) diff --git a/packages/cre-sdk/src/sdk/impl/runtime-impl.test.ts b/packages/cre-sdk/src/sdk/impl/runtime-impl.test.ts index c5081e40..ca824224 100644 --- a/packages/cre-sdk/src/sdk/impl/runtime-impl.test.ts +++ b/packages/cre-sdk/src/sdk/impl/runtime-impl.test.ts @@ -74,6 +74,7 @@ function createRuntimeHelpersMock(overrides: Partial = {}): Runt throw new Error('Method not implemented: now') }), log: mock(() => {}), + emitMetric: mock(() => true), } // Return a merged object with overrides taking precedence diff --git a/packages/cre-sdk/src/sdk/impl/runtime-impl.ts b/packages/cre-sdk/src/sdk/impl/runtime-impl.ts index 861e91cd..f3e4b9e0 100644 --- a/packages/cre-sdk/src/sdk/impl/runtime-impl.ts +++ b/packages/cre-sdk/src/sdk/impl/runtime-impl.ts @@ -1,4 +1,4 @@ -import { create, type Message } from '@bufbuild/protobuf' +import { create, type Message, toBinary } from '@bufbuild/protobuf' import type { GenMessage } from '@bufbuild/protobuf/codegenv2' import { type Any, anyPack, anyUnpack } from '@bufbuild/protobuf/wkt' import { @@ -21,6 +21,10 @@ import { SimpleConsensusInputsSchema, } from '@cre/generated/sdk/v1alpha/sdk_pb' import type { Value as ProtoValue } from '@cre/generated/values/v1/values_pb' +import { + UserMetricType, + WorkflowUserMetricSchema, +} from '@cre/generated/workflows/v2/workflow_user_metric_pb' import { ConsensusCapability } from '@cre/generated-sdk/capabilities/internal/consensus/v1alpha/consensus_sdk_gen' import type { BaseRuntime, @@ -208,6 +212,29 @@ export class BaseRuntimeImpl implements BaseRuntime { log(message: string): void { this.helpers.log(message) } + + emitMetric( + name: string, + value: number, + type: MetricType, + labels?: Record, + ): boolean { + const metric = create(WorkflowUserMetricSchema, { + name, + value, + type: METRIC_TYPE_TO_PROTO[type], + labels: labels ?? {}, + }) + return this.helpers.emitMetric(toBinary(WorkflowUserMetricSchema, metric)) + } +} + +/** Ergonomic union for {@link BaseRuntimeImpl.emitMetric}. */ +export type MetricType = 'counter' | 'gauge' + +const METRIC_TYPE_TO_PROTO: Record = { + counter: UserMetricType.COUNTER, + gauge: UserMetricType.GAUGE, } /** @@ -451,6 +478,12 @@ export interface RuntimeHelpers { /** Logs a message to the host environment. */ log(message: string): void + + /** + * Emits a user metric to the host. Payload is a protobuf-encoded + * `workflows.v2.WorkflowUserMetric`. Returns false if the host rejected it. + */ + emitMetric(payload: Uint8Array): boolean } function clearIgnoredFields(value: ProtoValue, descriptor: ConsensusDescriptor): void { diff --git a/packages/cre-sdk/src/sdk/runtime.ts b/packages/cre-sdk/src/sdk/runtime.ts index fd1c3be5..b5f1e353 100644 --- a/packages/cre-sdk/src/sdk/runtime.ts +++ b/packages/cre-sdk/src/sdk/runtime.ts @@ -1,11 +1,12 @@ import type { Message } from '@bufbuild/protobuf' import type { GenMessage } from '@bufbuild/protobuf/codegenv2' import type { ReportRequest, ReportRequestJson } from '@cre/generated/sdk/v1alpha/sdk_pb' +import type { MetricType } from '@cre/sdk/impl/runtime-impl' import type { Report } from '@cre/sdk/report' import type { ConsensusAggregation, PrimitiveTypes, UnwrapOptions } from '@cre/sdk/utils' import type { SecretsProvider } from '.' -export type { ReportRequest, ReportRequestJson } +export type { ReportRequest, ReportRequestJson, MetricType } export type CallCapabilityParams = { capabilityId: string @@ -30,6 +31,13 @@ export interface BaseRuntime { now(): Date log(message: string): void + + emitMetric( + name: string, + value: number, + type: MetricType, + labels?: Record, + ): boolean } /** diff --git a/packages/cre-sdk/src/sdk/testutils/test-runtime.ts b/packages/cre-sdk/src/sdk/testutils/test-runtime.ts index 94e94882..75db1b2b 100644 --- a/packages/cre-sdk/src/sdk/testutils/test-runtime.ts +++ b/packages/cre-sdk/src/sdk/testutils/test-runtime.ts @@ -36,6 +36,7 @@ import { } from '@cre/generated/sdk/v1alpha/sdk_pb' import type { Value as ProtoValue } from '@cre/generated/values/v1/values_pb' import { ValueSchema } from '@cre/generated/values/v1/values_pb' +import type { WorkflowUserMetric } from '@cre/generated/workflows/v2/workflow_user_metric_pb' import type { RuntimeHelpers } from '../impl/runtime-impl' import { RuntimeImpl } from '../impl/runtime-impl' import { TestWriter } from './test-writer' @@ -324,6 +325,11 @@ function createTestRuntimeHelpers( log(message: string): void { testWriter.log(message) }, + + emitMetric(payload: Uint8Array): boolean { + testWriter.emitMetric(payload) + return true + }, } } @@ -411,6 +417,10 @@ export class TestRuntime extends RuntimeImpl { return this.testWriter.getLogs() } + getMetrics(): WorkflowUserMetric[] { + return this.testWriter.getMetrics() + } + setTimeProvider(timeProvider: () => number): void { this.state.timeProvider = timeProvider } diff --git a/packages/cre-sdk/src/sdk/testutils/test-writer.ts b/packages/cre-sdk/src/sdk/testutils/test-writer.ts index 7c620b3c..2cc8068b 100644 --- a/packages/cre-sdk/src/sdk/testutils/test-writer.ts +++ b/packages/cre-sdk/src/sdk/testutils/test-writer.ts @@ -1,9 +1,16 @@ +import { fromBinary } from '@bufbuild/protobuf' +import { + type WorkflowUserMetric, + WorkflowUserMetricSchema, +} from '@cre/generated/workflows/v2/workflow_user_metric_pb' + /** * In-memory log sink for tests. Captures messages so tests can assert on log output. * Equivalent to Go's cre/testutils/test_writer.go. */ export class TestWriter { private logs: string[] = [] + private metrics: Uint8Array[] = [] /** Appends a message to the captured log buffer. */ log(message: string): void { @@ -15,8 +22,19 @@ export class TestWriter { return [...this.logs] } - /** Clears the captured log buffer. */ + /** Captures a serialized WorkflowUserMetric payload. */ + emitMetric(payload: Uint8Array): void { + this.metrics.push(payload) + } + + /** Returns captured metric payloads decoded as `WorkflowUserMetric` protos. */ + getMetrics(): WorkflowUserMetric[] { + return this.metrics.map((bytes) => fromBinary(WorkflowUserMetricSchema, bytes)) + } + + /** Clears captured logs and metrics. */ clear(): void { this.logs = [] + this.metrics = [] } } diff --git a/packages/cre-sdk/src/sdk/types/global.d.ts b/packages/cre-sdk/src/sdk/types/global.d.ts index 1acc7282..e592dc5a 100644 --- a/packages/cre-sdk/src/sdk/types/global.d.ts +++ b/packages/cre-sdk/src/sdk/types/global.d.ts @@ -45,6 +45,14 @@ declare global { */ function log(message: string): void + /** + * Emits a user metric to the host. The payload is a protobuf-encoded + * `workflows.v2.WorkflowUserMetric` message. + * @param payload - protobuf-encoded WorkflowUserMetric bytes + * @returns 0 on success, negative on error (rate-limited, oversized, invalid name, etc.) + */ + function emitMetric(payload: Uint8Array): number + /** * Sends a response back to the host * @param response - bytes response diff --git a/packages/cre-sdk/src/sdk/wasm/host-bindings.ts b/packages/cre-sdk/src/sdk/wasm/host-bindings.ts index eb431a24..a2c87d24 100644 --- a/packages/cre-sdk/src/sdk/wasm/host-bindings.ts +++ b/packages/cre-sdk/src/sdk/wasm/host-bindings.ts @@ -28,6 +28,10 @@ const globalHostBindingsSchema = z.object({ .returns(z.union([z.instanceof(Uint8Array), z.custom>()])), getWasiArgs: z.function().args().returns(z.string()), now: z.function().args().returns(z.number()), + emitMetric: z + .function() + .args(z.union([z.instanceof(Uint8Array), z.custom>()])) + .returns(z.number()), }) type GlobalHostBindingsMap = z.infer diff --git a/packages/cre-sdk/src/sdk/wasm/runner.test.ts b/packages/cre-sdk/src/sdk/wasm/runner.test.ts index 7359b2dc..1f936a32 100644 --- a/packages/cre-sdk/src/sdk/wasm/runner.test.ts +++ b/packages/cre-sdk/src/sdk/wasm/runner.test.ts @@ -104,6 +104,9 @@ const proxyHostBindings = { now: () => { throw new Error('now called unexpectedly in test') }, + emitMetric: (_payload: Uint8Array) => { + throw new Error('emitMetric called unexpectedly in test') + }, } Object.assign(globalThis, proxyHostBindings) diff --git a/packages/cre-sdk/src/sdk/wasm/runtime.ts b/packages/cre-sdk/src/sdk/wasm/runtime.ts index d42ebd3b..94b15e2d 100644 --- a/packages/cre-sdk/src/sdk/wasm/runtime.ts +++ b/packages/cre-sdk/src/sdk/wasm/runtime.ts @@ -91,4 +91,8 @@ class WasmRuntimeHelpers implements RuntimeHelpers { log(message: string): void { hostBindings.log(message) } + + emitMetric(payload: Uint8Array): boolean { + return hostBindings.emitMetric(payload) >= 0 + } }