Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Visualize Azure Function App with F# and GraphViz
(*
Reading out bindings
*)
type Direction =
| Trigger
| In
| Out
type Properties = Map<string,string>
type Binding = {
Argument:string
Direction:Direction
Type:string
Properties:Properties
}
with member this.Value key = this.Properties.TryFind key
#I "./packages/"
#r "FSharp.Data/lib/net40/FSharp.Data.dll"
open FSharp.Data
open FSharp.Data.JsonExtensions
let bindingType (``type``:string, dir:string) =
if ``type``.EndsWith "Trigger"
then
Trigger, ``type``.Replace("Trigger","")
else
if (dir = "in") then In, ``type``
elif (dir = "out") then Out, ``type``
else failwith "Unknown binding"
let extractBindings (contents:string) =
contents
|> JsonValue.Parse
|> fun elements -> elements.GetProperty "bindings"
|> JsonExtensions.AsArray
|> Array.map (fun binding ->
// retrieve the properties we care about
let ``type`` = binding?``type``.AsString()
let direction = binding?direction.AsString()
let name = binding?name.AsString()
// retrieve the "other" properties
let properties =
binding.Properties
|> Array.filter (fun (key,value) ->
key <> "type" && key <> "name" && key <> "direction")
|> Array.map (fun (key,value) -> key, value.AsString())
|> Map
// detect the direction and type
let direction, ``type`` = bindingType (``type``,direction)
// create and return a binding
{
Type = ``type``
Direction = direction
Argument = name
Properties = properties
}
)
(*
Reading out packages
*)
type Package = {
Name:string
Version:string
}
let extractDependencies (contents:string) =
contents
|> JsonValue.Parse
|> fun elements -> elements.GetProperty "frameworks"
|> fun elements -> elements.GetProperty "net46"
|> fun elements -> elements.GetProperty "dependencies"
|> fun elements -> elements.Properties
|> Array.map (fun (package,version) ->
{
Name = package
Version = version.AsString()
}
)
(*
Extracting all the data from a root folder
*)
open System.IO
let candidates root =
root
|> Directory.EnumerateDirectories
|> Seq.filter (fun dir ->
Directory.EnumerateFiles(dir)
|> Seq.map FileInfo
|> Seq.exists (fun file -> file.Name = "function.json")
)
|> Seq.map DirectoryInfo
type AppGraph = {
Bindings: (string * Binding) []
Dependencies: (string * Package) []
}
let extractGraph (root:string) =
let functions = candidates root
let bindings =
functions
|> Seq.map (fun dir ->
let functionName = dir.Name
Path.Combine (dir.FullName,"function.json")
|> File.ReadAllText
|> extractBindings
|> Array.map (fun binding ->
functionName, binding)
)
|> Seq.collect id
|> Seq.toArray
let dependencies =
functions
|> Seq.map (fun dir ->
let functionName = dir.Name
let project = Path.Combine (dir.FullName,"project.json")
if File.Exists project
then
project
|> File.ReadAllText
|> extractDependencies
|> Array.map (fun package ->
functionName, package)
else Array.empty
)
|> Seq.collect id
|> Seq.toArray
{
Bindings = bindings
Dependencies = dependencies
}
(*
Rendering the graph
*)
let quoted (text:string) = sprintf "\"%s\"" text
let indent = " "
let bindingDescription (binding:Binding) =
match binding.Type with
| "timer" -> "Timer"
| "queue" -> "Queue " + (binding.Properties.["queueName"])
| "blob" -> "Blob " + (binding.Properties.["path"])
| _ -> binding.Type
|> quoted
let packageDescription (package:Package) =
sprintf "%s (%s)" package.Name package.Version
|> quoted
let functionDescription = quoted
let renderFunctionNodes format (graph:AppGraph) =
let functionNames =
graph.Bindings
|> Seq.map (fst >> functionDescription)
|> Seq.distinct
Seq.append
[ format ]
functionNames
|> Seq.map (fun name -> indent + name)
|> String.concat "\n"
let renderBindingNodes format (graph:AppGraph) =
let bindingNames =
graph.Bindings
|> Seq.map (snd >> bindingDescription)
|> Seq.distinct
Seq.append
[ format ]
bindingNames
|> Seq.map (fun name -> indent + name)
|> String.concat "\n"
let renderPackageNodes format (graph:AppGraph) =
let packageNames =
graph.Dependencies
|> Seq.map (snd >> packageDescription)
|> Seq.distinct
Seq.append
[ format ]
packageNames
|> Seq.map (fun name -> indent + name)
|> String.concat "\n"
let renderTriggers format (graph:AppGraph) =
let triggers =
graph.Bindings
|> Seq.filter (fun (_,binding) -> binding.Direction = Trigger)
|> Seq.map (fun (fn,binding) ->
sprintf "%s -> %s [ label = %s ]"
(bindingDescription binding)
(functionDescription fn)
(binding.Argument |> quoted)
)
|> Seq.distinct
Seq.append
[ format ]
triggers
|> Seq.map (fun name -> indent + name)
|> String.concat "\n"
let renderInBindings format (graph:AppGraph) =
let bindings =
graph.Bindings
|> Seq.filter (fun (_,binding) -> binding.Direction = In)
|> Seq.map (fun (fn,binding) ->
sprintf "%s -> %s [ label = %s ]"
(bindingDescription binding)
(functionDescription fn)
(binding.Argument |> quoted)
)
|> Seq.distinct
Seq.append
[ format ]
bindings
|> Seq.map (fun name -> indent + name)
|> String.concat "\n"
let renderOutBindings format (graph:AppGraph) =
let bindings =
graph.Bindings
|> Seq.filter (fun (_,binding) -> binding.Direction = Out)
|> Seq.map (fun (fn,binding) ->
sprintf "%s -> %s [ label = %s ]"
(functionDescription fn)
(bindingDescription binding)
(binding.Argument |> quoted)
)
|> Seq.distinct
Seq.append
[ format ]
bindings
|> Seq.map (fun name -> indent + name)
|> String.concat "\n"
let renderDependencies format (graph:AppGraph) =
let dependencies =
graph.Dependencies
|> Seq.map (fun (fn,package) ->
sprintf "%s -> %s"
(packageDescription package)
(functionDescription fn)
)
|> Seq.distinct
Seq.append
[ format ]
dependencies
|> Seq.map (fun name -> indent + name)
|> String.concat "\n"
type GraphFormat = {
FunctionNode:string
BindingNode:string
PackageNode:string
Trigger:string
InBinding:string
OutBinding:string
Dependency:string
}
let graphFormat = {
FunctionNode = "node [shape=doublecircle,style=filled,color=orange]"
BindingNode = "node [shape=box,style=filled,color=yellow]"
PackageNode = "node [shape=box,style=filled,color=lightblue]"
Trigger = "edge [ style=bold ]"
InBinding = "edge [ style=solid ]"
OutBinding = "edge [ style=solid ]"
Dependency = "edge [ arrowhead=none,style=dotted,dir=none ]"
}
let renderGraph (format:GraphFormat) (app:AppGraph) =
let functionNodes = renderFunctionNodes format.FunctionNode app
let bindingrNodes = renderBindingNodes format.BindingNode app
let packageNodes = renderPackageNodes format.PackageNode app
let triggers = renderTriggers format.Trigger app
let ins = renderInBindings format.InBinding app
let outs = renderOutBindings format.OutBinding app
let dependencies = renderDependencies format.Dependency app
sprintf """digraph app {
%s
%s
%s
%s
%s
%s
%s
}""" functionNodes bindingrNodes packageNodes triggers ins outs dependencies
(*
Example usage
Function app fsibot-serverless has been cloned locally
// https://github.com/mathias-brandewinder/fsibot-serverless/
*)
// location on disk
let root = @"C:/Users/Mathias Brandewinder/Documents/GitHub/fsibot-serverless/"
// generate a graphviz file
root
|> extractGraph
|> renderGraph graphFormat
|> fun content ->
File.WriteAllText(__SOURCE_DIRECTORY__ + "/fsibot2", content)
// use then graphviz command line to generate a chart, ex:
// dot "graph-file-path" -Tpng -o "output-file-path.png"
framework:net45
source https://www.nuget.org/api/v2
nuget FSharp.Data
@PabloJomer

This comment has been minimized.

Copy link

@PabloJomer PabloJomer commented Apr 21, 2021

Is this generalized enough that I could use it to visualize another project? Also I'm quite unsure how to run this. I tried running it the way you specify in the comment but I don't seam to get it to work. Perhaps because I'm on the wrong platform.

Any help would be much appreciated.

@mathias-brandewinder

This comment has been minimized.

Copy link
Owner Author

@mathias-brandewinder mathias-brandewinder commented May 2, 2021

@PabloJomer This is a bit old, and there have been quite a few changes to Azure Functions since, I am not sure if this would still work!

@PabloJomer

This comment has been minimized.

Copy link

@PabloJomer PabloJomer commented May 3, 2021

Ok thanks any way. Seems like an awesome tool.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment