masih perubahan
This commit is contained in:
15
README.md
15
README.md
@@ -67,6 +67,11 @@ api-service/
|
||||
│ ├── routes/ # 🛣️ API routing (Presentation)
|
||||
│ ├── services/ # 💼 Business logic (Application)
|
||||
│ └── repository/ # 💾 Data access (Infrastructure)
|
||||
├── 📁 pkg/ # 💼 Package
|
||||
│ ├── logger/ # 🎯 General generators
|
||||
│ └── data/ # Hasil Log yang tersimpan
|
||||
│ ├── utils/ # 🏥 BPJS specific tools
|
||||
│ └── validator/ # 🩺 SATUSEHAT tools
|
||||
├── 📁 tools/ # 🔧 Development tools
|
||||
│ ├── general/ # 🎯 General generators
|
||||
│ ├── bpjs/ # 🏥 BPJS specific tools
|
||||
@@ -114,6 +119,10 @@ make migrate-up
|
||||
go run cmd/api/main.go
|
||||
```
|
||||
|
||||
### Update Swagger
|
||||
```bash
|
||||
swag init -g cmd/api/main.go --parseDependency --parseInternal # Alternative
|
||||
swag init -g cmd/api/main.go -o docs/
|
||||
|
||||
### 3️⃣ Verify Installation
|
||||
|
||||
@@ -256,16 +265,16 @@ go run tools/general/generate-handler.go orders get post put delete dynamic sear
|
||||
|
||||
```bash
|
||||
# Single service
|
||||
go run tools/bpjs/generate-bpjs-handler.go reference/peserta get
|
||||
go run tools/bpjs/generate-bpjs-handler.go tools/bpjs/reference/peserta get
|
||||
|
||||
# Semua service dari config
|
||||
go run tools/bpjs/generate-handler.go services-config-bpjs.yaml
|
||||
go run tools/bpjs/generate-handler.go tools/bpjs/services-config-bpjs.yaml
|
||||
```
|
||||
|
||||
**🩺 Generate SATUSEHAT Handler**
|
||||
|
||||
```bash
|
||||
go run tools/satusehat/generate-satusehat-handler.go services-config-satusehat.yaml patient
|
||||
go run tools/satusehat/generate-satusehat-handler.go tools/satusehat/services-config-satusehat.yaml patient
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -1,610 +0,0 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestQueryBuilder_BuildQuery(t *testing.T) {
|
||||
builder := NewQueryBuilder("test_table").
|
||||
SetColumnMapping(map[string]string{
|
||||
"name": "full_name",
|
||||
}).
|
||||
SetAllowedColumns([]string{"id", "name", "age", "status"})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
query DynamicQuery
|
||||
expected string
|
||||
argsLen int
|
||||
}{
|
||||
{
|
||||
name: "simple equality filter",
|
||||
query: DynamicQuery{
|
||||
Filters: []FilterGroup{{
|
||||
Filters: []DynamicFilter{{
|
||||
Column: "name",
|
||||
Operator: OpEqual,
|
||||
Value: "John",
|
||||
}},
|
||||
}},
|
||||
},
|
||||
expected: "SELECT * FROM test_table WHERE \"full_name\" = $1",
|
||||
argsLen: 1,
|
||||
},
|
||||
{
|
||||
name: "IN operator with multiple values",
|
||||
query: DynamicQuery{
|
||||
Filters: []FilterGroup{{
|
||||
Filters: []DynamicFilter{{
|
||||
Column: "status",
|
||||
Operator: OpIn,
|
||||
Value: []interface{}{"active", "pending"},
|
||||
}},
|
||||
}},
|
||||
},
|
||||
expected: "SELECT * FROM test_table WHERE \"status\" IN ($1, $2)",
|
||||
argsLen: 2,
|
||||
},
|
||||
{
|
||||
name: "BETWEEN operator",
|
||||
query: DynamicQuery{
|
||||
Filters: []FilterGroup{{
|
||||
Filters: []DynamicFilter{{
|
||||
Column: "age",
|
||||
Operator: OpBetween,
|
||||
Value: []interface{}{"18", "65"},
|
||||
}},
|
||||
}},
|
||||
},
|
||||
expected: "SELECT * FROM test_table WHERE \"age\" BETWEEN $1 AND $2",
|
||||
argsLen: 2,
|
||||
},
|
||||
{
|
||||
name: "NOT BETWEEN operator",
|
||||
query: DynamicQuery{
|
||||
Filters: []FilterGroup{{
|
||||
Filters: []DynamicFilter{{
|
||||
Column: "age",
|
||||
Operator: OpNotBetween,
|
||||
Value: []interface{}{"18", "65"},
|
||||
}},
|
||||
}},
|
||||
},
|
||||
expected: "SELECT * FROM test_table WHERE \"age\" NOT BETWEEN $1 AND $2",
|
||||
argsLen: 2,
|
||||
},
|
||||
{
|
||||
name: "LIKE operator",
|
||||
query: DynamicQuery{
|
||||
Filters: []FilterGroup{{
|
||||
Filters: []DynamicFilter{{
|
||||
Column: "name",
|
||||
Operator: OpLike,
|
||||
Value: "John%",
|
||||
}},
|
||||
}},
|
||||
},
|
||||
expected: "SELECT * FROM test_table WHERE \"full_name\" LIKE $1",
|
||||
argsLen: 1,
|
||||
},
|
||||
{
|
||||
name: "ILIKE operator",
|
||||
query: DynamicQuery{
|
||||
Filters: []FilterGroup{{
|
||||
Filters: []DynamicFilter{{
|
||||
Column: "name",
|
||||
Operator: OpILike,
|
||||
Value: "john%",
|
||||
}},
|
||||
}},
|
||||
},
|
||||
expected: "SELECT * FROM test_table WHERE \"full_name\" ILIKE $1",
|
||||
argsLen: 1,
|
||||
},
|
||||
{
|
||||
name: "greater than operator",
|
||||
query: DynamicQuery{
|
||||
Filters: []FilterGroup{{
|
||||
Filters: []DynamicFilter{{
|
||||
Column: "age",
|
||||
Operator: OpGreaterThan,
|
||||
Value: "30",
|
||||
}},
|
||||
}},
|
||||
},
|
||||
expected: "SELECT * FROM test_table WHERE \"age\" > $1",
|
||||
argsLen: 1,
|
||||
},
|
||||
{
|
||||
name: "less than or equal operator",
|
||||
query: DynamicQuery{
|
||||
Filters: []FilterGroup{{
|
||||
Filters: []DynamicFilter{{
|
||||
Column: "age",
|
||||
Operator: OpLessThanEqual,
|
||||
Value: "65",
|
||||
}},
|
||||
}},
|
||||
},
|
||||
expected: "SELECT * FROM test_table WHERE \"age\" <= $1",
|
||||
argsLen: 1,
|
||||
},
|
||||
{
|
||||
name: "NOT IN operator",
|
||||
query: DynamicQuery{
|
||||
Filters: []FilterGroup{{
|
||||
Filters: []DynamicFilter{{
|
||||
Column: "status",
|
||||
Operator: OpNotIn,
|
||||
Value: []interface{}{"deleted", "inactive"},
|
||||
}},
|
||||
}},
|
||||
},
|
||||
expected: "SELECT * FROM test_table WHERE \"status\" NOT IN ($1, $2)",
|
||||
argsLen: 2,
|
||||
},
|
||||
{
|
||||
name: "multiple filters with AND",
|
||||
query: DynamicQuery{
|
||||
Filters: []FilterGroup{{
|
||||
Filters: []DynamicFilter{
|
||||
{
|
||||
Column: "status",
|
||||
Operator: OpEqual,
|
||||
Value: "active",
|
||||
},
|
||||
{
|
||||
Column: "age",
|
||||
Operator: OpGreaterThan,
|
||||
Value: "18",
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
expected: "SELECT * FROM test_table WHERE \"status\" = $1 AND \"age\" > $2",
|
||||
argsLen: 2,
|
||||
},
|
||||
{
|
||||
name: "NULL check",
|
||||
query: DynamicQuery{
|
||||
Filters: []FilterGroup{{
|
||||
Filters: []DynamicFilter{{
|
||||
Column: "name",
|
||||
Operator: OpNull,
|
||||
}},
|
||||
}},
|
||||
},
|
||||
expected: "SELECT * FROM test_table WHERE \"full_name\" IS NULL",
|
||||
argsLen: 0,
|
||||
},
|
||||
{
|
||||
name: "NOT NULL check",
|
||||
query: DynamicQuery{
|
||||
Filters: []FilterGroup{{
|
||||
Filters: []DynamicFilter{{
|
||||
Column: "name",
|
||||
Operator: OpNotNull,
|
||||
}},
|
||||
}},
|
||||
},
|
||||
expected: "SELECT * FROM test_table WHERE \"full_name\" IS NOT NULL",
|
||||
argsLen: 0,
|
||||
},
|
||||
{
|
||||
name: "CONTAINS operator",
|
||||
query: DynamicQuery{
|
||||
Filters: []FilterGroup{{
|
||||
Filters: []DynamicFilter{{
|
||||
Column: "name",
|
||||
Operator: OpContains,
|
||||
Value: "John",
|
||||
}},
|
||||
}},
|
||||
},
|
||||
expected: "SELECT * FROM test_table WHERE \"full_name\" ILIKE $1",
|
||||
argsLen: 1,
|
||||
},
|
||||
{
|
||||
name: "STARTS WITH operator",
|
||||
query: DynamicQuery{
|
||||
Filters: []FilterGroup{{
|
||||
Filters: []DynamicFilter{{
|
||||
Column: "name",
|
||||
Operator: OpStartsWith,
|
||||
Value: "John",
|
||||
}},
|
||||
}},
|
||||
},
|
||||
expected: "SELECT * FROM test_table WHERE \"full_name\" ILIKE $1",
|
||||
argsLen: 1,
|
||||
},
|
||||
{
|
||||
name: "ENDS WITH operator",
|
||||
query: DynamicQuery{
|
||||
Filters: []FilterGroup{{
|
||||
Filters: []DynamicFilter{{
|
||||
Column: "name",
|
||||
Operator: OpEndsWith,
|
||||
Value: "son",
|
||||
}},
|
||||
}},
|
||||
},
|
||||
expected: "SELECT * FROM test_table WHERE \"full_name\" ILIKE $1",
|
||||
argsLen: 1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
sql, args, err := builder.BuildQuery(tt.query)
|
||||
if err != nil {
|
||||
t.Errorf("BuildQuery() error = %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if sql != tt.expected {
|
||||
t.Errorf("BuildQuery() sql = %v, expected %v", sql, tt.expected)
|
||||
}
|
||||
|
||||
if len(args) != tt.argsLen {
|
||||
t.Errorf("BuildQuery() args length = %v, expected %v", len(args), tt.argsLen)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryBuilder_BuildCountQuery(t *testing.T) {
|
||||
builder := NewQueryBuilder("test_table").
|
||||
SetColumnMapping(map[string]string{
|
||||
"name": "full_name",
|
||||
}).
|
||||
SetAllowedColumns([]string{"id", "name", "age", "status"})
|
||||
|
||||
query := DynamicQuery{
|
||||
Filters: []FilterGroup{{
|
||||
Filters: []DynamicFilter{{
|
||||
Column: "status",
|
||||
Operator: OpEqual,
|
||||
Value: "active",
|
||||
}},
|
||||
}},
|
||||
}
|
||||
|
||||
sql, args, err := builder.BuildCountQuery(query)
|
||||
if err != nil {
|
||||
t.Errorf("BuildCountQuery() error = %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
expected := "SELECT COUNT(*) FROM test_table WHERE \"status\" = $1"
|
||||
if sql != expected {
|
||||
t.Errorf("BuildCountQuery() sql = %v, expected %v", sql, expected)
|
||||
}
|
||||
|
||||
if len(args) != 1 {
|
||||
t.Errorf("BuildCountQuery() args length = %v, expected 1", len(args))
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryBuilder_WithSorting(t *testing.T) {
|
||||
builder := NewQueryBuilder("test_table").
|
||||
SetColumnMapping(map[string]string{
|
||||
"name": "full_name",
|
||||
}).
|
||||
SetAllowedColumns([]string{"id", "name", "age", "status"})
|
||||
|
||||
query := DynamicQuery{
|
||||
Sort: []SortField{
|
||||
{Column: "name", Order: "ASC"},
|
||||
{Column: "age", Order: "DESC"},
|
||||
},
|
||||
}
|
||||
|
||||
sql, _, err := builder.BuildQuery(query)
|
||||
if err != nil {
|
||||
t.Errorf("BuildQuery() error = %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
expected := "SELECT * FROM test_table ORDER BY \"full_name\" ASC, \"age\" DESC"
|
||||
if sql != expected {
|
||||
t.Errorf("BuildQuery() sql = %v, expected %v", sql, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryBuilder_WithPagination(t *testing.T) {
|
||||
builder := NewQueryBuilder("test_table")
|
||||
|
||||
query := DynamicQuery{
|
||||
Limit: 10,
|
||||
Offset: 20,
|
||||
}
|
||||
|
||||
sql, args, err := builder.BuildQuery(query)
|
||||
if err != nil {
|
||||
t.Errorf("BuildQuery() error = %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
expected := "SELECT * FROM test_table LIMIT $1 OFFSET $2"
|
||||
if sql != expected {
|
||||
t.Errorf("BuildQuery() sql = %v, expected %v", sql, expected)
|
||||
}
|
||||
|
||||
if len(args) != 2 {
|
||||
t.Errorf("BuildQuery() args length = %v, expected 2", len(args))
|
||||
}
|
||||
|
||||
if args[0] != 10 || args[1] != 20 {
|
||||
t.Errorf("BuildQuery() args = %v, expected [10, 20]", args)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryBuilder_WithFields(t *testing.T) {
|
||||
builder := NewQueryBuilder("test_table").
|
||||
SetAllowedColumns([]string{"id", "name", "age"})
|
||||
|
||||
query := DynamicQuery{
|
||||
Fields: []string{"id", "name"},
|
||||
}
|
||||
|
||||
sql, _, err := builder.BuildQuery(query)
|
||||
if err != nil {
|
||||
t.Errorf("BuildQuery() error = %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
expected := "SELECT \"id\", \"name\" FROM test_table"
|
||||
if sql != expected {
|
||||
t.Errorf("BuildQuery() sql = %v, expected %v", sql, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryBuilder_SecurityChecks(t *testing.T) {
|
||||
builder := NewQueryBuilder("test_table").
|
||||
SetAllowedColumns([]string{"id", "name"})
|
||||
|
||||
query := DynamicQuery{
|
||||
Filters: []FilterGroup{{
|
||||
Filters: []DynamicFilter{{
|
||||
Column: "password", // Not in allowed columns
|
||||
Operator: OpEqual,
|
||||
Value: "secret",
|
||||
}},
|
||||
}},
|
||||
}
|
||||
|
||||
sql, args, err := builder.BuildQuery(query)
|
||||
if err != nil {
|
||||
t.Errorf("BuildQuery() error = %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Should not include the password filter
|
||||
expected := "SELECT * FROM test_table"
|
||||
if sql != expected {
|
||||
t.Errorf("BuildQuery() sql = %v, expected %v", sql, expected)
|
||||
}
|
||||
|
||||
if len(args) != 0 {
|
||||
t.Errorf("BuildQuery() args length = %v, expected 0", len(args))
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryBuilder_GroupByAndHaving(t *testing.T) {
|
||||
builder := NewQueryBuilder("test_table").
|
||||
SetAllowedColumns([]string{"status", "count"})
|
||||
|
||||
query := DynamicQuery{
|
||||
Fields: []string{"status", "COUNT(*) as count"},
|
||||
GroupBy: []string{"status"},
|
||||
Having: []FilterGroup{{
|
||||
Filters: []DynamicFilter{{
|
||||
Column: "count",
|
||||
Operator: OpGreaterThan,
|
||||
Value: "5",
|
||||
}},
|
||||
}},
|
||||
}
|
||||
|
||||
sql, args, err := builder.BuildQuery(query)
|
||||
if err != nil {
|
||||
t.Errorf("BuildQuery() error = %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
expected := "SELECT \"status\", COUNT(*) as count FROM test_table GROUP BY \"status\" HAVING \"count\" > $1"
|
||||
if sql != expected {
|
||||
t.Errorf("BuildQuery() sql = %v, expected %v", sql, expected)
|
||||
}
|
||||
|
||||
if len(args) != 1 {
|
||||
t.Errorf("BuildQuery() args length = %v, expected 1", len(args))
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryBuilder_ComplexQuery(t *testing.T) {
|
||||
builder := NewQueryBuilder("test_table").
|
||||
SetColumnMapping(map[string]string{
|
||||
"name": "full_name",
|
||||
}).
|
||||
SetAllowedColumns([]string{"id", "name", "age", "status", "created_at"})
|
||||
|
||||
query := DynamicQuery{
|
||||
Fields: []string{"id", "name", "age"},
|
||||
Filters: []FilterGroup{{
|
||||
Filters: []DynamicFilter{
|
||||
{
|
||||
Column: "status",
|
||||
Operator: OpIn,
|
||||
Value: []string{"active", "pending"},
|
||||
},
|
||||
{
|
||||
Column: "age",
|
||||
Operator: OpBetween,
|
||||
Value: []interface{}{"18", "65"},
|
||||
},
|
||||
},
|
||||
}},
|
||||
Sort: []SortField{
|
||||
{Column: "name", Order: "ASC"},
|
||||
{Column: "created_at", Order: "DESC"},
|
||||
},
|
||||
Limit: 20,
|
||||
Offset: 10,
|
||||
}
|
||||
|
||||
sql, args, err := builder.BuildQuery(query)
|
||||
if err != nil {
|
||||
t.Errorf("BuildQuery() error = %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
expected := "SELECT \"id\", \"full_name\", \"age\" FROM test_table WHERE \"status\" IN ($1, $2) AND \"age\" BETWEEN $3 AND $4 ORDER BY \"full_name\" ASC, \"created_at\" DESC LIMIT $5 OFFSET $6"
|
||||
if sql != expected {
|
||||
t.Errorf("BuildQuery() sql = %v, expected %v", sql, expected)
|
||||
}
|
||||
|
||||
if len(args) != 6 {
|
||||
t.Errorf("BuildQuery() args length = %v, expected 6", len(args))
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryBuilder_ErrorCases(t *testing.T) {
|
||||
builder := NewQueryBuilder("test_table")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
query DynamicQuery
|
||||
}{
|
||||
{
|
||||
name: "invalid between operator - not enough values",
|
||||
query: DynamicQuery{
|
||||
Filters: []FilterGroup{{
|
||||
Filters: []DynamicFilter{{
|
||||
Column: "age",
|
||||
Operator: OpBetween,
|
||||
Value: []interface{}{"18"},
|
||||
}},
|
||||
}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid not between operator - not enough values",
|
||||
query: DynamicQuery{
|
||||
Filters: []FilterGroup{{
|
||||
Filters: []DynamicFilter{{
|
||||
Column: "age",
|
||||
Operator: OpNotBetween,
|
||||
Value: []interface{}{"18"},
|
||||
}},
|
||||
}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, _, err := builder.BuildQuery(tt.query)
|
||||
if err == nil {
|
||||
t.Errorf("BuildQuery() expected error but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryBuilder_UnsupportedOperator(t *testing.T) {
|
||||
builder := NewQueryBuilder("test_table")
|
||||
|
||||
query := DynamicQuery{
|
||||
Filters: []FilterGroup{{
|
||||
Filters: []DynamicFilter{{
|
||||
Column: "name",
|
||||
Operator: FilterOperator("unsupported"),
|
||||
Value: "test",
|
||||
}},
|
||||
}},
|
||||
}
|
||||
|
||||
_, _, err := builder.BuildQuery(query)
|
||||
if err == nil {
|
||||
t.Errorf("BuildQuery() expected error for unsupported operator")
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryBuilder_EmptyFilters(t *testing.T) {
|
||||
builder := NewQueryBuilder("test_table")
|
||||
|
||||
query := DynamicQuery{
|
||||
Filters: []FilterGroup{{
|
||||
Filters: []DynamicFilter{},
|
||||
}},
|
||||
}
|
||||
|
||||
sql, args, err := builder.BuildQuery(query)
|
||||
if err != nil {
|
||||
t.Errorf("BuildQuery() error = %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
expected := "SELECT * FROM test_table"
|
||||
if sql != expected {
|
||||
t.Errorf("BuildQuery() sql = %v, expected %v", sql, expected)
|
||||
}
|
||||
|
||||
if len(args) != 0 {
|
||||
t.Errorf("BuildQuery() args length = %v, expected 0", len(args))
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryBuilder_ParseArrayValue(t *testing.T) {
|
||||
builder := NewQueryBuilder("test_table")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input interface{}
|
||||
expected []interface{}
|
||||
}{
|
||||
{
|
||||
name: "nil value",
|
||||
input: nil,
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "string value",
|
||||
input: "test",
|
||||
expected: []interface{}{"test"},
|
||||
},
|
||||
{
|
||||
name: "comma separated string",
|
||||
input: "a,b,c",
|
||||
expected: []interface{}{"a", "b", "c"},
|
||||
},
|
||||
{
|
||||
name: "slice value",
|
||||
input: []interface{}{"a", "b", "c"},
|
||||
expected: []interface{}{"a", "b", "c"},
|
||||
},
|
||||
{
|
||||
name: "integer value",
|
||||
input: 42,
|
||||
expected: []interface{}{42},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := builder.parseArrayValue(tt.input)
|
||||
if len(result) != len(tt.expected) {
|
||||
t.Errorf("parseArrayValue() length = %v, expected %v", len(result), len(tt.expected))
|
||||
return
|
||||
}
|
||||
|
||||
for i, v := range result {
|
||||
if v != tt.expected[i] {
|
||||
t.Errorf("parseArrayValue()[%d] = %v, expected %v", i, v, tt.expected[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -91,6 +91,26 @@ type SatuSehatConfig struct {
|
||||
}
|
||||
|
||||
// SetHeader generates required headers for BPJS VClaim API
|
||||
// func (cfg BpjsConfig) SetHeader() (string, string, string, string, string) {
|
||||
// timenow := time.Now().UTC()
|
||||
// t, err := time.Parse(time.RFC3339, "1970-01-01T00:00:00Z")
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
|
||||
// tstamp := timenow.Unix() - t.Unix()
|
||||
// secret := []byte(cfg.SecretKey)
|
||||
// message := []byte(cfg.ConsID + "&" + fmt.Sprint(tstamp))
|
||||
// hash := hmac.New(sha256.New, secret)
|
||||
// hash.Write(message)
|
||||
|
||||
// // to lowercase hexits
|
||||
// hex.EncodeToString(hash.Sum(nil))
|
||||
// // to base64
|
||||
// xSignature := base64.StdEncoding.EncodeToString(hash.Sum(nil))
|
||||
|
||||
// return cfg.ConsID, cfg.SecretKey, cfg.UserKey, fmt.Sprint(tstamp), xSignature
|
||||
// }
|
||||
func (cfg BpjsConfig) SetHeader() (string, string, string, string, string) {
|
||||
timenow := time.Now().UTC()
|
||||
t, err := time.Parse(time.RFC3339, "1970-01-01T00:00:00Z")
|
||||
|
||||
@@ -18,8 +18,6 @@ import (
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// cleanResponse removes invalid characters and BOM from the response string
|
||||
|
||||
// VClaimService interface for VClaim operations
|
||||
type VClaimService interface {
|
||||
Get(ctx context.Context, endpoint string, result interface{}) error
|
||||
@@ -101,7 +99,7 @@ func (s *Service) SetHTTPClient(client *http.Client) {
|
||||
}
|
||||
|
||||
// prepareRequest prepares HTTP request with required headers
|
||||
func (s *Service) prepareRequest(ctx context.Context, method, endpoint string, body io.Reader) (*http.Request, error) {
|
||||
func (s *Service) prepareRequest(ctx context.Context, method, endpoint string, body io.Reader) (*http.Request, string, string, string, string, error) {
|
||||
fullURL := s.config.BaseURL + endpoint
|
||||
|
||||
log.Info().
|
||||
@@ -117,11 +115,11 @@ func (s *Service) prepareRequest(ctx context.Context, method, endpoint string, b
|
||||
Str("method", method).
|
||||
Str("endpoint", endpoint).
|
||||
Msg("Failed to create HTTP request")
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
return nil, "", "", "", "", fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
// Set headers using the SetHeader method
|
||||
consID, _, userKey, tstamp, xSignature := s.config.SetHeader()
|
||||
consID, secretKey, userKey, tstamp, xSignature := s.config.SetHeader()
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-cons-id", consID)
|
||||
@@ -137,11 +135,11 @@ func (s *Service) prepareRequest(ctx context.Context, method, endpoint string, b
|
||||
Str("user_key", userKey).
|
||||
Msg("Request headers set")
|
||||
|
||||
return req, nil
|
||||
return req, consID, secretKey, tstamp, xSignature, nil
|
||||
}
|
||||
|
||||
// processResponse processes response from VClaim API
|
||||
func (s *Service) processResponse(res *http.Response) (*ResponDTOVclaim, error) {
|
||||
func (s *Service) processResponse(res *http.Response, consID, secretKey, tstamp string) (*ResponDTOVclaim, error) {
|
||||
defer res.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
@@ -182,8 +180,7 @@ func (s *Service) processResponse(res *http.Response) (*ResponDTOVclaim, error)
|
||||
return finalResp, nil
|
||||
}
|
||||
|
||||
// Decrypt response
|
||||
consID, secretKey, _, tstamp, _ := s.config.SetHeader()
|
||||
// Decrypt response using the same timestamp from the request
|
||||
decryptionKey := consID + secretKey + tstamp
|
||||
|
||||
log.Debug().
|
||||
@@ -192,17 +189,6 @@ func (s *Service) processResponse(res *http.Response) (*ResponDTOVclaim, error)
|
||||
Int("key_length", len(decryptionKey)).
|
||||
Msg("Decryption key components")
|
||||
|
||||
// // Decrypt response
|
||||
// consID, secretKey, userKey, tstamp, _ := s.config.SetHeader()
|
||||
// decryptionKey := GenerateBPJSKey(consID, tstamp, secretKey) // Menggunakan fungsi baru
|
||||
// log.Debug().
|
||||
// Str("consID", consID).
|
||||
// Str("tstamp", tstamp).
|
||||
// Str("userKey", userKey).
|
||||
// Str("secretKey", secretKey).
|
||||
// Int("key_length", len(decryptionKey)).
|
||||
// Msg("Decryption key components")
|
||||
|
||||
respDecrypt, err := ResponseVclaim(respMentah.Response, decryptionKey)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to decrypt response")
|
||||
@@ -271,7 +257,7 @@ func (s *Service) Put(ctx context.Context, endpoint string, payload interface{},
|
||||
}
|
||||
}
|
||||
|
||||
req, err := s.prepareRequest(ctx, http.MethodPut, endpoint, &buf)
|
||||
req, consID, secretKey, tstamp, _, err := s.prepareRequest(ctx, http.MethodPut, endpoint, &buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -281,7 +267,7 @@ func (s *Service) Put(ctx context.Context, endpoint string, payload interface{},
|
||||
return fmt.Errorf("failed to execute PUT request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := s.processResponse(res)
|
||||
resp, err := s.processResponse(res, consID, secretKey, tstamp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -291,7 +277,7 @@ func (s *Service) Put(ctx context.Context, endpoint string, payload interface{},
|
||||
|
||||
// Delete performs HTTP DELETE request
|
||||
func (s *Service) Delete(ctx context.Context, endpoint string, result interface{}) error {
|
||||
req, err := s.prepareRequest(ctx, http.MethodDelete, endpoint, nil)
|
||||
req, consID, secretKey, tstamp, _, err := s.prepareRequest(ctx, http.MethodDelete, endpoint, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -301,7 +287,7 @@ func (s *Service) Delete(ctx context.Context, endpoint string, result interface{
|
||||
return fmt.Errorf("failed to execute DELETE request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := s.processResponse(res)
|
||||
resp, err := s.processResponse(res, consID, secretKey, tstamp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -318,7 +304,7 @@ func (s *Service) Patch(ctx context.Context, endpoint string, payload interface{
|
||||
}
|
||||
}
|
||||
|
||||
req, err := s.prepareRequest(ctx, http.MethodPatch, endpoint, &buf)
|
||||
req, consID, secretKey, tstamp, _, err := s.prepareRequest(ctx, http.MethodPatch, endpoint, &buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -328,7 +314,7 @@ func (s *Service) Patch(ctx context.Context, endpoint string, payload interface{
|
||||
return fmt.Errorf("failed to execute PATCH request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := s.processResponse(res)
|
||||
resp, err := s.processResponse(res, consID, secretKey, tstamp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -338,7 +324,7 @@ func (s *Service) Patch(ctx context.Context, endpoint string, payload interface{
|
||||
|
||||
// GetRawResponse returns raw response without mapping
|
||||
func (s *Service) GetRawResponse(ctx context.Context, endpoint string) (*ResponDTOVclaim, error) {
|
||||
req, err := s.prepareRequest(ctx, http.MethodGet, endpoint, nil)
|
||||
req, consID, secretKey, tstamp, _, err := s.prepareRequest(ctx, http.MethodGet, endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -348,7 +334,7 @@ func (s *Service) GetRawResponse(ctx context.Context, endpoint string) (*ResponD
|
||||
return nil, fmt.Errorf("failed to execute GET request: %w", err)
|
||||
}
|
||||
|
||||
return s.processResponse(res)
|
||||
return s.processResponse(res, consID, secretKey, tstamp)
|
||||
}
|
||||
|
||||
// PostRawResponse returns raw response without mapping
|
||||
@@ -360,7 +346,7 @@ func (s *Service) PostRawResponse(ctx context.Context, endpoint string, payload
|
||||
}
|
||||
}
|
||||
|
||||
req, err := s.prepareRequest(ctx, http.MethodPost, endpoint, &buf)
|
||||
req, consID, secretKey, tstamp, _, err := s.prepareRequest(ctx, http.MethodPost, endpoint, &buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -370,7 +356,7 @@ func (s *Service) PostRawResponse(ctx context.Context, endpoint string, payload
|
||||
return nil, fmt.Errorf("failed to execute POST request: %w", err)
|
||||
}
|
||||
|
||||
return s.processResponse(res)
|
||||
return s.processResponse(res, consID, secretKey, tstamp)
|
||||
}
|
||||
|
||||
// PatchRawResponse returns raw response without mapping
|
||||
@@ -382,7 +368,7 @@ func (s *Service) PatchRawResponse(ctx context.Context, endpoint string, payload
|
||||
}
|
||||
}
|
||||
|
||||
req, err := s.prepareRequest(ctx, http.MethodPatch, endpoint, &buf)
|
||||
req, consID, secretKey, tstamp, _, err := s.prepareRequest(ctx, http.MethodPatch, endpoint, &buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -392,7 +378,7 @@ func (s *Service) PatchRawResponse(ctx context.Context, endpoint string, payload
|
||||
return nil, fmt.Errorf("failed to execute PATCH request: %w", err)
|
||||
}
|
||||
|
||||
return s.processResponse(res)
|
||||
return s.processResponse(res, consID, secretKey, tstamp)
|
||||
}
|
||||
|
||||
// PutRawResponse returns raw response without mapping
|
||||
@@ -404,7 +390,7 @@ func (s *Service) PutRawResponse(ctx context.Context, endpoint string, payload i
|
||||
}
|
||||
}
|
||||
|
||||
req, err := s.prepareRequest(ctx, http.MethodPut, endpoint, &buf)
|
||||
req, consID, secretKey, tstamp, _, err := s.prepareRequest(ctx, http.MethodPut, endpoint, &buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -414,12 +400,12 @@ func (s *Service) PutRawResponse(ctx context.Context, endpoint string, payload i
|
||||
return nil, fmt.Errorf("failed to execute PUT request: %w", err)
|
||||
}
|
||||
|
||||
return s.processResponse(res)
|
||||
return s.processResponse(res, consID, secretKey, tstamp)
|
||||
}
|
||||
|
||||
// DeleteRawResponse returns raw response without mapping
|
||||
func (s *Service) DeleteRawResponse(ctx context.Context, endpoint string) (*ResponDTOVclaim, error) {
|
||||
req, err := s.prepareRequest(ctx, http.MethodDelete, endpoint, nil)
|
||||
req, consID, secretKey, tstamp, _, err := s.prepareRequest(ctx, http.MethodDelete, endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -429,7 +415,7 @@ func (s *Service) DeleteRawResponse(ctx context.Context, endpoint string) (*Resp
|
||||
return nil, fmt.Errorf("failed to execute DELETE request: %w", err)
|
||||
}
|
||||
|
||||
return s.processResponse(res)
|
||||
return s.processResponse(res, consID, secretKey, tstamp)
|
||||
}
|
||||
|
||||
// mapToResult maps the final response to the result interface
|
||||
@@ -498,6 +484,7 @@ func PostRequest(endpoint string, cfg interface{}, data interface{}) interface{}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
func cleanResponse(s string) string {
|
||||
// Remove UTF-8 BOM dan variasi BOM lainnya
|
||||
s = strings.TrimPrefix(s, "\xef\xbb\xbf") // UTF-8 BOM
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestDynamicLogging(t *testing.T) {
|
||||
// Pastikan direktori data ada
|
||||
os.RemoveAll("pkg/logger/data")
|
||||
|
||||
t.Run("TestSaveLogText", testSaveLogText)
|
||||
t.Run("TestSaveLogJSON", testSaveLogJSON)
|
||||
t.Run("TestSaveLogToDatabase", testSaveLogToDatabase)
|
||||
}
|
||||
|
||||
func testSaveLogText(t *testing.T) {
|
||||
logger := New("test-service", INFO, false)
|
||||
|
||||
entry := LogEntry{
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
Level: "INFO",
|
||||
Service: "test-service",
|
||||
Message: "Test log message",
|
||||
File: "test.go",
|
||||
Line: 10,
|
||||
Fields: map[string]interface{}{
|
||||
"test_field": "test_value",
|
||||
"number": 42,
|
||||
},
|
||||
}
|
||||
|
||||
err := logger.SaveLogText(entry)
|
||||
if err != nil {
|
||||
t.Errorf("SaveLogText failed: %v", err)
|
||||
}
|
||||
|
||||
// Verifikasi file dibuat
|
||||
if _, err := os.Stat("pkg/logger/data/logs.txt"); os.IsNotExist(err) {
|
||||
t.Error("Text log file was not created")
|
||||
}
|
||||
}
|
||||
|
||||
func testSaveLogJSON(t *testing.T) {
|
||||
logger := New("test-service", INFO, false)
|
||||
|
||||
entry := LogEntry{
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
Level: "INFO",
|
||||
Service: "test-service",
|
||||
Message: "Test JSON log message",
|
||||
File: "test.go",
|
||||
Line: 20,
|
||||
Fields: map[string]interface{}{
|
||||
"json_field": "json_value",
|
||||
"count": 100,
|
||||
},
|
||||
}
|
||||
|
||||
err := logger.SaveLogJSON(entry)
|
||||
if err != nil {
|
||||
t.Errorf("SaveLogJSON failed: %v", err)
|
||||
}
|
||||
|
||||
// Verifikasi file dibuat
|
||||
if _, err := os.Stat("pkg/logger/data/logs.json"); os.IsNotExist(err) {
|
||||
t.Error("JSON log file was not created")
|
||||
}
|
||||
}
|
||||
|
||||
func testSaveLogToDatabase(t *testing.T) {
|
||||
logger := New("test-service", INFO, false)
|
||||
|
||||
entry := LogEntry{
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
Level: "INFO",
|
||||
Service: "test-service",
|
||||
Message: "Test database log message",
|
||||
File: "test.go",
|
||||
Line: 30,
|
||||
}
|
||||
|
||||
err := logger.SaveLogToDatabase(entry)
|
||||
if err != nil {
|
||||
t.Errorf("SaveLogToDatabase failed: %v", err)
|
||||
}
|
||||
|
||||
// Verifikasi file dibuat (placeholder untuk database)
|
||||
if _, err := os.Stat("pkg/logger/data/database_logs.txt"); os.IsNotExist(err) {
|
||||
t.Error("Database log file was not created")
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ExampleDynamicLogging menunjukkan cara menggunakan fungsi penyimpanan log dinamis
|
||||
func ExampleDynamicLogging() {
|
||||
// Buat logger instance
|
||||
logger := New("test-service", DEBUG, false)
|
||||
|
||||
// Contoh 1: Log biasa dengan penyimpanan otomatis
|
||||
fmt.Println("=== Contoh 1: Log biasa dengan penyimpanan otomatis ===")
|
||||
logger.LogAndSave(INFO, "Aplikasi dimulai", map[string]interface{}{
|
||||
"version": "1.0.0",
|
||||
"mode": "development",
|
||||
})
|
||||
|
||||
// Contoh 2: Log dengan request ID
|
||||
fmt.Println("\n=== Contoh 2: Log dengan request ID ===")
|
||||
reqLogger := logger.WithRequestID("req-123456")
|
||||
reqLogger.LogAndSave(INFO, "Request diproses", map[string]interface{}{
|
||||
"endpoint": "/api/v1/users",
|
||||
"method": "GET",
|
||||
"user_id": 1001,
|
||||
})
|
||||
|
||||
// Contoh 3: Log error
|
||||
fmt.Println("\n=== Contoh 3: Log error ===")
|
||||
logger.LogAndSave(ERROR, "Database connection failed", map[string]interface{}{
|
||||
"error": "connection timeout",
|
||||
"timeout": "30s",
|
||||
"host": "localhost:5432",
|
||||
})
|
||||
|
||||
// Contoh 4: Manual save ke berbagai format
|
||||
fmt.Println("\n=== Contoh 4: Manual save ke berbagai format ===")
|
||||
manualEntry := LogEntry{
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
Level: "INFO",
|
||||
Service: "manual-service",
|
||||
Message: "Manual log entry",
|
||||
RequestID: "manual-req-001",
|
||||
File: "example.go",
|
||||
Line: 42,
|
||||
Fields: map[string]interface{}{
|
||||
"custom_field": "custom_value",
|
||||
"number": 42,
|
||||
},
|
||||
}
|
||||
|
||||
// Simpan manual ke berbagai format
|
||||
if err := SaveLogText(manualEntry); err != nil {
|
||||
fmt.Printf("Error saving text log: %v\n", err)
|
||||
}
|
||||
|
||||
if err := SaveLogJSON(manualEntry); err != nil {
|
||||
fmt.Printf("Error saving JSON log: %v\n", err)
|
||||
}
|
||||
|
||||
if err := SaveLogToDatabase(manualEntry); err != nil {
|
||||
fmt.Printf("Error saving to database log: %v\n", err)
|
||||
}
|
||||
|
||||
// Contoh 5: Log dengan durasi
|
||||
fmt.Println("\n=== Contoh 5: Log dengan durasi ===")
|
||||
start := time.Now()
|
||||
time.Sleep(100 * time.Millisecond) // Simulasi proses
|
||||
duration := time.Since(start)
|
||||
|
||||
logger.LogAndSave(INFO, "Process completed", map[string]interface{}{
|
||||
"operation": "data_processing",
|
||||
"duration": duration.String(),
|
||||
"items": 150,
|
||||
})
|
||||
|
||||
fmt.Println("\n=== Semua log telah disimpan dalam berbagai format ===")
|
||||
fmt.Println("1. Format teks dengan pemisah |: pkg/logger/data/logs.txt")
|
||||
fmt.Println("2. Format JSON: pkg/logger/data/logs.json")
|
||||
fmt.Println("3. Format database (placeholder): pkg/logger/data/database_logs.txt")
|
||||
}
|
||||
|
||||
// ExampleMiddlewareLogging menunjukkan penggunaan dalam middleware
|
||||
func ExampleMiddlewareLogging() {
|
||||
fmt.Println("\n=== Contoh Penggunaan dalam Middleware ===")
|
||||
|
||||
middlewareLogger := New("middleware-service", INFO, false)
|
||||
|
||||
// Simulasi request processing
|
||||
middlewareLogger.LogAndSave(INFO, "Request received", map[string]interface{}{
|
||||
"method": "POST",
|
||||
"path": "/api/v1/auth/login",
|
||||
"client_ip": "192.168.1.100",
|
||||
"user_agent": "Mozilla/5.0",
|
||||
"content_type": "application/json",
|
||||
})
|
||||
|
||||
// Simulasi response
|
||||
middlewareLogger.LogAndSave(INFO, "Response sent", map[string]interface{}{
|
||||
"status_code": 200,
|
||||
"duration": "150ms",
|
||||
"response_size": "2.5KB",
|
||||
})
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestLoggerExamples(t *testing.T) {
|
||||
// Example 1: Basic logging
|
||||
Info("Application starting up")
|
||||
Debug("Debug information", map[string]interface{}{"config_loaded": true})
|
||||
|
||||
// Example 2: Service-specific logging
|
||||
authLogger := AuthServiceLogger()
|
||||
authLogger.Info("User authentication successful", map[string]interface{}{
|
||||
"user_id": "12345",
|
||||
"method": "oauth2",
|
||||
})
|
||||
|
||||
// Example 3: Error logging with context
|
||||
Error("Database connection failed", map[string]interface{}{
|
||||
"error": "connection timeout",
|
||||
"retry_count": 3,
|
||||
"max_retries": 5,
|
||||
"service": "database-service",
|
||||
})
|
||||
|
||||
// Example 4: Performance timing
|
||||
start := time.Now()
|
||||
time.Sleep(10 * time.Millisecond) // Simulate work
|
||||
globalLogger.LogDuration(start, "Database query completed", map[string]interface{}{
|
||||
"query": "SELECT * FROM users",
|
||||
"rows": 150,
|
||||
"database": "postgres",
|
||||
})
|
||||
|
||||
// Example 5: JSON format logging
|
||||
jsonLogger := New("test-service", DEBUG, true)
|
||||
jsonLogger.Info("JSON formatted log", map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
"id": "user-123",
|
||||
"name": "John Doe",
|
||||
"email": "john@example.com",
|
||||
},
|
||||
"request": map[string]interface{}{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/users",
|
||||
},
|
||||
})
|
||||
|
||||
t.Log("Logger examples executed successfully")
|
||||
}
|
||||
|
||||
func TestLoggerLevels(t *testing.T) {
|
||||
// Test different log levels
|
||||
Debug("This is a debug message")
|
||||
Info("This is an info message")
|
||||
Warn("This is a warning message")
|
||||
Error("This is an error message")
|
||||
|
||||
t.Log("All log levels tested")
|
||||
}
|
||||
|
||||
func TestLoggerWithRequestContext(t *testing.T) {
|
||||
// Simulate request context with IDs
|
||||
logger := Default().
|
||||
WithRequestID("req-123456").
|
||||
WithCorrelationID("corr-789012")
|
||||
|
||||
logger.Info("Request processing started", map[string]interface{}{
|
||||
"endpoint": "/api/v1/data",
|
||||
"method": "POST",
|
||||
"client_ip": "192.168.1.100",
|
||||
})
|
||||
|
||||
t.Log("Request context logging tested")
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -47,6 +48,8 @@ type Logger struct {
|
||||
output *log.Logger
|
||||
mu sync.Mutex
|
||||
jsonFormat bool
|
||||
|
||||
logDir string
|
||||
}
|
||||
|
||||
// LogEntry represents a structured log entry
|
||||
@@ -64,12 +67,81 @@ type LogEntry struct {
|
||||
}
|
||||
|
||||
// New creates a new logger instance
|
||||
func New(serviceName string, level LogLevel, jsonFormat bool) *Logger {
|
||||
func New(serviceName string, level LogLevel, jsonFormat bool, logDir ...string) *Logger {
|
||||
// Tentukan direktori log berdasarkan prioritas:
|
||||
// 1. Parameter logDir (jika disediakan)
|
||||
// 2. Environment variable LOG_DIR (jika ada)
|
||||
// 3. Default ke pkg/logger/data relatif terhadap root proyek
|
||||
|
||||
var finalLogDir string
|
||||
|
||||
// Cek apakah logDir disediakan sebagai parameter
|
||||
if len(logDir) > 0 && logDir[0] != "" {
|
||||
finalLogDir = logDir[0]
|
||||
} else {
|
||||
// Cek environment variable
|
||||
if envLogDir := os.Getenv("LOG_DIR"); envLogDir != "" {
|
||||
finalLogDir = envLogDir
|
||||
} else {
|
||||
// Default: dapatkan path relatif terhadap root proyek
|
||||
// Dapatkan path executable
|
||||
exePath, err := os.Executable()
|
||||
if err != nil {
|
||||
// Fallback ke current working directory jika gagal
|
||||
finalLogDir = filepath.Join(".", "pkg", "logger", "data")
|
||||
} else {
|
||||
// Dapatkan direktori executable
|
||||
exeDir := filepath.Dir(exePath)
|
||||
|
||||
// Jika berjalan dengan go run, executable ada di temp directory
|
||||
// Coba dapatkan path source code
|
||||
if strings.Contains(exeDir, "go-build") || strings.Contains(exeDir, "tmp") {
|
||||
// Gunakan runtime.Caller untuk mendapatkan path source
|
||||
_, file, _, ok := runtime.Caller(0)
|
||||
if ok {
|
||||
// Dapatkan direktori source (2 level up dari pkg/logger)
|
||||
sourceDir := filepath.Dir(file)
|
||||
for i := 0; i < 3; i++ { // Naik 3 level ke root proyek
|
||||
sourceDir = filepath.Dir(sourceDir)
|
||||
}
|
||||
finalLogDir = filepath.Join(sourceDir, "pkg", "logger", "data")
|
||||
} else {
|
||||
// Fallback
|
||||
finalLogDir = filepath.Join(".", "pkg", "logger", "data")
|
||||
}
|
||||
} else {
|
||||
// Untuk binary yang sudah dikompilasi, asumsikan struktur proyek
|
||||
finalLogDir = filepath.Join(exeDir, "pkg", "logger", "data")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Konversi ke path absolut
|
||||
absPath, err := filepath.Abs(finalLogDir)
|
||||
if err == nil {
|
||||
finalLogDir = absPath
|
||||
}
|
||||
|
||||
// Buat direktori jika belum ada
|
||||
if err := os.MkdirAll(finalLogDir, 0755); err != nil {
|
||||
// Fallback ke stdout jika gagal membuat direktori
|
||||
fmt.Printf("Warning: Failed to create log directory %s: %v\n", finalLogDir, err)
|
||||
return &Logger{
|
||||
serviceName: serviceName,
|
||||
level: level,
|
||||
output: log.New(os.Stdout, "", 0),
|
||||
jsonFormat: jsonFormat,
|
||||
logDir: "", // Kosongkan karena gagal
|
||||
}
|
||||
}
|
||||
|
||||
return &Logger{
|
||||
serviceName: serviceName,
|
||||
level: level,
|
||||
output: log.New(os.Stdout, "", 0),
|
||||
jsonFormat: jsonFormat,
|
||||
logDir: finalLogDir,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,6 +167,7 @@ func (l *Logger) WithService(serviceName string) *Logger {
|
||||
level: l.level,
|
||||
output: l.output,
|
||||
jsonFormat: l.jsonFormat,
|
||||
logDir: l.logDir,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,6 +259,7 @@ func (l *Logger) WithFields(fields map[string]interface{}) *Logger {
|
||||
level: l.level,
|
||||
output: l.output,
|
||||
jsonFormat: l.jsonFormat,
|
||||
logDir: l.logDir,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,6 +386,7 @@ func (l *Logger) withField(key string, value interface{}) *Logger {
|
||||
level: l.level,
|
||||
output: l.output,
|
||||
jsonFormat: l.jsonFormat,
|
||||
logDir: l.logDir,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -402,13 +477,15 @@ func (l *Logger) SaveLogText(entry LogEntry) error {
|
||||
logLine += "\n"
|
||||
|
||||
// Buat direktori jika belum ada
|
||||
dirPath := "pkg/logger/data"
|
||||
if err := os.MkdirAll(dirPath, 0755); err != nil {
|
||||
if err := os.MkdirAll(l.logDir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Tulis ke file
|
||||
filePath := dirPath + "/logs.txt"
|
||||
// Tulis ke file dengan mutex lock untuk concurrency safety
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
filePath := filepath.Join(l.logDir, "logs.txt")
|
||||
f, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -429,13 +506,15 @@ func (l *Logger) SaveLogJSON(entry LogEntry) error {
|
||||
}
|
||||
|
||||
// Buat direktori jika belum ada
|
||||
dirPath := "pkg/logger/data"
|
||||
if err := os.MkdirAll(dirPath, 0755); err != nil {
|
||||
if err := os.MkdirAll(l.logDir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Tulis ke file
|
||||
filePath := dirPath + "/logs.json"
|
||||
// Tulis ke file dengan mutex lock for concurrency safety
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
filePath := filepath.Join(l.logDir, "logs.json")
|
||||
f, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -458,12 +537,15 @@ func (l *Logger) SaveLogToDatabase(entry LogEntry) error {
|
||||
dbLogLine := fmt.Sprintf("DB_LOG: %s|%s|%s|%s\n",
|
||||
entry.Timestamp, entry.Level, entry.Service, entry.Message)
|
||||
|
||||
dirPath := "pkg/logger/data"
|
||||
if err := os.MkdirAll(dirPath, 0755); err != nil {
|
||||
if err := os.MkdirAll(l.logDir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
filePath := dirPath + "/database_logs.txt"
|
||||
// Tulis ke file dengan mutex lock for concurrency safety
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
filePath := filepath.Join(l.logDir, "database_logs.txt")
|
||||
f, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
Reference in New Issue
Block a user