A "no-code" (yet extensible) Azure API Performance benchmarking tool (for Developers) based on App-Insights (request-duration) - with multiple output-formats and customizations
💡 If App-Insights is configured, the durations are taken from Request-logs; else, from local timestamps
installation
# Install from nuget.org
dotnet tool install -g perfx --no-cache
# Upgrade to latest version from nuget.org
dotnet tool update -g perfx --no-cache
# Install a specific version from nuget.org
dotnet tool install -g perfx --version 1.0.x
# Uninstall
dotnet tool uninstall -g perfx
🔰 If you don't have .NET Core 3.1 installed, download and install it.
🔰 If the Tool is not accessible post installation, add
%USERPROFILE%\.dotnet\toolsto the PATH env-var.
configuration
Populate the following JSON and save it to your Documents/Perfx folder with the
name: Perfx.Settings.json (or something like MyApp1.Settings.json)
{
"Tenant": "YOUR_COMPANY_NAME",
"UserId": "",
"Password": "",
"ClientId": "",
"ClientSecret": "",
"ResourceUrl": "",
"ReplyUrl": "",
"ApiScopes": [
"api://YOUR-API-SCOPES"
],
"FormatArgs": {
"api": "https://YOUR-{env}-API.COM"
},
"Endpoints": [
"{api}/route1",
"{api}/route2"
],
"ResponseTimeSla": 5,
"ResponseSizeSla": 200,
"Iterations": 5,
"OutputFormats": [
"Excel",
"Sql::Conn-string..."
],
"AppInsightsAppId": "",
"AppInsightsApiKey": "",
"ReadResponseHeadersOnly": false,
"InputsFile": "Perfx_Inputs.xlsx",
"PluginClassName": null
}
details
| KEY | TYPE | VALUE | DEFAULT |
|---|---|---|---|
| QuiteMode | bool |
Just run (based on the information specified in the Settings file) and exit! Used in case of DevOps/CI pipelines |
false |
| Tenant | string |
Azure Tenant Name/ID Used in case of User-Creds / Client-Creds authentication |
|
| UserId | string |
Azure AD User-ID Used in case of User-Creds silent-authentication only |
|
| Password | string |
Azure AD Password Used in case of User-Creds silent-authentication only |
|
| ClientId | string |
Azure AD Application/Client ID Used in case of User-Creds / Client-Creds authentication |
|
| ClientSecret | string |
Azure AD Application/Client Secret Used in case of Client-Creds authentication only |
|
| ResourceUrl | string |
Azure AD Application Resource Url Used in case of User-Creds / Client-Creds authentication |
ClientId |
| ReplyUrl | string |
Azure AD Application Reply/Redirect Url Used in case of User-Creds interactive-authentication only |
|
| ApiScopes | string[] |
Azure AD API Scopes Used in case of User-Creds / Client-Creds authentication |
api://ClientId/.default |
| FormatArgs | string-object pairs |
Macros to be replaced in the Endpoints | |
| Endpoints | string[] |
URLs to test To override the default ResponseTimeSla/ResponseSizeSla prefix the endpoint with expected-duration-sla-in-seconds|expected-size-sla-in-kilo-bytes::e.g. 2.5::https://my-api/...(where 2.5 seconds is the expected SLA for the specific endpoint) e.g. 2.5|200::https://my-api/...(where 2.5 seconds is the expected duration SLA and 200 Kb is the expected size SLA for the specific endpoint) |
|
| AppInsightsAppId | string |
Azure Application Insights App-ID to fetch the request-duration-logs from | |
| AppInsightsApiKey | string |
Azure Application Insights API-Key to fetch the request-duration-logs from | |
| Iterations | int |
# of iterations to run for each Endpoint | 5 |
| OutputFormats | string[] |
The output formats in which the results would be saved Values: Excel / Csv / Json / Sql To provide the conn-strings or custom output-file-names, suffix it with ::conn-stringe.g. Sql::Data Source=(localdb)... or Excel::My_Run_Results.xlsx
|
|
| ResponseTimeSla | float |
The expected response-time SLA in seconds (generic for all Endpoints) | 5 |
| ResponseSizeSla | float |
The expected response-size SLA in Kb (generic for all Endpoints) | 200 |
| ReadResponseHeadersOnly | bool |
Specifies if the HttpClient should assume completion as soon as a response is available and headers are read (content is not read yet) | false |
| InputsFile | string |
File (under 'Documents/Perfx' folder) containing additional information for each of the Endpoints | Perfx_Inputs.xlsx |
| PluginClassName | string |
One of the classes that implements IPlugin interface (only used when there are multiple
implementations in the dlls placed under 'Documents/Perfx/Plugins' folder)
|
Order of Authentication:
- If
Tenantis null or empty>No Authentication- If
Passwordis provided>Silent User-credentials authentication flowAcquireTokenByUsernamePassword(scopes, username, password)- If
ClientSecretis provided>Client-credentials authentication flowAcquireTokenAsync(resource, clientCredential)- If
ReplyUrlis provided>Interactive User-credentials authentication flowAcquireTokenAsync(resource, clientId, redirectUri, parameters)
logging
You can (optionally) populate the following JSON and save it to your 'Documents/Perfx' folder with the
name: Perfx.Logging.json
{
"Logging": {
"LogLevel": {
"Default": "Warning"
},
"Console": {
"IncludeScopes": true,
"LogLevel": {
"Default": "Warning"
}
},
"Debug": {
"LogLevel": {
"Default": "Information"
}
}
}
}
"System.Net.Http.HttpClient": "Information"
inputs
Additional details (e.g. http-method (defaults to GET) / headers / body / query-params) for the Endpoints
defined the Perfx.Settings.json (default) under Documents/Perfx can be provided in Perfx_Inputs.xlsx
under the same folder (or using the Plugin model outlined below)
By default, if an entry/row for an Endpoint exists in Perfx_Inputs.xlsx, the corresponding row-index of that
entry is considered for the specific iteration. If the number of entries/rows for an Endpoint do not match up
with the number of iterations, the first entry is taken into consideration for the subsequent iterations
outputs
Results are saved to your Documents/Perfx with the name of the input settings JSON file (default is Perfx.Settings.json).
e.g. Perfx_Results.xlsx / Perfx_Results.csv & Perfx_Stats.csv / Perfx_Results.json & Perfx_Stats.json / [dbo.][Perfx_Results] & [dbo].[Perfx_Stats] Sql tables (depending on the specified OutputFormat in
the settings file
usage
perfx [MyApp1.Settings.json]
- If the specified path or the current-dir contains a valid Settings file, the tool uses/creates the
Perfxfolder under that path (for processing inputs/outputs); else, it falls back to thePerfxfolder under My Documents- If no argument is supplied,
Perfx.Settings.jsonis used as input by default- Set QuiteMode to
truein the Settings file to run once and exit (e.g. DevOps Pipelines)
- Enter
r:10to run the benchmarks (10 times) - Enter
sto print the stats/details for the previous run - Enter
l:1h:10to fetch app-insights request-duration logs for the previous run (in the last1 hourwith10 retries) - Enter
cto clear the console - Enter
qto quit - Enter
?to print this help
dotnet tool install -g perfx
Theexit-code / %errorlevel%of the app will be equal to the count of results that match the following criteria:
90th-percentile of requests>response-time-sla(OR)
max-response-size>response-size-sla(OR)
status-codes other than 200>1%
plugins
- Create a .NET Standard project and add reference to
Prefx.Coreproject - Add a class that implements
IPlugininterface (which in turn implementsIOutputinterface)
public Task IPlugin.GetAuthToken(Settings settings)
{
// NOTE: By default Perfx uses IPublicClientApplication's AcquireTokenSilent/AcquireTokenByUsernamePassword/AcquireTokenAsync (see 'Order of Authentication' note in the docs)
// If you want to override that behavior and provide a custom implementation, go ahead...
// If not, throw NotImplementedException or NotSupportedException, to trigger the default implementation
var userId = settings.UserId;
var pwd = settings.Password;
// Get more settings as required...
return Task.FromResult("someToken1");
}
public Task> IPlugin.GetEndpointDetails(Settings settings)
{
// NOTE: By default Perfx uses Documents/Perfx/Perfx_Inputs.xlsx
// If you want to override that behavior and provide a custom implementation, go ahead...
// If not, throw NotImplementedException or NotSupportedException, to trigger the default implementation
var endpointDetails = new List();
foreach (var endpoint in settings.Endpoints.Select((e, i) => (url: e, index: i)))
{
if (endpoint.url.Contains("odata"))
{
// Do whatever - based on the endpoint
endpointDetails.Add(new Endpoint { Method = HttpMethod.Get.ToString(), Query = "?$top=10" });
}
else if (endpoint.url.EndsWith("route1"))
{
// Do whatever - based on the endpoint
endpointDetails.Add(new Endpoint { Method = HttpMethod.Get.ToString(), Query = "/1" });
}
}
return Task.FromResult(endpointDetails);
}
public Task IOutput.Save(IEnumerable results, Settings settings)
{
// If you want to override that behavior and provide a custom implementation, go ahead...
// If not, throw NotImplementedException or NotSupportedException, to trigger the default implementation
// Do something and return true
return Task.FromResult(false);
}
public Task> IOutput.Read(Settings settings)
{
// If you want to override that behavior and provide a custom implementation, go ahead...
// If not, throw NotImplementedException or NotSupportedException, to trigger the default implementation
// Do and return something
return Task.FromResult(default(IList));
}
csproj file as follows:
...
<TargetFramework>netcoreapp3.1</TargetFramework>
<Platforms>x64</Platforms>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
...
<ProjectReference Include="Perfx.Core.csproj">
<Private>false</Private>
<ExcludeAssets>runtime</ExcludeAssets>
</ProjectReference>
...
Documents/Perfx/Plugins folder (Documents can be some other base-folder depending on your usage)IPlugin implementations, you can also update the value of PluginClassName with the specific implementation-class-full-name (e.g. MyPluginAssembly.MyPlugin1)Sample: Perfx.SamplePlugin