From c7b89c80e172bda07b37a149f32207c8eaa0911e Mon Sep 17 00:00:00 2001 From: Ryan Palmer Date: Tue, 6 Jan 2026 14:54:43 +0000 Subject: [PATCH 01/21] Sketched out Table resource --- src/Farmer/Arm/LogAnalytics.fs | 55 ++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/src/Farmer/Arm/LogAnalytics.fs b/src/Farmer/Arm/LogAnalytics.fs index 95353959e..f28256065 100644 --- a/src/Farmer/Arm/LogAnalytics.fs +++ b/src/Farmer/Arm/LogAnalytics.fs @@ -6,6 +6,61 @@ open Farmer let workspaces = ResourceType("Microsoft.OperationalInsights/workspaces", "2020-03-01-preview") +let tables = + ResourceType("Microsoft.OperationalInsights/workspaces/tables", "2023-09-01") + +type Column = { + Name: string + Type: string +} + +type Plan = + | Analytics of RetentionInDays : int option + | Auxiliary + | Basic + with + member this.ArmValue = + match this with + | Analytics _ -> "Analytics" + | Auxiliary -> "Auxiliary" + | Basic -> "Basic" + + member this.RetentionInDays = + match this with + | Analytics days -> + match days with + | Some d -> Some d + | None -> Some -1 + | Auxiliary -> None + | Basic -> None + +type Table = { + Name: ResourceName + Plan: Plan + Columns: Column list + TotalRetentionInDays: int option + LogAnalyticsWorkspace: ResourceId +} with + interface IArmResource with + member this.ResourceId = tables.resourceId this.Name + member this.JsonModel = {| + tables.Create(this.LogAnalyticsWorkspace.Name/this.Name, dependsOn = [ this.LogAnalyticsWorkspace ]) with + properties = {| + plan = this.Plan.ArmValue + retentionInDays = this.Plan.RetentionInDays |> Option.toNullable + totalRetentionInDays = this.TotalRetentionInDays |> Option.defaultValue -1 + schema = {| + name = this.Name.Value + columns = + this.Columns + |> List.map (fun c -> {| + name = c.Name + ``type`` = c.Type + |}) + |} + |} + |} + type Workspace = { Name: ResourceName Location: Location From ec5671eda480a3bc361d8f105e6db0d3b45278cf Mon Sep 17 00:00:00 2001 From: Ryan Palmer Date: Tue, 6 Jan 2026 16:45:28 +0000 Subject: [PATCH 02/21] Table config and builder --- src/Farmer/Arm/LogAnalytics.fs | 2 +- src/Farmer/Builders/Builders.LogAnalytics.fs | 48 ++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/Farmer/Arm/LogAnalytics.fs b/src/Farmer/Arm/LogAnalytics.fs index f28256065..548a7f887 100644 --- a/src/Farmer/Arm/LogAnalytics.fs +++ b/src/Farmer/Arm/LogAnalytics.fs @@ -42,7 +42,7 @@ type Table = { LogAnalyticsWorkspace: ResourceId } with interface IArmResource with - member this.ResourceId = tables.resourceId this.Name + member this.ResourceId = tables.resourceId (this.LogAnalyticsWorkspace.Name/this.Name) member this.JsonModel = {| tables.Create(this.LogAnalyticsWorkspace.Name/this.Name, dependsOn = [ this.LogAnalyticsWorkspace ]) with properties = {| diff --git a/src/Farmer/Builders/Builders.LogAnalytics.fs b/src/Farmer/Builders/Builders.LogAnalytics.fs index d03274590..581ead3ee 100644 --- a/src/Farmer/Builders/Builders.LogAnalytics.fs +++ b/src/Farmer/Builders/Builders.LogAnalytics.fs @@ -9,6 +9,26 @@ let private (|InBounds|OutOfBounds|) days = elif days > 730 then OutOfBounds days else InBounds days +type TableConfig = { + Name: ResourceName + Plan: Plan + Columns: Column list + TotalRetentionInDays: int option + LogAnalyticsWorkspace: ResourceId +} with + interface IBuilder with + member this.ResourceId = tables.resourceId (this.LogAnalyticsWorkspace.Name/this.Name) + member this.BuildResources _ = [ + let t : Table = { + Name = this.Name + Plan = this.Plan + Columns = this.Columns + TotalRetentionInDays = this.TotalRetentionInDays + LogAnalyticsWorkspace = this.LogAnalyticsWorkspace + } + t + ] + type WorkspaceConfig = { Name: ResourceName RetentionPeriod: int option @@ -39,6 +59,34 @@ type WorkspaceConfig = { } ] +type TableBuilder() = + member _.Yield _ = { + Name = ResourceName.Empty + Plan = Basic + Columns = [] + TotalRetentionInDays = None + LogAnalyticsWorkspace = ResourceId.Empty + } + /// Sets the name of the Log Analytics table. + [] + member _.Name(state: TableConfig, name) = { state with Name = ResourceName name } + /// Sets the plan of the Log Analytics table. + [] + member _.Plan(state: TableConfig, plan) = { state with Plan = plan } + /// Sets the columns of the Log Analytics table. + [] + member _.Columns(state: TableConfig, columns) = { state with Columns = columns } + /// Sets the total retention period of the Log Analytics table. + [] + member _.TotalRetentionInDays(state: TableConfig, days) = { state with TotalRetentionInDays = Some days } + /// Sets the Log Analytics workspace for the table. + [] + member _.LogAnalyticsWorkspace(state: TableConfig, workspaceId : ResourceId) = + if workspaceId.Type.Type <> Arm.LogAnalytics.workspaces.Type then + raiseFarmer $"given resource was not of type '{Arm.LogAnalytics.workspaces.Type}'." + { state with LogAnalyticsWorkspace = workspaceId } + + type WorkspaceBuilder() = member _.Yield _ = { Name = ResourceName.Empty From e9a989a4b998ba2bcc890623f11935260992593d Mon Sep 17 00:00:00 2001 From: Ryan Palmer Date: Tue, 6 Jan 2026 17:00:56 +0000 Subject: [PATCH 03/21] Submit tables as children of workspace --- src/Farmer/Builders/Builders.ContainerApps.fs | 1 + src/Farmer/Builders/Builders.LogAnalytics.fs | 61 ++++++------------- 2 files changed, 21 insertions(+), 41 deletions(-) diff --git a/src/Farmer/Builders/Builders.ContainerApps.fs b/src/Farmer/Builders/Builders.ContainerApps.fs index c9e07b037..f5df33358 100644 --- a/src/Farmer/Builders/Builders.ContainerApps.fs +++ b/src/Farmer/Builders/Builders.ContainerApps.fs @@ -108,6 +108,7 @@ type ContainerEnvironmentConfig = { IngestionSupport = None QuerySupport = None DailyCap = None + Tables = [] Tags = Map.empty } :> IBuilder diff --git a/src/Farmer/Builders/Builders.LogAnalytics.fs b/src/Farmer/Builders/Builders.LogAnalytics.fs index 581ead3ee..02961101c 100644 --- a/src/Farmer/Builders/Builders.LogAnalytics.fs +++ b/src/Farmer/Builders/Builders.LogAnalytics.fs @@ -14,20 +14,16 @@ type TableConfig = { Plan: Plan Columns: Column list TotalRetentionInDays: int option - LogAnalyticsWorkspace: ResourceId } with - interface IBuilder with - member this.ResourceId = tables.resourceId (this.LogAnalyticsWorkspace.Name/this.Name) - member this.BuildResources _ = [ - let t : Table = { - Name = this.Name - Plan = this.Plan - Columns = this.Columns - TotalRetentionInDays = this.TotalRetentionInDays - LogAnalyticsWorkspace = this.LogAnalyticsWorkspace - } - t - ] + member this.BuildResources logAnalyticsWorkspace = [ + { + Name = this.Name + Plan = this.Plan + Columns = this.Columns + TotalRetentionInDays = this.TotalRetentionInDays + LogAnalyticsWorkspace = logAnalyticsWorkspace + } :> IArmResource + ] type WorkspaceConfig = { Name: ResourceName @@ -35,6 +31,7 @@ type WorkspaceConfig = { IngestionSupport: FeatureFlag option QuerySupport: FeatureFlag option DailyCap: int option + Tables : TableConfig list Tags: Map } with @@ -57,36 +54,10 @@ type WorkspaceConfig = { DailyCap = this.DailyCap Tags = this.Tags } + for table in this.Tables do + yield! table.BuildResources (this :> IBuilder).ResourceId ] -type TableBuilder() = - member _.Yield _ = { - Name = ResourceName.Empty - Plan = Basic - Columns = [] - TotalRetentionInDays = None - LogAnalyticsWorkspace = ResourceId.Empty - } - /// Sets the name of the Log Analytics table. - [] - member _.Name(state: TableConfig, name) = { state with Name = ResourceName name } - /// Sets the plan of the Log Analytics table. - [] - member _.Plan(state: TableConfig, plan) = { state with Plan = plan } - /// Sets the columns of the Log Analytics table. - [] - member _.Columns(state: TableConfig, columns) = { state with Columns = columns } - /// Sets the total retention period of the Log Analytics table. - [] - member _.TotalRetentionInDays(state: TableConfig, days) = { state with TotalRetentionInDays = Some days } - /// Sets the Log Analytics workspace for the table. - [] - member _.LogAnalyticsWorkspace(state: TableConfig, workspaceId : ResourceId) = - if workspaceId.Type.Type <> Arm.LogAnalytics.workspaces.Type then - raiseFarmer $"given resource was not of type '{Arm.LogAnalytics.workspaces.Type}'." - { state with LogAnalyticsWorkspace = workspaceId } - - type WorkspaceBuilder() = member _.Yield _ = { Name = ResourceName.Empty @@ -94,6 +65,7 @@ type WorkspaceBuilder() = DailyCap = None IngestionSupport = None QuerySupport = None + Tables = [] Tags = Map.empty } @@ -135,6 +107,13 @@ type WorkspaceBuilder() = [] member _.DailyCap(state: WorkspaceConfig, cap) = { state with DailyCap = Some cap } + /// Adds tables to the Log Analytics workspace. + [] + member _.Tables(state: WorkspaceConfig, tables: TableConfig list) = { + state with + Tables = tables + } + interface ITaggable with member _.Add state tags = { state with From 0124d005d3816cd7e08c5b91cf43d98e19358a40 Mon Sep 17 00:00:00 2001 From: Ryan Palmer Date: Tue, 6 Jan 2026 18:07:32 +0000 Subject: [PATCH 04/21] Unit test for table creation --- src/Tests/LogAnalytics.fs | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/Tests/LogAnalytics.fs b/src/Tests/LogAnalytics.fs index ebe24bb71..269ed0ca8 100644 --- a/src/Tests/LogAnalytics.fs +++ b/src/Tests/LogAnalytics.fs @@ -1,4 +1,4 @@ -module LogAnalytics +module LogAnalytics open Expecto open Farmer @@ -9,6 +9,7 @@ open Microsoft.Azure.Management.OperationalInsights open Microsoft.Azure.Management.OperationalInsights.Models open Microsoft.Rest open System +open Newtonsoft.Json.Linq let dummyClient = new OperationalInsightsManagementClient(Uri "http://management.azure.com", TokenCredentials "NotNullOrWhiteSpace") @@ -41,6 +42,33 @@ let tests = Expect.equal workspace.RetentionInDays (Nullable 30) "Incorrect Retention In Days" } + test "Table created under workspace resource" { + let logging = logAnalytics { + name "MyAnalytics" + tables [ + { + Name = ResourceName "MyTable" + Plan = Analytics (Some 1) + Columns = [ + { Name = "TimeGenerated"; Type = "datetime" } + { Name = "Event"; Type = "dynamic" } + ] + TotalRetentionInDays = Some 2 + } + ] + } + let deployment = arm { add_resource logging } + let table = + deployment.Template.Resources + |> List.tryFind (fun r -> + r.ResourceId.Name.Value = "MyAnalytics" + && not(r.ResourceId.Segments |> List.isEmpty) + && (r.ResourceId.Segments |> List.exactlyOne).Value = "MyTable") + |> Option.map (fun t -> t :?> Farmer.Arm.LogAnalytics.Table) + + Expect.isSome table "Table resource not found" + } + test "Ingestion and Query are disabled by default" { let workspace = logAnalytics { name "" } |> asAzureResource From 8a05cb7d93fff8fa68a376a9ec8784ebc810891e Mon Sep 17 00:00:00 2001 From: Ryan Palmer Date: Wed, 7 Jan 2026 13:41:47 +0000 Subject: [PATCH 05/21] Comprehensive table resource tests --- src/Tests/LogAnalytics.fs | 43 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/src/Tests/LogAnalytics.fs b/src/Tests/LogAnalytics.fs index 269ed0ca8..52e327acf 100644 --- a/src/Tests/LogAnalytics.fs +++ b/src/Tests/LogAnalytics.fs @@ -66,7 +66,48 @@ let tests = && (r.ResourceId.Segments |> List.exactlyOne).Value = "MyTable") |> Option.map (fun t -> t :?> Farmer.Arm.LogAnalytics.Table) - Expect.isSome table "Table resource not found" + Expect.equal (table.Value.Columns.Length) 2 "Incorrect number of columns in table" + Expect.equal (table.Value.Columns[0].Name) "TimeGenerated" "Incorrect first column name" + Expect.equal (table.Value.Columns[0].Type) "datetime" "Incorrect first column type" + Expect.equal (table.Value.Columns[1].Name) "Event" "Incorrect second column name" + Expect.equal (table.Value.Columns[1].Type) "dynamic" "Incorrect second column type" + Expect.equal (table.Value.TotalRetentionInDays) (Some 2) "Incorrect total retention in days" + Expect.equal (table.Value.Plan.ArmValue) "Analytics" "Incorrect plan type" + Expect.equal (table.Value.Plan.RetentionInDays) (Some 1) "Incorrect plan retention in days" + Expect.equal (table.Value.LogAnalyticsWorkspace.Name.Value) "MyAnalytics" "Incorrect workspace name in table resource" + } + + test "Table JSON emitted correctly" { + let logging = logAnalytics { + name "MyAnalytics" + tables [ + { + Name = ResourceName "MyTable" + Plan = Analytics (Some 1) + Columns = [ + { Name = "TimeGenerated"; Type = "datetime" } + { Name = "Event"; Type = "dynamic" } + ] + TotalRetentionInDays = Some 2 + } + ] + } + let deployment = arm { add_resource logging } + let jsonTemplate = deployment.Template |> Writer.toJson + let jobj = JObject.Parse jsonTemplate + let tableJson = jobj["resources"][1] + Expect.equal (tableJson["type"] |> string) LogAnalytics.tables.Type "Incorrect resource type" + Expect.equal (tableJson["apiVersion"] |> string) LogAnalytics.tables.ApiVersion "Incorrect api version" + Expect.equal (tableJson["dependsOn"][0] |> string) "[resourceId('Microsoft.OperationalInsights/workspaces', 'MyAnalytics')]" "Incorrect dependsOn" + Expect.equal (tableJson["name"] |> string) "MyAnalytics/MyTable" "Incorrect resource name" + Expect.equal (tableJson["properties"]["plan"] |> string) "Analytics" "Incorrect plan type" + Expect.equal (tableJson["properties"]["retentionInDays"] |> int) 1 "Incorrect plan retention in days" + Expect.equal (tableJson["properties"]["totalRetentionInDays"] |> int) 2 "Incorrect total retention in days" + let columns = tableJson["properties"].["schema"].["columns"] + Expect.equal (columns.[0]["name"] |> string) "TimeGenerated" "Incorrect first column name" + Expect.equal (columns.[0]["type"] |> string) "datetime" "Incorrect first column type" + Expect.equal (columns.[1]["name"] |> string) "Event" "Incorrect second column name" + Expect.equal (columns.[1]["type"] |> string) "dynamic" "Incorrect second column type" } test "Ingestion and Query are disabled by default" { From a9a697fe21b82cb64bc5da6b2bbc93f7ec12904a Mon Sep 17 00:00:00 2001 From: Ryan Palmer Date: Wed, 7 Jan 2026 14:05:42 +0000 Subject: [PATCH 06/21] Docs --- .../api-overview/resources/loganalytics.md | 24 ++++++++++++++++++- src/Tests/LogAnalytics.fs | 1 - 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/docs/content/api-overview/resources/loganalytics.md b/docs/content/api-overview/resources/loganalytics.md index 1101625cd..4ea3d53d9 100644 --- a/docs/content/api-overview/resources/loganalytics.md +++ b/docs/content/api-overview/resources/loganalytics.md @@ -10,8 +10,9 @@ weight: 12 The Log Analytics builder is used to create Work space instances. - Log Analytics (`Microsoft.OperationalInsights/workspaces`) +- Tables (`Microsoft.OperationalInsights/workspaces/tables`) -#### Builder Keywords +#### Log Analytics Builder Keywords | Keyword | Purpose | | ---------------- | --------------------------------------------------------------- | @@ -20,9 +21,19 @@ The Log Analytics builder is used to create Work space instances. | enable_ingestion | Enables ingestion network traffic. | | enable_query | Enables query network traffic. | | daily_cap | Specifies an upper limit on the amount of data to ingest daily. | +| tables | Defines tables to be created in the workspace. | | add_tags | Adds a set of tags to the resource | | add_tag | Adds a tag to the resource | +#### Table Builder Keywords + +| Keyword | Purpose | +| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------| +| Name | Sets the name of the table. | +| Plan | Sets the table plan. Analytics plans can set a retention period between 4 and 730. If ommited will default to the workspace retention.| +| Columns | Sets the columns of the table. Each column has a name and type. | +| TotalRetentionInDays | Sets the total retention period for the table in days, between 4 and 4383. If ommited will default to the Plan retention. | + #### Configuration Members | Member | Purpose | @@ -42,6 +53,17 @@ let myAnalytics = logAnalytics { enable_ingestion enable_query daily_cap 5 + tables [ + { + Name = ResourceName "Serilog" + Plan = Analytics (Some 30) + Columns = [ + { Name = "TimeGenerated"; Type = "datetime" } + { Name = "Event"; Type = "dynamic" } + ] + TotalRetentionInDays = None + } + ] add_tag "tag1" "myTestResourceFarmer" } diff --git a/src/Tests/LogAnalytics.fs b/src/Tests/LogAnalytics.fs index 52e327acf..70af51f41 100644 --- a/src/Tests/LogAnalytics.fs +++ b/src/Tests/LogAnalytics.fs @@ -4,7 +4,6 @@ open Expecto open Farmer open Farmer.Arm open Farmer.Builders -open Farmer.Helpers open Microsoft.Azure.Management.OperationalInsights open Microsoft.Azure.Management.OperationalInsights.Models open Microsoft.Rest From 1b7063fc4c30c5a4cf1cebc77a9d71a3e75c2ba2 Mon Sep 17 00:00:00 2001 From: Ryan Palmer Date: Wed, 7 Jan 2026 14:45:20 +0000 Subject: [PATCH 07/21] Table names need "_CL" appended. --- src/Farmer/Builders/Builders.LogAnalytics.fs | 2 +- src/Tests/LogAnalytics.fs | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Farmer/Builders/Builders.LogAnalytics.fs b/src/Farmer/Builders/Builders.LogAnalytics.fs index 02961101c..3a32c52f0 100644 --- a/src/Farmer/Builders/Builders.LogAnalytics.fs +++ b/src/Farmer/Builders/Builders.LogAnalytics.fs @@ -17,7 +17,7 @@ type TableConfig = { } with member this.BuildResources logAnalyticsWorkspace = [ { - Name = this.Name + Name = ResourceName $"{this.Name.Value}_CL" Plan = this.Plan Columns = this.Columns TotalRetentionInDays = this.TotalRetentionInDays diff --git a/src/Tests/LogAnalytics.fs b/src/Tests/LogAnalytics.fs index 70af51f41..ad7b25ad7 100644 --- a/src/Tests/LogAnalytics.fs +++ b/src/Tests/LogAnalytics.fs @@ -43,7 +43,7 @@ let tests = test "Table created under workspace resource" { let logging = logAnalytics { - name "MyAnalytics" + name "log-analytics" tables [ { Name = ResourceName "MyTable" @@ -60,9 +60,9 @@ let tests = let table = deployment.Template.Resources |> List.tryFind (fun r -> - r.ResourceId.Name.Value = "MyAnalytics" + r.ResourceId.Name.Value = "log-analytics" && not(r.ResourceId.Segments |> List.isEmpty) - && (r.ResourceId.Segments |> List.exactlyOne).Value = "MyTable") + && (r.ResourceId.Segments |> List.exactlyOne).Value = "MyTable_CL") |> Option.map (fun t -> t :?> Farmer.Arm.LogAnalytics.Table) Expect.equal (table.Value.Columns.Length) 2 "Incorrect number of columns in table" @@ -78,7 +78,7 @@ let tests = test "Table JSON emitted correctly" { let logging = logAnalytics { - name "MyAnalytics" + name "log-analytics" tables [ { Name = ResourceName "MyTable" @@ -98,10 +98,11 @@ let tests = Expect.equal (tableJson["type"] |> string) LogAnalytics.tables.Type "Incorrect resource type" Expect.equal (tableJson["apiVersion"] |> string) LogAnalytics.tables.ApiVersion "Incorrect api version" Expect.equal (tableJson["dependsOn"][0] |> string) "[resourceId('Microsoft.OperationalInsights/workspaces', 'MyAnalytics')]" "Incorrect dependsOn" - Expect.equal (tableJson["name"] |> string) "MyAnalytics/MyTable" "Incorrect resource name" + Expect.equal (tableJson["name"] |> string) "log-analytics/MyTable_CL" "Incorrect resource name" Expect.equal (tableJson["properties"]["plan"] |> string) "Analytics" "Incorrect plan type" Expect.equal (tableJson["properties"]["retentionInDays"] |> int) 1 "Incorrect plan retention in days" Expect.equal (tableJson["properties"]["totalRetentionInDays"] |> int) 2 "Incorrect total retention in days" + Expect.equal (tableJson["properties"].["schema"].["name"] |> string) "MyTable_CL" "Incorrect table name" let columns = tableJson["properties"].["schema"].["columns"] Expect.equal (columns.[0]["name"] |> string) "TimeGenerated" "Incorrect first column name" Expect.equal (columns.[0]["type"] |> string) "datetime" "Incorrect first column type" From 19f5689709489e9ab4faa006fd7b5f5cd52debf9 Mon Sep 17 00:00:00 2001 From: Ryan Palmer Date: Wed, 7 Jan 2026 15:58:04 +0000 Subject: [PATCH 08/21] Arm DCE model --- src/Farmer/Arm/Insights.fs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Farmer/Arm/Insights.fs b/src/Farmer/Arm/Insights.fs index 42bb32eb0..c72853531 100644 --- a/src/Farmer/Arm/Insights.fs +++ b/src/Farmer/Arm/Insights.fs @@ -6,6 +6,10 @@ open Farmer let private createComponents version = ResourceType("Microsoft.Insights/components", version) +let dces = ResourceType("Microsoft.Insights/dataCollectionEndpoints", "2023-03-11") + +let dcrs = ResourceType("Microsoft.Insights/dataCollectionRules", "2023-03-11") + /// Classic AI instance let components = createComponents "2014-04-01" /// Workspace-enabled AI instance @@ -66,4 +70,12 @@ type Components = { | Workspace resourceId -> resourceId.Eval() | Classic -> null |} - |} \ No newline at end of file + |} + +type DataCollectionEndpoint = { + Name: ResourceName + Location: Location +} with + interface IArmResource with + member this.ResourceId = dces.resourceId this.Name + member this.JsonModel = dces.Create(this.Name, this.Location) \ No newline at end of file From 92d3fbc36a275e89724a2a76f301d3f00d2a0948 Mon Sep 17 00:00:00 2001 From: Ryan Palmer Date: Wed, 7 Jan 2026 16:08:25 +0000 Subject: [PATCH 09/21] Move column def to Common as needed for DCR too. Fix tests. --- src/Farmer/Arm/LogAnalytics.fs | 5 ----- src/Farmer/Common.fs | 7 ++++++- src/Tests/LogAnalytics.fs | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Farmer/Arm/LogAnalytics.fs b/src/Farmer/Arm/LogAnalytics.fs index 548a7f887..d99c9dbbd 100644 --- a/src/Farmer/Arm/LogAnalytics.fs +++ b/src/Farmer/Arm/LogAnalytics.fs @@ -9,11 +9,6 @@ let workspaces = let tables = ResourceType("Microsoft.OperationalInsights/workspaces/tables", "2023-09-01") -type Column = { - Name: string - Type: string -} - type Plan = | Analytics of RetentionInDays : int option | Auxiliary diff --git a/src/Farmer/Common.fs b/src/Farmer/Common.fs index d82be4ae8..dfcd807fd 100644 --- a/src/Farmer/Common.fs +++ b/src/Farmer/Common.fs @@ -1,7 +1,12 @@ -namespace Farmer +namespace Farmer open System +type Column = { + Name: string + Type: string +} + type NonEmptyList<'T> = private | NonEmptyList of List<'T> diff --git a/src/Tests/LogAnalytics.fs b/src/Tests/LogAnalytics.fs index ad7b25ad7..70a66ac63 100644 --- a/src/Tests/LogAnalytics.fs +++ b/src/Tests/LogAnalytics.fs @@ -73,7 +73,7 @@ let tests = Expect.equal (table.Value.TotalRetentionInDays) (Some 2) "Incorrect total retention in days" Expect.equal (table.Value.Plan.ArmValue) "Analytics" "Incorrect plan type" Expect.equal (table.Value.Plan.RetentionInDays) (Some 1) "Incorrect plan retention in days" - Expect.equal (table.Value.LogAnalyticsWorkspace.Name.Value) "MyAnalytics" "Incorrect workspace name in table resource" + Expect.equal (table.Value.LogAnalyticsWorkspace.Name.Value) "log-analytics" "Incorrect workspace name in table resource" } test "Table JSON emitted correctly" { @@ -97,7 +97,7 @@ let tests = let tableJson = jobj["resources"][1] Expect.equal (tableJson["type"] |> string) LogAnalytics.tables.Type "Incorrect resource type" Expect.equal (tableJson["apiVersion"] |> string) LogAnalytics.tables.ApiVersion "Incorrect api version" - Expect.equal (tableJson["dependsOn"][0] |> string) "[resourceId('Microsoft.OperationalInsights/workspaces', 'MyAnalytics')]" "Incorrect dependsOn" + Expect.equal (tableJson["dependsOn"][0] |> string) "[resourceId('Microsoft.OperationalInsights/workspaces', 'log-analytics')]" "Incorrect dependsOn" Expect.equal (tableJson["name"] |> string) "log-analytics/MyTable_CL" "Incorrect resource name" Expect.equal (tableJson["properties"]["plan"] |> string) "Analytics" "Incorrect plan type" Expect.equal (tableJson["properties"]["retentionInDays"] |> int) 1 "Incorrect plan retention in days" From f33e9fd94fd82f8b3da2bc7be93b4b3783e458c6 Mon Sep 17 00:00:00 2001 From: Ryan Palmer Date: Wed, 7 Jan 2026 16:45:42 +0000 Subject: [PATCH 10/21] Sketched out DCR Arm model --- src/Farmer/Arm/Insights.fs | 55 +++++++++++++++++++++++++++++++++++++- src/Farmer/Common.fs | 7 ++++- 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/src/Farmer/Arm/Insights.fs b/src/Farmer/Arm/Insights.fs index c72853531..a95e0b7a2 100644 --- a/src/Farmer/Arm/Insights.fs +++ b/src/Farmer/Arm/Insights.fs @@ -78,4 +78,57 @@ type DataCollectionEndpoint = { } with interface IArmResource with member this.ResourceId = dces.resourceId this.Name - member this.JsonModel = dces.Create(this.Name, this.Location) \ No newline at end of file + member this.JsonModel = dces.Create(this.Name, this.Location) + +type DataFlow = { + Streams : string list + Destinations : string list + TransformKQL : string option + OutputStream : string option +} + +type DataCollectionRule = { + Name: ResourceName + Location: Location + DceResourceId : ResourceId + StreamDeclarations : Map + DataSources : Map list> + Destinations : Map list> + DataFlows : DataFlow list + Tags: Map +} with + interface IArmResource with + member this.ResourceId = dcrs.resourceId this.Name + member this.JsonModel = + {| + dcrs.Create(this.Name, this.Location, tags = this.Tags) with + properties = {| + dataCollectionEndpointId = this.DceResourceId.Eval() + streamDeclarations = + this.StreamDeclarations + |> Map.map (fun _ columns -> + {| + columns = + columns + |> List.map (fun col -> + {| + name = col.Name + ``type`` = col.Type + |} + ) + |} + ) + dataSources = this.DataSources + destinations = this.Destinations + dataFlows = + this.DataFlows + |> List.map (fun flow -> + {| + streams = flow.Streams + destinations = flow.Destinations + transformKql = flow.TransformKQL |> Option.defaultValue "source" + outputStream = flow.OutputStream |> Option.toObj + |} + ) + |} + |} \ No newline at end of file diff --git a/src/Farmer/Common.fs b/src/Farmer/Common.fs index d82be4ae8..dfcd807fd 100644 --- a/src/Farmer/Common.fs +++ b/src/Farmer/Common.fs @@ -1,7 +1,12 @@ -namespace Farmer +namespace Farmer open System +type Column = { + Name: string + Type: string +} + type NonEmptyList<'T> = private | NonEmptyList of List<'T> From a403733b53b26d3805f04bd6ff0e950d14ee39e3 Mon Sep 17 00:00:00 2001 From: Ryan Palmer Date: Wed, 7 Jan 2026 16:55:43 +0000 Subject: [PATCH 11/21] Deps --- src/Farmer/Arm/Insights.fs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Farmer/Arm/Insights.fs b/src/Farmer/Arm/Insights.fs index a95e0b7a2..2f739292b 100644 --- a/src/Farmer/Arm/Insights.fs +++ b/src/Farmer/Arm/Insights.fs @@ -91,6 +91,7 @@ type DataCollectionRule = { Name: ResourceName Location: Location DceResourceId : ResourceId + LogAnalyticsWorkspaceResourceId : ResourceId option StreamDeclarations : Map DataSources : Map list> Destinations : Map list> @@ -100,8 +101,15 @@ type DataCollectionRule = { interface IArmResource with member this.ResourceId = dcrs.resourceId this.Name member this.JsonModel = + let deps = + [ + this.DceResourceId + match this.LogAnalyticsWorkspaceResourceId with + | Some logging -> logging + | None -> () + ] {| - dcrs.Create(this.Name, this.Location, tags = this.Tags) with + dcrs.Create(this.Name, this.Location, dependsOn = deps, tags = this.Tags) with properties = {| dataCollectionEndpointId = this.DceResourceId.Eval() streamDeclarations = From d9ead10984c397d839f8d3964eebe616c2df9ad6 Mon Sep 17 00:00:00 2001 From: Ryan Palmer Date: Thu, 8 Jan 2026 11:32:07 +0000 Subject: [PATCH 12/21] DCE / DCR configs and builders --- src/Farmer/Arm/Insights.fs | 12 +-- src/Farmer/Builders/Builders.Insights.fs | 122 +++++++++++++++++++++++ src/Farmer/Farmer.fsproj | 1 + 3 files changed, 129 insertions(+), 6 deletions(-) create mode 100644 src/Farmer/Builders/Builders.Insights.fs diff --git a/src/Farmer/Arm/Insights.fs b/src/Farmer/Arm/Insights.fs index 2f739292b..b58d1e1b2 100644 --- a/src/Farmer/Arm/Insights.fs +++ b/src/Farmer/Arm/Insights.fs @@ -6,9 +6,9 @@ open Farmer let private createComponents version = ResourceType("Microsoft.Insights/components", version) -let dces = ResourceType("Microsoft.Insights/dataCollectionEndpoints", "2023-03-11") +let dataCollectionEndpoints = ResourceType("Microsoft.Insights/dataCollectionEndpoints", "2023-03-11") -let dcrs = ResourceType("Microsoft.Insights/dataCollectionRules", "2023-03-11") +let dataCollectionRules = ResourceType("Microsoft.Insights/dataCollectionRules", "2023-03-11") /// Classic AI instance let components = createComponents "2014-04-01" @@ -77,8 +77,8 @@ type DataCollectionEndpoint = { Location: Location } with interface IArmResource with - member this.ResourceId = dces.resourceId this.Name - member this.JsonModel = dces.Create(this.Name, this.Location) + member this.ResourceId = dataCollectionEndpoints.resourceId this.Name + member this.JsonModel = dataCollectionEndpoints.Create(this.Name, this.Location) type DataFlow = { Streams : string list @@ -99,7 +99,7 @@ type DataCollectionRule = { Tags: Map } with interface IArmResource with - member this.ResourceId = dcrs.resourceId this.Name + member this.ResourceId = dataCollectionRules.resourceId this.Name member this.JsonModel = let deps = [ @@ -109,7 +109,7 @@ type DataCollectionRule = { | None -> () ] {| - dcrs.Create(this.Name, this.Location, dependsOn = deps, tags = this.Tags) with + dataCollectionRules.Create(this.Name, this.Location, dependsOn = deps, tags = this.Tags) with properties = {| dataCollectionEndpointId = this.DceResourceId.Eval() streamDeclarations = diff --git a/src/Farmer/Builders/Builders.Insights.fs b/src/Farmer/Builders/Builders.Insights.fs new file mode 100644 index 000000000..47cf4c7a0 --- /dev/null +++ b/src/Farmer/Builders/Builders.Insights.fs @@ -0,0 +1,122 @@ +[] +module Farmer.Builders.Insights + +open Farmer +open Farmer.Arm.Insights + +type DataCollectionEndpointConfig = { + Name: ResourceName +} with + interface IBuilder with + member this.ResourceId = dces.resourceId this.Name + member this.BuildResources location = [ + { + Name = this.Name + Location = location + } + ] + +type DataCollectionRuleConfig = { + Name: ResourceName + DceResourceId : ResourceId + LogAnalyticsWorkspaceResourceId : ResourceId option + StreamDeclarations : Map + DataSources : Map list> + Destinations : Map list> + DataFlows : DataFlow list + Tags: Map +} with + interface IBuilder with + member this.ResourceId = dataCollectionRules.resourceId this.Name + + member this.BuildResources location = [ + { + Name = this.Name + Location = location + DceResourceId = this.DceResourceId + LogAnalyticsWorkspaceResourceId = this.LogAnalyticsWorkspaceResourceId + StreamDeclarations = this.StreamDeclarations + DataSources = this.DataSources + Destinations = this.Destinations + DataFlows = this.DataFlows + Tags = this.Tags + } + ] + +type DataCollectionEndpointBuilder() = + member _.Yield _ = { + Name = ResourceName.Empty + } + + member _.Run(state: DataCollectionEndpointConfig) = + state + + /// Sets the name of the Data Collection Endpoint. + [] + member _.Name(state: DataCollectionEndpointConfig, name) = + { state with Name = ResourceName name } + + +type DataCollectionRuleBuilder() = + member _.Yield _ = { + Name = ResourceName.Empty + DceResourceId = ResourceId.Empty + LogAnalyticsWorkspaceResourceId = None + StreamDeclarations = Map.empty + DataSources = Map.empty + Destinations = Map.empty + DataFlows = [] + Tags = Map.empty + } + + member _.Run(state: DataCollectionRuleConfig) = + state + + /// Sets the name of the Data Collection Rule. + [] + member _.Name(state: DataCollectionRuleConfig, name) = + { state with Name = ResourceName name } + + /// Sets the Data Collection Endpoint Resource ID. + [] + member _.DceResourceId(state: DataCollectionRuleConfig, dceResourceId: ResourceId) = + if dceResourceId.Type.Type <> Arm.Insights.dataCollectionEndpoints.Type then + raiseFarmer $"given resource was not of type '{Arm.Insights.dataCollectionEndpoints.Type}'." + { state with DceResourceId = dceResourceId } + + /// Sets the Log Analytics Workspace Resource ID. + [] + member _.LogAnalyticsWorkspaceResourceId(state: DataCollectionRuleConfig, logAnalyticsWorkspaceResourceId: ResourceId) = + if logAnalyticsWorkspaceResourceId.Type.Type <> Arm.LogAnalytics.workspaces.Type then + raiseFarmer $"given resource was not of type '{Arm.LogAnalytics.workspaces.Type}'." + { state with LogAnalyticsWorkspaceResourceId = Some logAnalyticsWorkspaceResourceId } + + /// Adds stream declarations. + [] + member _.StreamDeclarations(state: DataCollectionRuleConfig, streams: Map) = + { state with StreamDeclarations = streams } + + /// Adds data sources. + [] + member _.DataSources(state: DataCollectionRuleConfig, dataSources: Map list>) = + { state with DataSources = dataSources } + + /// Adds destinations. + [] + member _.Destinations(state: DataCollectionRuleConfig, destinations: Map list>) = + { state with Destinations = destinations } + + /// Adds data flows. + [] + member _.DataFlows(state: DataCollectionRuleConfig, dataFlows: DataFlow list) = + { state with DataFlows = dataFlows } + + interface ITaggable with + member _.Add state tags = { + state with + Tags = state.Tags |> Map.merge tags + } + +let dataCollectionEndpoint = DataCollectionEndpointBuilder() + +let dataCollectionRule = DataCollectionRuleBuilder() \ No newline at end of file diff --git a/src/Farmer/Farmer.fsproj b/src/Farmer/Farmer.fsproj index 1a05dd366..13ee06782 100644 --- a/src/Farmer/Farmer.fsproj +++ b/src/Farmer/Farmer.fsproj @@ -126,6 +126,7 @@ + From 37c7ec5b5c6328ce95b4b37def066d591faae48a Mon Sep 17 00:00:00 2001 From: Ryan Palmer Date: Thu, 8 Jan 2026 12:17:40 +0000 Subject: [PATCH 13/21] Update builder names --- src/Farmer/Builders/Builders.Insights.fs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Farmer/Builders/Builders.Insights.fs b/src/Farmer/Builders/Builders.Insights.fs index 47cf4c7a0..b777cbcfa 100644 --- a/src/Farmer/Builders/Builders.Insights.fs +++ b/src/Farmer/Builders/Builders.Insights.fs @@ -8,7 +8,7 @@ type DataCollectionEndpointConfig = { Name: ResourceName } with interface IBuilder with - member this.ResourceId = dces.resourceId this.Name + member this.ResourceId = dataCollectionEndpoints.resourceId this.Name member this.BuildResources location = [ { Name = this.Name @@ -78,33 +78,33 @@ type DataCollectionRuleBuilder() = { state with Name = ResourceName name } /// Sets the Data Collection Endpoint Resource ID. - [] - member _.DceResourceId(state: DataCollectionRuleConfig, dceResourceId: ResourceId) = + [] + member _.DataCollectionEndpoint(state: DataCollectionRuleConfig, dceResourceId: ResourceId) = if dceResourceId.Type.Type <> Arm.Insights.dataCollectionEndpoints.Type then raiseFarmer $"given resource was not of type '{Arm.Insights.dataCollectionEndpoints.Type}'." { state with DceResourceId = dceResourceId } /// Sets the Log Analytics Workspace Resource ID. - [] - member _.LogAnalyticsWorkspaceResourceId(state: DataCollectionRuleConfig, logAnalyticsWorkspaceResourceId: ResourceId) = + [] + member _.LogAnalytics(state: DataCollectionRuleConfig, logAnalyticsWorkspaceResourceId: ResourceId) = if logAnalyticsWorkspaceResourceId.Type.Type <> Arm.LogAnalytics.workspaces.Type then raiseFarmer $"given resource was not of type '{Arm.LogAnalytics.workspaces.Type}'." { state with LogAnalyticsWorkspaceResourceId = Some logAnalyticsWorkspaceResourceId } /// Adds stream declarations. [] - member _.StreamDeclarations(state: DataCollectionRuleConfig, streams: Map) = - { state with StreamDeclarations = streams } + member _.StreamDeclarations(state: DataCollectionRuleConfig, streams: List) = + { state with StreamDeclarations = streams |> List.map (fun (k, v) -> $"Custom-{k}_CL", v) |> Map.ofList } /// Adds data sources. [] - member _.DataSources(state: DataCollectionRuleConfig, dataSources: Map list>) = - { state with DataSources = dataSources } + member _.DataSources(state: DataCollectionRuleConfig, dataSources: List list>) = + { state with DataSources = dataSources |> List.map (fun (k, v) -> k, v |> List.map Map.ofList) |> Map.ofList } /// Adds destinations. [] - member _.Destinations(state: DataCollectionRuleConfig, destinations: Map list>) = - { state with Destinations = destinations } + member _.Destinations(state: DataCollectionRuleConfig, destinations: List list>) = + { state with Destinations = destinations |> List.map (fun (k, v) -> k, v |> List.map Map.ofList) |> Map.ofList } /// Adds data flows. [] From 7fec95d85b24107b11529fa3f6416d15c21472ef Mon Sep 17 00:00:00 2001 From: Ryan Palmer Date: Thu, 8 Jan 2026 12:29:44 +0000 Subject: [PATCH 14/21] dce needs empty properties element --- src/Farmer/Arm/Insights.fs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Farmer/Arm/Insights.fs b/src/Farmer/Arm/Insights.fs index b58d1e1b2..437a2b650 100644 --- a/src/Farmer/Arm/Insights.fs +++ b/src/Farmer/Arm/Insights.fs @@ -78,7 +78,9 @@ type DataCollectionEndpoint = { } with interface IArmResource with member this.ResourceId = dataCollectionEndpoints.resourceId this.Name - member this.JsonModel = dataCollectionEndpoints.Create(this.Name, this.Location) + member this.JsonModel = {| + dataCollectionEndpoints.Create(this.Name, this.Location) with properties = {||} + |} type DataFlow = { Streams : string list From 40455f094f3d6e5a7c860ff4bc8822c2c6a62009 Mon Sep 17 00:00:00 2001 From: Ryan Palmer Date: Thu, 8 Jan 2026 13:15:12 +0000 Subject: [PATCH 15/21] Format table name for stream --- src/Farmer/Builders/Builders.Insights.fs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Farmer/Builders/Builders.Insights.fs b/src/Farmer/Builders/Builders.Insights.fs index b777cbcfa..84463ab7b 100644 --- a/src/Farmer/Builders/Builders.Insights.fs +++ b/src/Farmer/Builders/Builders.Insights.fs @@ -109,7 +109,14 @@ type DataCollectionRuleBuilder() = /// Adds data flows. [] member _.DataFlows(state: DataCollectionRuleConfig, dataFlows: DataFlow list) = - { state with DataFlows = dataFlows } + let dfs = + dataFlows + |> List.map (fun df -> + { df with + Streams = df.Streams |> List.map (fun s -> $"Custom-{s}_CL") + OutputStream = df.OutputStream |> Option.map (fun s -> $"Custom-{s}_CL") + }) + { state with DataFlows = dfs } interface ITaggable with member _.Add state tags = { From 68a2e7ce34b93d7484e22b0e74337100e7a40a7d Mon Sep 17 00:00:00 2001 From: Ryan Palmer Date: Thu, 8 Jan 2026 15:26:07 +0000 Subject: [PATCH 16/21] Remove my dcr / dce as they already existed in another namespace. Updated the existing model to support Log Analytics. --- src/Farmer/Arm/Insights.fs | 75 ----------- src/Farmer/Arm/Monitor.fs | 42 +++++- src/Farmer/Builders/Builders.AzureMonitor.fs | 32 ++++- src/Farmer/Builders/Builders.Insights.fs | 129 ------------------- src/Farmer/Farmer.fsproj | 1 - 5 files changed, 66 insertions(+), 213 deletions(-) delete mode 100644 src/Farmer/Builders/Builders.Insights.fs diff --git a/src/Farmer/Arm/Insights.fs b/src/Farmer/Arm/Insights.fs index 437a2b650..42bb32eb0 100644 --- a/src/Farmer/Arm/Insights.fs +++ b/src/Farmer/Arm/Insights.fs @@ -6,10 +6,6 @@ open Farmer let private createComponents version = ResourceType("Microsoft.Insights/components", version) -let dataCollectionEndpoints = ResourceType("Microsoft.Insights/dataCollectionEndpoints", "2023-03-11") - -let dataCollectionRules = ResourceType("Microsoft.Insights/dataCollectionRules", "2023-03-11") - /// Classic AI instance let components = createComponents "2014-04-01" /// Workspace-enabled AI instance @@ -70,75 +66,4 @@ type Components = { | Workspace resourceId -> resourceId.Eval() | Classic -> null |} - |} - -type DataCollectionEndpoint = { - Name: ResourceName - Location: Location -} with - interface IArmResource with - member this.ResourceId = dataCollectionEndpoints.resourceId this.Name - member this.JsonModel = {| - dataCollectionEndpoints.Create(this.Name, this.Location) with properties = {||} - |} - -type DataFlow = { - Streams : string list - Destinations : string list - TransformKQL : string option - OutputStream : string option -} - -type DataCollectionRule = { - Name: ResourceName - Location: Location - DceResourceId : ResourceId - LogAnalyticsWorkspaceResourceId : ResourceId option - StreamDeclarations : Map - DataSources : Map list> - Destinations : Map list> - DataFlows : DataFlow list - Tags: Map -} with - interface IArmResource with - member this.ResourceId = dataCollectionRules.resourceId this.Name - member this.JsonModel = - let deps = - [ - this.DceResourceId - match this.LogAnalyticsWorkspaceResourceId with - | Some logging -> logging - | None -> () - ] - {| - dataCollectionRules.Create(this.Name, this.Location, dependsOn = deps, tags = this.Tags) with - properties = {| - dataCollectionEndpointId = this.DceResourceId.Eval() - streamDeclarations = - this.StreamDeclarations - |> Map.map (fun _ columns -> - {| - columns = - columns - |> List.map (fun col -> - {| - name = col.Name - ``type`` = col.Type - |} - ) - |} - ) - dataSources = this.DataSources - destinations = this.Destinations - dataFlows = - this.DataFlows - |> List.map (fun flow -> - {| - streams = flow.Streams - destinations = flow.Destinations - transformKql = flow.TransformKQL |> Option.defaultValue "source" - outputStream = flow.OutputStream |> Option.toObj - |} - ) - |} |} \ No newline at end of file diff --git a/src/Farmer/Arm/Monitor.fs b/src/Farmer/Arm/Monitor.fs index 14e6ec1af..c5cba9649 100644 --- a/src/Farmer/Arm/Monitor.fs +++ b/src/Farmer/Arm/Monitor.fs @@ -19,6 +19,7 @@ type DataCollectionEndpoint = { member this.JsonModel = {| dataCollectionEndpoints.Create(this.Name, this.Location, tags = this.Tags) with kind = string this.OsType + properties = {||} |} let dataCollectionRules = @@ -92,24 +93,43 @@ module Destinations = accountResourceId = this.AccountResourceId.Eval() |} + type LogAnalytics = { + WorkspaceResourceId: ResourceId + Name: ResourceName + } with + static member Default = { + WorkspaceResourceId = ResourceId.Empty + Name = ResourceName.Empty + } + member this.ToArmJson = {| + workspaceResourceId = this.WorkspaceResourceId.Eval() + name = this.Name.Value + |} + type Destination = { MonitoringAccounts: (MonitoringAccount list) option + LogAnalytics: (LogAnalytics list) option } with - static member Default = { MonitoringAccounts = None } + static member Default = { MonitoringAccounts = None; LogAnalytics = None } let ToArmJson (destinations: Destination) = {| monitoringAccounts = destinations.MonitoringAccounts |> Option.map (List.map (fun d -> d.ToArmJson)) |> Option.defaultValue Unchecked.defaultof<_> + logAnalytics = + destinations.LogAnalytics + |> Option.map (List.map (fun d -> d.ToArmJson)) + |> Option.defaultValue Unchecked.defaultof<_> |} type DataCollectionRule = { Name: ResourceName - OsType: OS + OsType: OS option Location: Location Endpoint: ResourceId + StreamDeclarations : Map DataFlows: (DataFlow list) option DataSources: DataSources.DataSource option Destinations: Destinations.Destination option @@ -125,9 +145,25 @@ type DataCollectionRule = { {| dataCollectionRules.Create(this.Name, this.Location, dependencies, this.Tags) with - kind = string this.OsType + kind = this.OsType |> Option.map string |> Option.defaultValue Unchecked.defaultof<_> properties = {| dataCollectionEndpointId = this.Endpoint.Eval() + streamDeclarations = + this.StreamDeclarations + |> Map.toList + |> List.map (fun (stream, columns) -> + Stream.Print stream, + {| + columns = + columns + |> List.map (fun col -> + {| + name = col.Name + ``type`` = col.Type + |} + ) + |}) + |> Map.ofList dataFlows = this.DataFlows |> Option.map (List.map (fun flow -> flow.ToArmJson)) diff --git a/src/Farmer/Builders/Builders.AzureMonitor.fs b/src/Farmer/Builders/Builders.AzureMonitor.fs index d142e6cda..68052ba9b 100644 --- a/src/Farmer/Builders/Builders.AzureMonitor.fs +++ b/src/Farmer/Builders/Builders.AzureMonitor.fs @@ -61,21 +61,34 @@ type DataSourceConfig = type DestinationConfig = | MonitoringAccounts of MonitoringAccount list + | LogAnalytics of LogAnalytics list static member BuildConfig(destinations: DestinationConfig list) : Destinations.Destination = { MonitoringAccounts = destinations |> List.tryFind (function - | MonitoringAccounts _ -> true) + | MonitoringAccounts _ -> true + | _ -> false + ) |> function | Some(MonitoringAccounts accounts) -> Some accounts - | None -> None + | _ -> None + LogAnalytics = + destinations + |> List.tryFind (function + | LogAnalytics _ -> true + | _ -> false + ) + |> function + | Some(LogAnalytics workspaces) -> Some workspaces + | _ -> None } type DataCollectionRuleConfig = { Name: ResourceName - OsType: OS + OsType: OS option Endpoint: ResourceId + StreamDeclarations: (Stream * Column list) list DataFlows: (DataFlow list) option DataSources: DataSourceConfig list Destinations: DestinationConfig list @@ -92,6 +105,7 @@ type DataCollectionRuleConfig = { OsType = this.OsType Location = location Endpoint = this.Endpoint + StreamDeclarations = this.StreamDeclarations |> Map.ofList DataFlows = this.DataFlows DataSources = match this.DataSources with @@ -109,8 +123,9 @@ type DataCollectionRuleConfig = { type DataCollectionRuleBuilder() = member _.Yield _ = { Name = ResourceName.Empty - OsType = Linux + OsType = None Endpoint = ResourceId.Empty + StreamDeclarations = [] DataFlows = None DataSources = [] Destinations = [] @@ -129,12 +144,19 @@ type DataCollectionRuleBuilder() = /// Sets the kind for the data collection rule (Windows or Linux). [] - member _.OsType(state: DataCollectionRuleConfig, osType) = { state with OsType = osType } + member _.OsType(state: DataCollectionRuleConfig, osType) = { state with OsType = Some osType } /// Sets the endpoint for the data collection rule. [] member _.Endpoint(state: DataCollectionRuleConfig, endpoint) = { state with Endpoint = endpoint } + /// Sets the stream declarations for the data collection rule. + [] + member _.StreamDeclarations(state: DataCollectionRuleConfig, streamDeclarations) = { + state with + StreamDeclarations = streamDeclarations + } + /// Sets the data flows for the data collection rule. [] member _.DataFlows(state: DataCollectionRuleConfig, dataFlows) = { diff --git a/src/Farmer/Builders/Builders.Insights.fs b/src/Farmer/Builders/Builders.Insights.fs deleted file mode 100644 index 84463ab7b..000000000 --- a/src/Farmer/Builders/Builders.Insights.fs +++ /dev/null @@ -1,129 +0,0 @@ -[] -module Farmer.Builders.Insights - -open Farmer -open Farmer.Arm.Insights - -type DataCollectionEndpointConfig = { - Name: ResourceName -} with - interface IBuilder with - member this.ResourceId = dataCollectionEndpoints.resourceId this.Name - member this.BuildResources location = [ - { - Name = this.Name - Location = location - } - ] - -type DataCollectionRuleConfig = { - Name: ResourceName - DceResourceId : ResourceId - LogAnalyticsWorkspaceResourceId : ResourceId option - StreamDeclarations : Map - DataSources : Map list> - Destinations : Map list> - DataFlows : DataFlow list - Tags: Map -} with - interface IBuilder with - member this.ResourceId = dataCollectionRules.resourceId this.Name - - member this.BuildResources location = [ - { - Name = this.Name - Location = location - DceResourceId = this.DceResourceId - LogAnalyticsWorkspaceResourceId = this.LogAnalyticsWorkspaceResourceId - StreamDeclarations = this.StreamDeclarations - DataSources = this.DataSources - Destinations = this.Destinations - DataFlows = this.DataFlows - Tags = this.Tags - } - ] - -type DataCollectionEndpointBuilder() = - member _.Yield _ = { - Name = ResourceName.Empty - } - - member _.Run(state: DataCollectionEndpointConfig) = - state - - /// Sets the name of the Data Collection Endpoint. - [] - member _.Name(state: DataCollectionEndpointConfig, name) = - { state with Name = ResourceName name } - - -type DataCollectionRuleBuilder() = - member _.Yield _ = { - Name = ResourceName.Empty - DceResourceId = ResourceId.Empty - LogAnalyticsWorkspaceResourceId = None - StreamDeclarations = Map.empty - DataSources = Map.empty - Destinations = Map.empty - DataFlows = [] - Tags = Map.empty - } - - member _.Run(state: DataCollectionRuleConfig) = - state - - /// Sets the name of the Data Collection Rule. - [] - member _.Name(state: DataCollectionRuleConfig, name) = - { state with Name = ResourceName name } - - /// Sets the Data Collection Endpoint Resource ID. - [] - member _.DataCollectionEndpoint(state: DataCollectionRuleConfig, dceResourceId: ResourceId) = - if dceResourceId.Type.Type <> Arm.Insights.dataCollectionEndpoints.Type then - raiseFarmer $"given resource was not of type '{Arm.Insights.dataCollectionEndpoints.Type}'." - { state with DceResourceId = dceResourceId } - - /// Sets the Log Analytics Workspace Resource ID. - [] - member _.LogAnalytics(state: DataCollectionRuleConfig, logAnalyticsWorkspaceResourceId: ResourceId) = - if logAnalyticsWorkspaceResourceId.Type.Type <> Arm.LogAnalytics.workspaces.Type then - raiseFarmer $"given resource was not of type '{Arm.LogAnalytics.workspaces.Type}'." - { state with LogAnalyticsWorkspaceResourceId = Some logAnalyticsWorkspaceResourceId } - - /// Adds stream declarations. - [] - member _.StreamDeclarations(state: DataCollectionRuleConfig, streams: List) = - { state with StreamDeclarations = streams |> List.map (fun (k, v) -> $"Custom-{k}_CL", v) |> Map.ofList } - - /// Adds data sources. - [] - member _.DataSources(state: DataCollectionRuleConfig, dataSources: List list>) = - { state with DataSources = dataSources |> List.map (fun (k, v) -> k, v |> List.map Map.ofList) |> Map.ofList } - - /// Adds destinations. - [] - member _.Destinations(state: DataCollectionRuleConfig, destinations: List list>) = - { state with Destinations = destinations |> List.map (fun (k, v) -> k, v |> List.map Map.ofList) |> Map.ofList } - - /// Adds data flows. - [] - member _.DataFlows(state: DataCollectionRuleConfig, dataFlows: DataFlow list) = - let dfs = - dataFlows - |> List.map (fun df -> - { df with - Streams = df.Streams |> List.map (fun s -> $"Custom-{s}_CL") - OutputStream = df.OutputStream |> Option.map (fun s -> $"Custom-{s}_CL") - }) - { state with DataFlows = dfs } - - interface ITaggable with - member _.Add state tags = { - state with - Tags = state.Tags |> Map.merge tags - } - -let dataCollectionEndpoint = DataCollectionEndpointBuilder() - -let dataCollectionRule = DataCollectionRuleBuilder() \ No newline at end of file diff --git a/src/Farmer/Farmer.fsproj b/src/Farmer/Farmer.fsproj index 13ee06782..1a05dd366 100644 --- a/src/Farmer/Farmer.fsproj +++ b/src/Farmer/Farmer.fsproj @@ -126,7 +126,6 @@ - From 5b9e9e0d8eefec29ef248b43f048d57d29263018 Mon Sep 17 00:00:00 2001 From: Ryan Palmer Date: Thu, 8 Jan 2026 16:47:21 +0000 Subject: [PATCH 17/21] DU for column types. --- src/Farmer/Arm/LogAnalytics.fs | 2 +- src/Farmer/Arm/Monitor.fs | 2 +- src/Farmer/Builders/Builders.ContainerApps.fs | 2 +- src/Farmer/Builders/Builders.LogAnalytics.fs | 12 ++++---- src/Farmer/Common.fs | 29 ++++++++++++++++++- src/Tests/Gallery.fs | 1 + src/Tests/LogAnalytics.fs | 16 +++++----- src/Tests/PostgreSQL.fs | 1 + 8 files changed, 47 insertions(+), 18 deletions(-) diff --git a/src/Farmer/Arm/LogAnalytics.fs b/src/Farmer/Arm/LogAnalytics.fs index d99c9dbbd..abb842e72 100644 --- a/src/Farmer/Arm/LogAnalytics.fs +++ b/src/Farmer/Arm/LogAnalytics.fs @@ -50,7 +50,7 @@ type Table = { this.Columns |> List.map (fun c -> {| name = c.Name - ``type`` = c.Type + ``type`` = c.Type.ArmValue |}) |} |} diff --git a/src/Farmer/Arm/Monitor.fs b/src/Farmer/Arm/Monitor.fs index c5cba9649..0b92c1650 100644 --- a/src/Farmer/Arm/Monitor.fs +++ b/src/Farmer/Arm/Monitor.fs @@ -159,7 +159,7 @@ type DataCollectionRule = { |> List.map (fun col -> {| name = col.Name - ``type`` = col.Type + ``type`` = col.Type.ArmValue |} ) |}) diff --git a/src/Farmer/Builders/Builders.ContainerApps.fs b/src/Farmer/Builders/Builders.ContainerApps.fs index f5df33358..2b2b8f3f0 100644 --- a/src/Farmer/Builders/Builders.ContainerApps.fs +++ b/src/Farmer/Builders/Builders.ContainerApps.fs @@ -108,7 +108,7 @@ type ContainerEnvironmentConfig = { IngestionSupport = None QuerySupport = None DailyCap = None - Tables = [] + CustomTables = [] Tags = Map.empty } :> IBuilder diff --git a/src/Farmer/Builders/Builders.LogAnalytics.fs b/src/Farmer/Builders/Builders.LogAnalytics.fs index 3a32c52f0..ced75616f 100644 --- a/src/Farmer/Builders/Builders.LogAnalytics.fs +++ b/src/Farmer/Builders/Builders.LogAnalytics.fs @@ -31,7 +31,7 @@ type WorkspaceConfig = { IngestionSupport: FeatureFlag option QuerySupport: FeatureFlag option DailyCap: int option - Tables : TableConfig list + CustomTables : TableConfig list Tags: Map } with @@ -54,7 +54,7 @@ type WorkspaceConfig = { DailyCap = this.DailyCap Tags = this.Tags } - for table in this.Tables do + for table in this.CustomTables do yield! table.BuildResources (this :> IBuilder).ResourceId ] @@ -65,7 +65,7 @@ type WorkspaceBuilder() = DailyCap = None IngestionSupport = None QuerySupport = None - Tables = [] + CustomTables = [] Tags = Map.empty } @@ -108,10 +108,10 @@ type WorkspaceBuilder() = member _.DailyCap(state: WorkspaceConfig, cap) = { state with DailyCap = Some cap } /// Adds tables to the Log Analytics workspace. - [] - member _.Tables(state: WorkspaceConfig, tables: TableConfig list) = { + [] + member _.CustomTables(state: WorkspaceConfig, customTables: TableConfig list) = { state with - Tables = tables + CustomTables = customTables } interface ITaggable with diff --git a/src/Farmer/Common.fs b/src/Farmer/Common.fs index dfcd807fd..168818c47 100644 --- a/src/Farmer/Common.fs +++ b/src/Farmer/Common.fs @@ -1,10 +1,37 @@ namespace Farmer open System +(* +'boolean' +'datetime' +'dynamic' +'int' +'long' +'real' +'string' +*) +type ColumnType = + | Boolean + | DateTime + | Dynamic + | Int + | Long + | Real + | String + with + member this.ArmValue = + match this with + | Boolean -> "boolean" + | DateTime -> "datetime" + | Dynamic -> "dynamic" + | Int -> "int" + | Long -> "long" + | Real -> "real" + | String -> "string" type Column = { Name: string - Type: string + Type: ColumnType } type NonEmptyList<'T> = diff --git a/src/Tests/Gallery.fs b/src/Tests/Gallery.fs index da7d73ebc..164cfa084 100644 --- a/src/Tests/Gallery.fs +++ b/src/Tests/Gallery.fs @@ -6,6 +6,7 @@ open Farmer open Farmer.Builders open Farmer.Arm.Gallery open Newtonsoft.Json.Linq +open System let tests = testList "Image Gallery" [ diff --git a/src/Tests/LogAnalytics.fs b/src/Tests/LogAnalytics.fs index 70a66ac63..84cce1230 100644 --- a/src/Tests/LogAnalytics.fs +++ b/src/Tests/LogAnalytics.fs @@ -44,13 +44,13 @@ let tests = test "Table created under workspace resource" { let logging = logAnalytics { name "log-analytics" - tables [ + custom_tables [ { Name = ResourceName "MyTable" Plan = Analytics (Some 1) Columns = [ - { Name = "TimeGenerated"; Type = "datetime" } - { Name = "Event"; Type = "dynamic" } + { Name = "TimeGenerated"; Type = ColumnType.DateTime } + { Name = "Event"; Type = ColumnType.Dynamic } ] TotalRetentionInDays = Some 2 } @@ -67,9 +67,9 @@ let tests = Expect.equal (table.Value.Columns.Length) 2 "Incorrect number of columns in table" Expect.equal (table.Value.Columns[0].Name) "TimeGenerated" "Incorrect first column name" - Expect.equal (table.Value.Columns[0].Type) "datetime" "Incorrect first column type" + Expect.equal (table.Value.Columns[0].Type) ColumnType.DateTime "Incorrect first column type" Expect.equal (table.Value.Columns[1].Name) "Event" "Incorrect second column name" - Expect.equal (table.Value.Columns[1].Type) "dynamic" "Incorrect second column type" + Expect.equal (table.Value.Columns[1].Type) ColumnType.Dynamic "Incorrect second column type" Expect.equal (table.Value.TotalRetentionInDays) (Some 2) "Incorrect total retention in days" Expect.equal (table.Value.Plan.ArmValue) "Analytics" "Incorrect plan type" Expect.equal (table.Value.Plan.RetentionInDays) (Some 1) "Incorrect plan retention in days" @@ -79,13 +79,13 @@ let tests = test "Table JSON emitted correctly" { let logging = logAnalytics { name "log-analytics" - tables [ + custom_tables [ { Name = ResourceName "MyTable" Plan = Analytics (Some 1) Columns = [ - { Name = "TimeGenerated"; Type = "datetime" } - { Name = "Event"; Type = "dynamic" } + { Name = "TimeGenerated"; Type = ColumnType.DateTime } + { Name = "Event"; Type = ColumnType.Dynamic } ] TotalRetentionInDays = Some 2 } diff --git a/src/Tests/PostgreSQL.fs b/src/Tests/PostgreSQL.fs index 4335b2998..9eb51cb26 100644 --- a/src/Tests/PostgreSQL.fs +++ b/src/Tests/PostgreSQL.fs @@ -9,6 +9,7 @@ open Farmer open Farmer.Arm open Farmer.Builders open Farmer.PostgreSQL +open System type PostgresSku = { name: string From 457e5bccb7ccfbde1bd483276ca6ddb6a0a5a5d4 Mon Sep 17 00:00:00 2001 From: Ryan Palmer Date: Thu, 8 Jan 2026 16:50:09 +0000 Subject: [PATCH 18/21] tidy --- src/Farmer/Common.fs | 10 +--------- src/Tests/Gallery.fs | 1 - src/Tests/PostgreSQL.fs | 2 -- 3 files changed, 1 insertion(+), 12 deletions(-) diff --git a/src/Farmer/Common.fs b/src/Farmer/Common.fs index 168818c47..bd4b2bee8 100644 --- a/src/Farmer/Common.fs +++ b/src/Farmer/Common.fs @@ -1,15 +1,7 @@ namespace Farmer open System -(* -'boolean' -'datetime' -'dynamic' -'int' -'long' -'real' -'string' -*) + type ColumnType = | Boolean | DateTime diff --git a/src/Tests/Gallery.fs b/src/Tests/Gallery.fs index 164cfa084..8d373041a 100644 --- a/src/Tests/Gallery.fs +++ b/src/Tests/Gallery.fs @@ -1,6 +1,5 @@ module Gallery -open System open Expecto open Farmer open Farmer.Builders diff --git a/src/Tests/PostgreSQL.fs b/src/Tests/PostgreSQL.fs index 9eb51cb26..91f76077e 100644 --- a/src/Tests/PostgreSQL.fs +++ b/src/Tests/PostgreSQL.fs @@ -2,8 +2,6 @@ module PostgreSQL #nowarn "0044" // disable obsolete warning as error - needed -open System - open Expecto open Farmer open Farmer.Arm From 7dac0f23c85164fb237cac8ff031a0a091076e94 Mon Sep 17 00:00:00 2001 From: "Ryan-Laptop\\ryanp" Date: Mon, 12 Jan 2026 18:11:18 +0000 Subject: [PATCH 19/21] fantomas format --- src/Farmer/Arm/LogAnalytics.fs | 43 ++++++++++--------- src/Farmer/Arm/Monitor.fs | 21 ++++++---- src/Farmer/Builders/Builders.AzureMonitor.fs | 6 +-- src/Farmer/Builders/Builders.LogAnalytics.fs | 6 ++- src/Farmer/Common.fs | 27 ++++++------ src/Tests/LogAnalytics.fs | 44 ++++++++++++++++---- 6 files changed, 88 insertions(+), 59 deletions(-) diff --git a/src/Farmer/Arm/LogAnalytics.fs b/src/Farmer/Arm/LogAnalytics.fs index abb842e72..912dfbe46 100644 --- a/src/Farmer/Arm/LogAnalytics.fs +++ b/src/Farmer/Arm/LogAnalytics.fs @@ -10,24 +10,24 @@ let tables = ResourceType("Microsoft.OperationalInsights/workspaces/tables", "2023-09-01") type Plan = - | Analytics of RetentionInDays : int option + | Analytics of RetentionInDays: int option | Auxiliary | Basic - with - member this.ArmValue = - match this with - | Analytics _ -> "Analytics" - | Auxiliary -> "Auxiliary" - | Basic -> "Basic" - - member this.RetentionInDays = - match this with - | Analytics days -> - match days with - | Some d -> Some d - | None -> Some -1 - | Auxiliary -> None - | Basic -> None + + member this.ArmValue = + match this with + | Analytics _ -> "Analytics" + | Auxiliary -> "Auxiliary" + | Basic -> "Basic" + + member this.RetentionInDays = + match this with + | Analytics days -> + match days with + | Some d -> Some d + | None -> Some -1 + | Auxiliary -> None + | Basic -> None type Table = { Name: ResourceName @@ -35,18 +35,21 @@ type Table = { Columns: Column list TotalRetentionInDays: int option LogAnalyticsWorkspace: ResourceId -} with +} with + interface IArmResource with - member this.ResourceId = tables.resourceId (this.LogAnalyticsWorkspace.Name/this.Name) + member this.ResourceId = + tables.resourceId (this.LogAnalyticsWorkspace.Name / this.Name) + member this.JsonModel = {| - tables.Create(this.LogAnalyticsWorkspace.Name/this.Name, dependsOn = [ this.LogAnalyticsWorkspace ]) with + tables.Create(this.LogAnalyticsWorkspace.Name / this.Name, dependsOn = [ this.LogAnalyticsWorkspace ]) with properties = {| plan = this.Plan.ArmValue retentionInDays = this.Plan.RetentionInDays |> Option.toNullable totalRetentionInDays = this.TotalRetentionInDays |> Option.defaultValue -1 schema = {| name = this.Name.Value - columns = + columns = this.Columns |> List.map (fun c -> {| name = c.Name diff --git a/src/Farmer/Arm/Monitor.fs b/src/Farmer/Arm/Monitor.fs index 0b92c1650..3fd34cc31 100644 --- a/src/Farmer/Arm/Monitor.fs +++ b/src/Farmer/Arm/Monitor.fs @@ -19,7 +19,7 @@ type DataCollectionEndpoint = { member this.JsonModel = {| dataCollectionEndpoints.Create(this.Name, this.Location, tags = this.Tags) with kind = string this.OsType - properties = {||} + properties = {| |} |} let dataCollectionRules = @@ -97,10 +97,12 @@ module Destinations = WorkspaceResourceId: ResourceId Name: ResourceName } with + static member Default = { WorkspaceResourceId = ResourceId.Empty Name = ResourceName.Empty } + member this.ToArmJson = {| workspaceResourceId = this.WorkspaceResourceId.Eval() name = this.Name.Value @@ -111,7 +113,10 @@ module Destinations = LogAnalytics: (LogAnalytics list) option } with - static member Default = { MonitoringAccounts = None; LogAnalytics = None } + static member Default = { + MonitoringAccounts = None + LogAnalytics = None + } let ToArmJson (destinations: Destination) = {| monitoringAccounts = @@ -129,7 +134,7 @@ type DataCollectionRule = { OsType: OS option Location: Location Endpoint: ResourceId - StreamDeclarations : Map + StreamDeclarations: Map DataFlows: (DataFlow list) option DataSources: DataSources.DataSource option Destinations: Destinations.Destination option @@ -156,12 +161,10 @@ type DataCollectionRule = { {| columns = columns - |> List.map (fun col -> - {| - name = col.Name - ``type`` = col.Type.ArmValue - |} - ) + |> List.map (fun col -> {| + name = col.Name + ``type`` = col.Type.ArmValue + |}) |}) |> Map.ofList dataFlows = diff --git a/src/Farmer/Builders/Builders.AzureMonitor.fs b/src/Farmer/Builders/Builders.AzureMonitor.fs index 68052ba9b..c87973ef7 100644 --- a/src/Farmer/Builders/Builders.AzureMonitor.fs +++ b/src/Farmer/Builders/Builders.AzureMonitor.fs @@ -68,8 +68,7 @@ type DestinationConfig = destinations |> List.tryFind (function | MonitoringAccounts _ -> true - | _ -> false - ) + | _ -> false) |> function | Some(MonitoringAccounts accounts) -> Some accounts | _ -> None @@ -77,8 +76,7 @@ type DestinationConfig = destinations |> List.tryFind (function | LogAnalytics _ -> true - | _ -> false - ) + | _ -> false) |> function | Some(LogAnalytics workspaces) -> Some workspaces | _ -> None diff --git a/src/Farmer/Builders/Builders.LogAnalytics.fs b/src/Farmer/Builders/Builders.LogAnalytics.fs index ced75616f..be6240991 100644 --- a/src/Farmer/Builders/Builders.LogAnalytics.fs +++ b/src/Farmer/Builders/Builders.LogAnalytics.fs @@ -15,6 +15,7 @@ type TableConfig = { Columns: Column list TotalRetentionInDays: int option } with + member this.BuildResources logAnalyticsWorkspace = [ { Name = ResourceName $"{this.Name.Value}_CL" @@ -22,7 +23,8 @@ type TableConfig = { Columns = this.Columns TotalRetentionInDays = this.TotalRetentionInDays LogAnalyticsWorkspace = logAnalyticsWorkspace - } :> IArmResource + } + :> IArmResource ] type WorkspaceConfig = { @@ -31,7 +33,7 @@ type WorkspaceConfig = { IngestionSupport: FeatureFlag option QuerySupport: FeatureFlag option DailyCap: int option - CustomTables : TableConfig list + CustomTables: TableConfig list Tags: Map } with diff --git a/src/Farmer/Common.fs b/src/Farmer/Common.fs index bd4b2bee8..741071ef7 100644 --- a/src/Farmer/Common.fs +++ b/src/Farmer/Common.fs @@ -10,21 +10,18 @@ type ColumnType = | Long | Real | String - with - member this.ArmValue = - match this with - | Boolean -> "boolean" - | DateTime -> "datetime" - | Dynamic -> "dynamic" - | Int -> "int" - | Long -> "long" - | Real -> "real" - | String -> "string" - -type Column = { - Name: string - Type: ColumnType -} + + member this.ArmValue = + match this with + | Boolean -> "boolean" + | DateTime -> "datetime" + | Dynamic -> "dynamic" + | Int -> "int" + | Long -> "long" + | Real -> "real" + | String -> "string" + +type Column = { Name: string; Type: ColumnType } type NonEmptyList<'T> = private diff --git a/src/Tests/LogAnalytics.fs b/src/Tests/LogAnalytics.fs index 84cce1230..f7910b2b7 100644 --- a/src/Tests/LogAnalytics.fs +++ b/src/Tests/LogAnalytics.fs @@ -44,24 +44,33 @@ let tests = test "Table created under workspace resource" { let logging = logAnalytics { name "log-analytics" + custom_tables [ { Name = ResourceName "MyTable" - Plan = Analytics (Some 1) + Plan = Analytics(Some 1) Columns = [ - { Name = "TimeGenerated"; Type = ColumnType.DateTime } - { Name = "Event"; Type = ColumnType.Dynamic } + { + Name = "TimeGenerated" + Type = ColumnType.DateTime + } + { + Name = "Event" + Type = ColumnType.Dynamic + } ] TotalRetentionInDays = Some 2 } ] } + let deployment = arm { add_resource logging } + let table = deployment.Template.Resources |> List.tryFind (fun r -> r.ResourceId.Name.Value = "log-analytics" - && not(r.ResourceId.Segments |> List.isEmpty) + && not (r.ResourceId.Segments |> List.isEmpty) && (r.ResourceId.Segments |> List.exactlyOne).Value = "MyTable_CL") |> Option.map (fun t -> t :?> Farmer.Arm.LogAnalytics.Table) @@ -73,31 +82,48 @@ let tests = Expect.equal (table.Value.TotalRetentionInDays) (Some 2) "Incorrect total retention in days" Expect.equal (table.Value.Plan.ArmValue) "Analytics" "Incorrect plan type" Expect.equal (table.Value.Plan.RetentionInDays) (Some 1) "Incorrect plan retention in days" - Expect.equal (table.Value.LogAnalyticsWorkspace.Name.Value) "log-analytics" "Incorrect workspace name in table resource" + + Expect.equal + (table.Value.LogAnalyticsWorkspace.Name.Value) + "log-analytics" + "Incorrect workspace name in table resource" } test "Table JSON emitted correctly" { let logging = logAnalytics { name "log-analytics" + custom_tables [ { Name = ResourceName "MyTable" - Plan = Analytics (Some 1) + Plan = Analytics(Some 1) Columns = [ - { Name = "TimeGenerated"; Type = ColumnType.DateTime } - { Name = "Event"; Type = ColumnType.Dynamic } + { + Name = "TimeGenerated" + Type = ColumnType.DateTime + } + { + Name = "Event" + Type = ColumnType.Dynamic + } ] TotalRetentionInDays = Some 2 } ] } + let deployment = arm { add_resource logging } let jsonTemplate = deployment.Template |> Writer.toJson let jobj = JObject.Parse jsonTemplate let tableJson = jobj["resources"][1] Expect.equal (tableJson["type"] |> string) LogAnalytics.tables.Type "Incorrect resource type" Expect.equal (tableJson["apiVersion"] |> string) LogAnalytics.tables.ApiVersion "Incorrect api version" - Expect.equal (tableJson["dependsOn"][0] |> string) "[resourceId('Microsoft.OperationalInsights/workspaces', 'log-analytics')]" "Incorrect dependsOn" + + Expect.equal + (tableJson["dependsOn"][0] |> string) + "[resourceId('Microsoft.OperationalInsights/workspaces', 'log-analytics')]" + "Incorrect dependsOn" + Expect.equal (tableJson["name"] |> string) "log-analytics/MyTable_CL" "Incorrect resource name" Expect.equal (tableJson["properties"]["plan"] |> string) "Analytics" "Incorrect plan type" Expect.equal (tableJson["properties"]["retentionInDays"] |> int) 1 "Incorrect plan retention in days" From 330dc547fa48cd9fa955dda4f1f649d55c52e3a1 Mon Sep 17 00:00:00 2001 From: Ryan Palmer Date: Tue, 13 Jan 2026 12:18:45 +0000 Subject: [PATCH 20/21] Include KQL transform and output stream fields. Set dataSources to an empty object if not specified otherwise the DCR Visualizer doesn't work. --- src/Farmer/Arm/Monitor.fs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Farmer/Arm/Monitor.fs b/src/Farmer/Arm/Monitor.fs index 3fd34cc31..cd9013b85 100644 --- a/src/Farmer/Arm/Monitor.fs +++ b/src/Farmer/Arm/Monitor.fs @@ -65,16 +65,23 @@ type Stream = | Perf -> "Microsoft-Perf" | Syslog -> "Microsoft-Syslog" | WindowsEvent -> "Microsoft-WindowsEvent" - | CustomStream name -> name + | CustomStream tableName -> $"Custom-{tableName}_CL" type DataFlow = { Destinations: string list Streams: Stream list + TransformKQL: string option + OutputStream: Stream option } with member this.ToArmJson = {| destinations = this.Destinations streams = this.Streams |> List.map Stream.Print + transformKql = this.TransformKQL |> Option.defaultValue Unchecked.defaultof<_> + outputStream = + this.OutputStream + |> Option.map Stream.Print + |> Option.defaultValue Unchecked.defaultof<_> |} module Destinations = @@ -173,8 +180,8 @@ type DataCollectionRule = { |> Option.defaultValue Unchecked.defaultof<_> dataSources = this.DataSources - |> Option.map DataSources.ToArmJson - |> Option.defaultValue Unchecked.defaultof<_> + |> Option.defaultValue DataSources.DataSource.Default + |> DataSources.ToArmJson destinations = this.Destinations |> Option.map Destinations.ToArmJson From b7b3c05433d7a4dc9b16842266c042525a032958 Mon Sep 17 00:00:00 2001 From: Ryan Palmer Date: Tue, 13 Jan 2026 15:13:57 +0000 Subject: [PATCH 21/21] fix test --- src/Tests/DataCollection.fs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Tests/DataCollection.fs b/src/Tests/DataCollection.fs index ab4fd9c50..31333bd39 100644 --- a/src/Tests/DataCollection.fs +++ b/src/Tests/DataCollection.fs @@ -42,6 +42,8 @@ let tests = { Streams = [ (CustomStream "Microsoft-PrometheusMetrics") ] Destinations = [ "Account1" ] + TransformKQL = None + OutputStream = None } ]