Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 167 additions & 0 deletions cmd/logs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package cmd

import (
"context"
"fmt"
"os"
"os/signal"
"syscall"
"time"

"github.com/ory/viper"
"github.com/spf13/cobra"

"knative.dev/func/pkg/config"
fn "knative.dev/func/pkg/functions"
"knative.dev/func/pkg/knative"
)

func NewLogsCmd(newClient ClientFactory) *cobra.Command {
cmd := &cobra.Command{
Use: "logs",
Short: "Stream logs from a deployed function",
Long: `Stream logs from a deployed function

Streams logs for the function in the current directory or from the directory
specified with --path. Abstracts away the underlying service name and pod details.
`,
Example: `
# Stream logs for the function in the current directory
{{rootCmdUse}} logs

# Stream logs for a function by name
{{rootCmdUse}} logs --name my-function

# Stream logs from a specific namespace
{{rootCmdUse}} logs --namespace my-namespace

# Stream logs with a specific time window
{{rootCmdUse}} logs --since 5m
`,
SuggestFor: []string{"log", "tail"},
ValidArgsFunction: CompleteFunctionList,
PreRunE: bindEnv("name", "namespace", "path", "since", "verbose"),
RunE: func(cmd *cobra.Command, args []string) error {
return runLogs(cmd, newClient)
},
}

// Config
cfg, err := config.NewDefault()
if err != nil {
fmt.Fprintf(cmd.OutOrStdout(), "error loading config at '%v'. %v\n", config.File(), err)
}

// Flags
cmd.Flags().StringP("name", "", "", "Name of the function to get logs from ($FUNC_NAME)")
cmd.Flags().StringP("namespace", "n", defaultNamespace(fn.Function{}, false), "The namespace of the function ($FUNC_NAMESPACE)")
cmd.Flags().StringP("since", "", "1m", "Return logs newer than a relative duration like 5s, 2m, or 3h ($FUNC_LOGS_SINCE)")
addPathFlag(cmd)
addVerboseFlag(cmd, cfg.Verbose)

return cmd
}

func runLogs(cmd *cobra.Command, newClient ClientFactory) error {
cfg, err := newLogsConfig(cmd)
if err != nil {
return err
}

client, done := newClient(ClientConfig{Verbose: cfg.Verbose})
defer done()

// Get function details
var f fn.Function
if cfg.Name != "" {
// Get function by name
instance, err := client.Describe(cmd.Context(), cfg.Name, cfg.Namespace, fn.Function{})
if err != nil {
return fmt.Errorf("failed to get function details: %w", err)
}
f.Name = instance.Name
f.Namespace = instance.Namespace
f.Image = instance.Image
} else {
// Load function from path
f, err = fn.NewFunction(cfg.Path)
if err != nil {
return err
}
if !f.Initialized() {
return NewErrNotInitializedFromPath(f.Root, "logs")
}

// Get deployed function details to ensure it exists
instance, err := client.Describe(cmd.Context(), "", "", f)
if err != nil {
return fmt.Errorf("function not deployed or not found: %w", err)
}
f.Name = instance.Name
f.Namespace = instance.Namespace
f.Image = instance.Image
}

// Parse since duration
var sinceTime *time.Time
if cfg.Since != "" {
duration, err := time.ParseDuration(cfg.Since)
if err != nil {
return fmt.Errorf("invalid duration format for --since: %w", err)
}
t := time.Now().Add(-duration)
sinceTime = &t
}

// Create context that can be cancelled with Ctrl+C
ctx, cancel := context.WithCancel(cmd.Context())
defer cancel()

// Handle interrupt signal
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
go func() {
<-sigChan
fmt.Fprintln(os.Stderr, "\nStopping log stream...")
cancel()
}()

// Stream logs
fmt.Fprintf(os.Stderr, "Streaming logs for function '%s' in namespace '%s'...\n", f.Name, f.Namespace)
fmt.Fprintf(os.Stderr, "Press Ctrl+C to stop.\n\n")

err = knative.GetKServiceLogs(ctx, f.Namespace, f.Name, f.Image, sinceTime, os.Stdout)
if err != nil && err != context.Canceled {
return fmt.Errorf("failed to stream logs: %w", err)
}

return nil
}

// CLI Configuration (parameters)
// ------------------------------

type logsConfig struct {
Name string
Namespace string
Path string
Since string
Verbose bool
}

func newLogsConfig(cmd *cobra.Command) (cfg logsConfig, err error) {
cfg = logsConfig{
Name: viper.GetString("name"),
Namespace: viper.GetString("namespace"),
Path: viper.GetString("path"),
Since: viper.GetString("since"),
Verbose: viper.GetBool("verbose"),
}

if cfg.Name != "" && cmd.Flags().Changed("path") {
// logically inconsistent to provide both a name and a path to source.
err = ErrNameAndPathConflict
}

return
}
115 changes: 115 additions & 0 deletions cmd/logs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package cmd

import (
"testing"

fn "knative.dev/func/pkg/functions"
"knative.dev/func/pkg/mock"
)

// TestLogs_CommandStructure ensures the logs command is properly structured
func TestLogs_CommandStructure(t *testing.T) {
describer := mock.NewDescriber()
root := NewRootCmd(RootCommandConfig{
Name: "func",
NewClient: NewTestClient(fn.WithDescribers(describer)),
})

logsCmd, _, err := root.Find([]string{"logs"})
if err != nil {
t.Fatal(err)
}

if logsCmd == nil {
t.Fatal("logs command not found")
}

if logsCmd.Use != "logs" {
t.Errorf("expected Use to be 'logs', got '%s'", logsCmd.Use)
}

// Check that required flags exist
flags := []string{"name", "namespace", "path", "since", "verbose"}
for _, flag := range flags {
if logsCmd.Flags().Lookup(flag) == nil {
t.Errorf("expected flag '%s' to exist", flag)
}
}
}

// TestLogs_ConfigValidation tests the configuration validation
func TestLogs_ConfigValidation(t *testing.T) {
tests := []struct {
name string
cfg logsConfig
wantError bool
}{
{
name: "valid config with name",
cfg: logsConfig{
Name: "my-function",
Namespace: "default",
Since: "5m",
},
wantError: false,
},
{
name: "valid config with path",
cfg: logsConfig{
Path: "./testdata",
Namespace: "default",
Since: "1h",
},
wantError: false,
},
{
name: "valid config with default since",
cfg: logsConfig{
Name: "my-function",
Namespace: "default",
Since: "1m",
},
wantError: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Basic validation - just ensure the config structure is valid
if tt.cfg.Name == "" && tt.cfg.Path == "" {
t.Error("config should have either name or path")
}
if tt.cfg.Namespace == "" {
t.Error("namespace should not be empty")
}
})
}
}

// TestLogs_SuggestFor ensures the command has proper suggestions
func TestLogs_SuggestFor(t *testing.T) {
describer := mock.NewDescriber()
root := NewRootCmd(RootCommandConfig{
Name: "func",
NewClient: NewTestClient(fn.WithDescribers(describer)),
})

logsCmd, _, err := root.Find([]string{"logs"})
if err != nil {
t.Fatal(err)
}

expectedSuggestions := []string{"log", "tail"}
for _, suggestion := range expectedSuggestions {
found := false
for _, s := range logsCmd.SuggestFor {
if s == suggestion {
found = true
break
}
}
if !found {
t.Errorf("expected suggestion '%s' not found", suggestion)
}
}
}
1 change: 1 addition & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ Learn more about Knative at: https://knative.dev`, cfg.Name),
NewRunCmd(newClient),
NewInvokeCmd(newClient),
NewBuildCmd(newClient),
NewLogsCmd(newClient),
},
},
{
Expand Down
1 change: 1 addition & 0 deletions docs/reference/func.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Learn more about Knative at: https://knative.dev
* [func invoke](func_invoke.md) - Invoke a local or remote function
* [func languages](func_languages.md) - List available function language runtimes
* [func list](func_list.md) - List deployed functions
* [func logs](func_logs.md) - Stream logs from a deployed function
* [func mcp](func_mcp.md) - Model Context Protocol (MCP) server
* [func repository](func_repository.md) - Manage installed template repositories
* [func run](func_run.md) - Run the function locally
Expand Down
49 changes: 49 additions & 0 deletions docs/reference/func_logs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
## func logs

Stream logs from a deployed function

### Synopsis

Stream logs from a deployed function

Streams logs for the function in the current directory or from the directory
specified with --path. Abstracts away the underlying service name and pod details.


```
func logs
```

### Examples

```

# Stream logs for the function in the current directory
func logs

# Stream logs for a function by name
func logs --name my-function

# Stream logs from a specific namespace
func logs --namespace my-namespace

# Stream logs with a specific time window
func logs --since 5m

```

### Options

```
-h, --help help for logs
--name string Name of the function to get logs from ($FUNC_NAME)
-n, --namespace string The namespace of the function ($FUNC_NAMESPACE) (default "default")
-p, --path string Path to the function. Default is current directory ($FUNC_PATH)
--since string Return logs newer than a relative duration like 5s, 2m, or 3h ($FUNC_LOGS_SINCE) (default "1m")
-v, --verbose Print verbose logs ($FUNC_VERBOSE)
```

### SEE ALSO

* [func](func.md) - func manages Knative Functions