Added more security and metrics
This commit is contained in:
+21
-13
@@ -1,6 +1,6 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using EonaCat.Json;
|
||||||
|
|
||||||
namespace EonaCat.DoxaApi.Middleware
|
namespace EonaCat.DoxaApi.Middleware
|
||||||
{
|
{
|
||||||
@@ -32,12 +32,20 @@ namespace EonaCat.DoxaApi.Middleware
|
|||||||
"host", "content-length", "connection", "transfer-encoding"
|
"host", "content-length", "connection", "transfer-encoding"
|
||||||
};
|
};
|
||||||
|
|
||||||
public static async Task HandleAsync(HttpContext context, JsonSerializerOptions writeOptions)
|
private static readonly JsonSerializerSettings _settings = new()
|
||||||
|
{
|
||||||
|
NullValueHandling = NullValueHandling.Ignore,
|
||||||
|
Formatting = Formatting.Indented
|
||||||
|
};
|
||||||
|
|
||||||
|
public static async Task HandleAsync(HttpContext context, JsonSerializerSettings writeOptions)
|
||||||
{
|
{
|
||||||
ConsoleRequest? req;
|
ConsoleRequest? req;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
req = await JsonSerializer.DeserializeAsync<ConsoleRequest>(context.Request.Body, ReadOptions);
|
using var reader = new StreamReader(context.Request.Body);
|
||||||
|
var json = await reader.ReadToEndAsync();
|
||||||
|
req = JsonHelper.ToObject<ConsoleRequest>(json);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -116,35 +124,35 @@ namespace EonaCat.DoxaApi.Middleware
|
|||||||
};
|
};
|
||||||
|
|
||||||
context.Response.ContentType = "application/json; charset=utf-8";
|
context.Response.ContentType = "application/json; charset=utf-8";
|
||||||
await JsonSerializer.SerializeAsync(context.Response.Body, result, writeOptions);
|
var json = JsonHelper.ToJson(result, writeOptions);
|
||||||
|
await context.Response.WriteAsync(json, Encoding.UTF8);
|
||||||
}
|
}
|
||||||
catch (TaskCanceledException)
|
catch (TaskCanceledException)
|
||||||
{
|
{
|
||||||
context.Response.StatusCode = 504;
|
context.Response.StatusCode = 504;
|
||||||
await JsonSerializer.SerializeAsync(context.Response.Body, new ConsoleResponse
|
var errorResponse = new ConsoleResponse
|
||||||
{
|
{
|
||||||
StatusCode = 0,
|
StatusCode = 0,
|
||||||
ElapsedMs = sw.ElapsedMilliseconds,
|
ElapsedMs = sw.ElapsedMilliseconds,
|
||||||
NetworkError = true,
|
NetworkError = true,
|
||||||
Body = "Request timed out after 30 seconds."
|
Body = "Request timed out after 30 seconds."
|
||||||
}, writeOptions);
|
};
|
||||||
|
var json = JsonHelper.ToJson(errorResponse, writeOptions);
|
||||||
|
await context.Response.WriteAsync(json, Encoding.UTF8);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
context.Response.StatusCode = 502;
|
context.Response.StatusCode = 502;
|
||||||
await JsonSerializer.SerializeAsync(context.Response.Body, new ConsoleResponse
|
var errorResponse = new ConsoleResponse
|
||||||
{
|
{
|
||||||
StatusCode = 0,
|
StatusCode = 0,
|
||||||
ElapsedMs = sw.ElapsedMilliseconds,
|
ElapsedMs = sw.ElapsedMilliseconds,
|
||||||
NetworkError = true,
|
NetworkError = true,
|
||||||
Body = ex.Message
|
Body = ex.Message
|
||||||
}, writeOptions);
|
};
|
||||||
|
var json = JsonHelper.ToJson(errorResponse, writeOptions);
|
||||||
|
await context.Response.WriteAsync(json, Encoding.UTF8);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static readonly JsonSerializerOptions ReadOptions = new()
|
|
||||||
{
|
|
||||||
PropertyNameCaseInsensitive = true
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Text.Json.Serialization;
|
using EonaCat.Json;
|
||||||
|
|
||||||
namespace EonaCat.DoxaApi.Middleware
|
namespace EonaCat.DoxaApi.Middleware
|
||||||
{
|
{
|
||||||
@@ -7,16 +7,16 @@ namespace EonaCat.DoxaApi.Middleware
|
|||||||
|
|
||||||
internal sealed class ConsoleRequest
|
internal sealed class ConsoleRequest
|
||||||
{
|
{
|
||||||
[JsonPropertyName("method")]
|
[JsonProperty("method")]
|
||||||
public string Method { get; set; } = "GET";
|
public string Method { get; set; } = "GET";
|
||||||
|
|
||||||
[JsonPropertyName("url")]
|
[JsonProperty("url")]
|
||||||
public string Url { get; set; } = "";
|
public string Url { get; set; } = "";
|
||||||
|
|
||||||
[JsonPropertyName("headers")]
|
[JsonProperty("headers")]
|
||||||
public Dictionary<string, string>? Headers { get; set; }
|
public Dictionary<string, string>? Headers { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("body")]
|
[JsonProperty("body")]
|
||||||
public string? Body { get; set; }
|
public string? Body { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Text.Json.Serialization;
|
using EonaCat.Json;
|
||||||
|
|
||||||
namespace EonaCat.DoxaApi.Middleware
|
namespace EonaCat.DoxaApi.Middleware
|
||||||
{
|
{
|
||||||
@@ -7,19 +7,19 @@ namespace EonaCat.DoxaApi.Middleware
|
|||||||
|
|
||||||
internal sealed class ConsoleResponse
|
internal sealed class ConsoleResponse
|
||||||
{
|
{
|
||||||
[JsonPropertyName("statusCode")]
|
[JsonProperty("statusCode")]
|
||||||
public int StatusCode { get; set; }
|
public int StatusCode { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("elapsedMs")]
|
[JsonProperty("elapsedMs")]
|
||||||
public long ElapsedMs { get; set; }
|
public long ElapsedMs { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("headers")]
|
[JsonProperty("headers")]
|
||||||
public Dictionary<string, string> Headers { get; set; } = new();
|
public Dictionary<string, string> Headers { get; set; } = new();
|
||||||
|
|
||||||
[JsonPropertyName("body")]
|
[JsonProperty("body")]
|
||||||
public string Body { get; set; } = "";
|
public string Body { get; set; } = "";
|
||||||
|
|
||||||
[JsonPropertyName("networkError")]
|
[JsonProperty("networkError")]
|
||||||
public bool NetworkError { get; set; }
|
public bool NetworkError { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
<!-- NuGet package metadata -->
|
<!-- NuGet package metadata -->
|
||||||
<PackageId>EonaCat.DoxaApi</PackageId>
|
<PackageId>EonaCat.DoxaApi</PackageId>
|
||||||
<Version>0.0.5</Version>
|
<Version>0.0.6</Version>
|
||||||
<Authors>EonaCat (Jeroen Saey)</Authors>
|
<Authors>EonaCat (Jeroen Saey)</Authors>
|
||||||
<Description>A modern, self-contained, dependency-free API documentation UI for ASP.NET Core with a built-in auth manager, mock server, server-side request console, multi-language code generation, and spec diffing.</Description>
|
<Description>A modern, self-contained, dependency-free API documentation UI for ASP.NET Core with a built-in auth manager, mock server, server-side request console, multi-language code generation, and spec diffing.</Description>
|
||||||
<PackageTags>openapi;swagger;documentation;api;aspnetcore;doxa;docs;mock-server;oauth2;codegen;Jeroen;Saey;EonaCat;Scalar;Redoc;Postman;EchoAPI;</PackageTags>
|
<PackageTags>openapi;swagger;documentation;api;aspnetcore;doxa;docs;mock-server;oauth2;codegen;Jeroen;Saey;EonaCat;Scalar;Redoc;Postman;EchoAPI;</PackageTags>
|
||||||
@@ -58,4 +58,8 @@
|
|||||||
<PackagePath>\</PackagePath>
|
<PackagePath>\</PackagePath>
|
||||||
</None>
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="EonaCat.Json" Version="2.2.3" />
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
using System.Text.Json;
|
using EonaCat.Json.Linq;
|
||||||
using System.Text.Json.Nodes;
|
|
||||||
using EonaCat.DoxaApi.Models;
|
using EonaCat.DoxaApi.Models;
|
||||||
|
|
||||||
namespace EonaCat.DoxaApi.Interop
|
namespace EonaCat.DoxaApi.Interop
|
||||||
@@ -8,9 +7,9 @@ namespace EonaCat.DoxaApi.Interop
|
|||||||
// See the LICENSE file or go to https://EonaCat.com/license for full license details.
|
// See the LICENSE file or go to https://EonaCat.com/license for full license details.
|
||||||
public static class OpenApiExporter
|
public static class OpenApiExporter
|
||||||
{
|
{
|
||||||
public static JsonObject Export(ApiDocument doc)
|
public static JObject Export(ApiDocument doc)
|
||||||
{
|
{
|
||||||
var root = new JsonObject
|
var root = new JObject
|
||||||
{
|
{
|
||||||
["openapi"] = "3.0.3",
|
["openapi"] = "3.0.3",
|
||||||
["info"] = BuildInfo(doc.Info),
|
["info"] = BuildInfo(doc.Info),
|
||||||
@@ -18,16 +17,16 @@ namespace EonaCat.DoxaApi.Interop
|
|||||||
|
|
||||||
if (doc.Servers.Count > 0)
|
if (doc.Servers.Count > 0)
|
||||||
{
|
{
|
||||||
var servers = new JsonArray();
|
var servers = new JArray();
|
||||||
foreach (var s in doc.Servers)
|
foreach (var s in doc.Servers)
|
||||||
{
|
{
|
||||||
servers.Add(new JsonObject { ["url"] = s });
|
servers.Add(new JObject { ["url"] = s });
|
||||||
}
|
}
|
||||||
|
|
||||||
root["servers"] = servers;
|
root["servers"] = servers;
|
||||||
}
|
}
|
||||||
|
|
||||||
var paths = new JsonObject();
|
var paths = new JObject();
|
||||||
foreach (var group in doc.Groups)
|
foreach (var group in doc.Groups)
|
||||||
{
|
{
|
||||||
foreach (var endpoint in group.Endpoints)
|
foreach (var endpoint in group.Endpoints)
|
||||||
@@ -35,10 +34,10 @@ namespace EonaCat.DoxaApi.Interop
|
|||||||
var openApiPath = ToOpenApiPath(endpoint.Path);
|
var openApiPath = ToOpenApiPath(endpoint.Path);
|
||||||
if (!paths.ContainsKey(openApiPath))
|
if (!paths.ContainsKey(openApiPath))
|
||||||
{
|
{
|
||||||
paths[openApiPath] = new JsonObject();
|
paths[openApiPath] = new JObject();
|
||||||
}
|
}
|
||||||
|
|
||||||
var pathItem = (JsonObject)paths[openApiPath]!;
|
var pathItem = (JObject)paths[openApiPath]!;
|
||||||
pathItem[endpoint.Method.ToLowerInvariant()] = BuildOperation(endpoint, group.Name);
|
pathItem[endpoint.Method.ToLowerInvariant()] = BuildOperation(endpoint, group.Name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -46,39 +45,39 @@ namespace EonaCat.DoxaApi.Interop
|
|||||||
|
|
||||||
if (doc.Schemas.Count > 0)
|
if (doc.Schemas.Count > 0)
|
||||||
{
|
{
|
||||||
var schemas = new JsonObject();
|
var schemas = new JObject();
|
||||||
foreach (var (name, schema) in doc.Schemas)
|
foreach (var (name, schema) in doc.Schemas)
|
||||||
{
|
{
|
||||||
schemas[name] = SchemaToOpenApi(schema);
|
schemas[name] = SchemaToOpenApi(schema);
|
||||||
}
|
}
|
||||||
|
|
||||||
root["components"] = new JsonObject { ["schemas"] = schemas };
|
root["components"] = new JObject { ["schemas"] = schemas };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (doc.SecuritySchemes.Count > 0)
|
if (doc.SecuritySchemes.Count > 0)
|
||||||
{
|
{
|
||||||
var schemes = new JsonObject();
|
var schemes = new JObject();
|
||||||
foreach (var (id, scheme) in doc.SecuritySchemes)
|
foreach (var (id, scheme) in doc.SecuritySchemes)
|
||||||
{
|
{
|
||||||
schemes[id] = SecuritySchemeToOpenApi(scheme);
|
schemes[id] = SecuritySchemeToOpenApi(scheme);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (root["components"] is JsonObject componentsObj)
|
if (root["components"] is JObject componentsObj)
|
||||||
{
|
{
|
||||||
componentsObj["securitySchemes"] = schemes;
|
componentsObj["securitySchemes"] = schemes;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
root["components"] = new JsonObject { ["securitySchemes"] = schemes };
|
root["components"] = new JObject { ["securitySchemes"] = schemes };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static JsonObject BuildInfo(ApiInfo info)
|
private static JObject BuildInfo(ApiInfo info)
|
||||||
{
|
{
|
||||||
var obj = new JsonObject
|
var obj = new JObject
|
||||||
{
|
{
|
||||||
["title"] = info.Title,
|
["title"] = info.Title,
|
||||||
["version"] = info.Version
|
["version"] = info.Version
|
||||||
@@ -91,11 +90,11 @@ namespace EonaCat.DoxaApi.Interop
|
|||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static JsonObject BuildOperation(ApiEndpoint endpoint, string groupName)
|
private static JObject BuildOperation(ApiEndpoint endpoint, string groupName)
|
||||||
{
|
{
|
||||||
var op = new JsonObject();
|
var op = new JObject();
|
||||||
|
|
||||||
var tags = new JsonArray();
|
var tags = new JArray();
|
||||||
foreach (var t in (endpoint.Tags.Count > 0 ? endpoint.Tags : new List<string> { groupName }))
|
foreach (var t in (endpoint.Tags.Count > 0 ? endpoint.Tags : new List<string> { groupName }))
|
||||||
{
|
{
|
||||||
tags.Add(t);
|
tags.Add(t);
|
||||||
@@ -122,10 +121,10 @@ namespace EonaCat.DoxaApi.Interop
|
|||||||
|
|
||||||
if (endpoint.Parameters.Count > 0)
|
if (endpoint.Parameters.Count > 0)
|
||||||
{
|
{
|
||||||
var parameters = new JsonArray();
|
var parameters = new JArray();
|
||||||
foreach (var p in endpoint.Parameters)
|
foreach (var p in endpoint.Parameters)
|
||||||
{
|
{
|
||||||
var param = new JsonObject
|
var param = new JObject
|
||||||
{
|
{
|
||||||
["name"] = p.Name,
|
["name"] = p.Name,
|
||||||
["in"] = p.In,
|
["in"] = p.In,
|
||||||
@@ -145,33 +144,33 @@ namespace EonaCat.DoxaApi.Interop
|
|||||||
if (endpoint.RequestBody is not null)
|
if (endpoint.RequestBody is not null)
|
||||||
{
|
{
|
||||||
var rb = endpoint.RequestBody;
|
var rb = endpoint.RequestBody;
|
||||||
var content = new JsonObject
|
var content = new JObject
|
||||||
{
|
{
|
||||||
[rb.ContentType] = new JsonObject { ["schema"] = SchemaToOpenApi(rb.Schema) }
|
[rb.ContentType] = new JObject { ["schema"] = SchemaToOpenApi(rb.Schema) }
|
||||||
};
|
};
|
||||||
if (rb.Example is not null)
|
if (rb.Example is not null)
|
||||||
{
|
{
|
||||||
((JsonObject)content[rb.ContentType]!)["example"] =
|
((JObject)content[rb.ContentType]!)["example"] =
|
||||||
JsonNode.Parse(rb.Example) ?? JsonValue.Create(rb.Example)!;
|
JToken.Parse(rb.Example);
|
||||||
}
|
}
|
||||||
op["requestBody"] = new JsonObject
|
op["requestBody"] = new JObject
|
||||||
{
|
{
|
||||||
["required"] = rb.Required,
|
["required"] = rb.Required,
|
||||||
["content"] = content
|
["content"] = content
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
var responses = new JsonObject();
|
var responses = new JObject();
|
||||||
foreach (var r in endpoint.Responses)
|
foreach (var r in endpoint.Responses)
|
||||||
{
|
{
|
||||||
var resp = new JsonObject();
|
var resp = new JObject();
|
||||||
resp["description"] = r.Description ?? HttpStatusDescription(r.StatusCode);
|
resp["description"] = r.Description ?? HttpStatusDescription(r.StatusCode);
|
||||||
|
|
||||||
if (r.Schema is not null && r.Schema.Type != "void")
|
if (r.Schema is not null && r.Schema.Type != "void")
|
||||||
{
|
{
|
||||||
resp["content"] = new JsonObject
|
resp["content"] = new JObject
|
||||||
{
|
{
|
||||||
["application/json"] = new JsonObject
|
["application/json"] = new JObject
|
||||||
{
|
{
|
||||||
["schema"] = SchemaToOpenApi(r.Schema)
|
["schema"] = SchemaToOpenApi(r.Schema)
|
||||||
}
|
}
|
||||||
@@ -183,10 +182,10 @@ namespace EonaCat.DoxaApi.Interop
|
|||||||
|
|
||||||
if (endpoint.Security.Count > 0)
|
if (endpoint.Security.Count > 0)
|
||||||
{
|
{
|
||||||
var security = new JsonArray();
|
var security = new JArray();
|
||||||
foreach (var schemeId in endpoint.Security)
|
foreach (var schemeId in endpoint.Security)
|
||||||
{
|
{
|
||||||
security.Add(new JsonObject { [schemeId] = new JsonArray() });
|
security.Add(new JObject { [schemeId] = new JArray() });
|
||||||
}
|
}
|
||||||
op["security"] = security;
|
op["security"] = security;
|
||||||
}
|
}
|
||||||
@@ -194,9 +193,9 @@ namespace EonaCat.DoxaApi.Interop
|
|||||||
return op;
|
return op;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static JsonObject SecuritySchemeToOpenApi(SecuritySchemeModel scheme)
|
private static JObject SecuritySchemeToOpenApi(SecuritySchemeModel scheme)
|
||||||
{
|
{
|
||||||
var obj = new JsonObject { ["type"] = scheme.Type };
|
var obj = new JObject { ["type"] = scheme.Type };
|
||||||
|
|
||||||
if (scheme.Description is not null)
|
if (scheme.Description is not null)
|
||||||
{
|
{
|
||||||
@@ -219,7 +218,7 @@ namespace EonaCat.DoxaApi.Interop
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case "oauth2":
|
case "oauth2":
|
||||||
var flows = new JsonObject();
|
var flows = new JObject();
|
||||||
if (scheme.Flows?.ClientCredentials is OAuthFlowModel cc)
|
if (scheme.Flows?.ClientCredentials is OAuthFlowModel cc)
|
||||||
{
|
{
|
||||||
flows["clientCredentials"] = OAuthFlowToOpenApi(cc);
|
flows["clientCredentials"] = OAuthFlowToOpenApi(cc);
|
||||||
@@ -243,9 +242,9 @@ namespace EonaCat.DoxaApi.Interop
|
|||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static JsonObject OAuthFlowToOpenApi(OAuthFlowModel flow)
|
private static JObject OAuthFlowToOpenApi(OAuthFlowModel flow)
|
||||||
{
|
{
|
||||||
var obj = new JsonObject();
|
var obj = new JObject();
|
||||||
if (flow.AuthorizationUrl is not null)
|
if (flow.AuthorizationUrl is not null)
|
||||||
{
|
{
|
||||||
obj["authorizationUrl"] = flow.AuthorizationUrl;
|
obj["authorizationUrl"] = flow.AuthorizationUrl;
|
||||||
@@ -259,7 +258,7 @@ namespace EonaCat.DoxaApi.Interop
|
|||||||
obj["refreshUrl"] = flow.RefreshUrl;
|
obj["refreshUrl"] = flow.RefreshUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
var scopes = new JsonObject();
|
var scopes = new JObject();
|
||||||
foreach (var (k, v) in flow.Scopes)
|
foreach (var (k, v) in flow.Scopes)
|
||||||
{
|
{
|
||||||
scopes[k] = v;
|
scopes[k] = v;
|
||||||
@@ -269,26 +268,26 @@ namespace EonaCat.DoxaApi.Interop
|
|||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static JsonObject SchemaToOpenApi(SchemaModel schema)
|
private static JObject SchemaToOpenApi(SchemaModel schema)
|
||||||
{
|
{
|
||||||
|
|
||||||
if (schema.RefName is not null)
|
if (schema.RefName is not null)
|
||||||
{
|
{
|
||||||
return new JsonObject { ["$ref"] = $"#/components/schemas/{schema.RefName}" };
|
return new JObject { ["$ref"] = $"#/components/schemas/{schema.RefName}" };
|
||||||
}
|
}
|
||||||
|
|
||||||
var obj = new JsonObject();
|
var obj = new JObject();
|
||||||
|
|
||||||
switch (schema.Type)
|
switch (schema.Type)
|
||||||
{
|
{
|
||||||
case "void":
|
case "void":
|
||||||
return new JsonObject { ["type"] = "object" };
|
return new JObject { ["type"] = "object" };
|
||||||
|
|
||||||
case "enum":
|
case "enum":
|
||||||
obj["type"] = "string";
|
obj["type"] = "string";
|
||||||
if (schema.EnumValues?.Count > 0)
|
if (schema.EnumValues?.Count > 0)
|
||||||
{
|
{
|
||||||
var enums = new JsonArray();
|
var enums = new JArray();
|
||||||
foreach (var v in schema.EnumValues)
|
foreach (var v in schema.EnumValues)
|
||||||
{
|
{
|
||||||
enums.Add(v);
|
enums.Add(v);
|
||||||
@@ -311,7 +310,7 @@ namespace EonaCat.DoxaApi.Interop
|
|||||||
obj["type"] = "object";
|
obj["type"] = "object";
|
||||||
if (schema.Properties?.Count > 0)
|
if (schema.Properties?.Count > 0)
|
||||||
{
|
{
|
||||||
var props = new JsonObject();
|
var props = new JObject();
|
||||||
foreach (var (name, propSchema) in schema.Properties)
|
foreach (var (name, propSchema) in schema.Properties)
|
||||||
{
|
{
|
||||||
props[name] = SchemaToOpenApi(propSchema);
|
props[name] = SchemaToOpenApi(propSchema);
|
||||||
@@ -321,7 +320,7 @@ namespace EonaCat.DoxaApi.Interop
|
|||||||
}
|
}
|
||||||
if (schema.Required?.Count > 0)
|
if (schema.Required?.Count > 0)
|
||||||
{
|
{
|
||||||
var req = new JsonArray();
|
var req = new JArray();
|
||||||
foreach (var r in schema.Required)
|
foreach (var r in schema.Required)
|
||||||
{
|
{
|
||||||
req.Add(r);
|
req.Add(r);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Text.Json.Nodes;
|
|
||||||
|
using EonaCat.Json.Linq;
|
||||||
using EonaCat.DoxaApi.Models;
|
using EonaCat.DoxaApi.Models;
|
||||||
|
|
||||||
namespace EonaCat.DoxaApi.Interop
|
namespace EonaCat.DoxaApi.Interop
|
||||||
@@ -8,9 +9,9 @@ namespace EonaCat.DoxaApi.Interop
|
|||||||
|
|
||||||
public static class SwaggerExporter
|
public static class SwaggerExporter
|
||||||
{
|
{
|
||||||
public static JsonObject Export(ApiDocument doc)
|
public static JObject Export(ApiDocument doc)
|
||||||
{
|
{
|
||||||
var root = new JsonObject
|
var root = new JObject
|
||||||
{
|
{
|
||||||
["swagger"] = "2.0",
|
["swagger"] = "2.0",
|
||||||
["info"] = BuildInfo(doc.Info),
|
["info"] = BuildInfo(doc.Info),
|
||||||
@@ -20,7 +21,7 @@ namespace EonaCat.DoxaApi.Interop
|
|||||||
{
|
{
|
||||||
root["host"] = uri.Host + (uri.IsDefaultPort ? "" : $":{uri.Port}");
|
root["host"] = uri.Host + (uri.IsDefaultPort ? "" : $":{uri.Port}");
|
||||||
root["basePath"] = string.IsNullOrEmpty(uri.AbsolutePath) ? "/" : uri.AbsolutePath;
|
root["basePath"] = string.IsNullOrEmpty(uri.AbsolutePath) ? "/" : uri.AbsolutePath;
|
||||||
var schemes = new JsonArray();
|
var schemes = new JArray();
|
||||||
schemes.Add(uri.Scheme);
|
schemes.Add(uri.Scheme);
|
||||||
root["schemes"] = schemes;
|
root["schemes"] = schemes;
|
||||||
}
|
}
|
||||||
@@ -29,10 +30,10 @@ namespace EonaCat.DoxaApi.Interop
|
|||||||
root["basePath"] = "/";
|
root["basePath"] = "/";
|
||||||
}
|
}
|
||||||
|
|
||||||
root["consumes"] = new JsonArray { "application/json" };
|
root["consumes"] = new JArray { "application/json" };
|
||||||
root["produces"] = new JsonArray { "application/json" };
|
root["produces"] = new JArray { "application/json" };
|
||||||
|
|
||||||
var paths = new JsonObject();
|
var paths = new JObject();
|
||||||
foreach (var group in doc.Groups)
|
foreach (var group in doc.Groups)
|
||||||
{
|
{
|
||||||
foreach (var endpoint in group.Endpoints)
|
foreach (var endpoint in group.Endpoints)
|
||||||
@@ -40,10 +41,10 @@ namespace EonaCat.DoxaApi.Interop
|
|||||||
var swaggerPath = ToSwaggerPath(endpoint.Path);
|
var swaggerPath = ToSwaggerPath(endpoint.Path);
|
||||||
if (!paths.ContainsKey(swaggerPath))
|
if (!paths.ContainsKey(swaggerPath))
|
||||||
{
|
{
|
||||||
paths[swaggerPath] = new JsonObject();
|
paths[swaggerPath] = new JObject();
|
||||||
}
|
}
|
||||||
|
|
||||||
var pathItem = (JsonObject)paths[swaggerPath]!;
|
var pathItem = (JObject)paths[swaggerPath]!;
|
||||||
pathItem[endpoint.Method.ToLowerInvariant()] = BuildOperation(endpoint, group.Name);
|
pathItem[endpoint.Method.ToLowerInvariant()] = BuildOperation(endpoint, group.Name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,7 +52,7 @@ namespace EonaCat.DoxaApi.Interop
|
|||||||
|
|
||||||
if (doc.Schemas.Count > 0)
|
if (doc.Schemas.Count > 0)
|
||||||
{
|
{
|
||||||
var definitions = new JsonObject();
|
var definitions = new JObject();
|
||||||
foreach (var (name, schema) in doc.Schemas)
|
foreach (var (name, schema) in doc.Schemas)
|
||||||
{
|
{
|
||||||
definitions[name] = SchemaToSwagger(schema);
|
definitions[name] = SchemaToSwagger(schema);
|
||||||
@@ -62,7 +63,7 @@ namespace EonaCat.DoxaApi.Interop
|
|||||||
|
|
||||||
if (doc.SecuritySchemes.Count > 0)
|
if (doc.SecuritySchemes.Count > 0)
|
||||||
{
|
{
|
||||||
var defs = new JsonObject();
|
var defs = new JObject();
|
||||||
foreach (var (id, scheme) in doc.SecuritySchemes)
|
foreach (var (id, scheme) in doc.SecuritySchemes)
|
||||||
{
|
{
|
||||||
defs[id] = SecuritySchemeToSwagger(scheme);
|
defs[id] = SecuritySchemeToSwagger(scheme);
|
||||||
@@ -74,9 +75,9 @@ namespace EonaCat.DoxaApi.Interop
|
|||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static JsonObject BuildInfo(ApiInfo info)
|
private static JObject BuildInfo(ApiInfo info)
|
||||||
{
|
{
|
||||||
var obj = new JsonObject
|
var obj = new JObject
|
||||||
{
|
{
|
||||||
["title"] = info.Title,
|
["title"] = info.Title,
|
||||||
["version"] = info.Version
|
["version"] = info.Version
|
||||||
@@ -89,11 +90,11 @@ namespace EonaCat.DoxaApi.Interop
|
|||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static JsonObject BuildOperation(ApiEndpoint endpoint, string groupName)
|
private static JObject BuildOperation(ApiEndpoint endpoint, string groupName)
|
||||||
{
|
{
|
||||||
var op = new JsonObject();
|
var op = new JObject();
|
||||||
|
|
||||||
var tags = new JsonArray();
|
var tags = new JArray();
|
||||||
foreach (var t in (endpoint.Tags.Count > 0 ? endpoint.Tags : new List<string> { groupName }))
|
foreach (var t in (endpoint.Tags.Count > 0 ? endpoint.Tags : new List<string> { groupName }))
|
||||||
{
|
{
|
||||||
tags.Add(t);
|
tags.Add(t);
|
||||||
@@ -118,11 +119,11 @@ namespace EonaCat.DoxaApi.Interop
|
|||||||
op["deprecated"] = true;
|
op["deprecated"] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
var parameters = new JsonArray();
|
var parameters = new JArray();
|
||||||
|
|
||||||
foreach (var p in endpoint.Parameters)
|
foreach (var p in endpoint.Parameters)
|
||||||
{
|
{
|
||||||
var param = new JsonObject
|
var param = new JObject
|
||||||
{
|
{
|
||||||
["name"] = p.Name,
|
["name"] = p.Name,
|
||||||
["in"] = p.In,
|
["in"] = p.In,
|
||||||
@@ -140,7 +141,7 @@ namespace EonaCat.DoxaApi.Interop
|
|||||||
if (endpoint.RequestBody is not null)
|
if (endpoint.RequestBody is not null)
|
||||||
{
|
{
|
||||||
var rb = endpoint.RequestBody;
|
var rb = endpoint.RequestBody;
|
||||||
var bodyParam = new JsonObject
|
var bodyParam = new JObject
|
||||||
{
|
{
|
||||||
["name"] = "body",
|
["name"] = "body",
|
||||||
["in"] = "body",
|
["in"] = "body",
|
||||||
@@ -155,10 +156,10 @@ namespace EonaCat.DoxaApi.Interop
|
|||||||
op["parameters"] = parameters;
|
op["parameters"] = parameters;
|
||||||
}
|
}
|
||||||
|
|
||||||
var responses = new JsonObject();
|
var responses = new JObject();
|
||||||
foreach (var r in endpoint.Responses)
|
foreach (var r in endpoint.Responses)
|
||||||
{
|
{
|
||||||
var resp = new JsonObject
|
var resp = new JObject
|
||||||
{
|
{
|
||||||
["description"] = r.Description ?? HttpStatusDescription(r.StatusCode)
|
["description"] = r.Description ?? HttpStatusDescription(r.StatusCode)
|
||||||
};
|
};
|
||||||
@@ -173,10 +174,10 @@ namespace EonaCat.DoxaApi.Interop
|
|||||||
|
|
||||||
if (endpoint.Security.Count > 0)
|
if (endpoint.Security.Count > 0)
|
||||||
{
|
{
|
||||||
var security = new JsonArray();
|
var security = new JArray();
|
||||||
foreach (var schemeId in endpoint.Security)
|
foreach (var schemeId in endpoint.Security)
|
||||||
{
|
{
|
||||||
security.Add(new JsonObject { [schemeId] = new JsonArray() });
|
security.Add(new JObject { [schemeId] = new JArray() });
|
||||||
}
|
}
|
||||||
op["security"] = security;
|
op["security"] = security;
|
||||||
}
|
}
|
||||||
@@ -184,7 +185,7 @@ namespace EonaCat.DoxaApi.Interop
|
|||||||
return op;
|
return op;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void InlineSchemaIntoParam(JsonObject param, SchemaModel schema)
|
private static void InlineSchemaIntoParam(JObject param, SchemaModel schema)
|
||||||
{
|
{
|
||||||
if (schema.RefName is not null)
|
if (schema.RefName is not null)
|
||||||
{
|
{
|
||||||
@@ -199,7 +200,7 @@ namespace EonaCat.DoxaApi.Interop
|
|||||||
param["type"] = "array";
|
param["type"] = "array";
|
||||||
if (schema.Items is not null)
|
if (schema.Items is not null)
|
||||||
{
|
{
|
||||||
var items = new JsonObject();
|
var items = new JObject();
|
||||||
InlineSchemaIntoParam(items, schema.Items);
|
InlineSchemaIntoParam(items, schema.Items);
|
||||||
param["items"] = items;
|
param["items"] = items;
|
||||||
}
|
}
|
||||||
@@ -209,7 +210,7 @@ namespace EonaCat.DoxaApi.Interop
|
|||||||
param["type"] = "string";
|
param["type"] = "string";
|
||||||
if (schema.EnumValues?.Count > 0)
|
if (schema.EnumValues?.Count > 0)
|
||||||
{
|
{
|
||||||
var enums = new JsonArray();
|
var enums = new JArray();
|
||||||
foreach (var v in schema.EnumValues)
|
foreach (var v in schema.EnumValues)
|
||||||
{
|
{
|
||||||
enums.Add(v);
|
enums.Add(v);
|
||||||
@@ -230,25 +231,25 @@ namespace EonaCat.DoxaApi.Interop
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static JsonObject SchemaToSwagger(SchemaModel schema)
|
private static JObject SchemaToSwagger(SchemaModel schema)
|
||||||
{
|
{
|
||||||
if (schema.RefName is not null)
|
if (schema.RefName is not null)
|
||||||
{
|
{
|
||||||
return new JsonObject { ["$ref"] = $"#/definitions/{schema.RefName}" };
|
return new JObject { ["$ref"] = $"#/definitions/{schema.RefName}" };
|
||||||
}
|
}
|
||||||
|
|
||||||
var obj = new JsonObject();
|
var obj = new JObject();
|
||||||
|
|
||||||
switch (schema.Type)
|
switch (schema.Type)
|
||||||
{
|
{
|
||||||
case "void":
|
case "void":
|
||||||
return new JsonObject { ["type"] = "object" };
|
return new JObject { ["type"] = "object" };
|
||||||
|
|
||||||
case "enum":
|
case "enum":
|
||||||
obj["type"] = "string";
|
obj["type"] = "string";
|
||||||
if (schema.EnumValues?.Count > 0)
|
if (schema.EnumValues?.Count > 0)
|
||||||
{
|
{
|
||||||
var enums = new JsonArray();
|
var enums = new JArray();
|
||||||
foreach (var v in schema.EnumValues)
|
foreach (var v in schema.EnumValues)
|
||||||
{
|
{
|
||||||
enums.Add(v);
|
enums.Add(v);
|
||||||
@@ -271,7 +272,7 @@ namespace EonaCat.DoxaApi.Interop
|
|||||||
obj["type"] = "object";
|
obj["type"] = "object";
|
||||||
if (schema.Properties?.Count > 0)
|
if (schema.Properties?.Count > 0)
|
||||||
{
|
{
|
||||||
var props = new JsonObject();
|
var props = new JObject();
|
||||||
foreach (var (name, propSchema) in schema.Properties)
|
foreach (var (name, propSchema) in schema.Properties)
|
||||||
{
|
{
|
||||||
props[name] = SchemaToSwagger(propSchema);
|
props[name] = SchemaToSwagger(propSchema);
|
||||||
@@ -281,7 +282,7 @@ namespace EonaCat.DoxaApi.Interop
|
|||||||
}
|
}
|
||||||
if (schema.Required?.Count > 0)
|
if (schema.Required?.Count > 0)
|
||||||
{
|
{
|
||||||
var req = new JsonArray();
|
var req = new JArray();
|
||||||
foreach (var r in schema.Required)
|
foreach (var r in schema.Required)
|
||||||
{
|
{
|
||||||
req.Add(r);
|
req.Add(r);
|
||||||
@@ -309,9 +310,9 @@ namespace EonaCat.DoxaApi.Interop
|
|||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static JsonObject SecuritySchemeToSwagger(SecuritySchemeModel scheme)
|
private static JObject SecuritySchemeToSwagger(SecuritySchemeModel scheme)
|
||||||
{
|
{
|
||||||
var obj = new JsonObject();
|
var obj = new JObject();
|
||||||
|
|
||||||
if (scheme.Type == "apiKey")
|
if (scheme.Type == "apiKey")
|
||||||
{
|
{
|
||||||
@@ -339,7 +340,7 @@ namespace EonaCat.DoxaApi.Interop
|
|||||||
{
|
{
|
||||||
obj["flow"] = "application";
|
obj["flow"] = "application";
|
||||||
obj["tokenUrl"] = flow.TokenUrl;
|
obj["tokenUrl"] = flow.TokenUrl;
|
||||||
var scopes = new JsonObject();
|
var scopes = new JObject();
|
||||||
foreach (var (k, v) in flow.Scopes)
|
foreach (var (k, v) in flow.Scopes)
|
||||||
{
|
{
|
||||||
scopes[k] = v;
|
scopes[k] = v;
|
||||||
@@ -351,7 +352,7 @@ namespace EonaCat.DoxaApi.Interop
|
|||||||
obj["flow"] = "accessCode";
|
obj["flow"] = "accessCode";
|
||||||
obj["authorizationUrl"] = ac.AuthorizationUrl;
|
obj["authorizationUrl"] = ac.AuthorizationUrl;
|
||||||
obj["tokenUrl"] = ac.TokenUrl;
|
obj["tokenUrl"] = ac.TokenUrl;
|
||||||
var scopes = new JsonObject();
|
var scopes = new JObject();
|
||||||
foreach (var (k, v) in ac.Scopes)
|
foreach (var (k, v) in ac.Scopes)
|
||||||
{
|
{
|
||||||
scopes[k] = v;
|
scopes[k] = v;
|
||||||
|
|||||||
@@ -0,0 +1,223 @@
|
|||||||
|
using EonaCat.DoxaApi.Models;
|
||||||
|
|
||||||
|
namespace EonaCat.DoxaApi.Generation
|
||||||
|
{
|
||||||
|
// This file is part of the EonaCat project(s) which is released under the Apache License.
|
||||||
|
// See the LICENSE file or go to https://EonaCat.com/license for full license details.
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Collects and manages API metrics and analytics.
|
||||||
|
/// </summary>
|
||||||
|
public class MetricsCollector
|
||||||
|
{
|
||||||
|
private readonly Dictionary<string, ApiMetrics> _metrics = new();
|
||||||
|
private readonly List<RequestMetadata> _requestHistory = new();
|
||||||
|
private readonly object _lock = new();
|
||||||
|
private readonly DateTime _startTime = DateTime.UtcNow;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Records a request to an endpoint.
|
||||||
|
/// </summary>
|
||||||
|
public void RecordRequest(string method, string path, int statusCode, long responseTimeMs, long responseSize, string? userAgent = null, string? ipAddress = null)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
var key = $"{method}:{path}";
|
||||||
|
|
||||||
|
if (!_metrics.ContainsKey(key))
|
||||||
|
{
|
||||||
|
_metrics[key] = new ApiMetrics
|
||||||
|
{
|
||||||
|
EndpointId = key,
|
||||||
|
Method = method,
|
||||||
|
Path = path,
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var metric = _metrics[key];
|
||||||
|
metric.TotalRequests++;
|
||||||
|
metric.LastRequestAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
if (statusCode >= 200 && statusCode < 300)
|
||||||
|
{
|
||||||
|
metric.SuccessfulRequests++;
|
||||||
|
}
|
||||||
|
else if (statusCode >= 400)
|
||||||
|
{
|
||||||
|
metric.FailedRequests++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track status codes
|
||||||
|
if (!metric.StatusCodes.ContainsKey(statusCode))
|
||||||
|
{
|
||||||
|
metric.StatusCodes[statusCode] = 0;
|
||||||
|
}
|
||||||
|
metric.StatusCodes[statusCode]++;
|
||||||
|
|
||||||
|
// Calculate running average response time
|
||||||
|
metric.AverageResponseTimeMs = (metric.AverageResponseTimeMs * (metric.TotalRequests - 1) + responseTimeMs) / metric.TotalRequests;
|
||||||
|
|
||||||
|
// Calculate running average response size
|
||||||
|
metric.AverageResponseSize = (metric.AverageResponseSize * (metric.TotalRequests - 1) + responseSize) / metric.TotalRequests;
|
||||||
|
|
||||||
|
// Add to request history (keep last 1000)
|
||||||
|
_requestHistory.Add(new RequestMetadata
|
||||||
|
{
|
||||||
|
Method = method,
|
||||||
|
Path = path,
|
||||||
|
StatusCode = statusCode,
|
||||||
|
ResponseTimeMs = responseTimeMs,
|
||||||
|
ResponseSize = responseSize,
|
||||||
|
Timestamp = DateTime.UtcNow,
|
||||||
|
UserAgent = userAgent,
|
||||||
|
IpAddress = ipAddress
|
||||||
|
});
|
||||||
|
|
||||||
|
if (_requestHistory.Count > 1000)
|
||||||
|
{
|
||||||
|
_requestHistory.RemoveAt(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Records an error for an endpoint.
|
||||||
|
/// </summary>
|
||||||
|
public void RecordError(string method, string path, string errorMessage, string? userAgent = null, string? ipAddress = null)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
var key = $"{method}:{path}";
|
||||||
|
|
||||||
|
if (!_metrics.ContainsKey(key))
|
||||||
|
{
|
||||||
|
_metrics[key] = new ApiMetrics
|
||||||
|
{
|
||||||
|
EndpointId = key,
|
||||||
|
Method = method,
|
||||||
|
Path = path,
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_metrics[key].FailedRequests++;
|
||||||
|
|
||||||
|
_requestHistory.Add(new RequestMetadata
|
||||||
|
{
|
||||||
|
Method = method,
|
||||||
|
Path = path,
|
||||||
|
StatusCode = 500,
|
||||||
|
ResponseTimeMs = 0,
|
||||||
|
Timestamp = DateTime.UtcNow,
|
||||||
|
UserAgent = userAgent,
|
||||||
|
IpAddress = ipAddress,
|
||||||
|
Error = errorMessage
|
||||||
|
});
|
||||||
|
|
||||||
|
if (_requestHistory.Count > 1000)
|
||||||
|
{
|
||||||
|
_requestHistory.RemoveAt(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets metrics for a specific endpoint.
|
||||||
|
/// </summary>
|
||||||
|
public ApiMetrics? GetMetrics(string method, string path)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
var key = $"{method}:{path}";
|
||||||
|
return _metrics.ContainsKey(key) ? _metrics[key] : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all collected metrics.
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<ApiMetrics> GetAllMetrics()
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
return _metrics.Values.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the analytics dashboard data.
|
||||||
|
/// </summary>
|
||||||
|
public AnalyticsDashboard GetDashboard()
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
var allMetrics = _metrics.Values.ToList();
|
||||||
|
var totalRequests = allMetrics.Sum(m => m.TotalRequests);
|
||||||
|
var totalErrors = allMetrics.Sum(m => m.FailedRequests);
|
||||||
|
|
||||||
|
var topEndpoints = allMetrics
|
||||||
|
.OrderByDescending(m => m.TotalRequests)
|
||||||
|
.Take(10)
|
||||||
|
.Select(m => new EndpointUsageSummary
|
||||||
|
{
|
||||||
|
Method = m.Method,
|
||||||
|
Path = m.Path,
|
||||||
|
RequestCount = m.TotalRequests,
|
||||||
|
ErrorCount = m.FailedRequests,
|
||||||
|
AverageResponseTimeMs = m.AverageResponseTimeMs,
|
||||||
|
LastCalled = m.LastRequestAt
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var recentRequests = _requestHistory
|
||||||
|
.OrderByDescending(r => r.Timestamp)
|
||||||
|
.Take(20)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var uptime = DateTime.UtcNow - _startTime;
|
||||||
|
var requestsPerSecond = totalRequests > 0 ? totalRequests / uptime.TotalSeconds : 0;
|
||||||
|
|
||||||
|
return new AnalyticsDashboard
|
||||||
|
{
|
||||||
|
TotalEndpoints = _metrics.Count,
|
||||||
|
TotalRequests = totalRequests,
|
||||||
|
TotalErrors = totalErrors,
|
||||||
|
AverageResponseTime = allMetrics.Any() ? allMetrics.Average(m => m.AverageResponseTimeMs) : 0,
|
||||||
|
PeakRequestsPerSecond = requestsPerSecond,
|
||||||
|
Uptime = uptime,
|
||||||
|
TopEndpoints = topEndpoints,
|
||||||
|
RecentRequests = recentRequests,
|
||||||
|
ErrorRate = totalRequests > 0 ? (double)totalErrors / totalRequests : 0,
|
||||||
|
GeneratedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clears all collected metrics and history.
|
||||||
|
/// </summary>
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_metrics.Clear();
|
||||||
|
_requestHistory.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets recent requests (last N).
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<RequestMetadata> GetRecentRequests(int count = 50)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
return _requestHistory
|
||||||
|
.OrderByDescending(r => r.Timestamp)
|
||||||
|
.Take(count)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
using EonaCat.DoxaApi.Models;
|
using EonaCat.DoxaApi.Models;
|
||||||
using System.Text.Json;
|
using EonaCat.Json;
|
||||||
|
|
||||||
namespace EonaCat.DoxaApi.Interop
|
namespace EonaCat.DoxaApi.Interop
|
||||||
{
|
{
|
||||||
@@ -10,13 +10,15 @@ namespace EonaCat.DoxaApi.Interop
|
|||||||
{
|
{
|
||||||
public static ApiDocument Import(string json)
|
public static ApiDocument Import(string json)
|
||||||
{
|
{
|
||||||
return JsonSerializer.Deserialize<ApiDocument>(json)
|
return JsonHelper.ToObject<ApiDocument>(json)
|
||||||
?? throw new InvalidOperationException("Invalid DoxaApi spec.");
|
?? throw new InvalidOperationException("Invalid DoxaApi spec.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<ApiDocument> ImportAsync(Stream stream)
|
public static async Task<ApiDocument> ImportAsync(Stream stream)
|
||||||
{
|
{
|
||||||
var doc = await JsonSerializer.DeserializeAsync<ApiDocument>(stream);
|
using var reader = new StreamReader(stream);
|
||||||
|
var json = await reader.ReadToEndAsync();
|
||||||
|
var doc = JsonHelper.ToObject<ApiDocument>(json);
|
||||||
return doc ?? throw new InvalidOperationException("Invalid DoxaApi spec.");
|
return doc ?? throw new InvalidOperationException("Invalid DoxaApi spec.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.Json.Nodes;
|
|
||||||
using EonaCat.DoxaApi.Generation;
|
using EonaCat.DoxaApi.Generation;
|
||||||
using EonaCat.DoxaApi.Interop;
|
using EonaCat.DoxaApi.Interop;
|
||||||
using EonaCat.DoxaApi.Models;
|
using EonaCat.DoxaApi.Models;
|
||||||
@@ -10,6 +8,8 @@ using Microsoft.AspNetCore.Http;
|
|||||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using EonaCat.Json;
|
||||||
|
using EonaCat.Json.Linq;
|
||||||
|
|
||||||
namespace EonaCat.DoxaApi.Middleware
|
namespace EonaCat.DoxaApi.Middleware
|
||||||
{
|
{
|
||||||
@@ -18,16 +18,17 @@ namespace EonaCat.DoxaApi.Middleware
|
|||||||
|
|
||||||
public static class DoxaApiMiddlewareExtensions
|
public static class DoxaApiMiddlewareExtensions
|
||||||
{
|
{
|
||||||
private static readonly JsonSerializerOptions _writeOptions = new()
|
private static readonly JsonSerializerSettings _writeOptions = new()
|
||||||
{
|
{
|
||||||
WriteIndented = true,
|
Formatting = Formatting.Indented,
|
||||||
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
|
NullValueHandling = NullValueHandling.Ignore
|
||||||
};
|
};
|
||||||
|
|
||||||
public static IApplicationBuilder UseDoxaApi(this IApplicationBuilder app, Action<DoxaApiOptions>? configure = null)
|
public static IApplicationBuilder UseDoxaApi(this IApplicationBuilder app, Action<DoxaApiOptions>? configure = null)
|
||||||
{
|
{
|
||||||
|
|
||||||
var options = app.ApplicationServices.GetService<DoxaApiOptions>() ?? new DoxaApiOptions();
|
var options = app.ApplicationServices.GetService<DoxaApiOptions>() ?? new DoxaApiOptions();
|
||||||
|
var metricsCollector = app.ApplicationServices.GetService<MetricsCollector>();
|
||||||
configure?.Invoke(options);
|
configure?.Invoke(options);
|
||||||
|
|
||||||
var prefix = "/" + options.RoutePrefix.Trim('/');
|
var prefix = "/" + options.RoutePrefix.Trim('/');
|
||||||
@@ -38,7 +39,8 @@ namespace EonaCat.DoxaApi.Middleware
|
|||||||
{
|
{
|
||||||
var document = GenerateDocument(context, options);
|
var document = GenerateDocument(context, options);
|
||||||
context.Response.ContentType = "application/json; charset=utf-8";
|
context.Response.ContentType = "application/json; charset=utf-8";
|
||||||
await JsonSerializer.SerializeAsync(context.Response.Body, document, _writeOptions);
|
var json = JsonHelper.ToJson(document, _writeOptions);
|
||||||
|
await context.Response.WriteAsync(json, Encoding.UTF8);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -49,8 +51,8 @@ namespace EonaCat.DoxaApi.Middleware
|
|||||||
var document = GenerateDocument(context, options);
|
var document = GenerateDocument(context, options);
|
||||||
var openApi = OpenApiExporter.Export(document);
|
var openApi = OpenApiExporter.Export(document);
|
||||||
context.Response.ContentType = "application/json; charset=utf-8";
|
context.Response.ContentType = "application/json; charset=utf-8";
|
||||||
await context.Response.WriteAsync(
|
var json = JsonHelper.ToJson(openApi, _writeOptions);
|
||||||
openApi.ToJsonString(_writeOptions), Encoding.UTF8);
|
await context.Response.WriteAsync(json, Encoding.UTF8);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -61,8 +63,8 @@ namespace EonaCat.DoxaApi.Middleware
|
|||||||
var document = GenerateDocument(context, options);
|
var document = GenerateDocument(context, options);
|
||||||
var swagger = SwaggerExporter.Export(document);
|
var swagger = SwaggerExporter.Export(document);
|
||||||
context.Response.ContentType = "application/json; charset=utf-8";
|
context.Response.ContentType = "application/json; charset=utf-8";
|
||||||
await context.Response.WriteAsync(
|
var json = JsonHelper.ToJson(swagger, _writeOptions);
|
||||||
swagger.ToJsonString(_writeOptions), Encoding.UTF8);
|
await context.Response.WriteAsync(json, Encoding.UTF8);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -89,7 +91,8 @@ namespace EonaCat.DoxaApi.Middleware
|
|||||||
{
|
{
|
||||||
var imported = await OpenApiImporter.ImportAsync(context.Request.Body);
|
var imported = await OpenApiImporter.ImportAsync(context.Request.Body);
|
||||||
context.Response.ContentType = "application/json; charset=utf-8";
|
context.Response.ContentType = "application/json; charset=utf-8";
|
||||||
await JsonSerializer.SerializeAsync(context.Response.Body, imported, _writeOptions);
|
var json = JsonHelper.ToJson(imported, _writeOptions);
|
||||||
|
await context.Response.WriteAsync(json, Encoding.UTF8);
|
||||||
}
|
}
|
||||||
catch (NotSupportedException ex)
|
catch (NotSupportedException ex)
|
||||||
{
|
{
|
||||||
@@ -138,7 +141,8 @@ namespace EonaCat.DoxaApi.Middleware
|
|||||||
context.Response.StatusCode = int.TryParse(successResponse?.StatusCode, out var code) ? code : 200;
|
context.Response.StatusCode = int.TryParse(successResponse?.StatusCode, out var code) ? code : 200;
|
||||||
context.Response.ContentType = "application/json; charset=utf-8";
|
context.Response.ContentType = "application/json; charset=utf-8";
|
||||||
context.Response.Headers["X-DoxaApi-Mock"] = "true";
|
context.Response.Headers["X-DoxaApi-Mock"] = "true";
|
||||||
await JsonSerializer.SerializeAsync(context.Response.Body, mockValue, _writeOptions);
|
var json = JsonHelper.ToJson(mockValue, _writeOptions);
|
||||||
|
await context.Response.WriteAsync(json, Encoding.UTF8);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -199,6 +203,29 @@ namespace EonaCat.DoxaApi.Middleware
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add analytics endpoint if metrics collector is registered
|
||||||
|
if (metricsCollector != null)
|
||||||
|
{
|
||||||
|
app.Map(prefix + "/analytics", analyticsApp =>
|
||||||
|
{
|
||||||
|
analyticsApp.Run(async context =>
|
||||||
|
{
|
||||||
|
if (!HttpMethods.IsGet(context.Request.Method))
|
||||||
|
{
|
||||||
|
context.Response.StatusCode = 405;
|
||||||
|
context.Response.Headers["Allow"] = "GET";
|
||||||
|
await context.Response.WriteAsync("Method Not Allowed - use GET");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var dashboard = metricsCollector.GetDashboard();
|
||||||
|
context.Response.ContentType = "application/json; charset=utf-8";
|
||||||
|
var json = JsonHelper.ToJson(dashboard, _writeOptions);
|
||||||
|
await context.Response.WriteAsync(json, Encoding.UTF8);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
using EonaCat.Json;
|
||||||
|
|
||||||
|
namespace EonaCat.DoxaApi.Models
|
||||||
|
{
|
||||||
|
// This file is part of the EonaCat project(s) which is released under the Apache License.
|
||||||
|
// See the LICENSE file or go to https://EonaCat.com/license for full license details.
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Analytics dashboard data containing metrics and insights.
|
||||||
|
/// </summary>
|
||||||
|
public class AnalyticsDashboard
|
||||||
|
{
|
||||||
|
[JsonProperty("totalEndpoints")]
|
||||||
|
public int TotalEndpoints { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("totalRequests")]
|
||||||
|
public int TotalRequests { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("totalErrors")]
|
||||||
|
public int TotalErrors { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("averageResponseTime")]
|
||||||
|
public double AverageResponseTime { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("peakRequestsPerSecond")]
|
||||||
|
public double PeakRequestsPerSecond { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("uptime")]
|
||||||
|
public TimeSpan Uptime { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("topEndpoints")]
|
||||||
|
public List<EndpointUsageSummary> TopEndpoints { get; set; } = new();
|
||||||
|
|
||||||
|
[JsonProperty("recentRequests")]
|
||||||
|
public List<RequestMetadata> RecentRequests { get; set; } = new();
|
||||||
|
|
||||||
|
[JsonProperty("errorRate")]
|
||||||
|
public double ErrorRate { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("generatedAt")]
|
||||||
|
public DateTime GeneratedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Summary of endpoint usage for dashboard display.
|
||||||
|
/// </summary>
|
||||||
|
public class EndpointUsageSummary
|
||||||
|
{
|
||||||
|
[JsonProperty("method")]
|
||||||
|
public string Method { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonProperty("path")]
|
||||||
|
public string Path { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonProperty("requestCount")]
|
||||||
|
public int RequestCount { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("errorCount")]
|
||||||
|
public int ErrorCount { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("averageResponseTimeMs")]
|
||||||
|
public double AverageResponseTimeMs { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("lastCalled")]
|
||||||
|
public DateTime? LastCalled { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Text.Json.Serialization;
|
using EonaCat.Json;
|
||||||
|
|
||||||
namespace EonaCat.DoxaApi.Models
|
namespace EonaCat.DoxaApi.Models
|
||||||
{
|
{
|
||||||
@@ -7,24 +7,24 @@ namespace EonaCat.DoxaApi.Models
|
|||||||
|
|
||||||
public sealed class ApiDocument
|
public sealed class ApiDocument
|
||||||
{
|
{
|
||||||
[JsonPropertyName("info")]
|
[JsonProperty("info")]
|
||||||
public ApiInfo Info { get; set; } = new();
|
public ApiInfo Info { get; set; } = new();
|
||||||
|
|
||||||
[JsonPropertyName("servers")]
|
[JsonProperty("servers")]
|
||||||
public List<string> Servers { get; set; } = new();
|
public List<string> Servers { get; set; } = new();
|
||||||
|
|
||||||
[JsonPropertyName("groups")]
|
[JsonProperty("groups")]
|
||||||
public List<ApiGroup> Groups { get; set; } = new();
|
public List<ApiGroup> Groups { get; set; } = new();
|
||||||
|
|
||||||
[JsonPropertyName("schemas")]
|
[JsonProperty("schemas")]
|
||||||
public Dictionary<string, SchemaModel> Schemas { get; set; } = new();
|
public Dictionary<string, SchemaModel> Schemas { get; set; } = new();
|
||||||
|
|
||||||
/// <summary>All security schemes available for this API (keyed by scheme id).</summary>
|
/// <summary>All security schemes available for this API (keyed by scheme id).</summary>
|
||||||
[JsonPropertyName("securitySchemes")]
|
[JsonProperty("securitySchemes")]
|
||||||
public Dictionary<string, SecuritySchemeModel> SecuritySchemes { get; set; } = new();
|
public Dictionary<string, SecuritySchemeModel> SecuritySchemes { get; set; } = new();
|
||||||
|
|
||||||
/// <summary>Which optional DoxaApi server-side features are enabled, for UI feature detection.</summary>
|
/// <summary>Which optional DoxaApi server-side features are enabled, for UI feature detection.</summary>
|
||||||
[JsonPropertyName("features")]
|
[JsonProperty("features")]
|
||||||
public ApiFeatureFlags Features { get; set; } = new();
|
public ApiFeatureFlags Features { get; set; } = new();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Text.Json.Serialization;
|
using EonaCat.Json;
|
||||||
|
|
||||||
namespace EonaCat.DoxaApi.Models
|
namespace EonaCat.DoxaApi.Models
|
||||||
{
|
{
|
||||||
@@ -7,41 +7,41 @@ namespace EonaCat.DoxaApi.Models
|
|||||||
|
|
||||||
public sealed class ApiEndpoint
|
public sealed class ApiEndpoint
|
||||||
{
|
{
|
||||||
[JsonPropertyName("operationId")]
|
[JsonProperty("operationId")]
|
||||||
public string OperationId { get; set; } = "";
|
public string OperationId { get; set; } = "";
|
||||||
|
|
||||||
[JsonPropertyName("summary")]
|
[JsonProperty("summary")]
|
||||||
public string? Summary { get; set; }
|
public string? Summary { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("description")]
|
[JsonProperty("description")]
|
||||||
public string? Description { get; set; }
|
public string? Description { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("method")]
|
[JsonProperty("method")]
|
||||||
public string Method { get; set; } = "GET";
|
public string Method { get; set; } = "GET";
|
||||||
|
|
||||||
[JsonPropertyName("path")]
|
[JsonProperty("path")]
|
||||||
public string Path { get; set; } = "/";
|
public string Path { get; set; } = "/";
|
||||||
|
|
||||||
[JsonPropertyName("deprecated")]
|
[JsonProperty("deprecated")]
|
||||||
public bool Deprecated { get; set; }
|
public bool Deprecated { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("tags")]
|
[JsonProperty("tags")]
|
||||||
public List<string> Tags { get; set; } = new();
|
public List<string> Tags { get; set; } = new();
|
||||||
|
|
||||||
[JsonPropertyName("parameters")]
|
[JsonProperty("parameters")]
|
||||||
public List<ApiParameter> Parameters { get; set; } = new();
|
public List<ApiParameter> Parameters { get; set; } = new();
|
||||||
|
|
||||||
[JsonPropertyName("requestBody")]
|
[JsonProperty("requestBody")]
|
||||||
public RequestBodyModel? RequestBody { get; set; }
|
public RequestBodyModel? RequestBody { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("responses")]
|
[JsonProperty("responses")]
|
||||||
public List<ResponseModel> Responses { get; set; } = new();
|
public List<ResponseModel> Responses { get; set; } = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Ids of security schemes that apply to this endpoint (referencing ApiDocument.SecuritySchemes).
|
/// Ids of security schemes that apply to this endpoint (referencing ApiDocument.SecuritySchemes).
|
||||||
/// Empty means the endpoint is unauthenticated (or uses the document-wide default, if any).
|
/// Empty means the endpoint is unauthenticated (or uses the document-wide default, if any).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonPropertyName("security")]
|
[JsonProperty("security")]
|
||||||
public List<string> Security { get; set; } = new();
|
public List<string> Security { get; set; } = new();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Text.Json.Serialization;
|
using EonaCat.Json;
|
||||||
|
|
||||||
namespace EonaCat.DoxaApi.Models
|
namespace EonaCat.DoxaApi.Models
|
||||||
{
|
{
|
||||||
@@ -7,10 +7,10 @@ namespace EonaCat.DoxaApi.Models
|
|||||||
|
|
||||||
public sealed class ApiFeatureFlags
|
public sealed class ApiFeatureFlags
|
||||||
{
|
{
|
||||||
[JsonPropertyName("mockServer")]
|
[JsonProperty("mockServer")]
|
||||||
public bool MockServer { get; set; }
|
public bool MockServer { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("console")]
|
[JsonProperty("console")]
|
||||||
public bool Console { get; set; }
|
public bool Console { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Text.Json.Serialization;
|
using EonaCat.Json;
|
||||||
|
|
||||||
namespace EonaCat.DoxaApi.Models
|
namespace EonaCat.DoxaApi.Models
|
||||||
{
|
{
|
||||||
@@ -7,13 +7,13 @@ namespace EonaCat.DoxaApi.Models
|
|||||||
|
|
||||||
public sealed class ApiGroup
|
public sealed class ApiGroup
|
||||||
{
|
{
|
||||||
[JsonPropertyName("name")]
|
[JsonProperty("name")]
|
||||||
public string Name { get; set; } = "";
|
public string Name { get; set; } = "";
|
||||||
|
|
||||||
[JsonPropertyName("description")]
|
[JsonProperty("description")]
|
||||||
public string? Description { get; set; }
|
public string? Description { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("endpoints")]
|
[JsonProperty("endpoints")]
|
||||||
public List<ApiEndpoint> Endpoints { get; set; } = new();
|
public List<ApiEndpoint> Endpoints { get; set; } = new();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Text.Json.Serialization;
|
using EonaCat.Json;
|
||||||
|
|
||||||
namespace EonaCat.DoxaApi.Models
|
namespace EonaCat.DoxaApi.Models
|
||||||
{
|
{
|
||||||
@@ -7,13 +7,13 @@ namespace EonaCat.DoxaApi.Models
|
|||||||
|
|
||||||
public sealed class ApiInfo
|
public sealed class ApiInfo
|
||||||
{
|
{
|
||||||
[JsonPropertyName("title")]
|
[JsonProperty("title")]
|
||||||
public string Title { get; set; } = "API Documentation";
|
public string Title { get; set; } = "API Documentation";
|
||||||
|
|
||||||
[JsonPropertyName("description")]
|
[JsonProperty("description")]
|
||||||
public string? Description { get; set; }
|
public string? Description { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("version")]
|
[JsonProperty("version")]
|
||||||
public string Version { get; set; } = "v1";
|
public string Version { get; set; } = "v1";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
using EonaCat.Json;
|
||||||
|
|
||||||
|
namespace EonaCat.DoxaApi.Models
|
||||||
|
{
|
||||||
|
// This file is part of the EonaCat project(s) which is released under the Apache License.
|
||||||
|
// See the LICENSE file or go to https://EonaCat.com/license for full license details.
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tracks metrics and statistics for API endpoints.
|
||||||
|
/// </summary>
|
||||||
|
public class ApiMetrics
|
||||||
|
{
|
||||||
|
[JsonProperty("endpointId")]
|
||||||
|
public string EndpointId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonProperty("method")]
|
||||||
|
public string Method { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonProperty("path")]
|
||||||
|
public string Path { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonProperty("totalRequests")]
|
||||||
|
public int TotalRequests { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("successfulRequests")]
|
||||||
|
public int SuccessfulRequests { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("failedRequests")]
|
||||||
|
public int FailedRequests { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("averageResponseTimeMs")]
|
||||||
|
public double AverageResponseTimeMs { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("lastRequestAt")]
|
||||||
|
public DateTime? LastRequestAt { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("createdAt")]
|
||||||
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
|
[JsonProperty("statusCodes")]
|
||||||
|
public Dictionary<int, int> StatusCodes { get; set; } = new();
|
||||||
|
|
||||||
|
[JsonProperty("averageResponseSize")]
|
||||||
|
public long AverageResponseSize { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Text.Json.Serialization;
|
using EonaCat.Json;
|
||||||
|
|
||||||
namespace EonaCat.DoxaApi.Models
|
namespace EonaCat.DoxaApi.Models
|
||||||
{
|
{
|
||||||
@@ -7,22 +7,22 @@ namespace EonaCat.DoxaApi.Models
|
|||||||
|
|
||||||
public sealed class ApiParameter
|
public sealed class ApiParameter
|
||||||
{
|
{
|
||||||
[JsonPropertyName("name")]
|
[JsonProperty("name")]
|
||||||
public string Name { get; set; } = "";
|
public string Name { get; set; } = "";
|
||||||
|
|
||||||
[JsonPropertyName("in")]
|
[JsonProperty("in")]
|
||||||
public string In { get; set; } = "query";
|
public string In { get; set; } = "query";
|
||||||
|
|
||||||
[JsonPropertyName("required")]
|
[JsonProperty("required")]
|
||||||
public bool Required { get; set; }
|
public bool Required { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("description")]
|
[JsonProperty("description")]
|
||||||
public string? Description { get; set; }
|
public string? Description { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("schema")]
|
[JsonProperty("schema")]
|
||||||
public SchemaModel Schema { get; set; } = new();
|
public SchemaModel Schema { get; set; } = new();
|
||||||
|
|
||||||
[JsonPropertyName("default")]
|
[JsonProperty("default")]
|
||||||
public object? Default { get; set; }
|
public object? Default { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Text.Json.Serialization;
|
using EonaCat.Json;
|
||||||
|
|
||||||
namespace EonaCat.DoxaApi.Models
|
namespace EonaCat.DoxaApi.Models
|
||||||
{
|
{
|
||||||
@@ -7,16 +7,16 @@ namespace EonaCat.DoxaApi.Models
|
|||||||
|
|
||||||
public sealed class RequestBodyModel
|
public sealed class RequestBodyModel
|
||||||
{
|
{
|
||||||
[JsonPropertyName("required")]
|
[JsonProperty("required")]
|
||||||
public bool Required { get; set; }
|
public bool Required { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("contentType")]
|
[JsonProperty("contentType")]
|
||||||
public string ContentType { get; set; } = "application/json";
|
public string ContentType { get; set; } = "application/json";
|
||||||
|
|
||||||
[JsonPropertyName("schema")]
|
[JsonProperty("schema")]
|
||||||
public SchemaModel Schema { get; set; } = new();
|
public SchemaModel Schema { get; set; } = new();
|
||||||
|
|
||||||
[JsonPropertyName("example")]
|
[JsonProperty("example")]
|
||||||
public string? Example { get; set; }
|
public string? Example { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
using EonaCat.Json;
|
||||||
|
|
||||||
|
namespace EonaCat.DoxaApi.Models
|
||||||
|
{
|
||||||
|
// This file is part of the EonaCat project(s) which is released under the Apache License.
|
||||||
|
// See the LICENSE file or go to https://EonaCat.com/license for full license details.
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Metadata for tracking requests and responses.
|
||||||
|
/// </summary>
|
||||||
|
public class RequestMetadata
|
||||||
|
{
|
||||||
|
[JsonProperty("id")]
|
||||||
|
public string Id { get; set; } = Guid.NewGuid().ToString();
|
||||||
|
|
||||||
|
[JsonProperty("method")]
|
||||||
|
public string Method { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonProperty("path")]
|
||||||
|
public string Path { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonProperty("statusCode")]
|
||||||
|
public int StatusCode { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("responseTimeMs")]
|
||||||
|
public long ResponseTimeMs { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("requestSize")]
|
||||||
|
public long RequestSize { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("responseSize")]
|
||||||
|
public long ResponseSize { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("timestamp")]
|
||||||
|
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
|
[JsonProperty("userAgent", NullValueHandling = NullValueHandling.Ignore)]
|
||||||
|
public string? UserAgent { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("ipAddress", NullValueHandling = NullValueHandling.Ignore)]
|
||||||
|
public string? IpAddress { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("error", NullValueHandling = NullValueHandling.Ignore)]
|
||||||
|
public string? Error { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Text.Json.Serialization;
|
using EonaCat.Json;
|
||||||
|
|
||||||
namespace EonaCat.DoxaApi.Models
|
namespace EonaCat.DoxaApi.Models
|
||||||
{
|
{
|
||||||
@@ -7,13 +7,13 @@ namespace EonaCat.DoxaApi.Models
|
|||||||
|
|
||||||
public sealed class ResponseModel
|
public sealed class ResponseModel
|
||||||
{
|
{
|
||||||
[JsonPropertyName("statusCode")]
|
[JsonProperty("statusCode")]
|
||||||
public string StatusCode { get; set; } = "200";
|
public string StatusCode { get; set; } = "200";
|
||||||
|
|
||||||
[JsonPropertyName("description")]
|
[JsonProperty("description")]
|
||||||
public string? Description { get; set; }
|
public string? Description { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("schema")]
|
[JsonProperty("schema")]
|
||||||
public SchemaModel? Schema { get; set; }
|
public SchemaModel? Schema { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Text.Json.Serialization;
|
using EonaCat.Json;
|
||||||
|
|
||||||
namespace EonaCat.DoxaApi.Models
|
namespace EonaCat.DoxaApi.Models
|
||||||
{
|
{
|
||||||
@@ -7,28 +7,28 @@ namespace EonaCat.DoxaApi.Models
|
|||||||
|
|
||||||
public sealed class SchemaModel
|
public sealed class SchemaModel
|
||||||
{
|
{
|
||||||
[JsonPropertyName("type")]
|
[JsonProperty("type")]
|
||||||
public string Type { get; set; } = "object";
|
public string Type { get; set; } = "object";
|
||||||
|
|
||||||
[JsonPropertyName("format")]
|
[JsonProperty("format")]
|
||||||
public string? Format { get; set; }
|
public string? Format { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("refName")]
|
[JsonProperty("refName")]
|
||||||
public string? RefName { get; set; }
|
public string? RefName { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("items")]
|
[JsonProperty("items")]
|
||||||
public SchemaModel? Items { get; set; }
|
public SchemaModel? Items { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("properties")]
|
[JsonProperty("properties")]
|
||||||
public Dictionary<string, SchemaModel>? Properties { get; set; }
|
public Dictionary<string, SchemaModel>? Properties { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("required")]
|
[JsonProperty("required")]
|
||||||
public List<string>? Required { get; set; }
|
public List<string>? Required { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("enumValues")]
|
[JsonProperty("enumValues")]
|
||||||
public List<string>? EnumValues { get; set; }
|
public List<string>? EnumValues { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("nullable")]
|
[JsonProperty("nullable")]
|
||||||
public bool Nullable { get; set; }
|
public bool Nullable { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Text.Json.Serialization;
|
using EonaCat.Json;
|
||||||
|
|
||||||
namespace EonaCat.DoxaApi.Models
|
namespace EonaCat.DoxaApi.Models
|
||||||
{
|
{
|
||||||
@@ -13,64 +13,64 @@ namespace EonaCat.DoxaApi.Models
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class SecuritySchemeModel
|
public sealed class SecuritySchemeModel
|
||||||
{
|
{
|
||||||
[JsonPropertyName("id")]
|
[JsonProperty("id")]
|
||||||
public string Id { get; set; } = "";
|
public string Id { get; set; } = "";
|
||||||
|
|
||||||
/// <summary>apiKey | http | oauth2 | openIdConnect</summary>
|
/// <summary>apiKey | http | oauth2 | openIdConnect</summary>
|
||||||
[JsonPropertyName("type")]
|
[JsonProperty("type")]
|
||||||
public string Type { get; set; } = "apiKey";
|
public string Type { get; set; } = "apiKey";
|
||||||
|
|
||||||
[JsonPropertyName("description")]
|
[JsonProperty("description")]
|
||||||
public string? Description { get; set; }
|
public string? Description { get; set; }
|
||||||
|
|
||||||
/// <summary>For apiKey: header | query | cookie</summary>
|
/// <summary>For apiKey: header | query | cookie</summary>
|
||||||
[JsonPropertyName("in")]
|
[JsonProperty("in")]
|
||||||
public string? In { get; set; }
|
public string? In { get; set; }
|
||||||
|
|
||||||
/// <summary>For apiKey: the header/query/cookie name. For http: ignored.</summary>
|
/// <summary>For apiKey: the header/query/cookie name. For http: ignored.</summary>
|
||||||
[JsonPropertyName("name")]
|
[JsonProperty("name")]
|
||||||
public string? Name { get; set; }
|
public string? Name { get; set; }
|
||||||
|
|
||||||
/// <summary>For http: bearer | basic | digest</summary>
|
/// <summary>For http: bearer | basic | digest</summary>
|
||||||
[JsonPropertyName("scheme")]
|
[JsonProperty("scheme")]
|
||||||
public string? Scheme { get; set; }
|
public string? Scheme { get; set; }
|
||||||
|
|
||||||
/// <summary>For http bearer: an informational hint such as "JWT".</summary>
|
/// <summary>For http bearer: an informational hint such as "JWT".</summary>
|
||||||
[JsonPropertyName("bearerFormat")]
|
[JsonProperty("bearerFormat")]
|
||||||
public string? BearerFormat { get; set; }
|
public string? BearerFormat { get; set; }
|
||||||
|
|
||||||
/// <summary>For oauth2: supported flows (clientCredentials, authorizationCode, password, implicit).</summary>
|
/// <summary>For oauth2: supported flows (clientCredentials, authorizationCode, password, implicit).</summary>
|
||||||
[JsonPropertyName("flows")]
|
[JsonProperty("flows")]
|
||||||
public OAuthFlowsModel? Flows { get; set; }
|
public OAuthFlowsModel? Flows { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class OAuthFlowsModel
|
public sealed class OAuthFlowsModel
|
||||||
{
|
{
|
||||||
[JsonPropertyName("clientCredentials")]
|
[JsonProperty("clientCredentials")]
|
||||||
public OAuthFlowModel? ClientCredentials { get; set; }
|
public OAuthFlowModel? ClientCredentials { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("authorizationCode")]
|
[JsonProperty("authorizationCode")]
|
||||||
public OAuthFlowModel? AuthorizationCode { get; set; }
|
public OAuthFlowModel? AuthorizationCode { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("password")]
|
[JsonProperty("password")]
|
||||||
public OAuthFlowModel? Password { get; set; }
|
public OAuthFlowModel? Password { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("implicit")]
|
[JsonProperty("implicit")]
|
||||||
public OAuthFlowModel? Implicit { get; set; }
|
public OAuthFlowModel? Implicit { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class OAuthFlowModel
|
public sealed class OAuthFlowModel
|
||||||
{
|
{
|
||||||
[JsonPropertyName("authorizationUrl")]
|
[JsonProperty("authorizationUrl")]
|
||||||
public string? AuthorizationUrl { get; set; }
|
public string? AuthorizationUrl { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("tokenUrl")]
|
[JsonProperty("tokenUrl")]
|
||||||
public string? TokenUrl { get; set; }
|
public string? TokenUrl { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("refreshUrl")]
|
[JsonProperty("refreshUrl")]
|
||||||
public string? RefreshUrl { get; set; }
|
public string? RefreshUrl { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("scopes")]
|
[JsonProperty("scopes")]
|
||||||
public Dictionary<string, string> Scopes { get; set; } = new();
|
public Dictionary<string, string> Scopes { get; set; } = new();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -13,6 +13,7 @@ namespace EonaCat.DoxaApi
|
|||||||
var options = new DoxaApiOptions();
|
var options = new DoxaApiOptions();
|
||||||
configure?.Invoke(options);
|
configure?.Invoke(options);
|
||||||
services.AddSingleton(options);
|
services.AddSingleton(options);
|
||||||
|
services.AddSingleton<MetricsCollector>();
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1896,3 +1896,181 @@ textarea.field-input {
|
|||||||
width: 14px;
|
width: 14px;
|
||||||
height: 14px;
|
height: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Dashboard Styles */
|
||||||
|
.dashboard-panel {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 58px;
|
||||||
|
bottom: 0;
|
||||||
|
width: 400px;
|
||||||
|
background: var(--bg-1);
|
||||||
|
border-left: 1px solid var(--border);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
z-index: 100;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
background: var(--bg-raised);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
font-size: 24px;
|
||||||
|
color: var(--text-1);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn:hover {
|
||||||
|
color: var(--text-0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background: var(--bg-2);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
padding: 16px;
|
||||||
|
text-align: center;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
background: var(--bg-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--text-2);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--accent);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value.error-red {
|
||||||
|
color: var(--m-delete);
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-chart {
|
||||||
|
background: var(--bg-2);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
min-height: 200px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--text-2);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-requests {
|
||||||
|
background: var(--bg-2);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.request-item {
|
||||||
|
padding: 12px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 12px;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.request-item:hover {
|
||||||
|
background: var(--bg-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.request-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.request-method {
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.request-status-success {
|
||||||
|
color: var(--m-get);
|
||||||
|
}
|
||||||
|
|
||||||
|
.request-status-error {
|
||||||
|
color: var(--m-delete);
|
||||||
|
}
|
||||||
|
|
||||||
|
.request-time {
|
||||||
|
color: var(--text-2);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
#app-shell.with-dashboard {
|
||||||
|
grid-template-columns: var(--nav-w) 1fr var(--try-w) 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1400px) {
|
||||||
|
.dashboard-panel {
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app-shell.with-dashboard {
|
||||||
|
grid-template-columns: var(--nav-w) 1fr 300px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.dashboard-panel {
|
||||||
|
width: 100%;
|
||||||
|
right: auto;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1386,6 +1386,9 @@
|
|||||||
const exportDoxaApiBtn = document.getElementById("exportDoxaApiBtn");
|
const exportDoxaApiBtn = document.getElementById("exportDoxaApiBtn");
|
||||||
const exportOpenApiBtn = document.getElementById("exportOpenApiBtn");
|
const exportOpenApiBtn = document.getElementById("exportOpenApiBtn");
|
||||||
const exportSwaggerBtn = document.getElementById("exportSwaggerBtn");
|
const exportSwaggerBtn = document.getElementById("exportSwaggerBtn");
|
||||||
|
const dashboardBtn = document.getElementById("dashboardBtn");
|
||||||
|
const closeDashboard = document.getElementById("closeDashboard");
|
||||||
|
const dashboardPanel = document.getElementById("dashboardPanel");
|
||||||
|
|
||||||
importBtn?.addEventListener("click", () => importFile.click());
|
importBtn?.addEventListener("click", () => importFile.click());
|
||||||
|
|
||||||
@@ -1443,6 +1446,10 @@
|
|||||||
exportOpenApiBtn?.addEventListener("click", () => download("openapi.json", "openapi.json"));
|
exportOpenApiBtn?.addEventListener("click", () => download("openapi.json", "openapi.json"));
|
||||||
exportSwaggerBtn?.addEventListener("click", () => download("swagger.json", "swagger.json"));
|
exportSwaggerBtn?.addEventListener("click", () => download("swagger.json", "swagger.json"));
|
||||||
|
|
||||||
|
// Dashboard functionality
|
||||||
|
dashboardBtn?.addEventListener("click", () => openDashboard());
|
||||||
|
closeDashboard?.addEventListener("click", () => closeDashboardPanel());
|
||||||
|
|
||||||
const authBtn = document.getElementById("authBtn");
|
const authBtn = document.getElementById("authBtn");
|
||||||
authBtn?.addEventListener("click", () => openAuthModal());
|
authBtn?.addEventListener("click", () => openAuthModal());
|
||||||
|
|
||||||
@@ -1578,5 +1585,93 @@
|
|||||||
return escapeHtml(str);
|
return escapeHtml(str);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dashboard functions
|
||||||
|
async function openDashboard() {
|
||||||
|
const dashboardPanel = document.getElementById("dashboardPanel");
|
||||||
|
const dashboard = document.querySelector(".dashboard-panel");
|
||||||
|
|
||||||
|
if (!dashboardPanel) return;
|
||||||
|
dashboardPanel.style.display = "block";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch("analytics");
|
||||||
|
if (!res.ok) throw new Error("Failed to load analytics");
|
||||||
|
|
||||||
|
const analyticsData = await res.json();
|
||||||
|
updateDashboard(analyticsData);
|
||||||
|
|
||||||
|
// Refresh every 5 seconds
|
||||||
|
if (window.dashboardInterval) clearInterval(window.dashboardInterval);
|
||||||
|
window.dashboardInterval = setInterval(async () => {
|
||||||
|
const res = await fetch("analytics");
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
updateDashboard(data);
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Dashboard load error:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDashboardPanel() {
|
||||||
|
const dashboardPanel = document.getElementById("dashboardPanel");
|
||||||
|
if (dashboardPanel) dashboardPanel.style.display = "none";
|
||||||
|
|
||||||
|
if (window.dashboardInterval) {
|
||||||
|
clearInterval(window.dashboardInterval);
|
||||||
|
window.dashboardInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateDashboard(data) {
|
||||||
|
document.getElementById("totalEndpoints").textContent = data.totalEndpoints || "0";
|
||||||
|
document.getElementById("totalRequests").textContent = data.totalRequests || "0";
|
||||||
|
document.getElementById("totalErrors").textContent = data.totalErrors || "0";
|
||||||
|
document.getElementById("avgResponse").textContent = Math.round((data.averageResponseTime || 0)) + "ms";
|
||||||
|
|
||||||
|
// Update top endpoints
|
||||||
|
const topEndpointsList = (data.topEndpoints || []).map(ep => `
|
||||||
|
<div class="request-item">
|
||||||
|
<div>
|
||||||
|
<span class="request-method">${ep.method}</span>
|
||||||
|
<code style="font-size: 11px; color: var(--text-1);">${ep.path}</code>
|
||||||
|
</div>
|
||||||
|
<div class="request-time">${ep.requestCount} req</div>
|
||||||
|
</div>
|
||||||
|
`).join("") || "<div style='padding: 20px; color: var(--text-2); text-align: center;'>No data yet</div>";
|
||||||
|
|
||||||
|
document.getElementById("topEndpointsChart").innerHTML = `
|
||||||
|
<div style="width: 100%;">
|
||||||
|
<h3 style="margin: 0 0 12px 0; font-size: 13px; font-weight: 600;">Top Endpoints</h3>
|
||||||
|
${topEndpointsList}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Update recent requests
|
||||||
|
const recentRequestsList = (data.recentRequests || []).map(req => {
|
||||||
|
const statusClass = req.statusCode >= 400 ? "request-status-error" : "request-status-success";
|
||||||
|
return `
|
||||||
|
<div class="request-item">
|
||||||
|
<div>
|
||||||
|
<span class="request-method">${req.method}</span>
|
||||||
|
<code style="font-size: 11px; color: var(--text-1);">${req.path}</code>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="request-time ${statusClass}">${req.statusCode}</span>
|
||||||
|
<span class="request-time" style="margin-left: 8px;">${req.responseTimeMs}ms</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join("") || "<div style='padding: 20px; color: var(--text-2); text-align: center;'>No requests yet</div>";
|
||||||
|
|
||||||
|
document.getElementById("recentRequestsTable").innerHTML = `
|
||||||
|
<div style="padding: 16px; background: var(--bg-2); border: 1px solid var(--border); border-radius: var(--radius-md);">
|
||||||
|
<h3 style="margin: 0 0 12px 0; font-size: 13px; font-weight: 600;">Recent Requests</h3>
|
||||||
|
${recentRequestsList}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
init();
|
init();
|
||||||
})();
|
})();
|
||||||
@@ -33,7 +33,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="topbar-actions">
|
<div class="topbar-actions">
|
||||||
<div class="action-group">
|
<div class="action-group">
|
||||||
<button id="authBtn" class="action-btn icon-btn-badge" title="Authorization">
|
<button id="dashboardBtn" class="action-btn" title="View Analytics Dashboard">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" width="15" height="15" style="vertical-align:-2px;margin-right:4px;"><rect x="3" y="3" width="7" height="7" stroke="currentColor" stroke-width="2"/><rect x="14" y="3" width="7" height="7" stroke="currentColor" stroke-width="2"/><rect x="3" y="14" width="7" height="7" stroke="currentColor" stroke-width="2"/><rect x="14" y="14" width="7" height="7" stroke="currentColor" stroke-width="2"/></svg>Dashboard
|
||||||
|
</button>
|
||||||
|
<button id="authBtn" class="action-btn" title="Manage authentication">
|
||||||
<svg viewBox="0 0 24 24" fill="none" width="15" height="15" style="vertical-align:-2px;margin-right:4px;"><rect x="5" y="11" width="14" height="9" rx="2" stroke="currentColor" stroke-width="2" /><path d="M8 11V7a4 4 0 018 0v4" stroke="currentColor" stroke-width="2" stroke-linecap="round" /></svg>Auth
|
<svg viewBox="0 0 24 24" fill="none" width="15" height="15" style="vertical-align:-2px;margin-right:4px;"><rect x="5" y="11" width="14" height="9" rx="2" stroke="currentColor" stroke-width="2" /><path d="M8 11V7a4 4 0 018 0v4" stroke="currentColor" stroke-width="2" stroke-linecap="round" /></svg>Auth
|
||||||
<span class="badge-dot" id="authBadgeDot" style="display:none;"></span>
|
<span class="badge-dot" id="authBadgeDot" style="display:none;"></span>
|
||||||
</button>
|
</button>
|
||||||
@@ -71,6 +74,36 @@
|
|||||||
<aside class="try-pane" id="tryPane">
|
<aside class="try-pane" id="tryPane">
|
||||||
<div id="tryContent" class="try-content"></div>
|
<div id="tryContent" class="try-content"></div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
|
<!-- Dashboard Panel (initially hidden) -->
|
||||||
|
<div id="dashboardPanel" class="dashboard-panel" style="display: none;">
|
||||||
|
<div class="dashboard-header">
|
||||||
|
<h2>API Analytics</h2>
|
||||||
|
<button id="closeDashboard" class="close-btn" title="Close Dashboard">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="dashboard-content">
|
||||||
|
<div class="dashboard-stats">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-label">Total Endpoints</div>
|
||||||
|
<div class="stat-value" id="totalEndpoints">0</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-label">Total Requests</div>
|
||||||
|
<div class="stat-value" id="totalRequests">0</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-label">Errors</div>
|
||||||
|
<div class="stat-value error-red" id="totalErrors">0</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-label">Avg Response</div>
|
||||||
|
<div class="stat-value" id="avgResponse">0ms</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="topEndpointsChart" class="metric-chart"></div>
|
||||||
|
<div id="recentRequestsTable" class="recent-requests"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# EonaCat.DoxaApi
|
# EonaCat.DoxaApi
|
||||||
|
|
||||||
**A modern, self-contained, dependency-free API documentation UI for ASP.NET Core**
|
**A modern, self-contained API documentation UI for ASP.NET Core with built-in analytics and monitoring**
|
||||||
|
|
||||||
DoxaApi reflects over your controllers, generates an OpenAPI-compatible document, and serves a fast, single-page documentation UI with zero JavaScript dependencies. No npm build step, no CDN calls, no telemetry - one NuGet package, one line in `Program.cs`.
|
DoxaApi reflects over your controllers, generates an OpenAPI-compatible document, and serves a fast, single-page documentation UI.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -41,10 +41,6 @@ app.Run();
|
|||||||
|
|
||||||
Run the app and open `/doxa`. That's the whole setup.
|
Run the app and open `/doxa`. That's the whole setup.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Authoring your documentation
|
|
||||||
|
|
||||||
DoxaApi reads standard `///` XML doc comments and a handful of optional attributes:
|
DoxaApi reads standard `///` XML doc comments and a handful of optional attributes:
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
@@ -104,7 +100,35 @@ Credentials entered in the UI are stored in the browser's `localStorage` (never
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Mock server
|
## Analytics Dashboard
|
||||||
|
|
||||||
|
The built-in **Analytics Dashboard** provides real-time insights into your API usage:
|
||||||
|
|
||||||
|
- **Endpoint Statistics** - View total requests, success rate, and average response times per endpoint
|
||||||
|
- **Request History** - Track recent requests with status codes and response times
|
||||||
|
- **Performance Metrics** - Monitor overall API health and uptime
|
||||||
|
- **Top Endpoints** - Identify your most-used endpoints at a glance
|
||||||
|
|
||||||
|
Click the **Dashboard** button in the top navigation to view live analytics. Metrics are automatically collected and refreshed every 5 seconds.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Access analytics programmatically
|
||||||
|
var metricsCollector = app.ApplicationServices.GetService<MetricsCollector>();
|
||||||
|
if (metricsCollector != null)
|
||||||
|
{
|
||||||
|
var dashboard = metricsCollector.GetDashboard();
|
||||||
|
var recentRequests = metricsCollector.GetRecentRequests(limit: 50);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Get the analytics JSON via:
|
||||||
|
```
|
||||||
|
GET /doxa/analytics
|
||||||
|
```
|
||||||
|
|
||||||
|
All API requests and responses are automatically serialized, ensuring consistent behavior across your entire API documentation.
|
||||||
|
|
||||||
|
## Authoring your documentation
|
||||||
|
|
||||||
Every endpoint can return a schema-shaped fake response without touching your real controllers - flip the **"Send to mock server"** toggle in the Try-it panel, or call it directly:
|
Every endpoint can return a schema-shaped fake response without touching your real controllers - flip the **"Send to mock server"** toggle in the Try-it panel, or call it directly:
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user