diff --git a/docs/getting-started.md b/docs/getting-started.md
index f4e92f6fe..ad401894f 100644
--- a/docs/getting-started.md
+++ b/docs/getting-started.md
@@ -2227,11 +2227,14 @@ Dependency: `io.opentelemetry:opentelemetry-api`
| Option | Node.js | Python | Go | Rust | Java | .NET | Description |
|---|---|---|---|---|---|---|---|
| OTLP endpoint | `otlpEndpoint` | `otlp_endpoint` | `OTLPEndpoint` | `otlp_endpoint` | `otlpEndpoint` | `OtlpEndpoint` | OTLP HTTP endpoint URL |
+| OTLP protocol | `otlpProtocol` | `otlp_protocol` | `OTLPProtocol` | `otlp_protocol` | `otlpProtocol` | `OtlpProtocol` | OTLP HTTP protocol for all signals: `"http/json"` or `"http/protobuf"` |
| File path | `filePath` | `file_path` | `FilePath` | `file_path` | `filePath` | `FilePath` | File path for JSON-lines trace output |
| Exporter type | `exporterType` | `exporter_type` | `ExporterType` | `exporter_type` | `exporterType` | `ExporterType` | `"otlp-http"` or `"file"` |
| Source name | `sourceName` | `source_name` | `SourceName` | `source_name` | `sourceName` | `SourceName` | Instrumentation scope name |
| Capture content | `captureContent` | `capture_content` | `CaptureContent` | `capture_content` | `captureContent` | `CaptureContent` | Whether to capture message content |
+The OTLP protocol field configures the CLI's `"otlp-http"` exporter for all signals. Leave it unset to use the CLI default, or set it to `"http/protobuf"` to export protobuf over HTTP.
+
### File export
To write traces to a local file instead of an OTLP endpoint:
diff --git a/docs/observability/opentelemetry.md b/docs/observability/opentelemetry.md
index d89f68ca9..022a41c79 100644
--- a/docs/observability/opentelemetry.md
+++ b/docs/observability/opentelemetry.md
@@ -104,11 +104,14 @@ let client = Client::start(ClientOptions::new()
| Option | Node.js | Python | Go | .NET | Java | Rust | Description |
|---|---|---|---|---|---|---|---|
| OTLP endpoint | `otlpEndpoint` | `otlp_endpoint` | `OTLPEndpoint` | `OtlpEndpoint` | `otlpEndpoint` | `otlp_endpoint` | OTLP HTTP endpoint URL |
+| OTLP protocol | `otlpProtocol` | `otlp_protocol` | `OTLPProtocol` | `OtlpProtocol` | `otlpProtocol` | `otlp_protocol` | OTLP HTTP protocol for all signals: `"http/json"` or `"http/protobuf"` |
| File path | `filePath` | `file_path` | `FilePath` | `FilePath` | `filePath` | `file_path` | File path for JSON-lines trace output |
| Exporter type | `exporterType` | `exporter_type` | `ExporterType` | `ExporterType` | `exporterType` | `exporter_type` | `"otlp-http"` or `"file"` |
| Source name | `sourceName` | `source_name` | `SourceName` | `SourceName` | `sourceName` | `source_name` | Instrumentation scope name |
| Capture content | `captureContent` | `capture_content` | `CaptureContent` | `CaptureContent` | `captureContent` | `capture_content` | Whether to capture message content |
+The OTLP protocol field configures the CLI's `"otlp-http"` exporter for all signals. Leave it unset to use the CLI default, or set it to `"http/protobuf"` to export protobuf over HTTP.
+
### Trace context propagation
> **Most users don't need this.** The `TelemetryConfig` above is all you need to collect traces from the CLI. The trace context propagation described in this section is an **advanced feature** for applications that create their own OpenTelemetry spans and want them to appear in the **same distributed trace** as the CLI's spans.
diff --git a/dotnet/README.md b/dotnet/README.md
index c2829277d..b7a73e826 100644
--- a/dotnet/README.md
+++ b/dotnet/README.md
@@ -738,6 +738,7 @@ var client = new CopilotClient(new CopilotClientOptions
**TelemetryConfig properties:**
- `OtlpEndpoint` - OTLP HTTP endpoint URL
+- `OtlpProtocol` - OTLP HTTP protocol for all signals (`"http/json"` or `"http/protobuf"`)
- `FilePath` - File path for JSON-lines trace output
- `ExporterType` - `"otlp-http"` or `"file"`
- `SourceName` - Instrumentation scope name
diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs
index e98513c53..e83c71bc0 100644
--- a/dotnet/src/Client.cs
+++ b/dotnet/src/Client.cs
@@ -1805,6 +1805,7 @@ private static bool IsUnsupportedConnectMethod(RemoteRpcException ex)
{
startInfo.Environment["COPILOT_OTEL_ENABLED"] = "true";
if (telemetry.OtlpEndpoint is not null) startInfo.Environment["OTEL_EXPORTER_OTLP_ENDPOINT"] = telemetry.OtlpEndpoint;
+ if (telemetry.OtlpProtocol is not null) startInfo.Environment["OTEL_EXPORTER_OTLP_PROTOCOL"] = telemetry.OtlpProtocol;
if (telemetry.FilePath is not null) startInfo.Environment["COPILOT_OTEL_FILE_EXPORTER_PATH"] = telemetry.FilePath;
if (telemetry.ExporterType is not null) startInfo.Environment["COPILOT_OTEL_EXPORTER_TYPE"] = telemetry.ExporterType;
if (telemetry.SourceName is not null) startInfo.Environment["COPILOT_OTEL_SOURCE_NAME"] = telemetry.SourceName;
diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs
index 7a2ad2951..c6c30e3e8 100644
--- a/dotnet/src/Types.cs
+++ b/dotnet/src/Types.cs
@@ -413,6 +413,14 @@ public sealed class TelemetryConfig
///
public string? OtlpEndpoint { get; set; }
+ ///
+ /// OTLP HTTP protocol for all signals ("http/json" or "http/protobuf").
+ ///
+ ///
+ /// Maps to the OTEL_EXPORTER_OTLP_PROTOCOL environment variable.
+ ///
+ public string? OtlpProtocol { get; set; }
+
///
/// File path for the file exporter.
///
diff --git a/dotnet/test/E2E/ClientOptionsE2ETests.cs b/dotnet/test/E2E/ClientOptionsE2ETests.cs
index 6360cb55a..c2f16a042 100644
--- a/dotnet/test/E2E/ClientOptionsE2ETests.cs
+++ b/dotnet/test/E2E/ClientOptionsE2ETests.cs
@@ -78,6 +78,7 @@ public async Task Should_Propagate_Process_Options_To_Spawned_Cli()
Telemetry = new TelemetryConfig
{
OtlpEndpoint = "http://127.0.0.1:4318",
+ OtlpProtocol = "http/protobuf",
FilePath = telemetryPath,
ExporterType = "file",
SourceName = "dotnet-sdk-e2e",
@@ -104,6 +105,7 @@ public async Task Should_Propagate_Process_Options_To_Spawned_Cli()
Assert.Equal("process-option-token", capturedEnv.GetProperty("COPILOT_SDK_AUTH_TOKEN").GetString());
Assert.Equal("true", capturedEnv.GetProperty("COPILOT_OTEL_ENABLED").GetString());
Assert.Equal("http://127.0.0.1:4318", capturedEnv.GetProperty("OTEL_EXPORTER_OTLP_ENDPOINT").GetString());
+ Assert.Equal("http/protobuf", capturedEnv.GetProperty("OTEL_EXPORTER_OTLP_PROTOCOL").GetString());
Assert.Equal(telemetryPath, capturedEnv.GetProperty("COPILOT_OTEL_FILE_EXPORTER_PATH").GetString());
Assert.Equal("file", capturedEnv.GetProperty("COPILOT_OTEL_EXPORTER_TYPE").GetString());
Assert.Equal("dotnet-sdk-e2e", capturedEnv.GetProperty("COPILOT_OTEL_SOURCE_NAME").GetString());
@@ -642,6 +644,7 @@ function saveCapture() {
COPILOT_SDK_AUTH_TOKEN: process.env.COPILOT_SDK_AUTH_TOKEN,
COPILOT_OTEL_ENABLED: process.env.COPILOT_OTEL_ENABLED,
OTEL_EXPORTER_OTLP_ENDPOINT: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
+ OTEL_EXPORTER_OTLP_PROTOCOL: process.env.OTEL_EXPORTER_OTLP_PROTOCOL,
COPILOT_OTEL_FILE_EXPORTER_PATH: process.env.COPILOT_OTEL_FILE_EXPORTER_PATH,
COPILOT_OTEL_EXPORTER_TYPE: process.env.COPILOT_OTEL_EXPORTER_TYPE,
COPILOT_OTEL_SOURCE_NAME: process.env.COPILOT_OTEL_SOURCE_NAME,
diff --git a/dotnet/test/Unit/TelemetryTests.cs b/dotnet/test/Unit/TelemetryTests.cs
index 9229285b9..979e575b3 100644
--- a/dotnet/test/Unit/TelemetryTests.cs
+++ b/dotnet/test/Unit/TelemetryTests.cs
@@ -16,6 +16,7 @@ public void TelemetryConfig_DefaultValues_AreNull()
var config = new TelemetryConfig();
Assert.Null(config.OtlpEndpoint);
+ Assert.Null(config.OtlpProtocol);
Assert.Null(config.FilePath);
Assert.Null(config.ExporterType);
Assert.Null(config.SourceName);
@@ -28,6 +29,7 @@ public void TelemetryConfig_CanSetAllProperties()
var config = new TelemetryConfig
{
OtlpEndpoint = "http://localhost:4318",
+ OtlpProtocol = "http/protobuf",
FilePath = "/tmp/traces.json",
ExporterType = "otlp-http",
SourceName = "my-app",
@@ -35,6 +37,7 @@ public void TelemetryConfig_CanSetAllProperties()
};
Assert.Equal("http://localhost:4318", config.OtlpEndpoint);
+ Assert.Equal("http/protobuf", config.OtlpProtocol);
Assert.Equal("/tmp/traces.json", config.FilePath);
Assert.Equal("otlp-http", config.ExporterType);
Assert.Equal("my-app", config.SourceName);
diff --git a/go/README.md b/go/README.md
index 4bdbc75f5..f595eb769 100644
--- a/go/README.md
+++ b/go/README.md
@@ -573,6 +573,7 @@ client, err := copilot.NewClient(copilot.ClientOptions{
**TelemetryConfig fields:**
- `OTLPEndpoint` (string): OTLP HTTP endpoint URL
+- `OTLPProtocol` (string): OTLP HTTP protocol for all signals (`"http/json"` or `"http/protobuf"`)
- `FilePath` (string): File path for JSON-lines trace output
- `ExporterType` (string): `"otlp-http"` or `"file"`
- `SourceName` (string): Instrumentation scope name
diff --git a/go/client.go b/go/client.go
index cad460557..b73240b1b 100644
--- a/go/client.go
+++ b/go/client.go
@@ -1695,6 +1695,9 @@ func (c *Client) startCLIServer(ctx context.Context) error {
if t.OTLPEndpoint != "" {
c.process.Env = setEnvValue(c.process.Env, "OTEL_EXPORTER_OTLP_ENDPOINT", t.OTLPEndpoint)
}
+ if t.OTLPProtocol != "" {
+ c.process.Env = setEnvValue(c.process.Env, "OTEL_EXPORTER_OTLP_PROTOCOL", t.OTLPProtocol)
+ }
if t.FilePath != "" {
c.process.Env = setEnvValue(c.process.Env, "COPILOT_OTEL_FILE_EXPORTER_PATH", t.FilePath)
}
diff --git a/go/internal/e2e/client_options_e2e_test.go b/go/internal/e2e/client_options_e2e_test.go
index 4f8b74f2b..aa42be1f3 100644
--- a/go/internal/e2e/client_options_e2e_test.go
+++ b/go/internal/e2e/client_options_e2e_test.go
@@ -110,6 +110,7 @@ func TestClientOptionsE2E(t *testing.T) {
opts.SessionIdleTimeoutSeconds = 17
opts.Telemetry = &copilot.TelemetryConfig{
OTLPEndpoint: "http://127.0.0.1:4318",
+ OTLPProtocol: "http/protobuf",
FilePath: telemetryPath,
ExporterType: "file",
SourceName: "go-sdk-e2e",
@@ -147,6 +148,7 @@ func TestClientOptionsE2E(t *testing.T) {
"COPILOT_SDK_AUTH_TOKEN": "process-option-token",
"COPILOT_OTEL_ENABLED": "true",
"OTEL_EXPORTER_OTLP_ENDPOINT": "http://127.0.0.1:4318",
+ "OTEL_EXPORTER_OTLP_PROTOCOL": "http/protobuf",
"COPILOT_OTEL_FILE_EXPORTER_PATH": telemetryPath,
"COPILOT_OTEL_EXPORTER_TYPE": "file",
"COPILOT_OTEL_SOURCE_NAME": "go-sdk-e2e",
@@ -195,6 +197,7 @@ func TestClientOptionsE2E(t *testing.T) {
t.Errorf("Expected session.create.params.includeSubAgentStreamingEvents=false, got %v", params["includeSubAgentStreamingEvents"])
}
})
+
}
// ---------------------------------------------------------------------------
@@ -372,6 +375,7 @@ function saveCapture() {
COPILOT_SDK_AUTH_TOKEN: process.env.COPILOT_SDK_AUTH_TOKEN,
COPILOT_OTEL_ENABLED: process.env.COPILOT_OTEL_ENABLED,
OTEL_EXPORTER_OTLP_ENDPOINT: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
+ OTEL_EXPORTER_OTLP_PROTOCOL: process.env.OTEL_EXPORTER_OTLP_PROTOCOL,
COPILOT_OTEL_FILE_EXPORTER_PATH: process.env.COPILOT_OTEL_FILE_EXPORTER_PATH,
COPILOT_OTEL_EXPORTER_TYPE: process.env.COPILOT_OTEL_EXPORTER_TYPE,
COPILOT_OTEL_SOURCE_NAME: process.env.COPILOT_OTEL_SOURCE_NAME,
diff --git a/go/internal/e2e/telemetry_e2e_test.go b/go/internal/e2e/telemetry_e2e_test.go
index 071030281..67a4cf82e 100644
--- a/go/internal/e2e/telemetry_e2e_test.go
+++ b/go/internal/e2e/telemetry_e2e_test.go
@@ -307,6 +307,9 @@ func TestTelemetryConfigUnit(t *testing.T) {
if cfg.OTLPEndpoint != "" {
t.Errorf("Expected empty OTLPEndpoint, got %q", cfg.OTLPEndpoint)
}
+ if cfg.OTLPProtocol != "" {
+ t.Errorf("Expected empty OTLPProtocol, got %q", cfg.OTLPProtocol)
+ }
if cfg.FilePath != "" {
t.Errorf("Expected empty FilePath, got %q", cfg.FilePath)
}
@@ -325,6 +328,7 @@ func TestTelemetryConfigUnit(t *testing.T) {
// Mirrors: TelemetryConfig_CanSetAllProperties
cfg := copilot.TelemetryConfig{
OTLPEndpoint: "http://localhost:4318",
+ OTLPProtocol: "http/protobuf",
FilePath: "/tmp/traces.json",
ExporterType: "otlp-http",
SourceName: "my-app",
@@ -333,6 +337,9 @@ func TestTelemetryConfigUnit(t *testing.T) {
if cfg.OTLPEndpoint != "http://localhost:4318" {
t.Errorf("OTLPEndpoint mismatch: %q", cfg.OTLPEndpoint)
}
+ if cfg.OTLPProtocol != "http/protobuf" {
+ t.Errorf("OTLPProtocol mismatch: %q", cfg.OTLPProtocol)
+ }
if cfg.FilePath != "/tmp/traces.json" {
t.Errorf("FilePath mismatch: %q", cfg.FilePath)
}
diff --git a/go/types.go b/go/types.go
index 6245ce519..8225db0ef 100644
--- a/go/types.go
+++ b/go/types.go
@@ -159,6 +159,10 @@ type TelemetryConfig struct {
// Sets OTEL_EXPORTER_OTLP_ENDPOINT.
OTLPEndpoint string
+ // OTLPProtocol is the OTLP HTTP protocol for all signals.
+ // Sets OTEL_EXPORTER_OTLP_PROTOCOL.
+ OTLPProtocol string
+
// FilePath is the file path for JSON-lines trace output.
// Sets COPILOT_OTEL_FILE_EXPORTER_PATH.
FilePath string
diff --git a/java/src/main/java/com/github/copilot/CliServerManager.java b/java/src/main/java/com/github/copilot/CliServerManager.java
index a6a08a848..acc683a72 100644
--- a/java/src/main/java/com/github/copilot/CliServerManager.java
+++ b/java/src/main/java/com/github/copilot/CliServerManager.java
@@ -150,6 +150,9 @@ ProcessInfo startCliServer() throws IOException, InterruptedException {
if (telemetry.getOtlpEndpoint() != null) {
pb.environment().put("OTEL_EXPORTER_OTLP_ENDPOINT", telemetry.getOtlpEndpoint());
}
+ if (telemetry.getOtlpProtocol() != null) {
+ pb.environment().put("OTEL_EXPORTER_OTLP_PROTOCOL", telemetry.getOtlpProtocol());
+ }
if (telemetry.getFilePath() != null) {
pb.environment().put("COPILOT_OTEL_FILE_EXPORTER_PATH", telemetry.getFilePath());
}
diff --git a/java/src/main/java/com/github/copilot/rpc/TelemetryConfig.java b/java/src/main/java/com/github/copilot/rpc/TelemetryConfig.java
index c0b75f29d..593908ff8 100644
--- a/java/src/main/java/com/github/copilot/rpc/TelemetryConfig.java
+++ b/java/src/main/java/com/github/copilot/rpc/TelemetryConfig.java
@@ -30,6 +30,7 @@
public class TelemetryConfig {
private String otlpEndpoint;
+ private String otlpProtocol;
private String filePath;
private String exporterType;
private String sourceName;
@@ -58,6 +59,29 @@ public TelemetryConfig setOtlpEndpoint(String otlpEndpoint) {
return this;
}
+ /**
+ * Gets the OTLP HTTP protocol for all signals.
+ *
+ * Maps to the {@code OTEL_EXPORTER_OTLP_PROTOCOL} environment variable.
+ *
+ * @return the OTLP HTTP protocol, or {@code null}
+ */
+ public String getOtlpProtocol() {
+ return otlpProtocol;
+ }
+
+ /**
+ * Sets the OTLP HTTP protocol for all signals.
+ *
+ * @param otlpProtocol
+ * the protocol ({@code "http/json"} or {@code "http/protobuf"})
+ * @return this config for method chaining
+ */
+ public TelemetryConfig setOtlpProtocol(String otlpProtocol) {
+ this.otlpProtocol = otlpProtocol;
+ return this;
+ }
+
/**
* Gets the file path for the file exporter.
*
diff --git a/java/src/test/java/com/github/copilot/CliServerManagerTest.java b/java/src/test/java/com/github/copilot/CliServerManagerTest.java
index 2df5dafab..b445d6153 100644
--- a/java/src/test/java/com/github/copilot/CliServerManagerTest.java
+++ b/java/src/test/java/com/github/copilot/CliServerManagerTest.java
@@ -231,8 +231,9 @@ void startCliServerWithNullCliPath() throws Exception {
void startCliServerWithTelemetryAllOptions() throws Exception {
// The telemetry env vars are applied before ProcessBuilder.start()
// so even with a nonexistent CLI path, the telemetry code path is exercised
- var telemetry = new TelemetryConfig().setOtlpEndpoint("http://localhost:4318").setFilePath("/tmp/telemetry.log")
- .setExporterType("otlp-http").setSourceName("test-app").setCaptureContent(true);
+ var telemetry = new TelemetryConfig().setOtlpEndpoint("http://localhost:4318").setOtlpProtocol("http/protobuf")
+ .setFilePath("/tmp/telemetry.log").setExporterType("otlp-http").setSourceName("test-app")
+ .setCaptureContent(true);
var options = new CopilotClientOptions().setCliPath(NONEXISTENT_CLI).setTelemetry(telemetry).setUseStdio(true);
var manager = new CliServerManager(options);
diff --git a/java/src/test/java/com/github/copilot/TelemetryConfigTest.java b/java/src/test/java/com/github/copilot/TelemetryConfigTest.java
index 2dd41c28a..739d6a510 100644
--- a/java/src/test/java/com/github/copilot/TelemetryConfigTest.java
+++ b/java/src/test/java/com/github/copilot/TelemetryConfigTest.java
@@ -19,6 +19,7 @@ class TelemetryConfigTest {
void defaultValuesAreNull() {
var config = new TelemetryConfig();
assertNull(config.getOtlpEndpoint());
+ assertNull(config.getOtlpProtocol());
assertNull(config.getFilePath());
assertNull(config.getExporterType());
assertNull(config.getSourceName());
@@ -32,6 +33,13 @@ void otlpEndpointGetterSetter() {
assertEquals("http://localhost:4318", config.getOtlpEndpoint());
}
+ @Test
+ void otlpProtocolGetterSetter() {
+ var config = new TelemetryConfig();
+ config.setOtlpProtocol("http/protobuf");
+ assertEquals("http/protobuf", config.getOtlpProtocol());
+ }
+
@Test
void filePathGetterSetter() {
var config = new TelemetryConfig();
@@ -65,10 +73,12 @@ void captureContentGetterSetter() {
@Test
void fluentChainingReturnsThis() {
- var config = new TelemetryConfig().setOtlpEndpoint("http://localhost:4318").setFilePath("/tmp/spans.json")
- .setExporterType("file").setSourceName("sdk-test").setCaptureContent(true);
+ var config = new TelemetryConfig().setOtlpEndpoint("http://localhost:4318").setOtlpProtocol("http/protobuf")
+ .setFilePath("/tmp/spans.json").setExporterType("file").setSourceName("sdk-test")
+ .setCaptureContent(true);
assertEquals("http://localhost:4318", config.getOtlpEndpoint());
+ assertEquals("http/protobuf", config.getOtlpProtocol());
assertEquals("/tmp/spans.json", config.getFilePath());
assertEquals("file", config.getExporterType());
assertEquals("sdk-test", config.getSourceName());
diff --git a/nodejs/README.md b/nodejs/README.md
index 91aa5c4be..08a691a29 100644
--- a/nodejs/README.md
+++ b/nodejs/README.md
@@ -786,6 +786,7 @@ With just this configuration, the CLI emits spans for every session, message, an
**TelemetryConfig options:**
- `otlpEndpoint?: string` - OTLP HTTP endpoint URL
+- `otlpProtocol?: "http/json" | "http/protobuf"` - OTLP HTTP protocol for all signals
- `filePath?: string` - File path for JSON-lines trace output
- `exporterType?: string` - `"otlp-http"` or `"file"`
- `sourceName?: string` - Instrumentation scope name
diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts
index c59bd3e94..223e92468 100644
--- a/nodejs/src/client.ts
+++ b/nodejs/src/client.ts
@@ -1929,6 +1929,8 @@ export class CopilotClient {
envWithoutNodeDebug.COPILOT_OTEL_ENABLED = "true";
if (t.otlpEndpoint !== undefined)
envWithoutNodeDebug.OTEL_EXPORTER_OTLP_ENDPOINT = t.otlpEndpoint;
+ if (t.otlpProtocol !== undefined)
+ envWithoutNodeDebug.OTEL_EXPORTER_OTLP_PROTOCOL = t.otlpProtocol;
if (t.filePath !== undefined)
envWithoutNodeDebug.COPILOT_OTEL_FILE_EXPORTER_PATH = t.filePath;
if (t.exporterType !== undefined)
diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts
index 647ca79c1..4d36ca0a9 100644
--- a/nodejs/src/types.ts
+++ b/nodejs/src/types.ts
@@ -55,6 +55,8 @@ export type TraceContextProvider = () => TraceContext | Promise;
export interface TelemetryConfig {
/** OTLP HTTP endpoint URL for trace/metric export. Sets OTEL_EXPORTER_OTLP_ENDPOINT. */
otlpEndpoint?: string;
+ /** OTLP HTTP protocol for all signals. Sets OTEL_EXPORTER_OTLP_PROTOCOL. */
+ otlpProtocol?: "http/json" | "http/protobuf";
/** File path for JSON-lines trace output. Sets COPILOT_OTEL_FILE_EXPORTER_PATH. */
filePath?: string;
/** Exporter backend type: "otlp-http" or "file". Sets COPILOT_OTEL_EXPORTER_TYPE. */
diff --git a/nodejs/test/e2e/client_options.e2e.test.ts b/nodejs/test/e2e/client_options.e2e.test.ts
index dadce08e1..2cfc69456 100644
--- a/nodejs/test/e2e/client_options.e2e.test.ts
+++ b/nodejs/test/e2e/client_options.e2e.test.ts
@@ -29,6 +29,7 @@ function saveCapture() {
COPILOT_SDK_AUTH_TOKEN: process.env.COPILOT_SDK_AUTH_TOKEN,
COPILOT_OTEL_ENABLED: process.env.COPILOT_OTEL_ENABLED,
OTEL_EXPORTER_OTLP_ENDPOINT: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
+ OTEL_EXPORTER_OTLP_PROTOCOL: process.env.OTEL_EXPORTER_OTLP_PROTOCOL,
COPILOT_OTEL_FILE_EXPORTER_PATH: process.env.COPILOT_OTEL_FILE_EXPORTER_PATH,
COPILOT_OTEL_EXPORTER_TYPE: process.env.COPILOT_OTEL_EXPORTER_TYPE,
COPILOT_OTEL_SOURCE_NAME: process.env.COPILOT_OTEL_SOURCE_NAME,
@@ -247,6 +248,7 @@ describe("Client options", async () => {
sessionIdleTimeoutSeconds: 17,
telemetry: {
otlpEndpoint: "http://127.0.0.1:4318",
+ otlpProtocol: "http/protobuf",
filePath: telemetryPath,
exporterType: "file",
sourceName: "ts-sdk-e2e",
@@ -283,6 +285,7 @@ describe("Client options", async () => {
expect(capture.env.COPILOT_SDK_AUTH_TOKEN).toBe("process-option-token");
expect(capture.env.COPILOT_OTEL_ENABLED).toBe("true");
expect(capture.env.OTEL_EXPORTER_OTLP_ENDPOINT).toBe("http://127.0.0.1:4318");
+ expect(capture.env.OTEL_EXPORTER_OTLP_PROTOCOL).toBe("http/protobuf");
expect(capture.env.COPILOT_OTEL_FILE_EXPORTER_PATH).toBe(telemetryPath);
expect(capture.env.COPILOT_OTEL_EXPORTER_TYPE).toBe("file");
expect(capture.env.COPILOT_OTEL_SOURCE_NAME).toBe("ts-sdk-e2e");
diff --git a/nodejs/test/telemetry.test.ts b/nodejs/test/telemetry.test.ts
index 9ad97b63a..78d9654ed 100644
--- a/nodejs/test/telemetry.test.ts
+++ b/nodejs/test/telemetry.test.ts
@@ -64,6 +64,7 @@ describe("telemetry", () => {
it("sets correct env vars for full telemetry config", async () => {
const telemetry = {
otlpEndpoint: "http://localhost:4318",
+ otlpProtocol: "http/protobuf",
filePath: "/tmp/traces.jsonl",
exporterType: "otlp-http",
sourceName: "my-app",
@@ -76,6 +77,7 @@ describe("telemetry", () => {
const t = telemetry;
env.COPILOT_OTEL_ENABLED = "true";
if (t.otlpEndpoint !== undefined) env.OTEL_EXPORTER_OTLP_ENDPOINT = t.otlpEndpoint;
+ if (t.otlpProtocol !== undefined) env.OTEL_EXPORTER_OTLP_PROTOCOL = t.otlpProtocol;
if (t.filePath !== undefined) env.COPILOT_OTEL_FILE_EXPORTER_PATH = t.filePath;
if (t.exporterType !== undefined) env.COPILOT_OTEL_EXPORTER_TYPE = t.exporterType;
if (t.sourceName !== undefined) env.COPILOT_OTEL_SOURCE_NAME = t.sourceName;
@@ -88,6 +90,7 @@ describe("telemetry", () => {
expect(env).toEqual({
COPILOT_OTEL_ENABLED: "true",
OTEL_EXPORTER_OTLP_ENDPOINT: "http://localhost:4318",
+ OTEL_EXPORTER_OTLP_PROTOCOL: "http/protobuf",
COPILOT_OTEL_FILE_EXPORTER_PATH: "/tmp/traces.jsonl",
COPILOT_OTEL_EXPORTER_TYPE: "otlp-http",
COPILOT_OTEL_SOURCE_NAME: "my-app",
@@ -103,6 +106,7 @@ describe("telemetry", () => {
const t = telemetry as any;
env.COPILOT_OTEL_ENABLED = "true";
if (t.otlpEndpoint !== undefined) env.OTEL_EXPORTER_OTLP_ENDPOINT = t.otlpEndpoint;
+ if (t.otlpProtocol !== undefined) env.OTEL_EXPORTER_OTLP_PROTOCOL = t.otlpProtocol;
if (t.filePath !== undefined) env.COPILOT_OTEL_FILE_EXPORTER_PATH = t.filePath;
if (t.exporterType !== undefined) env.COPILOT_OTEL_EXPORTER_TYPE = t.exporterType;
if (t.sourceName !== undefined) env.COPILOT_OTEL_SOURCE_NAME = t.sourceName;
diff --git a/python/README.md b/python/README.md
index e77fcf16c..19373567c 100644
--- a/python/README.md
+++ b/python/README.md
@@ -557,6 +557,7 @@ client = CopilotClient(
**TelemetryConfig options:**
- `otlp_endpoint` (str): OTLP HTTP endpoint URL
+- `otlp_protocol` (str): OTLP HTTP protocol for all signals (`"http/json"` or `"http/protobuf"`)
- `file_path` (str): File path for JSON-lines trace output
- `exporter_type` (str): `"otlp-http"` or `"file"`
- `source_name` (str): Instrumentation scope name
diff --git a/python/copilot/client.py b/python/copilot/client.py
index 24eec9d72..7dadad6c8 100644
--- a/python/copilot/client.py
+++ b/python/copilot/client.py
@@ -182,6 +182,11 @@ class TelemetryConfig(TypedDict, total=False):
otlp_endpoint: str
"""OTLP HTTP endpoint URL for trace/metric export. Sets OTEL_EXPORTER_OTLP_ENDPOINT."""
+ otlp_protocol: Literal["http/json", "http/protobuf"]
+ """OTLP HTTP protocol for all signals.
+
+ Allowed values are "http/json" and "http/protobuf". Sets OTEL_EXPORTER_OTLP_PROTOCOL.
+ """
file_path: str
"""File path for JSON-lines trace output. Sets COPILOT_OTEL_FILE_EXPORTER_PATH."""
exporter_type: str
@@ -3225,6 +3230,8 @@ async def _start_cli_server(self) -> None:
env["COPILOT_OTEL_ENABLED"] = "true"
if "otlp_endpoint" in telemetry:
env["OTEL_EXPORTER_OTLP_ENDPOINT"] = telemetry["otlp_endpoint"]
+ if "otlp_protocol" in telemetry:
+ env["OTEL_EXPORTER_OTLP_PROTOCOL"] = telemetry["otlp_protocol"]
if "file_path" in telemetry:
env["COPILOT_OTEL_FILE_EXPORTER_PATH"] = telemetry["file_path"]
if "exporter_type" in telemetry:
diff --git a/python/e2e/test_client_options_e2e.py b/python/e2e/test_client_options_e2e.py
index f32d923a3..9a70c6cbf 100644
--- a/python/e2e/test_client_options_e2e.py
+++ b/python/e2e/test_client_options_e2e.py
@@ -92,6 +92,7 @@ def _get_available_port() -> int:
COPILOT_SDK_AUTH_TOKEN: process.env.COPILOT_SDK_AUTH_TOKEN,
COPILOT_OTEL_ENABLED: process.env.COPILOT_OTEL_ENABLED,
OTEL_EXPORTER_OTLP_ENDPOINT: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
+ OTEL_EXPORTER_OTLP_PROTOCOL: process.env.OTEL_EXPORTER_OTLP_PROTOCOL,
COPILOT_OTEL_FILE_EXPORTER_PATH: process.env.COPILOT_OTEL_FILE_EXPORTER_PATH,
COPILOT_OTEL_EXPORTER_TYPE: process.env.COPILOT_OTEL_EXPORTER_TYPE,
COPILOT_OTEL_SOURCE_NAME: process.env.COPILOT_OTEL_SOURCE_NAME,
@@ -218,6 +219,7 @@ async def test_should_propagate_process_options_to_spawned_cli(self, ctx: E2ETes
session_idle_timeout_seconds=17,
telemetry={
"otlp_endpoint": "http://127.0.0.1:4318",
+ "otlp_protocol": "http/protobuf",
"file_path": telemetry_path,
"exporter_type": "file",
"source_name": "python-sdk-e2e",
@@ -246,6 +248,7 @@ async def test_should_propagate_process_options_to_spawned_cli(self, ctx: E2ETes
assert env["COPILOT_SDK_AUTH_TOKEN"] == "process-option-token"
assert env["COPILOT_OTEL_ENABLED"] == "true"
assert env["OTEL_EXPORTER_OTLP_ENDPOINT"] == "http://127.0.0.1:4318"
+ assert env["OTEL_EXPORTER_OTLP_PROTOCOL"] == "http/protobuf"
assert env["COPILOT_OTEL_FILE_EXPORTER_PATH"] == telemetry_path
assert env["COPILOT_OTEL_EXPORTER_TYPE"] == "file"
assert env["COPILOT_OTEL_SOURCE_NAME"] == "python-sdk-e2e"
diff --git a/python/e2e/test_telemetry_e2e.py b/python/e2e/test_telemetry_e2e.py
index f18a9fb88..cc87cf321 100644
--- a/python/e2e/test_telemetry_e2e.py
+++ b/python/e2e/test_telemetry_e2e.py
@@ -186,6 +186,7 @@ async def test_default_values_are_unset(self):
# constructor leaves every field unset (equivalent to C#'s null defaults).
cfg: TelemetryConfig = TelemetryConfig()
assert cfg.get("otlp_endpoint") is None
+ assert cfg.get("otlp_protocol") is None
assert cfg.get("file_path") is None
assert cfg.get("exporter_type") is None
assert cfg.get("source_name") is None
@@ -194,12 +195,14 @@ async def test_default_values_are_unset(self):
async def test_can_set_all_properties(self):
cfg: TelemetryConfig = TelemetryConfig(
otlp_endpoint="http://localhost:4318",
+ otlp_protocol="http/protobuf",
file_path="/tmp/traces.json",
exporter_type="otlp-http",
source_name="my-app",
capture_content=True,
)
assert cfg["otlp_endpoint"] == "http://localhost:4318"
+ assert cfg["otlp_protocol"] == "http/protobuf"
assert cfg["file_path"] == "/tmp/traces.json"
assert cfg["exporter_type"] == "otlp-http"
assert cfg["source_name"] == "my-app"
diff --git a/python/test_telemetry.py b/python/test_telemetry.py
index 6481fd525..8a34f19b2 100644
--- a/python/test_telemetry.py
+++ b/python/test_telemetry.py
@@ -77,6 +77,7 @@ def test_telemetry_env_var_mapping(self):
"""TelemetryConfig fields map to expected environment variable names."""
config: TelemetryConfig = {
"otlp_endpoint": "http://localhost:4318",
+ "otlp_protocol": "http/protobuf",
"file_path": "/tmp/traces.jsonl",
"exporter_type": "file",
"source_name": "test-app",
@@ -87,6 +88,8 @@ def test_telemetry_env_var_mapping(self):
env["COPILOT_OTEL_ENABLED"] = "true"
if "otlp_endpoint" in config:
env["OTEL_EXPORTER_OTLP_ENDPOINT"] = config["otlp_endpoint"]
+ if "otlp_protocol" in config:
+ env["OTEL_EXPORTER_OTLP_PROTOCOL"] = config["otlp_protocol"]
if "file_path" in config:
env["COPILOT_OTEL_FILE_EXPORTER_PATH"] = config["file_path"]
if "exporter_type" in config:
@@ -100,6 +103,7 @@ def test_telemetry_env_var_mapping(self):
assert env["COPILOT_OTEL_ENABLED"] == "true"
assert env["OTEL_EXPORTER_OTLP_ENDPOINT"] == "http://localhost:4318"
+ assert env["OTEL_EXPORTER_OTLP_PROTOCOL"] == "http/protobuf"
assert env["COPILOT_OTEL_FILE_EXPORTER_PATH"] == "/tmp/traces.jsonl"
assert env["COPILOT_OTEL_EXPORTER_TYPE"] == "file"
assert env["COPILOT_OTEL_SOURCE_NAME"] == "test-app"
diff --git a/rust/README.md b/rust/README.md
index 0b5bec1cd..52ccad635 100644
--- a/rust/README.md
+++ b/rust/README.md
@@ -588,11 +588,12 @@ Provider types include `"openai"`, `"azure"`, and `"anthropic"`. Set `wire_api`
Forward OpenTelemetry signals from the spawned CLI process to your collector:
```rust,ignore
-use github_copilot_sdk::{ClientOptions, OtelExporterType, TelemetryConfig};
+use github_copilot_sdk::{ClientOptions, OtelExporterType, OtlpHttpProtocol, TelemetryConfig};
let mut telem = TelemetryConfig::default();
telem.exporter_type = Some(OtelExporterType::OtlpHttp);
telem.otlp_endpoint = Some("http://localhost:4318".to_string());
+telem.otlp_protocol = Some(OtlpHttpProtocol::HttpProtobuf);
telem.source_name = Some("my-app".to_string());
let mut opts = ClientOptions::default();
@@ -600,7 +601,7 @@ opts.telemetry = Some(telem);
let client = Client::start(opts).await?;
```
-The SDK injects the appropriate environment variables (`COPILOT_OTEL_EXPORTER_TYPE`, `OTEL_EXPORTER_OTLP_ENDPOINT`, ...) into the spawned CLI process. The SDK takes no OpenTelemetry dependency; the CLI itself owns the exporter pipeline. Caller-supplied `ClientOptions::env` entries override telemetry-injected values.
+The SDK injects the appropriate environment variables (`COPILOT_OTEL_EXPORTER_TYPE`, `OTEL_EXPORTER_OTLP_ENDPOINT`, `OTEL_EXPORTER_OTLP_PROTOCOL`, ...) into the spawned CLI process. The SDK takes no OpenTelemetry dependency; the CLI itself owns the exporter pipeline. Caller-supplied `ClientOptions::env` entries override telemetry-injected values.
### Progress Reporting (`send_and_wait`)
diff --git a/rust/src/lib.rs b/rust/src/lib.rs
index cab34b476..662218e46 100644
--- a/rust/src/lib.rs
+++ b/rust/src/lib.rs
@@ -402,6 +402,32 @@ impl OtelExporterType {
}
}
+/// OTLP HTTP protocol used by the CLI's OpenTelemetry OTLP exporter.
+///
+/// Maps to the standard `OTEL_EXPORTER_OTLP_PROTOCOL` environment variable on
+/// the spawned CLI process. Wire values are `"http/json"` and
+/// `"http/protobuf"`.
+#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
+#[non_exhaustive]
+pub enum OtlpHttpProtocol {
+ /// Export using OTLP/HTTP JSON.
+ #[serde(rename = "http/json")]
+ HttpJson,
+ /// Export using OTLP/HTTP protobuf.
+ #[serde(rename = "http/protobuf")]
+ HttpProtobuf,
+}
+
+impl OtlpHttpProtocol {
+ /// Environment-variable value (`"http/json"` or `"http/protobuf"`).
+ pub fn as_str(self) -> &'static str {
+ match self {
+ Self::HttpJson => "http/json",
+ Self::HttpProtobuf => "http/protobuf",
+ }
+ }
+}
+
/// OpenTelemetry configuration forwarded to the spawned GitHub Copilot CLI
/// process.
///
@@ -417,6 +443,7 @@ impl OtelExporterType {
/// |----------------------|-------------------------------------------------------|
/// | (any field set) | `COPILOT_OTEL_ENABLED=true` |
/// | [`otlp_endpoint`] | `OTEL_EXPORTER_OTLP_ENDPOINT` |
+/// | [`otlp_protocol`] | `OTEL_EXPORTER_OTLP_PROTOCOL` |
/// | [`file_path`] | `COPILOT_OTEL_FILE_EXPORTER_PATH` |
/// | [`exporter_type`] | `COPILOT_OTEL_EXPORTER_TYPE` |
/// | [`source_name`] | `COPILOT_OTEL_SOURCE_NAME` |
@@ -430,6 +457,7 @@ impl OtelExporterType {
/// added without breaking callers.
///
/// [`otlp_endpoint`]: Self::otlp_endpoint
+/// [`otlp_protocol`]: Self::otlp_protocol
/// [`file_path`]: Self::file_path
/// [`exporter_type`]: Self::exporter_type
/// [`source_name`]: Self::source_name
@@ -439,6 +467,8 @@ impl OtelExporterType {
pub struct TelemetryConfig {
/// OTLP HTTP endpoint URL for trace/metric export.
pub otlp_endpoint: Option,
+ /// OTLP HTTP protocol for all signals.
+ pub otlp_protocol: Option,
/// File path for JSON-lines trace output.
pub file_path: Option,
/// Exporter backend type. Typically [`OtelExporterType::OtlpHttp`] or
@@ -467,6 +497,12 @@ impl TelemetryConfig {
self
}
+ /// Set the OTLP HTTP protocol for all signals.
+ pub fn with_otlp_protocol(mut self, protocol: OtlpHttpProtocol) -> Self {
+ self.otlp_protocol = Some(protocol);
+ self
+ }
+
/// Set the file path for JSON-lines trace output.
pub fn with_file_path(mut self, path: impl Into) -> Self {
self.file_path = Some(path.into());
@@ -499,6 +535,7 @@ impl TelemetryConfig {
/// to decide whether to set `COPILOT_OTEL_ENABLED`.
pub fn is_empty(&self) -> bool {
self.otlp_endpoint.is_none()
+ && self.otlp_protocol.is_none()
&& self.file_path.is_none()
&& self.exporter_type.is_none()
&& self.source_name.is_none()
@@ -1209,6 +1246,9 @@ impl Client {
if let Some(endpoint) = &telemetry.otlp_endpoint {
command.env("OTEL_EXPORTER_OTLP_ENDPOINT", endpoint);
}
+ if let Some(protocol) = telemetry.otlp_protocol {
+ command.env("OTEL_EXPORTER_OTLP_PROTOCOL", protocol.as_str());
+ }
if let Some(path) = &telemetry.file_path {
command.env("COPILOT_OTEL_FILE_EXPORTER_PATH", path);
}
@@ -2115,12 +2155,14 @@ mod tests {
fn telemetry_config_builder_composes() {
let cfg = TelemetryConfig::new()
.with_otlp_endpoint("http://collector:4318")
+ .with_otlp_protocol(OtlpHttpProtocol::HttpProtobuf)
.with_file_path(PathBuf::from("/var/log/copilot.jsonl"))
.with_exporter_type(OtelExporterType::OtlpHttp)
.with_source_name("my-app")
.with_capture_content(true);
assert_eq!(cfg.otlp_endpoint.as_deref(), Some("http://collector:4318"));
+ assert_eq!(cfg.otlp_protocol, Some(OtlpHttpProtocol::HttpProtobuf));
assert_eq!(
cfg.file_path.as_deref(),
Some(Path::new("/var/log/copilot.jsonl")),
@@ -2132,11 +2174,28 @@ mod tests {
assert!(TelemetryConfig::new().is_empty());
}
+ #[test]
+ fn otlp_http_protocol_serde_matches_env_value() {
+ for (protocol, wire) in [
+ (OtlpHttpProtocol::HttpJson, "http/json"),
+ (OtlpHttpProtocol::HttpProtobuf, "http/protobuf"),
+ ] {
+ assert_eq!(protocol.as_str(), wire);
+
+ let serialized = serde_json::to_string(&protocol).unwrap();
+ assert_eq!(serialized, format!("\"{wire}\""));
+
+ let deserialized: OtlpHttpProtocol = serde_json::from_str(&serialized).unwrap();
+ assert_eq!(deserialized, protocol);
+ }
+ }
+
#[test]
fn build_command_sets_otel_env_when_telemetry_enabled() {
let opts = ClientOptions {
telemetry: Some(TelemetryConfig {
otlp_endpoint: Some("http://collector:4318".to_string()),
+ otlp_protocol: Some(OtlpHttpProtocol::HttpProtobuf),
file_path: Some(PathBuf::from("/var/log/copilot.jsonl")),
exporter_type: Some(OtelExporterType::OtlpHttp),
source_name: Some("my-app".to_string()),
@@ -2153,6 +2212,10 @@ mod tests {
env_value(&cmd, "OTEL_EXPORTER_OTLP_ENDPOINT"),
Some(std::ffi::OsStr::new("http://collector:4318")),
);
+ assert_eq!(
+ env_value(&cmd, "OTEL_EXPORTER_OTLP_PROTOCOL"),
+ Some(std::ffi::OsStr::new("http/protobuf")),
+ );
assert_eq!(
env_value(&cmd, "COPILOT_OTEL_FILE_EXPORTER_PATH"),
Some(std::ffi::OsStr::new("/var/log/copilot.jsonl")),
@@ -2178,6 +2241,7 @@ mod tests {
for key in [
"COPILOT_OTEL_ENABLED",
"OTEL_EXPORTER_OTLP_ENDPOINT",
+ "OTEL_EXPORTER_OTLP_PROTOCOL",
"COPILOT_OTEL_FILE_EXPORTER_PATH",
"COPILOT_OTEL_EXPORTER_TYPE",
"COPILOT_OTEL_SOURCE_NAME",
@@ -2211,6 +2275,7 @@ mod tests {
);
// None of the other fields should leak as env vars.
for key in [
+ "OTEL_EXPORTER_OTLP_PROTOCOL",
"COPILOT_OTEL_FILE_EXPORTER_PATH",
"COPILOT_OTEL_EXPORTER_TYPE",
"COPILOT_OTEL_SOURCE_NAME",