Author a connector
This guide walks through building, testing, and packaging a pz connector:
a source and/or sink implementation, plus the manifest handshake that lets pz reject an
incompatible package before running any of its code. Read it if you need pz to read from or
write to a system none of the fifteen builtin connectors cover.
Prerequisites
Section titled “Prerequisites”- The .NET 10 SDK.
- Familiarity with pz’s connector vocabulary: connection, entity, builtin versus third-party, native versus universal tier.
- Know which side you’re building. A source reads into the staging database; a sink writes out of it. A connector may implement one or both.
-
Set up the project. A connector is an ordinary .NET class library that references
Pz.Connectors.Abstractions:MyCompany.Pz.Connector.MySource.csproj <Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><TargetFramework>net10.0</TargetFramework></PropertyGroup><ItemGroup><PackageReference Include="Pz.Connectors.Abstractions" Version="x.y.z" /></ItemGroup></Project>Pin
x.y.zto the version published alongside thepzbuild you’re targeting: the manifest step below is what catches a mismatch before any of your code runs.Every connector implements the shared
IConnectorsurface bothISourceConnectorandISinkConnectorextend:Info(a name, version, and the protocol major it speaks),Capabilities, two JSON Schema strings (ConnectionConfigSchemaandDatasetConfigSchema) that let the CLI validate a project’s YAML against your connector offline,ValidateAsyncfor offline cross-field checks, andCheckConnectionAsyncfor a live connectivity probe. -
Implement a minimal source. Implement
ISourceConnectorand mark your entry point with the assembly attribute the host looks for:SourceConnector.cs [assembly: PzConnector("mysource", typeof(MySourceConnector))]public sealed class MySourceConnector : ISourceConnector{public ConnectorInfo Info => new("mysource", "1.0.0", ProtocolVersion.Major);public ConnectorCapabilities Capabilities => ConnectorCapabilities.None;public string ConnectionConfigSchema => "{}";public string DatasetConfigSchema => "{}";public ValueTask<ValidationResult> ValidateAsync(ConnectorConfig config, CancellationToken ct)=> ValueTask.FromResult(ValidationResult.Success);public ValueTask<ConnectionCheck> CheckConnectionAsync(ConnectorConfig config, CancellationToken ct)=> ValueTask.FromResult(new ConnectionCheck(true));public ValueTask<ISource> OpenAsync(ConnectorConfig config, CancellationToken ct)=> ValueTask.FromResult<ISource>(new Source());// Source implements ISource: GetSchemaAsync, TryGetNativeScan, PlanReadAsync.}ISource.PlanReadAsyncreturns one or moreIDatasetPartitions, each yielding ArrowRecordBatches fromReadAsync. The fullISource/IDatasetPartitionsurface is in Connector architecture. -
Implement a minimal sink, the same way, against
ISinkConnector:SinkConnector.cs [assembly: PzConnector("mysink", typeof(MySinkConnector))]public sealed class MySinkConnector : ISinkConnector{public ConnectorInfo Info => new("mysink", "1.0.0", ProtocolVersion.Major);public ConnectorCapabilities Capabilities => ConnectorCapabilities.None;public string ConnectionConfigSchema => "{}";public string DatasetConfigSchema => "{}";public ValueTask<ValidationResult> ValidateAsync(ConnectorConfig config, CancellationToken ct)=> ValueTask.FromResult(ValidationResult.Success);public ValueTask<ConnectionCheck> CheckConnectionAsync(ConnectorConfig config, CancellationToken ct)=> ValueTask.FromResult(new ConnectionCheck(true));public ValueTask<ISink> OpenAsync(ConnectorConfig config, CancellationToken ct)=> ValueTask.FromResult<ISink>(new Sink());// Sink implements ISink: TryGetNativeCopy, BeginWriteAsync.}ISink.BeginWriteAsyncreturns anISinkWriteSession, which receives batches throughWriteBatchAsyncand finishes with exactly one ofCommitAsyncorAbortAsync. -
Declare what your connector can do. The planner reads
Capabilities, not your code, to decide whether an entity’s declared options are legal before any node runs:public ConnectorCapabilities Capabilities =>ConnectorCapabilities.Merge | ConnectorCapabilities.ReplaceWrites | ConnectorCapabilities.Transactional;Declare only what’s true. A connector that claims
Mergewithout actually handlingmode: mergeinBeginWriteAsyncis a defect the conformance suite (next step) exists to catch. The full capability list and what each flag means for a run’s delivery guarantees is in Connector architecture. A source with no universal read path at all implements the marker interfaceINativeOnlySourceinstead of declaring a flag; the SDK derivesConnectorCapabilities.NativeOnlyReadfrom it for the manifest and the handshake, sopz connector testknows to skip the vectors that need a realPlanReadto succeed.A sink can also implement
IOutputConfigSchemato get itswrite:/sink()options validated atpz validatethe same wayDatasetConfigSchemaalready validates read options — a JSON Schema for the options yourBeginWriteAsync/write session actually reads, excluding the engine-owned keys (strategy,keys,duplicates,on_delete,schema_policy,retry), which never reach it. A sink that skips the interface keeps working exactly as before: its write options simply aren’t checked for typos ahead of a run. -
Write the connector manifest. A connector package ships a
pz.connector.jsonfile at its root, read before pz spawns or loads anything:pz.connector.json { "name": "mysource", "protocolMajorMin": 1, "protocolMajorMax": 1,"capabilities": ["source"], "runtime": "process","entrypoints": { "linux-x64": "native/pz-mysource" } }protocolMajorMin/protocolMajorMaxdeclare the inclusive range ofPz.Connectors.Abstractionsprotocol majors your connector supports.runtime: "process"plusentrypointsis what makes an external package loadable at all: an external connector with noruntime: "process"manifest, or none at all, is refused withPZ0360when pz builds its connector registry, before spawning anything. The C# SDK also writes ansdk: { "name": ..., "version": ... }block identifying itself; you never author this by hand (see C# SDK below) — it shows up inpz connectors’sdkcolumn and in a handshake failure’s message.Pack it at the root of the nupkg:
<ItemGroup><None Include="pz.connector.json" Pack="true" PackagePath="" CopyToOutputDirectory="PreserveNewest" /></ItemGroup>A manifest that’s present but malformed, such as
protocolMajorMingreater thanprotocolMajorMax, also fails withPZ0306: a broken manifest fails loud rather than silently falling back to the no-manifest path. An optionalprojectDirectoryAnchor: truekey asks pz to resolve the connector’s own relative paths, such as a local cache directory, against the project directory rather than leaving them unanchored. -
Run the conformance tests.
Pz.Connectors.TestKitships an acceptance suite that exercises yourISourceConnector/ISinkConnectorimplementation directly, in-process, as a fast unit test:Terminal window $ dotnet testOnce you have a spawnable, process-hosted binary,
pz connector testruns the same contract black-box, over the wire, against the real package:Terminal window $ pz connector test ./dist/my-connector --config probe.yml--confignames a YAML file with the connection to configure and theread:/write:entity to probe. This is the onepzverb with no--project: it targets a connector package directly, not a project.Two of its vectors are worth knowing about ahead of time.
numeric-option-fidelityproves a whole number survives the wire round-trip (protobuf’sStructhas only anumbertype, so an SDK must turn an integral double back into an integer on receipt); it needs no author effort on a current SDK, and reports Skip rather than Fail against an older one. The schema/batch, cancellation, and ticket-handling vectors report Skip, not Fail, against a source that declaresNativeOnlyRead(see step 4) instead of demanding aPlanReadresult such a source can never give. -
Package it as a NuGet package. A connector is an ordinary NuGet package. Tag it
pipelinez-connectorin<PackageTags>, the ecosystem’s discovery tag, and name it under your own prefix (MyCompany.Pz.Connector.MySourceis idiomatic); the barePz.*prefix is reserved for first-party packages.Terminal window $ dotnet pack -c Release -
Declare it in a project, and restore it. A project references your package the same way it references any non-builtin connector:
project.yml connectors:- package: MyCompany.Pz.Connector.MySourceversion: 1.0.0Terminal window $ pz restorepz restoreresolves the package against the host feeds (--feeds, elsePZ_FEEDS, else nuget.org), materializes it under.pz/packages, and writespz.lock.json.pz runandpz validateload from the lock file after that. Commitpz.lock.json; never commit.pz/.Confirm it registered, with its declared capabilities, before wiring up a real
connections.ymlentry against it:Terminal window $ pz connectors
C# SDK
Section titled “C# SDK”Pz.Connectors.Sdk serves any Pz.Connectors.Abstractions connector over PCP and packs it as a
pz-installable package. Write the connector exactly as steps 1–4 above describe, then:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <PackageId>Pz.Connector.MySystem</PackageId> <AssemblyName>pz-mysystem</AssemblyName> <!-- Restore reads this line, not the SDK's packaging files: it is what pulls the Native AOT compiler pack. PzPackaging=self-contained turns it back off. --> <PublishAot>true</PublishAot> </PropertyGroup> <ItemGroup> <PackageReference Include="Pz.Connectors.Sdk" Version="x.y.z" /> </ItemGroup></Project>using Pz.Connectors.Sdk;
return await PzConnectorHost.RunAsync(args, new MySystemConnector());// To log to the pz host: RunAsync(args, ctx => new MySystemConnector(ctx.LoggerFactory))Rules the SDK enforces so a connector cannot lie to the host:
- Declare only what you implement. Every optional RPC is answered from the interfaces your
objects implement: a source that is not
INaturalReadShapeSourceanswers UNIMPLEMENTED; a partition that is notISyncStatePartitionanswers FAILED_PRECONDITION. ASyncStatedeclaration nothing backs failspz connector test’ssync-state-roundtripvector. - Sync-state tokens are captured by the SDK the moment a partition’s enumeration completes, before end-of-stream is written. Set the candidate anywhere before your iterator returns.
- Argv is not configuration.
--pz-socket <path>serves;--pz-manifest --out <file> [--entrypoint <rid>=<path>]...writespz.connector.jsonfrom the connector object itself (the packaging targets run this for you). Anything else exits 2. Configuration only ever arrives through theConfigureRPC. - Logging goes to the host as connector log events, rendered verbatim: never log configuration values or payloads.
- Stdout is discarded; stderr is the diagnostic channel. The host drains and throws away
whatever your process writes to stdout — never write your own diagnostics there, since nothing
reads them and a connector that writes enough of it with no newline could otherwise block on a
full pipe. A bounded tail of stderr is folded into a failure’s message, so an unhandled
exception or a crash still surfaces something to
pz run’s output. Prefer logging through the SDK (above) over stderr where you can: it reaches the host as a structured, per-connection event rather than free text. - Reading a numeric option.
ConnectorConfig.GetIntand its siblings turn an integral double within ±2^53 back into an integer; a fractional or non-finite value is refused rather than silently rounded. protobuf’sStructtype has no integer of its own, so this is what makesmax_pages: 50000in YAML arrive as the integer your code expects rather than50000.0.
Telemetry
Section titled “Telemetry”An out-of-process connector exports its own traces and metrics to the same collector the engine uses,
in the same run trace. When pz run exports telemetry, the host hands its target and the run id to your
process at the handshake (HostInfo: otel_endpoint for gRPC, or otel_protocol, otel_traces_endpoint,
otel_metrics_endpoint and otel_headers_file for http/protobuf) and puts a W3C traceparent on every
RPC. Only then does the SDK:
- export OTLP with resource
service.name=pz-connector,service.version,pz.connector.name, andpz.run.id: over gRPC as before 0.9.1, or over HTTP to each signal’s own URL, gzipped, with delta metrics and exponential histograms, re-reading the headers file before every export. A connector built on an SDK older than 0.9.1 exports nothing underhttp/protobuf; - open a
pcp.<Rpc>server span per RPC — every RPC butHostChannel— under the engine’snode.<Kind>span, taggedpz.instance(the connection name for an open the engine drives, else<connector>#<n>), plus apcp.read_stream/pcp.write_streamspan around each data-plane transfer; - from SDK 0.10.0, give a failing
pcp.<Rpc>span an error status,rpc.grpc.status_codeanderror.type(the gRPC code name), never the error message; - flush on shutdown, bounded to three seconds.
Your own instrumentation joins that trace with no extra wiring — start spans from ctx.ActivitySource
and record on ctx.Meter:
return await PzConnectorHost.RunAsync(args, ctx => new MyConnector(ctx.ActivitySource, ctx.Meter));With no endpoint nothing is built and every span is a BCL no-op. Never put a configuration value in a
span name, a tag, or a metric label of your own — what you emit on ctx.ActivitySource and ctx.Meter
is exported as written.
Exporting a client library’s telemetry
Section titled “Exporting a client library’s telemetry”To also export a client library’s own ActivitySource/Meter (Npgsql, the AWS SDK), name them in
PzConnectorHostOptions.ActivitySources and Meters. Library telemetry fails closed:
- Only a fixed set of safe attribute keys is exported; every other tag is dropped. The set covers
db.system,db.system.name,db.operation.name,db.operation.batch.size,db.namespace,db.collection.name,db.response.status_code,server.address,server.port,network.peer.address,network.peer.port,network.transport,network.type,network.protocol.name,network.protocol.version,http.request.method,http.response.status_code,url.scheme,rpc.system,rpc.service,rpc.method,rpc.grpc.status_code,messaging.system,messaging.operation.name,messaging.operation.type,messaging.destination.name,messaging.batch.message_count,error.type,az.namespaceandotel.status_code. Statements, parameters, URLs, headers, paths, ids and exception text are not on it. - Hostnames and resource names (
server.address,db.namespace,db.collection.name) are exported deliberately; they are what makes the spans useful, so do not point a library at a name you consider secret. - A span is dropped, not exported, when it has any event not named
exception, an event attribute other thanexception.type, or a link that carries attributes. Events and links cannot be edited, and exception messages and event names can carry data values. A failed library call therefore loses its span; your connector’s ownpcp.*andremote.*spans keeperror.type. Children of a dropped span keep a parent id that points at it. - A span’s name is rebuilt as
<source> <operation>(the operation is the first ofdb.operation.name,http.request.method,rpc.methodandmessaging.operation.nameit recorded, at most 96 characters), and its status description is cleared. - A
Meternamed inMetersgets the same attribute allowlist; measurements that differ only by a dropped attribute are aggregated together. Instruments you create yourself onctx.Meterare not filtered, so their labels must never carry configuration values or data. - Only the exact
Pz.Connectorsource and meter are the SDK’s own; a name such asPz.Connector.Foois filtered like any library.
Libraries built on Azure.Core emit their ActivitySource only when the AppContext switch
Azure.Experimental.EnableActivitySource is set. Set it before creating any client, for example
AppContext.SetSwitch("Azure.Experimental.EnableActivitySource", true);, then list the source in
ActivitySources.
Reporting remote stats
Section titled “Reporting remote stats”Since pz 0.11.0 (SDK 0.11.0) a connector can tell pz what its remote system did for a node. The figures
land in remote on node_completed and in run_results.json
(Run events), and on the node’s span and the pz.remote.* metrics
(Observe runs). Declare
ConnectorCapabilities.ReportsStats, then report from inside a read or write call through
ConnectorStats.Current (a no-op outside an operation, so call it unconditionally):
var stats = ConnectorStats.Current;stats.AddRequests(1);stats.AddWireBytes(page.Length);stats.AddRemoteMs(elapsedMs);stats.AddThrottled(1); // the remote system said slow downstats.AddCost(charge, "ru"); // one billing unit per nodestats.AddExtra("cosmos.partitions_scanned", 7); // connector-specificReporting never fails a read or write: an invalid value is dropped, and pz prints one note: per run
naming the connector and the rule that fired, never the value. The rules:
- Every number is finite and at least 0.
- A cost unit is one of
ru,bytes_billed,bytes_scanned,credits,dbu,requests, or any token matching[a-z][a-z0-9_]{0,23}. A node keeps the first unit it sees and drops amounts in another; cost is never summed across units. - An extra is named
<connector>.<what>(lowercase, dotted,[a-z][a-z0-9_]*\.[a-z][a-z0-9_.]*, at most 40 characters). A node keeps at most 8 distinct extras; repeats of a name sum.
Stats are summed across a node’s attempts and partitions. A sink’s stats are read after its write session is disposed, so figures reported while disposing are included. A commit or abort that fails reports no stats for that call, and a partition read abandoned early reports none for that stream.
ctx.Remote does the reporting and the tracing together. ctx.Remote.Start("get_object") opens a
remote.get_object client span under the current span and counts one request when you dispose it; chain
WireBytes, RemoteMs, Throttled, Cost and Extra on the call. Dispose cannot see an exception,
so call call.Fail(ex) in the catch block first (the span records the exception type, never its
message):
using var call = ctx.Remote.Start("get_object");try{ var response = await client.GetAsync(key, ct); call.WireBytes(response.Length).Cost(response.Charge, "ru");}catch (Exception ex){ call.Fail(ex); throw;}The operation is a short fixed verb matching [a-z][a-z0-9_]{0,31} (anything else is recorded as
call). For an HttpClient, new HttpClient(ctx.Remote.HttpHandler()) wraps every request in a
remote.http span and counts it, adds Content-Length bytes it can see, and counts HTTP 429 and 503 as
throttled. It records the method, host, port and status code, and never the URL path or query,
headers, bodies or exception messages. The span ends when the response headers arrive, so it times the
wait for the response, not the body download, but the bytes of streamed (chunked) request and response
bodies are counted as they are sent and read. A synchronous HttpClient.Send is not counted. At most 200 remote.* spans open per operation; further
calls still count as requests, and the number skipped is on the node span as pz.remote.spans_dropped.
Spans from a client library you export through PzConnectorHostOptions.ActivitySources are filtered as
described under Exporting a client library’s telemetry; the
SDK’s own Pz.Connector spans are untouched. In a unit test you can build a RemoteCalls yourself: it
has a public constructor taking an ActivitySource.
Name your own instruments <connector>.<what> (cosmos.partitions_scanned), and keep labels to
bounded, non-secret values; a label never carries a configuration value, a path, SQL or an identifier
from the data.
To test it, Pz.Connectors.TestKit runs Reported_stats_are_valid (sources) and
Reported_write_stats_are_valid (sinks) for a connector that declares ReportsStats: they read or write
a small dataset with an accumulator in place and fail if any rule above fired. Without the capability
they skip. pz connector test adds a commit-stats-valid vector for the same connectors; it commits once
more to the probe target, so point write: at a throwaway one.
Packaging
Section titled “Packaging”Native AOT per platform is the default; <PzPackaging>self-contained</PzPackaging> opts a connector
whose dependencies cannot be AOT-compiled into a single-file CoreCLR publish instead. The host sees
no difference. <PublishAot>true</PublishAot> belongs in the project file itself (or on the command
line as -p:PublishAot=true): NuGet restore evaluates a project with every package’s build files
excluded, so the compiler pack Native AOT needs is only restored when the project asks for it, never
because the SDK’s targets derived it from PzPackaging. A publish that would silently fall back to a
CoreCLR layout fails with PZSDK005 instead. self-contained turns PublishAot off again, so the
line costs a connector that opts out nothing but a download; switching such a connector to Native
AOT from the command line takes both flags (-p:PzPackaging=aot -p:PublishAot=true). Native AOT cannot cross-compile between operating systems, so a release publishes
once per platform and packs once:
$ dotnet publish -c Release -r linux-x64 # on Linux$ dotnet publish -c Release -r osx-arm64 # on macOS$ dotnet publish -c Release -r win-x64 # on Windows$ dotnet pack -c Release -p:PzNativeStaging=./stage # once, from a directory holding every RIDpack collects each <staging>/<rid>/ as runtimes/<rid>/native/, runs the packer’s own binary
with --pz-manifest to write the manifest (one --entrypoint <rid>=<path> per staged RID), and
packs it at the nupkg root. Missing RIDs warn (PZSDK002); no RID at all is an error (PZSDK001);
the packing machine must have published its own RID (PZSDK003). Set PzRuntimeIdentifiers to the
RID set you ship.
A connector that resolves relative paths from its own configuration (a local file root, a
certificate path) sets <PzProjectDirectoryAnchor>true</PzProjectDirectoryAnchor>; the manifest
then carries projectDirectoryAnchor, and pz passes the project directory to the connector as
the base_dir connection option so those paths resolve against the project rather than the
process working directory.
A complete GitHub Actions release workflow:
name: releaseon: push: tags: ['v*']jobs: publish: strategy: matrix: include: - { os: ubuntu-latest, rid: linux-x64 } - { os: ubuntu-24.04-arm, rid: linux-arm64 } - { os: macos-latest, rid: osx-arm64 } - { os: windows-latest, rid: win-x64 } runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 with: { fetch-depth: 0 } - uses: actions/setup-dotnet@v4 with: { dotnet-version: '10.0.x' } - run: dotnet publish src/MyConnector -c Release -r ${{ matrix.rid }} -p:PzNativeStaging=${{ github.workspace }}/stage/ - uses: actions/upload-artifact@v4 with: name: pz-native-${{ matrix.rid }} path: stage/${{ matrix.rid }} pack: needs: publish runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: { fetch-depth: 0 } - uses: actions/setup-dotnet@v4 with: { dotnet-version: '10.0.x' } - uses: actions/download-artifact@v4 with: pattern: pz-native-* path: stage # download-artifact nests each artifact under its name; rename to the RID directories pack expects. - run: for d in stage/pz-native-*; do mv "$d" "stage/${d#stage/pz-native-}"; done - run: dotnet publish src/MyConnector -c Release -r linux-x64 -p:PzNativeStaging=${{ github.workspace }}/stage/ - run: dotnet pack src/MyConnector -c Release -p:PzNativeStaging=${{ github.workspace }}/stage/ -o packages - run: dotnet nuget push packages/*.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }}(The pack job republishes linux-x64 because the manifest is produced by running the packer’s own
RID; the artifact download covers the others.)
Verify with pz connector test <package-dir> --config probe.yml against the restored package.
Rust SDK
Section titled “Rust SDK”The pz-connector crate is an SDK for writing pz’s out-of-process (PCP) connectors in Rust. Its
serve_sink entry point parses a --pz-socket argument, serves the connector’s control-plane
gRPC service on that AF_UNIX socket, and serves the raw Arrow IPC data plane on a paired socket,
dispatching every call to a SinkConnector/Sink/WriteSession you implement. Add the crate as
a dependency and implement those three traits; the crate’s own examples/memory_sink.rs is a
complete, minimal sink.
A Rust connector builds and runs on Linux and Windows, and on macOS through the same unix code
path as Linux. Building the crate needs protoc on
PATH (or its path in the PROTOC environment variable), because it compiles the PCP .proto
contract at build time. On Windows, take protoc-<version>-win64.zip from the
protobuf releases.
Source support is deferred in this SDK. Its trait surface covers sinks only; the wire protocol already covers sources, but no Rust trait exists yet to implement one against.
A minimal Rust sink implements three traits: SinkConnector (validate, check, open, and
try_native_copy), Sink (begin_write), and WriteSession (write_batch and the commit/abort
pair). It is the same shape as the C# ISinkConnector/ISink/ISinkWriteSession triad above, just in
Rust. The crate still produces the same pz.connector.json package shape described in step 5:
runtime: "process" with a per-RID entrypoint pointing at the compiled binary.
Telemetry
Section titled “Telemetry”The Rust SDK exports the same way, with the same resource attributes, span names, and three-second
shutdown flush as the C# SDK, on the same instrumentation scope (Pz.Connector). Ordinary tracing
spans and events inside your handlers reach the collector; pz_connector::meter() returns the meter
to record instruments on. At the handshake the SDK installs its own tracing subscriber. A connector
that installs its own subscriber composes pz_connector::layer() into it before serve_sink, so its
spans still export:
use tracing_subscriber::layer::SubscriberExt;
tracing::subscriber::set_global_default( tracing_subscriber::registry() .with(tracing_subscriber::fmt::layer().with_writer(std::io::stderr)) .with(pz_connector::layer()),)?;Without that layer an author-installed subscriber wins and no span is exported (the handshake says so on stderr); meters are unaffected either way.
Reporting remote stats
Section titled “Reporting remote stats”Since pz 0.11.0 the Rust SDK reports sinks’ remote stats. pz_connector::stats() returns the handle for
the write session the calling task runs in (a no-op outside one), with the same methods and the same
validation rules as the C# ConnectorStats above, in snake case: add_requests, add_wire_bytes,
add_remote_ms, add_throttled, add_cost(units, "ru"), add_extra("vendor.metric", n).
pz_connector::remote::start opens a remote.<operation> span and counts a request when the call drops;
call fail(&err) first on an error (only the error’s type name is recorded):
let call = pz_connector::remote::start("put_object");call.wire_bytes(body.len() as i64);match client.put(&key, body).await { Ok(resp) => { call.cost(resp.charge, "ru"); } Err(e) => { call.fail(&e); return Err(e.into()); }}The handle is task-local, so it reaches only code awaited inside the SDK’s call; work moved to
tokio::spawn reports nowhere. Rust stats cover sinks only (there is no Rust source support), and the
Rust SDK does not redact foreign tracing spans.
Verify
Section titled “Verify”Two independent checks confirm your connector is ready to ship: dotnet test against the
Pz.Connectors.TestKit acceptance suite passes with no skipped mode-honesty facts for the
capabilities you declared (including ReportsStats, if you report remote stats), and, once packaged, pz connector test <path> --config probe.yml
exits 0 against your real binary.
Troubleshooting
Section titled “Troubleshooting”| If you see | Do |
|---|---|
PZ0360 when pz builds its connector registry |
Your package’s manifest declares no runtime: "process", or ships no manifest at all. An external connector must be process-hosted; add runtime/entrypoints to pz.connector.json. |
PZ0306 |
The manifest’s protocolMajorMin/protocolMajorMax range doesn’t include the host’s protocol major. Upgrade pz, or pin an older connector version. |
PZ0312 at plan time |
An entity needs the universal tier, but your connector’s TryGetNativeScan/TryGetNativeCopy are the only path it offers. Implement the universal path, or mark the connector native-only with INativeOnlySource/INativeOnlySink. |
PZ0304 |
project.yml declares the package under connectors:, but it isn’t in .pz/packages. Run pz restore. |
PZ0307 |
pz loaded your assembly but found no [assembly: PzConnector(...)] attribute. Check it’s present and names the right implementing type. |
PZSDK001/PZSDK003 at pack time |
No staged binary (or none for the packing machine’s RID). Run dotnet publish -r <rid> before dotnet pack. |
PZSDK005 at publish time |
PzPackaging is aot but the Native AOT compiler pack was never restored, so the publish would have staged a CoreCLR layout under a native binary’s name. Add <PublishAot>true</PublishAot> to the project file and restore again, or opt out with <PzPackaging>self-contained</PzPackaging>. |
A PzConnectorException.Message leaks a credential |
Messages are published verbatim to run_results.json and the event stream. Redact anything sensitive before throwing; see Connector architecture for the full redaction contract. |
| A capability you declared isn’t honored | The conformance suite’s mode-honesty facts exist to catch exactly this. Re-run dotnet test and check which fact failed. |
Related
Section titled “Related”- Connectors: what a connector is, builtin versus third-party, and the two data-movement tiers.
- Connector architecture: the full ABI surface, PCP protocol, and capability semantics.
- CLI reference: every
pz connector testandpz restoreflag. - project.yml reference: the
connectors:key in full. - connections.yml reference: the connection and entity shape your connector’s config schemas validate against.