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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

> **Note for reviewers:** No version of this SDK has been published with metrics
> support. The `MetricsCollector` class and all metrics instrumentation exist only
> on development branches; no consumers are affected by the changes below.
> Metrics-related entries in this changelog describe changes to unreleased code.

### Added

- Canonical metrics aligned with the cross-SDK catalog -- see [docs/metrics.md](docs/metrics.md) for the full metric reference, configuration examples, and technical details

### Changed

- `Microsoft.Extensions.Logging` 6.0.0 → 10.0.0, `System.Diagnostics.DiagnosticSource` 8.0.1 → 10.0.0 -- these are transitive requirements of OpenTelemetry 1.15.x, which is now bundled for metrics support. The Prometheus HTTP listener exporter (`1.15.1-beta.1`) is a pre-release package because the [OTel Prometheus exporter specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk_exporters/prometheus.md) has never been finalized; no stable release exists or is expected ([tracking issue](https://github.com/open-telemetry/opentelemetry-dotnet/issues/2622)). This is the standard approach used across the .NET ecosystem.
- RestSharp `MaxTimeout` replaced with `Timeout` (TimeSpan) per deprecation warning

### Fixed

- `WorkflowTaskExecutor`: `OperationCanceledException` in the worker loop previously slept 10ms and re-entered `while(true)`, immediately re-throwing -- creating an infinite hot loop on shutdown. Now cleanly exits the loop.
- `WorkflowResourceApi.UpdateWorkflowVariables`: used C# string interpolation (`$"/workflow/{workflowId}/variables"`) instead of the path-template pattern used by every other API method, and was missing the `localVarPathParams.Add("workflowId", ...)` call. The HTTP request was functionally equivalent but the method is now consistent with the rest of the generated API surface.

### Removed

- Top-level `METRICS.md` replaced by `docs/metrics.md`
4 changes: 3 additions & 1 deletion Conductor/Api/WorkflowResourceApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,7 @@ public ApiResponse<Workflow> UpdateWorkflowVariablesWithHttpInfo(string workflow
if (variables == null)
throw new ApiException(400, "Missing required parameter 'variables' when calling WorkflowResourceApi->UpdateWorkflowVariables");

var localVarPath = $"/workflow/{workflowId}/variables";
var localVarPath = "/workflow/{workflowId}/variables";
var localVarPathParams = new Dictionary<String, String>();
var localVarQueryParams = new List<KeyValuePair<String, String>>();
var localVarHeaderParams = new Dictionary<String, String>(this.Configuration.DefaultHeader);
Expand All @@ -437,6 +437,8 @@ public ApiResponse<Workflow> UpdateWorkflowVariablesWithHttpInfo(string workflow
if (localVarHttpHeaderAccept != null)
localVarHeaderParams.Add("Accept", localVarHttpHeaderAccept);

if (workflowId != null) localVarPathParams.Add("workflowId", this.Configuration.ApiClient.ParameterToString(workflowId));

localVarPostBody = this.Configuration.ApiClient.Serialize(variables);

// authentication (api_key) required
Expand Down
81 changes: 62 additions & 19 deletions Conductor/Client/ApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
using Conductor.Client.Telemetry;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using RestSharp;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
Expand All @@ -29,6 +31,13 @@ namespace Conductor.Client
/// </summary>
public partial class ApiClient
{
/// <summary>
/// Optional metrics collector for recording http_api_client_request_seconds.
/// Assigned automatically when using DI via <c>AddConductorWorker()</c>;
/// set manually when constructing <see cref="ApiClient"/> outside DI.
/// </summary>
public MetricsCollector Metrics { get; set; }

public JsonSerializerSettings serializerSettings = new JsonSerializerSettings
{
ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor
Expand All @@ -54,7 +63,7 @@ public partial class ApiClient
public ApiClient(int timeOut)
{
Configuration = Conductor.Client.Configuration.Default;
RestClient = new RestClient(options: new RestClientOptions() { BaseUrl = new Uri("https://play.orkes.io/api"), MaxTimeout = timeOut });
RestClient = new RestClient(options: new RestClientOptions() { BaseUrl = new Uri("https://play.orkes.io/api"), Timeout = TimeSpan.FromMilliseconds(timeOut) });
}

/// <summary>
Expand Down Expand Up @@ -182,7 +191,6 @@ public Object CallApi(
int retryCount = 0;
RestResponse response = RetryRestClientCallApi(path, method, queryParams, postBody, headerParams,
formParams, fileParams, pathParams, contentType, configuration, ref retryCount);

return (Object)response;
}

Expand All @@ -191,17 +199,36 @@ private RestResponse RetryRestClientCallApi(String path, Method method, List<Key
Dictionary<String, FileParameter> fileParams, Dictionary<String, String> pathParams,
String contentType, Configuration configuration, ref int retryCount)
{
var methodStr = method.ToString().ToUpperInvariant();
var metricsUri = path;

RestResponse response = null;
while (retryCount < Constants.MAX_TOKEN_REFRESH_RETRY_COUNT)
{
var request = PrepareRequest(
path, method, queryParams, postBody, headerParams, formParams, fileParams,
pathParams, contentType);

InterceptRequest(request);
response = RestClient.Execute(request, method);
InterceptResponse(request, response);
FormatHeaders(response);
var sw = Stopwatch.StartNew();
string statusCode = "0";
try
{
var request = PrepareRequest(
path, method, queryParams, postBody, headerParams, formParams, fileParams,
pathParams, contentType);

InterceptRequest(request);
response = RestClient.Execute(request, method);
InterceptResponse(request, response);
FormatHeaders(response);
statusCode = ((int)response.StatusCode).ToString();
}
catch
{
statusCode = "0";
throw;
}
finally
{
sw.Stop();
Metrics?.RecordHttpApiClientRequest(methodStr, metricsUri, statusCode, sw.Elapsed.TotalSeconds);
}

if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
Expand All @@ -226,15 +253,31 @@ public async Task<object> CallApiAsync(
Dictionary<String, FileParameter> fileParams, Dictionary<String, String> pathParams,
String contentType)
{
var request = PrepareRequest(
path, method, queryParams, postBody, headerParams, formParams, fileParams,
pathParams, contentType);

InterceptRequest(request);
var response = await RestClient.ExecuteAsync(request, method);
InterceptResponse(request, response);
FormatHeaders(response);
return (object)response;
var sw = Stopwatch.StartNew();
string statusCode = "0";
try
{
var request = PrepareRequest(
path, method, queryParams, postBody, headerParams, formParams, fileParams,
pathParams, contentType);

InterceptRequest(request);
var response = await RestClient.ExecuteAsync(request, method);
InterceptResponse(request, response);
FormatHeaders(response);
statusCode = ((int)response.StatusCode).ToString();
return (object)response;
}
catch
{
statusCode = "0";
throw;
}
finally
{
sw.Stop();
Metrics?.RecordHttpApiClientRequest(method.ToString().ToUpperInvariant(), path, statusCode, sw.Elapsed.TotalSeconds);
}
}

/// <summary>
Expand Down
2 changes: 1 addition & 1 deletion Conductor/Client/Configuration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ public string BasePath
}
set
{
ApiClient.RestClient = new RestClient(new RestClientOptions() { BaseUrl = new Uri(value), MaxTimeout = Timeout });
ApiClient.RestClient = new RestClient(new RestClientOptions() { BaseUrl = new Uri(value), Timeout = TimeSpan.FromMilliseconds(Timeout) });
}
}

Expand Down
7 changes: 6 additions & 1 deletion Conductor/Client/Extensions/DependencyInjectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,12 @@ public static IServiceCollection AddConductorWorker(this IServiceCollection serv
}
services.AddSingleton<Configuration>(configuration);
services.AddSingleton<IWorkflowTaskClient, WorkflowTaskHttpClient>();
services.AddSingleton<MetricsCollector>();
services.AddSingleton<MetricsCollector>(sp =>
{
var collector = new MetricsCollector();
sp.GetRequiredService<Configuration>().ApiClient.Metrics = collector;
return collector;
});
services.AddTransient<IWorkflowTaskCoordinator, WorkflowTaskCoordinator>();
return services;
}
Expand Down
Loading
Loading