diff --git a/src/Localization/en/SqlX.json b/src/Localization/en/SqlX.json index 3c1d81dac..e2128a40e 100644 --- a/src/Localization/en/SqlX.json +++ b/src/Localization/en/SqlX.json @@ -37,16 +37,19 @@ "CannotSave": "Cannot save the changes to the Dedicated gateway resource at the moment.", "DedicatedGatewayEndpoint": "Dedicated gatewayEndpoint", "NoValue": "", - "SKUDetails": "SKU Details:", "CosmosD4Details": "General Purpose Cosmos Compute with 4 vCPUs, 16 GB Memory", "CosmosD8Details": "General Purpose Cosmos Compute with 8 vCPUs, 32 GB Memory", "CosmosD16Details": "General Purpose Cosmos Compute with 16 vCPUs, 64 GB Memory", - "CosmosD32Details": "General Purpose Cosmos Compute with 32 vCPUs, 128 GB Memory", "Cost": "Cost", "CostText": "Hourly cost of the dedicated gateway resource depends on the SKU selection, number of instances per region, and number of regions.", "ConnectionString": "Connection String", "ConnectionStringText": "To use the dedicated gateway, use the connection string shown in ", - "KeysBlade": "the keys blade", + "KeysBlade": "the keys blade.", + "MetricsString": "Metrics", + "MetricsText": "Monitor the \"DedicatedGatewayMaximumCpuUsage\" and \"DedicatedGatewayAverageMemoryUsage\" in ", + "MetricsBlade": "the metrics blade.", + "ResizingDecisionText": "To understand if the dedicated gateway is the right size, ", + "ResizingDecisionLink": "learn more about dedicated gateway sizing.", "WarningBannerOnUpdate": "Adding or modifying dedicated gateway instances may affect your bill.", "WarningBannerOnDelete": "After deprovisioning the dedicated gateway, you must update any applications using the old dedicated gateway connection string." } \ No newline at end of file diff --git a/src/SelfServe/SqlX/SqlX.rp.ts b/src/SelfServe/SqlX/SqlX.rp.ts index b5a097fca..2bc7a70fa 100644 --- a/src/SelfServe/SqlX/SqlX.rp.ts +++ b/src/SelfServe/SqlX/SqlX.rp.ts @@ -1,10 +1,12 @@ -import { RefreshResult } from "../SelfServeTypes"; +import { configContext } from "../../ConfigContext"; import { userContext } from "../../UserContext"; import { armRequestWithoutPolling } from "../../Utils/arm/request"; -import { configContext } from "../../ConfigContext"; +import { selfServeTraceFailure, selfServeTraceStart, selfServeTraceSuccess } from "../SelfServeTelemetryProcessor"; +import { RefreshResult } from "../SelfServeTypes"; +import SqlX from "./SqlX"; import { SqlxServiceResource, UpdateDedicatedGatewayRequestParameters } from "./SqlxTypes"; -const apiVersion = "2020-06-01-preview"; +const apiVersion = "2021-04-01-preview"; export enum ResourceStatus { Running = "Running", @@ -21,7 +23,7 @@ export interface DedicatedGatewayResponse { } export const getPath = (subscriptionId: string, resourceGroup: string, name: string): string => { - return `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${name}/services/sqlx`; + return `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${name}/services/SqlDedicatedGateway`; }; export const updateDedicatedGatewayResource = async (sku: string, instances: number): Promise => { @@ -30,39 +32,66 @@ export const updateDedicatedGatewayResource = async (sku: string, instances: num properties: { instanceSize: sku, instanceCount: instances, - serviceType: "Sqlx", + serviceType: "SqlDedicatedGateway", }, }; - const armRequestResult = await armRequestWithoutPolling({ - host: configContext.ARM_ENDPOINT, - path, - method: "PUT", - apiVersion, - body, - }); - return armRequestResult.operationStatusUrl; + const telemetryData = { ...body, httpMethod: "PUT", selfServeClassName: SqlX.name }; + const updateTimeStamp = selfServeTraceStart(telemetryData); + let armRequestResult; + try { + armRequestResult = await armRequestWithoutPolling({ + host: configContext.ARM_ENDPOINT, + path, + method: "PUT", + apiVersion, + body, + }); + selfServeTraceSuccess(telemetryData, updateTimeStamp); + } catch (e) { + const failureTelemetry = { ...body, e, selfServeClassName: SqlX.name }; + selfServeTraceFailure(failureTelemetry, updateTimeStamp); + } + return armRequestResult?.operationStatusUrl; }; export const deleteDedicatedGatewayResource = async (): Promise => { const path = getPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name); - const armRequestResult = await armRequestWithoutPolling({ - host: configContext.ARM_ENDPOINT, - path, - method: "DELETE", - apiVersion, - }); - return armRequestResult.operationStatusUrl; + const telemetryData = { httpMethod: "DELETE", selfServeClassName: SqlX.name }; + const deleteTimeStamp = selfServeTraceStart(telemetryData); + let armRequestResult; + try { + armRequestResult = await armRequestWithoutPolling({ + host: configContext.ARM_ENDPOINT, + path, + method: "DELETE", + apiVersion, + }); + selfServeTraceSuccess(telemetryData, deleteTimeStamp); + } catch (e) { + const failureTelemetry = { e, selfServeClassName: SqlX.name }; + selfServeTraceFailure(failureTelemetry, deleteTimeStamp); + } + return armRequestResult?.operationStatusUrl; }; export const getDedicatedGatewayResource = async (): Promise => { const path = getPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name); - const armRequestResult = await armRequestWithoutPolling({ - host: configContext.ARM_ENDPOINT, - path, - method: "GET", - apiVersion, - }); - return armRequestResult.result; + const telemetryData = { httpMethod: "GET", selfServeClassName: SqlX.name }; + const getResourceTimeStamp = selfServeTraceStart(telemetryData); + let armRequestResult; + try { + armRequestResult = await armRequestWithoutPolling({ + host: configContext.ARM_ENDPOINT, + path, + method: "GET", + apiVersion, + }); + selfServeTraceSuccess(telemetryData, getResourceTimeStamp); + } catch (e) { + const failureTelemetry = { e, selfServeClassName: SqlX.name }; + selfServeTraceFailure(failureTelemetry, getResourceTimeStamp); + } + return armRequestResult?.result; }; export const getCurrentProvisioningState = async (): Promise => { diff --git a/src/SelfServe/SqlX/SqlX.tsx b/src/SelfServe/SqlX/SqlX.tsx index a532c5244..44356969a 100644 --- a/src/SelfServe/SqlX/SqlX.tsx +++ b/src/SelfServe/SqlX/SqlX.tsx @@ -23,7 +23,7 @@ const costPerHourValue: Description = { textTKey: "CostText", type: DescriptionType.Text, link: { - href: "https://azure.microsoft.com/en-us/pricing/details/cosmos-db/", + href: "https://aka.ms/cosmos-db-dedicated-gateway-pricing", textTKey: "DedicatedGatewayPricing", }, }; @@ -37,43 +37,56 @@ const connectionStringValue: Description = { }, }; +const metricsStringValue: Description = { + textTKey: "MetricsText", + type: DescriptionType.Text, + link: { + href: generateBladeLink(BladeType.Metrics), + textTKey: "MetricsBlade", + }, +}; + +const resizingDecisionValue: Description = { + textTKey: "ResizingDecisionText", + type: DescriptionType.Text, + link: { + href: "https://aka.ms/cosmos-db-dedicated-gateway-size", + textTKey: "ResizingDecisionLink", + }, +}; + const CosmosD4s = "Cosmos.D4s"; const CosmosD8s = "Cosmos.D8s"; const CosmosD16s = "Cosmos.D16s"; -const CosmosD32s = "Cosmos.D32s"; - -const getSKUDetails = (sku: string): string => { - if (sku === CosmosD4s) { - return "CosmosD4Details"; - } else if (sku === CosmosD8s) { - return "CosmosD8Details"; - } else if (sku === CosmosD16s) { - return "CosmosD16Details"; - } else if (sku === CosmosD32s) { - return "CosmosD32Details"; - } - return "Not Supported Yet"; -}; const onSKUChange = (newValue: InputType, currentValues: Map): Map => { currentValues.set("sku", { value: newValue }); - currentValues.set("skuDetails", { - value: { textTKey: getSKUDetails(`${newValue.toString()}`), type: DescriptionType.Text } as Description, - }); currentValues.set("costPerHour", { value: costPerHourValue }); return currentValues; }; const onNumberOfInstancesChange = ( newValue: InputType, - currentValues: Map + currentValues: Map, + baselineValues: Map ): Map => { currentValues.set("instances", { value: newValue }); - currentValues.set("warningBanner", { - value: { textTKey: "WarningBannerOnUpdate" } as Description, - hidden: false, - }); - + const dedicatedGatewayOriginallyEnabled = baselineValues.get("enableDedicatedGateway")?.value as boolean; + const baselineInstances = baselineValues.get("instances")?.value as number; + if (!dedicatedGatewayOriginallyEnabled || baselineInstances !== newValue) { + currentValues.set("warningBanner", { + value: { + textTKey: "WarningBannerOnUpdate", + link: { + href: "https://aka.ms/cosmos-db-dedicated-gateway-overview", + textTKey: "DedicatedGatewayPricing", + }, + } as Description, + hidden: false, + }); + } else { + currentValues.set("warningBanner", undefined); + } return currentValues; }; @@ -87,10 +100,11 @@ const onEnableDedicatedGatewayChange = ( if (dedicatedGatewayOriginallyEnabled === newValue) { currentValues.set("sku", baselineValues.get("sku")); currentValues.set("instances", baselineValues.get("instances")); - currentValues.set("skuDetails", baselineValues.get("skuDetails")); currentValues.set("costPerHour", baselineValues.get("costPerHour")); currentValues.set("warningBanner", baselineValues.get("warningBanner")); currentValues.set("connectionString", baselineValues.get("connectionString")); + currentValues.set("metricsString", baselineValues.get("metricsString")); + currentValues.set("resizingDecisionString", baselineValues.get("resizingDecisionString")); return currentValues; } @@ -100,7 +114,7 @@ const onEnableDedicatedGatewayChange = ( value: { textTKey: "WarningBannerOnUpdate", link: { - href: "https://docs.microsoft.com/en-us/azure/cosmos-db/introduction", + href: "https://aka.ms/cosmos-db-dedicated-gateway-pricing", textTKey: "DedicatedGatewayPricing", }, } as Description, @@ -111,7 +125,7 @@ const onEnableDedicatedGatewayChange = ( value: { textTKey: "WarningBannerOnDelete", link: { - href: "https://docs.microsoft.com/en-us/azure/cosmos-db/introduction", + href: "https://aka.ms/cosmos-db-dedicated-gateway-overview", textTKey: "DeprovisioningDetailsText", }, } as Description, @@ -132,18 +146,22 @@ const onEnableDedicatedGatewayChange = ( disabled: dedicatedGatewayOriginallyEnabled, }); - currentValues.set("skuDetails", { - value: { textTKey: getSKUDetails(`${currentValues.get("sku").value}`), type: DescriptionType.Text } as Description, - hidden: hideAttributes, - disabled: dedicatedGatewayOriginallyEnabled, - }); - currentValues.set("costPerHour", { value: costPerHourValue, hidden: hideAttributes }); currentValues.set("connectionString", { value: connectionStringValue, hidden: !newValue || !dedicatedGatewayOriginallyEnabled, }); + currentValues.set("metricsString", { + value: metricsStringValue, + hidden: !newValue || !dedicatedGatewayOriginallyEnabled, + }); + + currentValues.set("resizingDecisionString", { + value: resizingDecisionValue, + hidden: !newValue || !dedicatedGatewayOriginallyEnabled, + }); + return currentValues; }; @@ -151,7 +169,6 @@ const skuDropDownItems: ChoiceItem[] = [ { labelTKey: "CosmosD4s", key: CosmosD4s }, { labelTKey: "CosmosD8s", key: CosmosD8s }, { labelTKey: "CosmosD16s", key: CosmosD16s }, - { labelTKey: "CosmosD32s", key: CosmosD32s }, ]; const getSkus = async (): Promise => { @@ -184,7 +201,6 @@ export default class SqlX extends SelfServeBaseClass { currentValues.set("warningBanner", undefined); - //TODO : Add try catch for each RP call and return relevant notifications if (dedicatedGatewayOriginallyEnabled) { if (!dedicatedGatewayCurrentlyEnabled) { const operationStatusUrl = await deleteDedicatedGatewayResource(); @@ -206,9 +222,11 @@ export default class SqlX extends SelfServeBaseClass { }, }; } else { - // Check for scaling up/down/in/out + const sku = currentValues.get("sku")?.value as string; + const instances = currentValues.get("instances").value as number; + const operationStatusUrl = await updateDedicatedGatewayResource(sku, instances); return { - operationStatusUrl: undefined, + operationStatusUrl: operationStatusUrl, portalNotification: { initialize: { titleTKey: "UpdateInitializeTitle", @@ -255,24 +273,37 @@ export default class SqlX extends SelfServeBaseClass { defaults.set("enableDedicatedGateway", { value: false }); defaults.set("sku", { value: CosmosD4s, hidden: true }); defaults.set("instances", { value: await getInstancesMin(), hidden: true }); - defaults.set("skuDetails", undefined); defaults.set("costPerHour", undefined); defaults.set("connectionString", undefined); + defaults.set("metricsString", { + value: undefined, + hidden: true, + }); + defaults.set("resizingDecisionString", { + value: undefined, + hidden: true, + }); const response = await getCurrentProvisioningState(); if (response.status && response.status !== "Deleting") { defaults.set("enableDedicatedGateway", { value: true }); defaults.set("sku", { value: response.sku, disabled: true }); - defaults.set("instances", { value: response.instances, disabled: true }); + defaults.set("instances", { value: response.instances, disabled: false }); defaults.set("costPerHour", { value: costPerHourValue }); - defaults.set("skuDetails", { - value: { textTKey: getSKUDetails(`${defaults.get("sku").value}`), type: DescriptionType.Text } as Description, - hidden: false, - }); defaults.set("connectionString", { value: connectionStringValue, hidden: false, }); + + defaults.set("metricsString", { + value: metricsStringValue, + hidden: false, + }); + + defaults.set("resizingDecisionString", { + value: resizingDecisionValue, + hidden: false, + }); } defaults.set("warningBanner", undefined); @@ -289,7 +320,7 @@ export default class SqlX extends SelfServeBaseClass { textTKey: "DedicatedGatewayDescription", type: DescriptionType.Text, link: { - href: "https://docs.microsoft.com/en-us/azure/cosmos-db/introduction", + href: "https://aka.ms/cosmos-db-dedicated-gateway-overview", textTKey: "LearnAboutDedicatedGateway", }, }, @@ -312,12 +343,6 @@ export default class SqlX extends SelfServeBaseClass { }) sku: ChoiceItem; - @Values({ - labelTKey: "SKUDetails", - isDynamicDescription: true, - }) - skuDetails: string; - @OnChange(onNumberOfInstancesChange) @Values({ labelTKey: "NumberOfInstances", @@ -328,6 +353,16 @@ export default class SqlX extends SelfServeBaseClass { }) instances: number; + @Values({ + description: metricsStringValue, + }) + metricsString: string; + + @Values({ + description: resizingDecisionValue, + }) + resizingDecisionString: string; + @Values({ labelTKey: "Cost", isDynamicDescription: true,