Skip to content
Merged
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
124 changes: 124 additions & 0 deletions diagnostics/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# Go Diagnostics Binding Architecture

This document describes the architecture used for the Go Diagnostics API in Speech SDK bindings.

## Goals

- Provide feature parity with Java/Python/C# diagnostics surfaces.
- Keep Go API idiomatic while preserving native SDK behavior.
- Isolate CGo/native interop details behind small, focused abstractions.
- Maintain backward compatibility for existing users of the legacy `diagnostics` package.

## Package Layout

### `diagnostics/logging` (new primary API)

This package contains the full diagnostics surface:

- `FileLogger` for file logging
- `MemoryLogger` for memory-ring logging and dump helpers
- `EventLogger` for callback-based log streaming
- `ConsoleLogger` for stdout/stderr logging
- `TraceError`, `TraceWarning`, `TraceInfo`, `TraceVerbose` (+ `WithCaller` variants)
- `Level` enum-like type (`Error`, `Warning`, `Info`, `Verbose`)

Files are intentionally split by logger/type to keep ownership and review scope small.

### `diagnostics` (deprecated compatibility layer)

This package keeps old entry points available and forwards behavior to equivalent native diagnostics APIs.

- Marked with `Deprecated:` comments.
- Intended only for compatibility during migration.
- New work should target `diagnostics/logging`.

## Architectural Decisions

### 1) Singleton logger model

Each logger is a package-level singleton (`FileLogger`, `MemoryLogger`, etc.) matching the native SDK's process-wide semantics and the shape used in other language bindings.

### 2) Thin wrappers around native C APIs

Go methods map ~1:1 to native diagnostics APIs from `speechapi_c_diagnostics.h`, keeping behavior changes centralized in the native layer.

### 3) Explicit CGo boundary ownership

All callback bridge code lives in `logging/cfunctions.go`: C trampoline -> exported Go function -> mutex-guarded dispatch. This avoids glue duplication and keeps thread-safety constraints visible.

### 4) Property bag for file logger configuration

`FileLogger.Start` passes `SPEECH-LogFilename` and `SPEECH-AppendToLogFile` via a temporary property bag, mirroring the native/C++/Java pattern.

### 5) Backward compatibility strategy

Legacy `diagnostics` package stays available with `Deprecated:` markers guiding users to `diagnostics/logging`.

## Error Handling

- Native result codes (`SPXHR`/`AZACHR`) surface as Go `error` values via lightweight wrappers.
- Native `void` APIs remain fire-and-forget. Invalid caller input is validated early where useful.

## Concurrency

- Native event callbacks arrive on SDK worker threads.
- `EventLogger` protects state with a mutex; callback handlers should be fast and non-blocking.

## Quick Start

```go
import "github.com/Microsoft/cognitive-services-speech-sdk-go/diagnostics/logging"

// File logging
logging.FileLogger.Start("/tmp/speech.log")
defer logging.FileLogger.Stop()

// Memory logging with dump
logging.MemoryLogger.Start()
defer logging.MemoryLogger.Stop()
logging.TraceInfo("recognized: %s", result.Text)
lines := logging.MemoryLogger.DumpToSlice()

// Event-based logging
logging.EventLogger.SetCallback(func(msg string) {
fmt.Println(msg)
})
defer logging.EventLogger.SetCallback(nil)

// Console logging
logging.ConsoleLogger.Start()
defer logging.ConsoleLogger.Stop()

// Set log level
logging.FileLogger.SetLevel(logging.Error)
```

## Testing

Integration tests are gated by `SPEECH_SDK_AVAILABLE=1`; pure unit tests (e.g. `TestLevelString`, `TestLoggingError`) run unconditionally.

## Local Validation

Requirements: Go toolchain, CGo-compatible C compiler, Speech SDK headers and native library.

Key environment variables: `CGO_ENABLED=1`, `CGO_CFLAGS` (header path), `CGO_LDFLAGS` (lib path), `SPEECH_SDK_AVAILABLE=1`.

Run tests via CMake:

```bash
cmake --build build --target go-tests --config Release
cmake --build build --target go-tests-race --config Release
```

## Extension Guidelines

1. Add new capabilities in `diagnostics/logging` first.
2. Keep Go naming idiomatic but parity-aligned with Java/Python/C#.
3. Mirror native signatures closely; add tests for success and error paths.
4. Only add compatibility shims in `diagnostics` when needed for non-breaking upgrades.

## Known Constraints

- Logging is process-wide by native SDK design.
- Event callback is a single registration point per process.
- Integration tests are opt-in via `SPEECH_SDK_AVAILABLE=1`.
85 changes: 40 additions & 45 deletions diagnostics/diagnostics.go
Original file line number Diff line number Diff line change
@@ -1,57 +1,68 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.

// Deprecated: Use the diagnostics/logging sub-package instead.
package diagnostics

// #include <stdlib.h>
// #include <speechapi_c_diagnostics.h>
import "C"
import "unsafe"
import (
"unsafe"

"github.com/Microsoft/cognitive-services-speech-sdk-go/diagnostics/logging"
)

// Deprecated: Use logging.FileLogger.Start() instead.
func StartFileLogging(filename string, appendMode ...bool) error {
Comment thread
rhurey marked this conversation as resolved.
return logging.FileLogger.Start(filename, appendMode...)
}

// Deprecated: Use logging.FileLogger.Stop() instead.
func StopFileLogging() error {
return logging.FileLogger.Stop()
}

// StartMemoryLogging starts logging to memory
// Deprecated: Use logging.MemoryLogger.Start() instead.
func StartMemoryLogging() {
C.diagnostics_log_memory_start_logging()
logging.MemoryLogger.Start()
}

// StopMemoryLogging stops logging to memory
// Deprecated: Use logging.MemoryLogger.Stop() instead.
func StopMemoryLogging() {
C.diagnostics_log_memory_stop_logging()
logging.MemoryLogger.Stop()
}

// SetMemoryLogFilters sets filters for memory logging
// Deprecated: Use logging.MemoryLogger.SetFilters() instead.
func SetMemoryLogFilters(filters string) {
cFilters := C.CString(filters)
defer C.free(unsafe.Pointer(cFilters))
C.diagnostics_log_memory_set_filters(cFilters)
logging.MemoryLogger.SetFilters(filters)
}

// GetMemoryLogLineNumOldest gets the line number of the oldest memory log entry
// Deprecated: Use logging.MemoryLogger.DumpToSlice() instead.
func GetMemoryLogLineNumOldest() uint {
return uint(C.diagnostics_log_memory_get_line_num_oldest())
}

// GetMemoryLogLineNumNewest gets the line number of the newest memory log entry
// Deprecated: Use logging.MemoryLogger.DumpToSlice() instead.
func GetMemoryLogLineNumNewest() uint {
return uint(C.diagnostics_log_memory_get_line_num_newest())
}

// GetMemoryLogLine gets a specific line from the memory log
// Deprecated: Use logging.MemoryLogger.DumpToSlice() instead.
func GetMemoryLogLine(lineNum uint) string {
cLine := C.diagnostics_log_memory_get_line(C.size_t(lineNum))
if cLine == nil {
return ""
}
return C.GoString(cLine)

}

// DumpMemoryLogToStderr dumps the memory log to stderr
// Deprecated: Use logging.MemoryLogger.DumpToStderr() instead.
func DumpMemoryLogToStderr() error {
ret := uintptr(C.diagnostics_log_memory_dump_to_stderr())
if ret != 0 {
return newDiagnosticsError("dumpMemoryLogToStderr", ret)
}
return nil
return logging.MemoryLogger.DumpToStderr()
}

// DumpMemoryLog dumps the memory log to a file and/or standard output
// Deprecated: Use logging.MemoryLogger.Dump() instead.
func DumpMemoryLog(filename string, linePrefix string, emitToStdOut bool, emitToStdErr bool) error {
var cFilename *C.char
if filename != "" {
Expand All @@ -70,38 +81,22 @@ func DumpMemoryLog(filename string, linePrefix string, emitToStdOut bool, emitTo
return nil
}

// DumpMemoryLogOnExit dumps the memory log when the program exits
// Deprecated: Use logging.MemoryLogger.DumpOnExit() instead.
func DumpMemoryLogOnExit(filename string, linePrefix string, emitToStdOut bool, emitToStdErr bool) error {
var cFilename *C.char
if filename != "" {
cFilename = C.CString(filename)
defer C.free(unsafe.Pointer(cFilename))
}
var cLinePrefix *C.char
if linePrefix != "" {
cLinePrefix = C.CString(linePrefix)
defer C.free(unsafe.Pointer(cLinePrefix))
}
ret := uintptr(C.diagnostics_log_memory_dump_on_exit(cFilename, cLinePrefix, C.bool(emitToStdOut), C.bool(emitToStdErr)))
if ret != 0 {
return newDiagnosticsError("dumpMemoryLogOnExit", ret)
}
return nil
return logging.MemoryLogger.DumpOnExit(filename, linePrefix, emitToStdOut, emitToStdErr)
}

// StartConsoleLogging starts logging to the console
func StartConsoleLogging(logToStderr bool) {
C.diagnostics_log_console_start_logging(C.bool(logToStderr))
// Deprecated: Use logging.ConsoleLogger.Start() instead.
func StartConsoleLogging(logToStderr ...bool) {
logging.ConsoleLogger.Start(logToStderr...)
}

// StopConsoleLogging stops logging to the console
// Deprecated: Use logging.ConsoleLogger.Stop() instead.
func StopConsoleLogging() {
C.diagnostics_log_console_stop_logging()
logging.ConsoleLogger.Stop()
}

// SetConsoleLogFilters sets filters for console logging
// Deprecated: Use logging.ConsoleLogger.SetFilters() instead.
func SetConsoleLogFilters(filters string) {
cFilters := C.CString(filters)
defer C.free(unsafe.Pointer(cFilters))
C.diagnostics_log_console_set_filters(cFilters)
logging.ConsoleLogger.SetFilters(filters)
}
48 changes: 48 additions & 0 deletions diagnostics/diagnostics_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.

package diagnostics

import (
"os"
"path/filepath"
"testing"
)

func skipIfNoSDK(t *testing.T) {
t.Helper()
if os.Getenv("SPEECH_SDK_AVAILABLE") != "1" {
t.Skip("Skipping integration test: SPEECH_SDK_AVAILABLE not set")
}
}

func TestStartStopFileLogging(t *testing.T) {
skipIfNoSDK(t)

logPath := filepath.Join(t.TempDir(), "legacy_diagnostics.log")
if err := StartFileLogging(logPath); err != nil {
t.Fatalf("StartFileLogging: %v", err)
}
if err := StopFileLogging(); err != nil {
t.Fatalf("StopFileLogging: %v", err)
}
}

func TestStartStopFileLoggingAppendMode(t *testing.T) {
skipIfNoSDK(t)

logPath := filepath.Join(t.TempDir(), "legacy_diagnostics_append.log")
if err := StartFileLogging(logPath, true); err != nil {
t.Fatalf("StartFileLogging(append): %v", err)
}
if err := StopFileLogging(); err != nil {
t.Fatalf("StopFileLogging: %v", err)
}
}

func TestStartConsoleLoggingDefault(t *testing.T) {
skipIfNoSDK(t)

StartConsoleLogging()
StopConsoleLogging()
}
23 changes: 15 additions & 8 deletions diagnostics/error.go
Original file line number Diff line number Diff line change
@@ -1,23 +1,30 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.

package diagnostics

// #include <azac_error.h>
// #include <azac_api_c_common.h>
//
// static const char* diagnostics_get_error_message(uintptr_t code) {
// return error_get_message((AZAC_HANDLE)code);
// }
import "C"

import "fmt"

type diagnosticsError struct {
operation string
code uintptr
operation string
code uintptr
}

func newDiagnosticsError(operation string, code uintptr) error {
return &diagnosticsError{
operation: operation,
code: code,
}
return &diagnosticsError{
operation: operation,
code: code,
}
}

func (e *diagnosticsError) Error() string {
return fmt.Sprintf("diagnostics operation '%s' failed with error code %d", e.operation, e.code)
msg := C.GoString(C.diagnostics_get_error_message(C.uintptr_t(e.code)))
return fmt.Sprintf("diagnostics operation '%s' failed with error code 0x%x (%s)", e.operation, e.code, msg)
}
25 changes: 25 additions & 0 deletions diagnostics/logging/cfunctions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.

package logging

// #include <stdlib.h>
// #include <speechapi_c_diagnostics.h>
//
// extern void goEventLoggerCallback(const char* logLine);
//
// static void cgo_event_logger_callback(const char* logLine)
// {
// goEventLoggerCallback(logLine);
// }
//
// uintptr_t cgo_register_event_callback()
// {
// return (uintptr_t)diagnostics_logmessage_set_callback(cgo_event_logger_callback);
// }
//
// uintptr_t cgo_unregister_event_callback()
// {
// return (uintptr_t)diagnostics_logmessage_set_callback((DIAGNOSTICS_CALLBACK_FUNC)0);
// }
import "C"
Loading
Loading