Skip to content

Instantly share code, notes, and snippets.

@mathias-brandewinder
Created April 11, 2017 21:43
Show Gist options
  • Save mathias-brandewinder/cdd1e0d23bd3047ffe438d48689b2b86 to your computer and use it in GitHub Desktop.
Save mathias-brandewinder/cdd1e0d23bd3047ffe438d48689b2b86 to your computer and use it in GitHub Desktop.
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
Copy link

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
Copy link
Author

@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
Copy link

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