From 3bd6686b9eff56ab1865def65c24e1e930011e6f Mon Sep 17 00:00:00 2001 From: isaac Date: Sat, 12 Mar 2022 17:39:02 +0100 Subject: [PATCH 1/5] App Insights now supports Log Analytics --- src/Farmer/Arm/Insights.fs | 46 ++++++++++++----- src/Farmer/Builders/Builders.AppInsights.fs | 31 ++++++++++-- src/Farmer/Builders/Builders.Functions.fs | 10 ++-- src/Farmer/Builders/Builders.WebApp.fs | 56 +++++++++++---------- src/Farmer/Types.fs | 4 +- src/Tests/AppInsights.fs | 26 ++++++++++ src/Tests/test-data/lots-of-resources.json | 2 + 7 files changed, 124 insertions(+), 51 deletions(-) diff --git a/src/Farmer/Arm/Insights.fs b/src/Farmer/Arm/Insights.fs index 0ee64a418..dd61a1ad6 100644 --- a/src/Farmer/Arm/Insights.fs +++ b/src/Farmer/Arm/Insights.fs @@ -4,6 +4,16 @@ module Farmer.Arm.Insights open Farmer let components = ResourceType("Microsoft.Insights/components", "2014-04-01") +let componentsWorkspace = ResourceType("Microsoft.Insights/components", "2020-02-02") + +/// The type of AI instance to create. +type ComponentsType = + | Classic + | Workspace of ResourceId + member this.ComponentsType = + match this with + | Classic -> components + | Workspace _ -> componentsWorkspace type Components = { Name : ResourceName @@ -11,7 +21,9 @@ type Components = LinkedWebsite : ResourceName option DisableIpMasking : bool SamplingPercentage : int - Tags: Map } + Type : ComponentsType + Tags: Map + Dependencies : ResourceId Set } interface IArmResource with member this.ResourceId = components.resourceId this.Name member this.JsonModel = @@ -19,16 +31,24 @@ type Components = match this.LinkedWebsite with | Some linkedWebsite -> this.Tags.Add($"[concat('hidden-link:', resourceGroup().id, '/providers/Microsoft.Web/sites/', '{linkedWebsite.Value}')]", "Resource") | None -> this.Tags - - {| components.Create(this.Name, this.Location, tags = tags) with - kind = "web" - properties = - {| name = this.Name.Value - Application_Type = "web" - ApplicationId = - match this.LinkedWebsite with - | Some linkedWebsite -> linkedWebsite.Value - | None -> null - DisableIpMasking = this.DisableIpMasking - SamplingPercentage = this.SamplingPercentage |} + {| this.Type.ComponentsType.Create(this.Name, this.Location, this.Dependencies, tags) with + kind = "web" + properties = + {| + name = this.Name.Value + Application_Type = "web" + ApplicationId = + match this.LinkedWebsite with + | Some linkedWebsite -> linkedWebsite.Value + | None -> null + DisableIpMasking = this.DisableIpMasking + SamplingPercentage = this.SamplingPercentage + IngestionMode = + match this.Type with + | Workspace _ -> "LogAnalytics" + | Classic -> null + WorkspaceResourceId = + match this.Type with + | Workspace resourceId -> resourceId.Eval() + | Classic -> null |} |} \ No newline at end of file diff --git a/src/Farmer/Builders/Builders.AppInsights.fs b/src/Farmer/Builders/Builders.AppInsights.fs index ddd7d87a1..35fdc5835 100644 --- a/src/Farmer/Builders/Builders.AppInsights.fs +++ b/src/Farmer/Builders/Builders.AppInsights.fs @@ -3,23 +3,27 @@ module Farmer.Builders.AppInsights open Farmer open Farmer.Arm.Insights +open Farmer.Arm.LogAnalytics type AppInsights = static member getInstrumentationKey (resourceId:ResourceId) = ArmExpression - .reference(components, resourceId) + .reference(resourceId) .Map(fun r -> r + ".InstrumentationKey") .WithOwner(resourceId) - static member getInstrumentationKey (name:ResourceName, ?resourceGroup) = - AppInsights.getInstrumentationKey(ResourceId.create (components, name, ?group = resourceGroup)) + static member getInstrumentationKey (name:ResourceName, ?resourceGroup, ?componentsType) = + let componentsType = componentsType |> Option.defaultValue components + AppInsights.getInstrumentationKey(ResourceId.create (componentsType, name, ?group = resourceGroup)) type AppInsightsConfig = { Name : ResourceName DisableIpMasking : bool SamplingPercentage : int + Type : ComponentsType + Dependencies : ResourceId Set Tags : Map } /// Gets the ARM expression path to the instrumentation key of this App Insights instance. - member this.InstrumentationKey = AppInsights.getInstrumentationKey this.Name + member this.InstrumentationKey = AppInsights.getInstrumentationKey(this.Name, componentsType = this.Type.ComponentsType) interface IBuilder with member this.ResourceId = components.resourceId this.Name member this.BuildResources location = [ @@ -28,6 +32,8 @@ type AppInsightsConfig = LinkedWebsite = None DisableIpMasking = this.DisableIpMasking SamplingPercentage = this.SamplingPercentage + Dependencies = this.Dependencies + Type = this.Type Tags = this.Tags } ] @@ -36,7 +42,10 @@ type AppInsightsBuilder() = { Name = ResourceName.Empty DisableIpMasking = false SamplingPercentage = 100 - Tags = Map.empty } + Tags = Map.empty + Dependencies = Set.empty + Type = Classic } + [] /// Sets the name of the App Insights instance. member _.Name(state:AppInsightsConfig, name) = { state with Name = ResourceName name } @@ -49,10 +58,22 @@ type AppInsightsBuilder() = /// Sets the name of the App Insights instance. member _.SamplingPercentage(state:AppInsightsConfig, samplingPercentage) = { state with SamplingPercentage = samplingPercentage } + /// Links this AI instance to a Log Analytics workspace, using the newer 2020-02-02-preview App Insights version. + [] + member _.Workspace(state:AppInsightsConfig, workspace:ResourceId) = + { state with + Type = Workspace workspace + Dependencies = state.Dependencies.Add workspace + } + member this.Workspace(state:AppInsightsConfig, workspace:WorkspaceConfig) = + this.Workspace(state, workspaces.resourceId workspace.Name) + member _.Run (state:AppInsightsConfig) = if state.SamplingPercentage > 100 then raiseFarmer "Sampling Percentage cannot be higher than 100%" elif state.SamplingPercentage <= 0 then raiseFarmer "Sampling Percentage cannot be lower than or equal to 0%" state + interface ITaggable with member _.Add state tags = { state with Tags = state.Tags |> Map.merge tags } + interface IDependable with member _.Add state resources = { state with Dependencies = state.Dependencies + resources } let appInsights = AppInsightsBuilder() \ No newline at end of file diff --git a/src/Farmer/Builders/Builders.Functions.fs b/src/Farmer/Builders/Builders.Functions.fs index f77d82ef4..c3fd8fd8a 100644 --- a/src/Farmer/Builders/Builders.Functions.fs +++ b/src/Farmer/Builders/Builders.Functions.fs @@ -15,7 +15,7 @@ open System type FunctionsRuntime = DotNet | DotNetIsolated | Node | Java | Python type VersionedFunctionsRuntime = FunctionsRuntime * string option type FunctionsRuntime with - // These values are defined on FunctionsRuntime to reduce the need for users to be aware of the distinction + // These values are defined on FunctionsRuntime to reduce the need for users to be aware of the distinction // between FunctionsRuntime and VersionedFunctionsRuntime as well as to provide parity with WebApp runtime static member DotNetCore31 = DotNet, Some "3.1" static member DotNet50 = DotNet, Some "5.0" @@ -246,12 +246,12 @@ type FunctionsConfig = HTTP20Enabled = None ClientAffinityEnabled = None WebSocketsEnabled = None - LinuxFxVersion = + LinuxFxVersion = match this.CommonWebConfig.OperatingSystem with | Windows -> None | Linux -> match this.VersionedRuntime with - | DotNet, Some version -> + | DotNet, Some version -> match Double.TryParse(version) with | true, versionNo when versionNo < 4.0 -> Some $"DOTNETCORE|{version}" | _ -> Some $"DOTNET|{version}" @@ -310,6 +310,8 @@ type FunctionsConfig = Location = location DisableIpMasking = false SamplingPercentage = 100 + Dependencies = Set.empty + Type = Classic LinkedWebsite = match this.CommonWebConfig.OperatingSystem with | Windows -> Some this.Name.ResourceName @@ -346,7 +348,7 @@ type FunctionsBuilder() = Slots = Map.empty WorkerProcess = None ZipDeployPath = None - HealthCheckPath = None + HealthCheckPath = None IpSecurityRestrictions = [] } StorageAccount = derived (fun config -> let storage = config.Name.ResourceName.Map (sprintf "%sstorage") |> sanitiseStorage |> ResourceName diff --git a/src/Farmer/Builders/Builders.WebApp.fs b/src/Farmer/Builders/Builders.WebApp.fs index 42ddfe4f4..cbf433bc4 100644 --- a/src/Farmer/Builders/Builders.WebApp.fs +++ b/src/Farmer/Builders/Builders.WebApp.fs @@ -160,27 +160,27 @@ type SlotBuilder() = member this.AddConnectionStrings(state, connectionStrings:string list) :SlotConfig = connectionStrings |> List.fold (fun state key -> this.AddConnectionString(state, key)) state - + /// Add Allowed ip for ip security restrictions - [] - member _.AllowIp(state, name, cidr:IPAddressCidr) : SlotConfig = + [] + member _.AllowIp(state, name, cidr:IPAddressCidr) : SlotConfig = { state with IpSecurityRestrictions = state.IpSecurityRestrictions @ [IpSecurityRestriction.Create name cidr Allow] } - member this.AllowIp(state, name, ip:Net.IPAddress) : SlotConfig = + member this.AllowIp(state, name, ip:Net.IPAddress) : SlotConfig = let cidr = { Address = ip; Prefix = 32 } this.AllowIp(state, name, cidr) - member this.AllowIp(state, name, ip:string) : SlotConfig = + member this.AllowIp(state, name, ip:string) : SlotConfig = let cidr = IPAddressCidr.parse ip this.AllowIp(state, name, cidr) /// Add Denied ip for ip security restrictions - [] - member _.DenyIp(state, name, cidr:IPAddressCidr) : SlotConfig = + [] + member _.DenyIp(state, name, cidr:IPAddressCidr) : SlotConfig = { state with IpSecurityRestrictions = state.IpSecurityRestrictions @ [IpSecurityRestriction.Create name cidr Deny] } - member this.DenyIp(state, name, ip:Net.IPAddress) : SlotConfig = + member this.DenyIp(state, name, ip:Net.IPAddress) : SlotConfig = let cidr = { Address = ip; Prefix = 32 } this.DenyIp(state, name, cidr) - member this.DenyIp(state, name, ip:string) : SlotConfig = + member this.DenyIp(state, name, ip:string) : SlotConfig = let cidr = IPAddressCidr.parse ip - this.DenyIp(state, name, cidr) + this.DenyIp(state, name, cidr) interface ITaggable with member _.Add state tags = { state with Tags = state.Tags |> Map.merge tags } interface IDependable with member _.Add state newDeps = { state with Dependencies = state.Dependencies + newDeps } @@ -228,7 +228,7 @@ type WebAppConfig = AutomaticLoggingExtension : bool SiteExtensions : ExtensionName Set PrivateEndpoints: (LinkedResource * string option) Set - CustomDomains : Map + CustomDomains : Map DockerPort: int option ZoneRedundant : FeatureFlag option } member this.Name = this.CommonWebConfig.Name @@ -311,9 +311,9 @@ type WebAppConfig = | Linux, Some _ | _ , None -> () - + yield! this.DockerPort |> Option.mapList AppSettings.WebsitesPort - + if this.DockerCi then "DOCKER_ENABLE_CI", "true" ] @@ -480,6 +480,8 @@ type WebAppConfig = Location = location DisableIpMasking = false SamplingPercentage = 100 + Type = Classic + Dependencies = Set.empty LinkedWebsite = match this.CommonWebConfig.OperatingSystem with | Windows -> Some this.Name.ResourceName @@ -499,7 +501,7 @@ type WebAppConfig = MaximumElasticWorkerCount = this.MaximumElasticWorkerCount OperatingSystem = this.CommonWebConfig.OperatingSystem ZoneRedundant = this.ZoneRedundant - Tags = this.Tags} + Tags = this.Tags } | _ -> () @@ -514,13 +516,13 @@ type WebAppConfig = { site with AppSettings = None; ConnectionStrings = None } // Don't deploy production slot settings as they could cause an app restart for (_,slot) in this.CommonWebConfig.Slots |> Map.toSeq do slot.ToSite site - + // Host Name Bindings must be deployed sequentially to avoid an error, as the site cannot be modified concurrently. // To do so we add a dependency to the previous binding. let mutable previousHostNameBinding = None for customDomain in this.CustomDomains |> Map.toSeq |> Seq.map snd do - let dependsOn = - match previousHostNameBinding with + let dependsOn = + match previousHostNameBinding with | Some previous -> Set.singleton previous | None -> Set.empty @@ -544,16 +546,16 @@ type WebAppConfig = hostNameBinding // Get the resource group which contains the app service plan - let aspRgName = + let aspRgName = match this.CommonWebConfig.ServicePlan with | LinkedResource linked -> linked.ResourceId.ResourceGroup | _ -> None // Create a nested resource group deployment for the certificate - this isn't strictly necessary when the app & app service plan are in the same resource group // however, when they are in different resource groups this is required to make the deployment succeed (there is an ARM bug which causes a Not Found / Conflict otherwise) // To keep the code simple, I opted to always nest the certificate deployment. - TheRSP 2021-12-14 - let certRg = resourceGroup { + let certRg = resourceGroup { name (aspRgName |> Option.defaultValue "[resourceGroup().name]") - add_resource + add_resource { cert with SiteId = Unmanaged cert.SiteId.ResourceId ServicePlanId = Unmanaged cert.ServicePlanId.ResourceId } @@ -838,7 +840,7 @@ module Extensions = let current = this.Get state connectionStrings |> List.fold (fun (state:CommonWebConfig) (key, value:ArmExpression) -> - { state with ConnectionStrings = state.ConnectionStrings.Add(key, (ExpressionSetting value, Custom)) }) current + { state with ConnectionStrings = state.ConnectionStrings.Add(key, (ExpressionSetting value, Custom)) }) current |> this.Wrap state /// Sets an app setting of the web app in the form "key" "value". [] @@ -956,16 +958,16 @@ module Extensions = /// Specifies the path Azure load balancers will ping to check for unhealthy instances. member this.HealthCheckPath(state:'T, healthCheckPath:string) = this.Map state (fun x -> {x with HealthCheckPath = Some(healthCheckPath)}) /// Add Allowed ip for ip security restrictions - [] - member this.AllowIp(state:'T, name, ip:IPAddressCidr) = + [] + member this.AllowIp(state:'T, name, ip:IPAddressCidr) = this.Map state (fun x -> { x with IpSecurityRestrictions = IpSecurityRestriction.Create name ip Allow :: x.IpSecurityRestrictions }) - member this.AllowIp(state:'T, name, ip:string) = + member this.AllowIp(state:'T, name, ip:string) = let ip = IPAddressCidr.parse ip this.Map state (fun x -> { x with IpSecurityRestrictions = IpSecurityRestriction.Create name ip Allow :: x.IpSecurityRestrictions }) /// Add Denied ip for ip security restrictions - [] - member this.DenyIp(state:'T, name, ip) = + [] + member this.DenyIp(state:'T, name, ip) = this.Map state (fun x -> { x with IpSecurityRestrictions = IpSecurityRestriction.Create name ip Deny :: x.IpSecurityRestrictions }) - member this.DenyIp(state:'T, name, ip:string) = + member this.DenyIp(state:'T, name, ip:string) = let ip = IPAddressCidr.parse ip this.Map state (fun x -> { x with IpSecurityRestrictions = IpSecurityRestriction.Create name ip Deny :: x.IpSecurityRestrictions }) diff --git a/src/Farmer/Types.fs b/src/Farmer/Types.fs index f46bdc679..2350f4779 100644 --- a/src/Farmer/Types.fs +++ b/src/Farmer/Types.fs @@ -244,8 +244,8 @@ type CertificateOptions = type DomainConfig = | SecureDomain of domain:string * cert:CertificateOptions | InsecureDomain of domain:string - member this.DomainName = - match this with + member this.DomainName = + match this with | SecureDomain (domainName,_) | InsecureDomain (domainName) -> domainName diff --git a/src/Tests/AppInsights.fs b/src/Tests/AppInsights.fs index 28772bcfe..1c883985b 100644 --- a/src/Tests/AppInsights.fs +++ b/src/Tests/AppInsights.fs @@ -3,6 +3,8 @@ module AppInsights open Expecto open Farmer open Farmer.Builders.AppInsights +open Farmer.Builders.LogAnalytics +open Newtonsoft.Json.Linq let tests = testList "AppInsights" [ test "Creates keys on an AI instance correctly" { @@ -11,8 +13,32 @@ let tests = testList "AppInsights" [ Expect.equal ai.InstrumentationKey.Value ("reference(resourceId('Microsoft.Insights/components', 'foo'), '2014-04-01').InstrumentationKey") "Incorrect Value" } + test "Creates with classic version by default" { + let deployment = arm { add_resource (appInsights { name "foo" }) } + let json = deployment.Template |> Writer.toJson |> JObject.Parse + let version = json.SelectToken("resources[?(@.name=='foo')].apiVersion").ToString() + Expect.equal version "2014-04-01" "Incorrect API version" + } + test "Create generated keys correctly" { let generatedKey = AppInsights.getInstrumentationKey(ResourceId.create(Arm.Insights.components, ResourceName "foo", "group")) Expect.equal generatedKey.Value "reference(resourceId('group', 'Microsoft.Insights/components', 'foo'), '2014-04-01').InstrumentationKey" "Incorrect generated key" } + + test "Creates LA-enabled workspace" { + let workspace = logAnalytics { name "la" } + let ai = appInsights { name "ai"; log_analytics_workspace workspace } + let deployment = arm { + add_resources [ workspace; ai ] + } + let json = deployment.Template |> Writer.toJson |> JObject.Parse + let version = json.SelectToken("resources[?(@.name=='ai')].apiVersion").ToString() + let resourceId = json.SelectToken("resources[?(@.name=='ai')].properties.WorkspaceResourceId").ToString() + let dependencies = json.SelectToken("resources[?(@.name=='ai')].dependsOn").Children() |> Seq.map string |> Seq.toArray + + Expect.equal resourceId "[resourceId('Microsoft.OperationalInsights/workspaces', 'la')]" "Incorrect workspace id" + Expect.equal version "2020-02-02-preview" "Incorrect API version" + Expect.equal ai.InstrumentationKey.Value ("reference(resourceId('Microsoft.Insights/components', 'ai'), '2020-02-02-preview').InstrumentationKey") "Incorrect Instrumentation Key reference" + Expect.sequenceEqual dependencies [ "[resourceId('Microsoft.OperationalInsights/workspaces', 'la')]" ] "Incorrect dependencies" + } ] \ No newline at end of file diff --git a/src/Tests/test-data/lots-of-resources.json b/src/Tests/test-data/lots-of-resources.json index 061382fbf..1c7556329 100644 --- a/src/Tests/test-data/lots-of-resources.json +++ b/src/Tests/test-data/lots-of-resources.json @@ -97,6 +97,7 @@ }, { "apiVersion": "2014-04-01", + "dependsOn": [], "kind": "web", "location": "northeurope", "name": "farmerwebapp1979-ai", @@ -236,6 +237,7 @@ }, { "apiVersion": "2014-04-01", + "dependsOn": [], "kind": "web", "location": "northeurope", "name": "farmerfuncs1979-ai", From 019a1ee3dde1b03166884c66b497e6061d01aa65 Mon Sep 17 00:00:00 2001 From: isaac Date: Sun, 13 Mar 2022 22:16:51 +0100 Subject: [PATCH 2/5] Add docs & samples. --- RELEASE_NOTES.md | 2 ++ .../api-overview/resources/app-insights.md | 4 +++ samples/scripts/appinsights-loganalytics.fsx | 26 +++++++++++++++++++ 3 files changed, 32 insertions(+) create mode 100644 samples/scripts/appinsights-loganalytics.fsx diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 3979a9e8b..db10e5e6c 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,7 @@ Release Notes ============= +## vNext +* Application Insights: Support for Workspace-enabled instances. ## 1.6.30 * WebApps/Functions: Allow adding IP restriction string with CIDR diff --git a/docs/content/api-overview/resources/app-insights.md b/docs/content/api-overview/resources/app-insights.md index 61ef6d4dd..949e2a6b9 100644 --- a/docs/content/api-overview/resources/app-insights.md +++ b/docs/content/api-overview/resources/app-insights.md @@ -10,6 +10,8 @@ The App Insights builder is used to create Application Insights accounts. Use th * Application Insights (`Microsoft.Insights/components`) +> This builder supports both "Classic" (standalone) and "Workspace Enabled" (Log Analytics-backed) instances of App Insights. See the `log_analytics_workspace` keyword to see how to create the latter type of instance. + #### Builder Keywords | Keyword | Purpose | @@ -17,6 +19,7 @@ The App Insights builder is used to create Application Insights accounts. Use th | name | Sets the name of the App Insights instance. | | disable_ip_masking | Disable IP masking. | | sampling_percentage | Define sampling percentage (0-100) | +| log_analytics_workspace | Use a Log Analytics workspace as the backing store for this AI instance. You can supply either a Farmer-generate Log Analytics`WorkspaceConfig` instance that exists in the same resource group, or a fully-qualified Resource ID path to that instance. This will also switch the AI instance over to creating a "workspace enabled" AI instance. | #### Configuration Members @@ -32,5 +35,6 @@ open Farmer.Builders let ai = appInsights { name "myAI" + log_analytics_workspace myWorkspace // use to activate workspace-enabled AI instances. } ``` \ No newline at end of file diff --git a/samples/scripts/appinsights-loganalytics.fsx b/samples/scripts/appinsights-loganalytics.fsx new file mode 100644 index 000000000..16b3bc6f6 --- /dev/null +++ b/samples/scripts/appinsights-loganalytics.fsx @@ -0,0 +1,26 @@ +#r @"nuget:Farmer" + +open Farmer +open Farmer.Builders + +let workspace = logAnalytics { + name "loganalytics-workspace" +} + +let myAppInsights = appInsights { + name "appInsights" + log_analytics_workspace workspace +} + +let myFunctions = functions { + name "functions-app" + link_to_app_insights myAppInsights.Name +} + +let template = arm { + location Location.NorthEurope + add_resources [ workspace; myAppInsights; myFunctions ] +} + +template +|> Deploy.execute "deleteme" Deploy.NoParameters \ No newline at end of file From 522b3cf3bb0787e0eccaf44a02fb351289e0a968 Mon Sep 17 00:00:00 2001 From: isaac Date: Sun, 13 Mar 2022 22:35:20 +0100 Subject: [PATCH 3/5] Small rename --- src/Farmer/Arm/Insights.fs | 19 +++++++++++-------- src/Farmer/Builders/Builders.AppInsights.fs | 16 ++++++++-------- src/Farmer/Builders/Builders.Functions.fs | 2 +- src/Farmer/Builders/Builders.WebApp.fs | 2 +- 4 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/Farmer/Arm/Insights.fs b/src/Farmer/Arm/Insights.fs index dd61a1ad6..3a48ce2fb 100644 --- a/src/Farmer/Arm/Insights.fs +++ b/src/Farmer/Arm/Insights.fs @@ -3,14 +3,17 @@ module Farmer.Arm.Insights open Farmer -let components = ResourceType("Microsoft.Insights/components", "2014-04-01") -let componentsWorkspace = ResourceType("Microsoft.Insights/components", "2020-02-02") +let private createComponents version = ResourceType("Microsoft.Insights/components", version) +/// Classic AI instance +let components = createComponents "2014-04-01" +/// Workspace-enabled AI instance +let componentsWorkspace = createComponents "2020-02-02" /// The type of AI instance to create. -type ComponentsType = +type InstanceKind = | Classic | Workspace of ResourceId - member this.ComponentsType = + member this.ResourceType = match this with | Classic -> components | Workspace _ -> componentsWorkspace @@ -21,7 +24,7 @@ type Components = LinkedWebsite : ResourceName option DisableIpMasking : bool SamplingPercentage : int - Type : ComponentsType + InstanceKind : InstanceKind Tags: Map Dependencies : ResourceId Set } interface IArmResource with @@ -31,7 +34,7 @@ type Components = match this.LinkedWebsite with | Some linkedWebsite -> this.Tags.Add($"[concat('hidden-link:', resourceGroup().id, '/providers/Microsoft.Web/sites/', '{linkedWebsite.Value}')]", "Resource") | None -> this.Tags - {| this.Type.ComponentsType.Create(this.Name, this.Location, this.Dependencies, tags) with + {| this.InstanceKind.ResourceType.Create(this.Name, this.Location, this.Dependencies, tags) with kind = "web" properties = {| @@ -44,11 +47,11 @@ type Components = DisableIpMasking = this.DisableIpMasking SamplingPercentage = this.SamplingPercentage IngestionMode = - match this.Type with + match this.InstanceKind with | Workspace _ -> "LogAnalytics" | Classic -> null WorkspaceResourceId = - match this.Type with + match this.InstanceKind with | Workspace resourceId -> resourceId.Eval() | Classic -> null |} |} \ No newline at end of file diff --git a/src/Farmer/Builders/Builders.AppInsights.fs b/src/Farmer/Builders/Builders.AppInsights.fs index 35fdc5835..fd5ddf62d 100644 --- a/src/Farmer/Builders/Builders.AppInsights.fs +++ b/src/Farmer/Builders/Builders.AppInsights.fs @@ -11,19 +11,19 @@ type AppInsights = .reference(resourceId) .Map(fun r -> r + ".InstrumentationKey") .WithOwner(resourceId) - static member getInstrumentationKey (name:ResourceName, ?resourceGroup, ?componentsType) = - let componentsType = componentsType |> Option.defaultValue components - AppInsights.getInstrumentationKey(ResourceId.create (componentsType, name, ?group = resourceGroup)) + static member getInstrumentationKey (name:ResourceName, ?resourceGroup, ?resourceType) = + let resourceType = resourceType |> Option.defaultValue components + AppInsights.getInstrumentationKey(ResourceId.create (resourceType, name, ?group = resourceGroup)) type AppInsightsConfig = { Name : ResourceName DisableIpMasking : bool SamplingPercentage : int - Type : ComponentsType + InstanceKind : InstanceKind Dependencies : ResourceId Set Tags : Map } /// Gets the ARM expression path to the instrumentation key of this App Insights instance. - member this.InstrumentationKey = AppInsights.getInstrumentationKey(this.Name, componentsType = this.Type.ComponentsType) + member this.InstrumentationKey = AppInsights.getInstrumentationKey(this.Name, resourceType = this.InstanceKind.ResourceType) interface IBuilder with member this.ResourceId = components.resourceId this.Name member this.BuildResources location = [ @@ -33,7 +33,7 @@ type AppInsightsConfig = DisableIpMasking = this.DisableIpMasking SamplingPercentage = this.SamplingPercentage Dependencies = this.Dependencies - Type = this.Type + InstanceKind = this.InstanceKind Tags = this.Tags } ] @@ -44,7 +44,7 @@ type AppInsightsBuilder() = SamplingPercentage = 100 Tags = Map.empty Dependencies = Set.empty - Type = Classic } + InstanceKind = Classic } [] /// Sets the name of the App Insights instance. @@ -62,7 +62,7 @@ type AppInsightsBuilder() = [] member _.Workspace(state:AppInsightsConfig, workspace:ResourceId) = { state with - Type = Workspace workspace + InstanceKind = Workspace workspace Dependencies = state.Dependencies.Add workspace } member this.Workspace(state:AppInsightsConfig, workspace:WorkspaceConfig) = diff --git a/src/Farmer/Builders/Builders.Functions.fs b/src/Farmer/Builders/Builders.Functions.fs index c3fd8fd8a..923881af6 100644 --- a/src/Farmer/Builders/Builders.Functions.fs +++ b/src/Farmer/Builders/Builders.Functions.fs @@ -311,7 +311,7 @@ type FunctionsConfig = DisableIpMasking = false SamplingPercentage = 100 Dependencies = Set.empty - Type = Classic + InstanceKind = Classic LinkedWebsite = match this.CommonWebConfig.OperatingSystem with | Windows -> Some this.Name.ResourceName diff --git a/src/Farmer/Builders/Builders.WebApp.fs b/src/Farmer/Builders/Builders.WebApp.fs index cbf433bc4..579e141d2 100644 --- a/src/Farmer/Builders/Builders.WebApp.fs +++ b/src/Farmer/Builders/Builders.WebApp.fs @@ -480,7 +480,7 @@ type WebAppConfig = Location = location DisableIpMasking = false SamplingPercentage = 100 - Type = Classic + InstanceKind = Classic Dependencies = Set.empty LinkedWebsite = match this.CommonWebConfig.OperatingSystem with From a9f32a43f5f25e71746e452c4e4876d335161432 Mon Sep 17 00:00:00 2001 From: isaac Date: Mon, 14 Mar 2022 22:53:39 +0100 Subject: [PATCH 4/5] Fix test --- src/Tests/AppInsights.fs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Tests/AppInsights.fs b/src/Tests/AppInsights.fs index 1c883985b..75e0db5a1 100644 --- a/src/Tests/AppInsights.fs +++ b/src/Tests/AppInsights.fs @@ -37,8 +37,8 @@ let tests = testList "AppInsights" [ let dependencies = json.SelectToken("resources[?(@.name=='ai')].dependsOn").Children() |> Seq.map string |> Seq.toArray Expect.equal resourceId "[resourceId('Microsoft.OperationalInsights/workspaces', 'la')]" "Incorrect workspace id" - Expect.equal version "2020-02-02-preview" "Incorrect API version" - Expect.equal ai.InstrumentationKey.Value ("reference(resourceId('Microsoft.Insights/components', 'ai'), '2020-02-02-preview').InstrumentationKey") "Incorrect Instrumentation Key reference" + Expect.equal version "2020-02-02" "Incorrect API version" + Expect.equal ai.InstrumentationKey.Value ("reference(resourceId('Microsoft.Insights/components', 'ai'), '2020-02-02').InstrumentationKey") "Incorrect Instrumentation Key reference" Expect.sequenceEqual dependencies [ "[resourceId('Microsoft.OperationalInsights/workspaces', 'la')]" ] "Incorrect dependencies" } ] \ No newline at end of file From 77c2b5b9a8ccbec2c4058e398b7a28d40cface7a Mon Sep 17 00:00:00 2001 From: isaac Date: Tue, 15 Mar 2022 14:19:08 +0100 Subject: [PATCH 5/5] Update release notes and simplify tests. --- RELEASE_NOTES.md | 4 +--- src/Tests/AppInsights.fs | 11 +++++------ 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index db10e5e6c..a6f1790b3 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,10 +1,8 @@ Release Notes ============= -## vNext -* Application Insights: Support for Workspace-enabled instances. - ## 1.6.30 * WebApps/Functions: Allow adding IP restriction string with CIDR +* Application Insights: Support for Workspace-enabled instances. ## 1.6.29 * CLI: include `--only-show-error` option when executing Azure CLI commands. diff --git a/src/Tests/AppInsights.fs b/src/Tests/AppInsights.fs index 75e0db5a1..6fa51462f 100644 --- a/src/Tests/AppInsights.fs +++ b/src/Tests/AppInsights.fs @@ -31,14 +31,13 @@ let tests = testList "AppInsights" [ let deployment = arm { add_resources [ workspace; ai ] } + let json = deployment.Template |> Writer.toJson |> JObject.Parse - let version = json.SelectToken("resources[?(@.name=='ai')].apiVersion").ToString() - let resourceId = json.SelectToken("resources[?(@.name=='ai')].properties.WorkspaceResourceId").ToString() - let dependencies = json.SelectToken("resources[?(@.name=='ai')].dependsOn").Children() |> Seq.map string |> Seq.toArray + let select query = json.SelectToken(query).ToString() - Expect.equal resourceId "[resourceId('Microsoft.OperationalInsights/workspaces', 'la')]" "Incorrect workspace id" - Expect.equal version "2020-02-02" "Incorrect API version" + Expect.equal (select "resources[?(@.name=='ai')].properties.WorkspaceResourceId") "[resourceId('Microsoft.OperationalInsights/workspaces', 'la')]" "Incorrect workspace id" + Expect.equal (select "resources[?(@.name=='ai')].apiVersion") "2020-02-02" "Incorrect API version" Expect.equal ai.InstrumentationKey.Value ("reference(resourceId('Microsoft.Insights/components', 'ai'), '2020-02-02').InstrumentationKey") "Incorrect Instrumentation Key reference" - Expect.sequenceEqual dependencies [ "[resourceId('Microsoft.OperationalInsights/workspaces', 'la')]" ] "Incorrect dependencies" + Expect.sequenceEqual (json.SelectToken("resources[?(@.name=='ai')].dependsOn").Children() |> Seq.map string |> Seq.toArray) [ "[resourceId('Microsoft.OperationalInsights/workspaces', 'la')]" ] "Incorrect dependencies" } ] \ No newline at end of file