/go-structurizr

A library for auto-generating C4 diagrams from Golang applications

Primary LanguageGoMIT LicenseMIT

CircleCI License: MIT

go-structurizr

This library allows you to auto-generate C4 component diagrams out from the Golang code.

Example

Usage and examples

If you want to get directly into usage of the library check my blog post with a step-by-step implementation guide.

You may also find a couple of examples implemented in the cmd directory. In order to run any of those examples, please run the shell script attached.

How it works?

The library provides a set of tools (Scraper and View) that allow you to scrape and render given Golang structures into a C4 component diagram in *.plantuml format.

Scraper component reflects given structure in accordance with structure interfaces, predefined rules and configuration.

You may pass the scraped structure into a View definition which you can then render into a plantUML diagram code.

Scraper identifies components to scrape in one of the following cases:

  • type that is being examined implements an interface model.HasInfo.
  • type that is being examined applies to one of the rules registered in the scraper.

Components

Component Info

Structure model.Info is a basic structure that defines a component included in the scraped structure of your code.

type Info struct {
	Kind        string      // kind of scraped component
	Name        string      // component name
	Description string      // component description
	Technology  string      // technology used within the component
	Tags        []string    // tags are used to match view styles to component
}

Scraper

Scraper may be instantiated in one of two ways:

  • from the code
  • from the YAML file

In order to instantiate the scraper you need to provide scraper configuration which contains a slice of prefixes of packages that you want to reflect. Types that do not match any of the given prefixes will not be traversed.

config := scraper.NewConfiguration(
    "github.com/org/pkg",
)
s := scraper.NewScraper(config)

Having a scraper instantiated, you can register a set of rules that will allow the scraper to identify the components to include in the output structure.

Each rule consists of:

  • Set of package regexp's - only types in a package matching at least one of the package regexp's will be processed
  • Name regexp - only type of name matching regexp will be processed
  • Apply function - function that produces model.Info describing the component included in the scraped structure.
r, err := scraper.NewRule().
    WithPkgRegexps("github.com/org/pkg/foo/.*").
    WithNameRegexp("^.*Client$").
    WithApplyFunc(
        func(name string, _ ...string) model.Info {
            return model.ComponentInfo(name, "foo client", "gRPC", "TAG")
        }).
    Build()
err = s.RegisterRule(r)

The apply function has two arguments: name and groups matched from the name regular expression.

See the example:

r, err := scraper.NewRule().
    WithPkgRegexps("github.com/org/pkg/foo/.*").
    WithNameRegexp(`^(\w*)\.(\w*)Client$`).
    WithApplyFunc(
        func(_ string, groups ...string) model.Info {
            // Do some groups sanity checks first, then:
            n := fmt.Sprintf("Client of external %s service", groups[1])
            return model.ComponentInfo(n, "foo client", "gRPC", "TAG")
        }).
    Build()
err = s.RegisterRule(r)

Alternatively, you can instantiate the scraper form YAML configuration file:

// go-structurizr.yml
configuration:
  pkgs:
    - "github.com/org/pkg"

rules:
  - name_regexp: "^.*Client$"
    pkg_regexps:
      - "github.com/org/pkg/foo/.*"
    component:
      description: "foo client"
      technology: "gRPC"
      tags:
        - TAG

Regex groups may also be used within yaml rule definition. Here you can find an example:

rules:
  - name_regexp: "(\\w*)\\.(\\w*)Client$"
    pkg_regexps:
      - "github.com/org/pkg/foo/.*"
    component:
      name: "Client of external {1} service"
      description: "foo client"
      technology: "gRPC"
      tags:
        - TAG
s, err := scraper.NewScraperFromConfigFile("./go-structurizr.yml")

Eventually, having the scraper instantiated and configured you can use it to scrape any structure you want. Scraper returns a struct model.Structure.

structure := s.Scrape(app)

View

Similarly, to the scraper, view may be instantiated in one of two ways:

  • from the code
  • from the YAML file

In order to render scraped structure, you will need to instantiate and configure a view. View consists of:

  • title
  • component styles - styles are applied to the components by matching first of component tags with style ids
  • additional styling (i.e. line color)
  • component tags - if specified, view will contain only components tagged with one of the view tags. When no tag is defined, all components will be included in the rendered view.
  • root component tags - if specified, view will contain only those components which have connection (direct or in-direct) to at least one of components with root tag.

In order to instantiate default view, use the view builder:

v := view.NewView().Build()

In case you need to customize it, use available builder methods:

v := view.NewView().
    WithTitle("Title")
    WithComponentStyle(
        view.NewComponentStyle("TAG").
            WithBackgroundColor(color.White).
            WithFontColor(color.Black).
            WithBorderColor(color.Black).
            WithShape("database").
            Build(),
    ).
    WithComponentTag("TAG").
    WithRootComponentTag("ROOT").
    Build()

Alternatively, you can instantiate the view form YAML configuration file:

// go-structurizr.yml
view:
  title: "Title"
  line_color: 000000ff
  styles:
    - id: TAG
      background_color: ffffffff
      font_color: 000000ff
      border_color: 000000ff
      shape: database
  root_component_tags:
    - ROOT
  component_tags:
    - TAG
v, err := view.NewViewFromConfigFile("./go-structurizr.yml")

As the view is initialized, you can now render the structure into planUML diagram.

outFile, _ := os.Create("c4.plantuml")
defer func() {
    _ = outFile.Close()
}()

err = v.RenderStructureTo(structure, outFile)

Debug mode

In order to see detailed scraping or view rendering logs, set LOG_LEVEL env variable with debug of DEBUG.

Good practices

The best results and experience in using the library will be ensured by enforcing the following practices:

  • Having a solid and well-organized application context following clean-architecture principles will make your diagrams simple and easy to read. Also, this will allow you to create a short list of component types and styles.
  • Following consistent naming conventions will help you in creating simple and straight-forward scraper rules.

Full code documentation

https://pkg.go.dev/github.com/krzysztofreczek/go-structurizr