Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
41 changes: 41 additions & 0 deletions .github/workflows/test-core.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Check swanlab-core

on:
pull_request:
paths:
- core/**


jobs:
check-and-test:
strategy:
matrix:
go: [stable, '1.24.4']
os: [ubuntu-latest, macos-latest, windows-latest]
name: Check and Test Go Core on ${{ matrix.os }} with Go ${{ matrix.go }}
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Go environment
uses: actions/[email protected]
with:
go-version: ${{ matrix.go }}
cache: 'true'

- name: Golangci-lint
uses: golangci/[email protected]
with:
working-directory: 'core'

- name: Run tests
run: |
cd core
go test $(go list ./... | grep -v '/pkg/')
go vet ./...
go mod verify
go mod tidy


427 changes: 427 additions & 0 deletions core/.golangci.yml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions core/LICENSE
48 changes: 48 additions & 0 deletions core/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<div align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="../readme_files/swanlab-logo-single-dark.svg">
<source media="(prefers-color-scheme: light)" srcset="../readme_files/swanlab-logo-single.svg">
<img alt="SwanLab" src="../readme_files/swanlab-logo-single.svg" width="70" height="70">
</picture>
</div>

# swanlab-core: A new backend for SwanLab SDK

To address the challenges posed by Python's asynchronous programming, we introduced swanlab-core, a brand-new, Go-based backend for metric uploading.


## IDE & Development Setup

In this part, we will guide you through the process of setting up your IDE and development environment for SwanLab SDK.
We will use:
1. [GoLand](https://www.jetbrains.com/go/) as the IDE.
2. [Go](https://go.dev/) as the programming language.
3. [golangci-lint](https://golangci-lint.run/) as the linter.

## IDE

We request using GoLand as the IDE for swanlab-core development.
It provides a rich set of features for Go development, including code completion, debugging, and integration with version control systems.

There is the code template for Go file in GoLand:

```text
// @Title ${FILE_NAME}
// @Description #[[$END$]]#
// @Create cunyue ${DATE} ${TIME}

package ${GO_PACKAGE_NAME}
```

## Linter

We use [golangci-lint](https://golangci-lint.run/) as the linter for swanlab-core development.
Since SwanLab is a Python project, some developers may not be very familiar with the Go language.
Therefore, golangci-lint is not mandatory and will only trigger this CI when changes are made to the core part.

If you plan to develop swanlab-core, configure the golangci-lint integration in GoLand, which can be found under Go -> Linter settings.

## Env

TODO

43 changes: 43 additions & 0 deletions core/cmd/core/core.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// @Title core.go
// @Description Package main implements a server for gRPC service.
// @Create kaikai 2025/6/11 11:24

package main

import (
"flag"
"fmt"
"log/slog"
"net"

"google.golang.org/grpc"
"google.golang.org/grpc/health"
grpcHealth "google.golang.org/grpc/health/grpc_health_v1"

"github.com/SwanHubX/SwanLab/core/internal/service"
"github.com/SwanHubX/SwanLab/core/pkg/pb"
)

func main() {
port := flag.Int("port", 0, "The server port")
flag.Parse()

lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
if err != nil {
slog.Error("failed to listen", "error", err)
}

s := grpc.NewServer()
// Health Check Service
healthCheck := health.NewServer()
grpcHealth.RegisterHealthServer(s, healthCheck)
// Collector Service
collector := &service.Collector{}
pb.RegisterCollectorServer(s, collector)
healthCheck.SetServingStatus("collector", grpcHealth.HealthCheckResponse_SERVING)
slog.Info("server listening at", "address", lis.Addr())

if err = s.Serve(lis); err != nil {
slog.Error("failed to serve", "error", err)
}
}
19 changes: 19 additions & 0 deletions core/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
module github.com/SwanHubX/SwanLab/core

go 1.24.4

require (
github.com/stretchr/testify v1.10.0
golang.org/x/net v0.38.0
google.golang.org/grpc v1.73.0
google.golang.org/protobuf v1.36.6
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.23.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
44 changes: 44 additions & 0 deletions core/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
5 changes: 5 additions & 0 deletions core/internal/api/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// @Title api.go
// @Description SwanLab API intended for requests to the SwanLab Server.
// @Create cunyue 2025/6/10 13:00

package api
85 changes: 85 additions & 0 deletions core/internal/api/parse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// @Title send.go
// @Description parse the proto message to json
// @Create cunyue 2025/6/12 18:49

package api

import (
"errors"
"strings"

"google.golang.org/protobuf/types/known/structpb"

"github.com/SwanHubX/SwanLab/core/pkg/pb"
)

// Parser provides methods to parse proto messages to JSON sent to the SwanLab Server.
type Parser struct {
}

// ColumnDTO represents the data transfer object for a column record.
// We use RESTFul naming conventions for the fields.
type ColumnDTO struct {
Class string `json:"class"`
Type string `json:"type"`
Key string `json:"key"`
Name string `json:"name,omitempty"`
// e.g. {data_class: '', excepted: ''}
Error *structpb.Struct `json:"error,omitempty"`
SectionName string `json:"section_name,omitempty"`
SectionType string `json:"section_type,omitempty"`
// allow [0, 100] or [0, null] for y_range
YRange []*int64 `json:"y_range,omitempty"`
ChartIndex string `json:"chart_index,omitempty"`
ChartName string `json:"chart_name,omitempty"`
MetricName string `json:"metric_name,omitempty"`
MetricColor []string `json:"metric_color,omitempty"`
}

// ParseColumnRecord parses a ColumnRecord proto message into ColumnDTO.
// Attention: y_range should be serialized to [0, null] in JSON, not ["0", "None"].
func (p *Parser) ParseColumnRecord(record *pb.ColumnRecord) (ColumnDTO, error) {
// 1. parse the y_range, if error, yRange will be nil
var yRange []*int64
if record.GetChartYRange() != nil {
yRange = []*int64{
//nolint:protogetter // We need pointers to represent left null during JSON serialization.
record.GetChartYRange().Minval,
//nolint:protogetter // We need pointers to represent right null during JSON serialization.
record.GetChartYRange().Maxval,
}
}
// 2. parse the enum type
// 2.1 column class
columnClass := strings.TrimPrefix(record.GetColumnClass().String(), "COL_CLASS_")
// 2.2 column type
if record.GetColumnType() == pb.ColumnRecord_COL_UNKNOWN {
return ColumnDTO{}, errors.New("column type is unknown")
}
columnType := strings.TrimPrefix(record.GetColumnType().String(), "COL_")
// 2.3 section type
sectionType := strings.TrimPrefix(record.GetSectionType().String(), "SEC_")
// 4. check if the key is empty, if so, return an error
key := record.GetColumnKey()
if key == "" {
return ColumnDTO{}, errors.New("column key cannot be EMPTY")
}
// 5. check the metric color length, if it is not empty, it must be 2 elements
if record.GetMetricColor() != nil && len(record.GetMetricColor()) > 0 && len(record.GetMetricColor()) != 2 {
return ColumnDTO{}, errors.New("metric color must be empty or have exactly 2 elements")
}
return ColumnDTO{
Class: columnClass,
Type: columnType,
Key: record.GetColumnKey(),
Name: record.GetColumnName(),
Error: record.GetColumnError(),
SectionName: record.GetSectionName(),
SectionType: sectionType,
YRange: yRange,
ChartIndex: record.GetChartIndex(),
ChartName: record.GetChartName(),
MetricName: record.GetMetricName(),
MetricColor: record.GetMetricColor(),
}, nil
}
Loading
Loading