Skip to content

Application Entry Point#

In Go, the main() function is the entry point of the binary application and is defined in the main.go file of our application:

package main

var (
    version = "0.0.0"
    commit  = ""
)

func main() {
    versionFlag := flag.Bool("version", false, "print the version and exit")

    flag.Parse()

    if *versionFlag {
        fmt.Println(version + "-" + commit)
        os.Exit(0)
    }

    logger := setupLogger()

    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    ctx = metadata.AppendToOutgoingContext(ctx, "agent_name", greeter.AppName)

    exitHandler(cancel)

    app := greeter.NewApp(ctx, &logger)
    app.Start(ctx)
}

Application version#

As you can see, the main function is rather simple. First, we handle the version CLI flag to make sure our application can return its version when asked.

Application config has a version-command field that indicates which command needs to be executed to get the application version. In our case, the version field is set to greeter --version and we just went through the handler of this flag.

In SR Linux CLI we can get the version of the greeter app by executing the greeter --version command:

--{ + running }--[  ]--
A:greeter# show system application greeter
  +---------+------+---------+-------------+--------------------------+
  |  Name   | PID  |  State  |   Version   |       Last Change        |
  +=========+======+=========+=============+==========================+
  | greeter | 4676 | running | dev-a6f880b | 2023-11-29T21:29:04.243Z |
  +---------+------+---------+-------------+--------------------------+
Why the version is dev-a6f880b?

Attentive readers might have noticed that the version of the greeter app is dev-a6f880b instead of v0.0.0- following the version and commit variables values in main.go file. This is because we setting the values for these variables at build time using the Go linker flags in the run.sh script:

LDFLAGS="-s -w -X main.version=dev -X main.commit=$(git rev-parse --short HEAD)"

These variables are then set to the correct values when we build the application with Goreleaser.

Setting up the Logger#

Logging is an important part of any application. It aids the developer in debugging the application and provides valuable information about the application's state for its users.

func main() {
    // snip
    logger := setupLogger()
    // snip
}

We create the logger before initializing the application so that we can pass it to the application and use it to log the application's state.

Logging from the NDK application is a separate topic that is covered in the Logging section of this guide.

Context, gRPC Requests and Metadata#

Moving down the main function, we create the context that will drive the lifecycle of our greeter application.

Once the context is created we attach the metadata to it. The metadata is a map of key-value pairs that will be sent along with the gRPC requests.

The NDK service uses the metadata to identify the application from which the request was sent.

    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    ctx = metadata.AppendToOutgoingContext(ctx, "agent_name", greeter.AppName)

The metadata must be attached to the parent context and it should has the agent_name key with the value of the application name. The application name in the metadata doesn't have to match anything, but should be unique among all the applications that are registered with the Application Manager.

Exit Handler#

Another important part of the application lifecycle is the exit handler. In the context of the NDK application life cycle the exit handler is a function that is called when the application receives Interrupt or SIGTERM signals.

The exit handler is a good place to perform cleanup actions like closing the open connections, releasing resources, etc.

We execute exitHandler function passing it the cancel function of the context:

func exitHandler(cancel context.CancelFunc) {
    // handle CTRL-C signal
    sig := make(chan os.Signal, 1)
    signal.Notify(sig, os.Interrupt, syscall.SIGTERM)
    go func() {
        <-sig

        cancel()
    }()
}

This function is non-blocking as it spawns a goroutine that waits for the registered signals and then execute the cancel function of the context. This will propagate the cancellation signal to all the child contexts and our application reacts to it.

greeter/app.go
func (a *App) Start(ctx context.Context) {
    go a.receiveConfigNotifications(ctx)

    for {
        select {
        case <-a.configReceivedCh:
            a.logger.Info().Msg("Received full config")

            a.processConfig(ctx)

            a.updateState(ctx)

        case <-ctx.Done():
            a.stop()
            return
        }
    }
}

We will cover the func (a *App) Start() function properly when we get there, but for now, it is important to highlight how cancellation of the main context is intercepted in this function and leading to a.stop() call.

The a.stop() function is responsible to perform the graceful shutdown of the application.

greeter/app.go
func (a *App) stop() {
    a.logger.Info().Msg("Got a signal to exit, unregistering greeter agent, bye!")

    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    ctx = metadata.AppendToOutgoingContext(ctx, "agent_name", AppName)
    defer cancel()

    // unregister agent
    r, err := a.SDKMgrServiceClient.AgentUnRegister(ctx, &ndk.AgentRegistrationRequest{})
    if err != nil || r.Status != ndk.SdkMgrStatus_kSdkMgrSuccess {
        a.logger.Error().
            Err(err).
            Str("status", r.GetStatus().String()).
            Msgf("Agent unregistration failed %s", r.GetErrorStr())

        return
    }

    err = a.gRPCConn.Close()
    if err != nil {
        a.logger.Error().Err(err).Msg("Closing gRPC connection to NDK server failed")
    }

    err = a.gNMITarget.Close()
    if err != nil {
        a.logger.Error().Err(err).Msg("Closing gNMI connection failed")
    }

    a.logger.Info().Msg("Greeter unregistered successfully!")
}

Following the Graceful Exit section we first unregister the agent with the NDK manager and then closing all connections that our app had opened.

Initializing the Application#

And finally in the main function we initialize the greeter application and start it:

main.go
    app := greeter.NewApp(ctx, &logger)
    app.Start(ctx)

This is where the application logic starts to kick in. Let's turn the page and start digging into it in the next chapter.

Comments