Created
April 11, 2017 21:43
-
-
Save mathias-brandewinder/cdd1e0d23bd3047ffe438d48689b2b86 to your computer and use it in GitHub Desktop.
Visualize Azure Function App with F# and GraphViz
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(* | |
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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
framework:net45 | |
source https://www.nuget.org/api/v2 | |
nuget FSharp.Data |
@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!
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
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.