diff --git a/.act.secrets.example b/.act.secrets.example index 554e577..806c07a 100644 --- a/.act.secrets.example +++ b/.act.secrets.example @@ -8,6 +8,7 @@ # Para obtener los valores reales, ver: docs/ES/paquete/publishing.md # ── Supabase (requerido para el job e2e, opcional para lint/test) ── +appLogger_supabaseUrl=https://hqvkrsmlphjnkefpfpzg.supabase.co APPLOGGER_SUPABASE_URL=https://hqvkrsmlphjnkefpfpzg.supabase.co APPLOGGER_SUPABASE_ANON_KEY= APPLOGGER_SUPABASE_SERVICE_KEY= diff --git a/.github/agents/applogger-delivery-engineer.agent.md b/.github/agents/applogger-delivery-engineer.agent.md index 35c92b3..3b8e803 100644 --- a/.github/agents/applogger-delivery-engineer.agent.md +++ b/.github/agents/applogger-delivery-engineer.agent.md @@ -7,6 +7,8 @@ user-invocable: true argument-hint: 'Describe the completed feature, fix, refactor, release, or repo-delivery task.' --- + + You are the delivery specialist for the AppLoggers repository. Your job is to take completed work from local validation to safe repository delivery without losing project structure, branching policy, CI expectations, release steps, or documentation obligations. ## Skills to Load @@ -77,4 +79,4 @@ Always report: 2. What was validated. 3. What branch/PR/tag state exists. 4. Any blocker or remaining risk. -5. Exact next step. \ No newline at end of file +5. Exact next step. diff --git a/.github/agents/applogger-docs-architecture-auditor.agent.md b/.github/agents/applogger-docs-architecture-auditor.agent.md index d5fc5fa..5f4d28b 100644 --- a/.github/agents/applogger-docs-architecture-auditor.agent.md +++ b/.github/agents/applogger-docs-architecture-auditor.agent.md @@ -7,6 +7,8 @@ user-invocable: false argument-hint: 'Describe the code or architecture change and what documentation or architectural consistency needs review.' --- + + You are the documentation and architecture audit subagent for AppLoggers. Your job is to inspect repository structure, architectural boundaries, and documentation drift across the SDK, docs, future CLI, and future frontend. You focus on factual alignment, not marketing. @@ -50,4 +52,4 @@ Return: 2. Architecture consistency findings. 3. Required doc updates. 4. Optional improvements. -5. Future CLI/frontend considerations if applicable. \ No newline at end of file +5. Future CLI/frontend considerations if applicable. diff --git a/.github/agents/applogger-release-reviewer.agent.md b/.github/agents/applogger-release-reviewer.agent.md index 9f44268..7e435e6 100644 --- a/.github/agents/applogger-release-reviewer.agent.md +++ b/.github/agents/applogger-release-reviewer.agent.md @@ -7,6 +7,8 @@ user-invocable: false argument-hint: 'Describe the change, PR, dependency update, or release candidate that needs review.' --- + + You are the release and delivery review subagent for AppLoggers. Your job is to judge release readiness with the discipline of a senior corporate engineer. You review current SDK release implications, CI/release workflows, Dependabot updates, and future cross-component effects as CLI and frontend are introduced. @@ -52,4 +54,4 @@ Return: 2. Tag eligibility decision. 3. CI/release/publish risks. 4. Dependabot or PR recommendation if relevant. -5. Future CLI/frontend/Supabase release considerations if applicable. \ No newline at end of file +5. Future CLI/frontend/Supabase release considerations if applicable. diff --git a/.github/skills/applogger-change-delivery/SKILL.md b/.github/skills/applogger-change-delivery/SKILL.md index 41f31a8..5b6884b 100644 --- a/.github/skills/applogger-change-delivery/SKILL.md +++ b/.github/skills/applogger-change-delivery/SKILL.md @@ -57,4 +57,4 @@ For larger changes, run read-only subagent passes in parallel before editing or 2. Report the selected validation profile (code, docs-only, or mixed). 3. Report whether docs changed or were intentionally unchanged. 4. Report push/PR status. -5. State whether the change is tag-eligible, not tag-eligible, or needs explicit release intent. \ No newline at end of file +5. State whether the change is tag-eligible, not tag-eligible, or needs explicit release intent. diff --git a/.github/skills/applogger-change-delivery/references/documentation-gate.md b/.github/skills/applogger-change-delivery/references/documentation-gate.md index 91b49da..0f717b0 100644 --- a/.github/skills/applogger-change-delivery/references/documentation-gate.md +++ b/.github/skills/applogger-change-delivery/references/documentation-gate.md @@ -8,4 +8,4 @@ After a completed change, review whether any of these need updates: 4. `docs/ES/paquete/` 5. release or publishing docs if versioning/release behavior changed -If docs are intentionally unchanged, the agent should say why. \ No newline at end of file +If docs are intentionally unchanged, the agent should say why. diff --git a/.github/skills/applogger-change-delivery/references/local-actions-validation.md b/.github/skills/applogger-change-delivery/references/local-actions-validation.md index 9399512..2f0de9f 100644 --- a/.github/skills/applogger-change-delivery/references/local-actions-validation.md +++ b/.github/skills/applogger-change-delivery/references/local-actions-validation.md @@ -11,4 +11,7 @@ Notes: 1. `act` uses `.actrc`. 2. Secrets come from `.act.secrets` based on `.act.secrets.example`. -3. `e2e` may require additional Supabase secrets and is not always mandatory for local verification. \ No newline at end of file +3. `e2e` may require additional Supabase secrets and is not always mandatory for local verification. +4. On Windows hosts, `act` may lose the Unix execute bit on `sdk/gradlew` when copying from NTFS into Linux containers. Workflows should run `chmod +x ./gradlew` before the first Gradle invocation. +5. `actions/upload-artifact` and related artifact steps can fail under `act` because `ACTIONS_RUNTIME_TOKEN` is not available locally. +6. CodeQL analysis depends on GitHub-hosted API context and is expected to remain partial or unavailable under local `act` runs. diff --git a/.github/skills/applogger-cli-agent-operator/SKILL.md b/.github/skills/applogger-cli-agent-operator/SKILL.md index c9f95cb..a8ede97 100644 --- a/.github/skills/applogger-cli-agent-operator/SKILL.md +++ b/.github/skills/applogger-cli-agent-operator/SKILL.md @@ -13,14 +13,35 @@ Use this skill when a Copilot agent or automation needs to interact with AppLogg ## Hard Rules -1. Prefer `--output agent` for machine consumption (TOON compact encoding via toon-go). -2. Use `--output json` when downstream systems require JSON strictly. -2. Treat exit code 0 as success, 1 as runtime failure, 2 as usage error. -3. Run `agent schema` and `capabilities` before implementing new automation assumptions. -4. Do not parse free-form text when JSON is available. +1. Before telemetry usage, ensure the CLI is installed and executable on the current host. +2. Prefer `--output agent` for machine consumption (TOON compact encoding via toon-go). +3. Use `--output json` when downstream systems require JSON strictly. +4. Treat exit code 0 as success, 1 as runtime failure, 2 as usage error. +5. Run `agent schema` and `capabilities` before implementing new automation assumptions. +6. Do not parse free-form text when JSON is available. + +## Installation Bootstrap + +If `applogger-cli` is not yet available, install it using the host-native one-line bootstrap: + +1. Linux: + - `curl -fsSL https://raw.githubusercontent.com/devzucca/appLoggers/main/cli/install/install.sh | bash` +2. macOS: + - `curl -fsSL https://raw.githubusercontent.com/devzucca/appLoggers/main/cli/install/install.sh | bash` +3. Windows PowerShell: + - `irm https://raw.githubusercontent.com/devzucca/appLoggers/main/cli/install/install.ps1 | iex` + +Bootstrap rules: + +1. Verify install by running `applogger-cli version --output json`. +2. If `PATH` changed during installation, start a new shell or invoke the installed binary by absolute path once. +3. To pin a specific version, set `APPLOGGER_CLI_VERSION=applogger-cli-vX.Y.Z` before invoking the installer. +4. On macOS/Linux, the bash installer enforces SHA-256 verification for release assets and fails if neither `sha256sum` nor `shasum` is available. ## Standard Command Set +0. Installation verification: + - `applogger-cli version --output json` 1. Metadata discovery: - `applogger-cli --syncbin-metadata --output json` 2. Version/build discovery: @@ -35,24 +56,48 @@ Use this skill when a Copilot agent or automation needs to interact with AppLogg - `applogger-cli telemetry query --output agent` 7. Dedicated compact orchestration response: - `applogger-cli telemetry agent-response --source logs --aggregate severity --preview-limit 5` +8. Warning anomaly inspection: + - `applogger-cli telemetry query --source logs --severity warn --anomaly-type slow_response --output json` ## Supabase Environment Setup Before `telemetry query`, ensure environment is configured: -1. Required: - - `APPLOGGER_SUPABASE_URL` - - `APPLOGGER_SUPABASE_KEY` (service_role key) -2. Optional: - - `APPLOGGER_SUPABASE_SCHEMA` - - `APPLOGGER_SUPABASE_LOG_TABLE` - - `APPLOGGER_SUPABASE_METRIC_TABLE` - - `APPLOGGER_SUPABASE_TIMEOUT_SECONDS` -3. If operating with Supabase MCP available, retrieve: +1. Preferred corporate mode (multi-project): + - `APPLOGGER_CONFIG` (shared JSON project registry) + - `APPLOGGER_PROJECT` (explicit project selection, optional) + - `--config` and `--project` flags for deterministic overrides +2. Legacy required (fallback mode): + - `appLogger_supabaseUrl` + - `appLogger_supabaseKey` (service_role key) +3. Optional: + - `appLogger_supabaseSchema` + - `appLogger_supabaseLogTable` + - `appLogger_supabaseMetricTable` + - `appLogger_supabaseTimeoutSeconds` +4. If operating with Supabase MCP available, retrieve: - project URL from `mcp_supabase_get_project_url` -4. Provision `APPLOGGER_SUPABASE_KEY` from secure secret storage (service_role). +5. Provision `appLogger_supabaseKey` from secure secret storage (service_role). - Do not use publishable/anon keys for CLI read operations. +Project-resolution precedence for automation: + +1. `--project` +2. `APPLOGGER_PROJECT` +3. Workspace autodetection via `workspace_roots` +4. `default_project` +5. Single configured project +6. Legacy env fallback (`appLogger_supabase*`, `APPLOGGER_SUPABASE_*`, `SUPABASE_*`) + +Auditability rule: + +1. Persist `project` and `config_source` from health/telemetry outputs in agent logs. + +Telemetry notes: + +1. Log rows may include `extra` with `extra.anomaly_type`. +2. Use `--anomaly-type` only with `--source=logs`. + ## Error Handling Contract 1. If exit code is 2, the caller should correct arguments and retry. @@ -63,5 +108,7 @@ Before `telemetry query`, ensure environment is configured: 1. Report executed commands. 2. Report parsed JSON fields used for decisions. -3. Report retries and final status. -4. Report remaining uncertainty if command is preview status. +3. If querying logs, state whether `extra` or `extra.anomaly_type` influenced the decision. +4. Report retries and final status. +5. Report remaining uncertainty if command is preview status. +6. If installation was required, report install source, resolved version tag, detected OS/arch, and installed path. diff --git a/.github/skills/applogger-dependabot-review/SKILL.md b/.github/skills/applogger-dependabot-review/SKILL.md index 140d281..7cb431b 100644 --- a/.github/skills/applogger-dependabot-review/SKILL.md +++ b/.github/skills/applogger-dependabot-review/SKILL.md @@ -35,4 +35,4 @@ Trigger phrases include: 1. Findings first, ordered by severity. 2. Clear merge recommendation. -3. Any follow-up validation needed. \ No newline at end of file +3. Any follow-up validation needed. diff --git a/.github/skills/applogger-dependabot-review/references/merge-policy.md b/.github/skills/applogger-dependabot-review/references/merge-policy.md index 16e21f6..baff6a9 100644 --- a/.github/skills/applogger-dependabot-review/references/merge-policy.md +++ b/.github/skills/applogger-dependabot-review/references/merge-policy.md @@ -9,4 +9,4 @@ Default stance: Post-merge duty: 1. Ensure `dev` stays healthy. -2. Review whether docs or release notes need adjustment. \ No newline at end of file +2. Review whether docs or release notes need adjustment. diff --git a/.github/skills/applogger-dependabot-review/references/review-criteria.md b/.github/skills/applogger-dependabot-review/references/review-criteria.md index b1aa3a5..e6b2ee7 100644 --- a/.github/skills/applogger-dependabot-review/references/review-criteria.md +++ b/.github/skills/applogger-dependabot-review/references/review-criteria.md @@ -17,4 +17,4 @@ Higher-risk examples: 1. Kotlin major version changes. 2. AGP or Gradle behavior shifts. -3. Publishing or release-action changes. \ No newline at end of file +3. Publishing or release-action changes. diff --git a/.github/skills/applogger-documentation-audit/SKILL.md b/.github/skills/applogger-documentation-audit/SKILL.md index fdef507..82d1ec3 100644 --- a/.github/skills/applogger-documentation-audit/SKILL.md +++ b/.github/skills/applogger-documentation-audit/SKILL.md @@ -35,4 +35,4 @@ Trigger phrases include: 1. List affected docs. 2. State what changed and why. -3. Note any docs intentionally left unchanged. \ No newline at end of file +3. Note any docs intentionally left unchanged. diff --git a/.github/skills/applogger-documentation-audit/references/audit-checklist.md b/.github/skills/applogger-documentation-audit/references/audit-checklist.md index f3870b6..94a67ac 100644 --- a/.github/skills/applogger-documentation-audit/references/audit-checklist.md +++ b/.github/skills/applogger-documentation-audit/references/audit-checklist.md @@ -5,4 +5,4 @@ 3. Do CI/release docs match `.github/workflows/ci.yml` and `release.yml`? 4. Does changelog reflect user-visible changes? 5. Do integration docs still match actual Android and iOS KMP APIs? -6. If dependabot or release behavior changed, is that reflected where needed? \ No newline at end of file +6. If dependabot or release behavior changed, is that reflected where needed? diff --git a/.github/skills/applogger-documentation-audit/references/documentation-scope.md b/.github/skills/applogger-documentation-audit/references/documentation-scope.md index b0d4b45..f5be1a7 100644 --- a/.github/skills/applogger-documentation-audit/references/documentation-scope.md +++ b/.github/skills/applogger-documentation-audit/references/documentation-scope.md @@ -12,4 +12,4 @@ Special attention areas: 1. Branching and release flow 2. Setup and local configuration 3. Publishing instructions -4. Platform support and integration guidance \ No newline at end of file +4. Platform support and integration guidance diff --git a/.github/skills/applogger-release-tagging/SKILL.md b/.github/skills/applogger-release-tagging/SKILL.md index 2cfd6a9..850474e 100644 --- a/.github/skills/applogger-release-tagging/SKILL.md +++ b/.github/skills/applogger-release-tagging/SKILL.md @@ -64,4 +64,4 @@ Interpret requests this way: 1. State the tag and target commit. 2. State workflow run result. -3. State whether GitHub Packages and JitPack are expected to resolve. \ No newline at end of file +3. State whether GitHub Packages and JitPack are expected to resolve. diff --git a/.github/skills/applogger-release-tagging/references/release-verification.md b/.github/skills/applogger-release-tagging/references/release-verification.md index 5af1b0f..5c1e338 100644 --- a/.github/skills/applogger-release-tagging/references/release-verification.md +++ b/.github/skills/applogger-release-tagging/references/release-verification.md @@ -13,4 +13,4 @@ If release fails, inspect: 1. package permissions 2. version passed to Gradle -3. tag/commit mismatch \ No newline at end of file +3. tag/commit mismatch diff --git a/.github/skills/applogger-repo-context/SKILL.md b/.github/skills/applogger-repo-context/SKILL.md index 04851a1..560bee4 100644 --- a/.github/skills/applogger-repo-context/SKILL.md +++ b/.github/skills/applogger-repo-context/SKILL.md @@ -33,4 +33,4 @@ Use this skill when you need to understand the repository before changing or del 1. Name the affected modules and docs. 2. State the branch/release implications. -3. Give exact commands when they matter. \ No newline at end of file +3. Give exact commands when they matter. diff --git a/.github/skills/applogger-repo-context/references/command-baseline.md b/.github/skills/applogger-repo-context/references/command-baseline.md index e08b3de..4b30a62 100644 --- a/.github/skills/applogger-repo-context/references/command-baseline.md +++ b/.github/skills/applogger-repo-context/references/command-baseline.md @@ -16,4 +16,4 @@ Release: 1. `git checkout main` 2. `git pull origin main` 3. `git tag -a vX.Y.Z -m "Release X.Y.Z"` -4. `git push origin vX.Y.Z` \ No newline at end of file +4. `git push origin vX.Y.Z` diff --git a/.github/skills/applogger-repo-context/references/delivery-matrix.md b/.github/skills/applogger-repo-context/references/delivery-matrix.md index 3dc5618..44ac11b 100644 --- a/.github/skills/applogger-repo-context/references/delivery-matrix.md +++ b/.github/skills/applogger-repo-context/references/delivery-matrix.md @@ -20,4 +20,4 @@ Release rule: 3. Push to `dev`. 4. Open PR `dev -> main`. 5. Verify merged `main`. -6. Create and push tag from `main`. \ No newline at end of file +6. Create and push tag from `main`. diff --git a/.github/skills/applogger-repo-context/references/repo-map.md b/.github/skills/applogger-repo-context/references/repo-map.md index c4889b1..2cbdc8f 100644 --- a/.github/skills/applogger-repo-context/references/repo-map.md +++ b/.github/skills/applogger-repo-context/references/repo-map.md @@ -14,4 +14,4 @@ Key SDK modules: 1. `sdk/logger-core/` 2. `sdk/logger-transport-supabase/` 3. `sdk/logger-test/` -4. `sdk/sample/` \ No newline at end of file +4. `sdk/sample/` diff --git a/.github/skills/syncbin-child-cli-architect/SKILL.md b/.github/skills/syncbin-child-cli-architect/SKILL.md index 6f96312..c975a50 100644 --- a/.github/skills/syncbin-child-cli-architect/SKILL.md +++ b/.github/skills/syncbin-child-cli-architect/SKILL.md @@ -228,7 +228,6 @@ Testing: testify (assertions) **Complete Implementation Template**: - **Step 1: Project Setup** ```bash # Create project structure @@ -1037,9 +1036,9 @@ START: Implement tests │ - Test exit codes │ └─ End-to-end tests - - Test full workflows - - Test with real dependencies - - CI/CD validation +- Test full workflows +- Test with real dependencies +- CI/CD validation ``` ### Tree 3: Distribution Strategy diff --git a/.github/workflows/applogger-cli.yml b/.github/workflows/applogger-cli.yml index 368b81d..3916467 100644 --- a/.github/workflows/applogger-cli.yml +++ b/.github/workflows/applogger-cli.yml @@ -5,6 +5,8 @@ on: branches: - main - dev + tags: + - 'applogger-cli-v*' paths: - 'cli/**' - '.github/workflows/applogger-cli.yml' @@ -33,6 +35,8 @@ jobs: go: 1.24.x env: GOFLAGS: -mod=readonly + appLogger_supabaseUrl: ${{ secrets.APPLOGGER_SUPABASE_URL }} + appLogger_supabaseKey: ${{ secrets.APPLOGGER_SUPABASE_KEY }} APPLOGGER_SUPABASE_URL: ${{ secrets.APPLOGGER_SUPABASE_URL }} APPLOGGER_SUPABASE_KEY: ${{ secrets.APPLOGGER_SUPABASE_KEY }} @@ -53,8 +57,7 @@ jobs: - name: Run tests with coverage working-directory: cli - run: go test -v -race -coverprofile=coverage.out -covermode=atomic ./... - continue-on-error: ${{ matrix.os == 'macos' || matrix.os == 'windows' }} + run: go test -v -race "-coverprofile=coverage.out" "-covermode=atomic" ./... - name: Upload coverage to CodeCov if: matrix.os == 'linux' @@ -89,7 +92,7 @@ jobs: name: Build ${{ matrix.os }}-${{ matrix.arch }} runs-on: ${{ matrix.runners }} needs: [test, lint] - if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev') + if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/applogger-cli-v')) strategy: matrix: include: @@ -128,7 +131,20 @@ jobs: - name: Set version info working-directory: cli run: | - echo "VERSION=$(git describe --tags --always)" >> $GITHUB_ENV + BASE_VERSION=$(tr -d ' \t\r\n' < VERSION) + if [[ -z "$BASE_VERSION" ]]; then + echo "cli/VERSION is empty" >&2 + exit 1 + fi + + if [[ "${GITHUB_REF:-}" == refs/tags/applogger-cli-v* ]]; then + VERSION="${GITHUB_REF_NAME}" + else + SHORT_SHA=$(git rev-parse --short HEAD) + VERSION="applogger-cli-v${BASE_VERSION}-dev.${SHORT_SHA}" + fi + + echo "VERSION=${VERSION}" >> $GITHUB_ENV echo "COMMIT=$(git rev-parse --short HEAD)" >> $GITHUB_ENV echo "BUILD_TIME=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV shell: bash @@ -142,7 +158,7 @@ jobs: shell: bash run: | go build \ - -ldflags="-s -w -X github.com/devzucca/appLoggers/cli/internal/cli.version=${{ env.VERSION }} -X github.com/devzucca/appLoggers/cli/internal/cli.commit=${{ env.COMMIT }} -X github.com/devzucca/appLoggers/cli/internal/cli.buildTime=${{ env.BUILD_TIME }}" \ + -ldflags="-s -w -X main.version=${{ env.VERSION }} -X main.commit=${{ env.COMMIT }} -X main.date=${{ env.BUILD_TIME }}" \ -o applogger-cli-${{ matrix.os }}-${{ matrix.arch }}${{ matrix.os == 'windows' && '.exe' || '' }} \ ./cmd/applogger-cli @@ -153,6 +169,25 @@ jobs: path: cli/applogger-cli-${{ matrix.os }}-${{ matrix.arch }}${{ matrix.os == 'windows' && '.exe' || '' }} retention-days: 5 + smoke-linux-artifact: + name: Smoke Test (Linux artifact) + runs-on: ubuntu-latest + needs: [build] + if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/applogger-cli-v')) + steps: + - name: Download Linux artifact + uses: actions/download-artifact@v4 + with: + name: applogger-cli-linux-amd64 + path: ./smoke + + - name: Execute smoke checks + run: | + chmod +x ./smoke/applogger-cli-linux-amd64 + ./smoke/applogger-cli-linux-amd64 version --output json + ./smoke/applogger-cli-linux-amd64 --syncbin-metadata --output json + ./smoke/applogger-cli-linux-amd64 capabilities --output json + security: name: Security Scan runs-on: ubuntu-latest @@ -167,15 +202,50 @@ jobs: cache: true cache-dependency-path: cli/go.sum + - name: Install gosec (pinned) + run: go install github.com/securego/gosec/v2/cmd/gosec@v2.22.2 + - name: Run gosec - uses: securego/gosec@master + run: | + "$(go env GOPATH)/bin/gosec" -no-fail ./cli/... + + governance-main: + name: Governance Gate (main/tag) + runs-on: ubuntu-latest + needs: [build, security] + if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/applogger-cli-v')) + steps: + - name: Download Linux artifact + uses: actions/download-artifact@v4 with: - args: '-no-fail ./cli/...' + name: applogger-cli-linux-amd64 + path: ./governance + + - name: Validate embedded build metadata + run: | + chmod +x ./governance/applogger-cli-linux-amd64 + ./governance/applogger-cli-linux-amd64 version --output json > version.json + VERSION_VALUE="$(jq -r '.version' version.json)" + COMMIT_VALUE="$(jq -r '.commit' version.json)" + DATE_VALUE="$(jq -r '.date' version.json)" + + if [[ -z "${VERSION_VALUE}" || "${VERSION_VALUE}" == "dev" ]]; then + echo "Invalid embedded version: ${VERSION_VALUE}" >&2 + exit 1 + fi + if [[ -z "${COMMIT_VALUE}" || "${COMMIT_VALUE}" == "none" ]]; then + echo "Invalid embedded commit: ${COMMIT_VALUE}" >&2 + exit 1 + fi + if [[ -z "${DATE_VALUE}" || "${DATE_VALUE}" == "unknown" ]]; then + echo "Invalid embedded date: ${DATE_VALUE}" >&2 + exit 1 + fi release: name: Create Release (on tag) runs-on: ubuntu-latest - needs: [test, lint, build, security] + needs: [test, lint, build, security, smoke-linux-artifact, governance-main] if: startsWith(github.ref, 'refs/tags/applogger-cli-v') permissions: contents: write @@ -185,6 +255,15 @@ jobs: - name: Checkout code uses: actions/checkout@v4 + - name: Validate tag against cli/VERSION + run: | + BASE_VERSION=$(tr -d ' \t\r\n' < cli/VERSION) + EXPECTED_TAG="applogger-cli-v${BASE_VERSION}" + if [[ "${GITHUB_REF_NAME}" != "${EXPECTED_TAG}" ]]; then + echo "Tag/version mismatch: got ${GITHUB_REF_NAME}, expected ${EXPECTED_TAG} from cli/VERSION" + exit 1 + fi + - name: Download all artifacts uses: actions/download-artifact@v4 with: @@ -197,6 +276,123 @@ jobs: sha256sum "$file" > "$file.sha256" done + - name: Generate package manager manifests + run: | + VERSION="${GITHUB_REF_NAME#applogger-cli-v}" + REPO_URL="https://github.com/${GITHUB_REPOSITORY}" + DOWNLOAD_BASE="${REPO_URL}/releases/download/${GITHUB_REF_NAME}" + + DARWIN_AMD64_SHA="$(cut -d' ' -f1 artifacts/applogger-cli-darwin-amd64/applogger-cli-darwin-amd64.sha256)" + DARWIN_ARM64_SHA="$(cut -d' ' -f1 artifacts/applogger-cli-darwin-arm64/applogger-cli-darwin-arm64.sha256)" + WINDOWS_AMD64_SHA="$(cut -d' ' -f1 artifacts/applogger-cli-windows-amd64/applogger-cli-windows-amd64.exe.sha256)" + + mkdir -p artifacts/manifests/homebrew + mkdir -p artifacts/manifests/scoop + mkdir -p artifacts/manifests/winget + + cat > artifacts/manifests/homebrew/applogger-cli.rb < "applogger-cli" + else + bin.install "applogger-cli-darwin-amd64" => "applogger-cli" + end + end + + test do + assert_match "applogger-cli", shell_output("#{bin}/applogger-cli version --output text") + end + end + EOF + + cat > artifacts/manifests/scoop/applogger-cli.json < artifacts/manifests/winget/DevZucca.AppLoggerCLI.yaml < artifacts/manifests/winget/DevZucca.AppLoggerCLI.locale.en-US.yaml < artifacts/manifests/winget/DevZucca.AppLoggerCLI.installer.yaml < artifacts/manifests/README.md < applogger-cli.rb < "applogger-cli" + else + bin.install "applogger-cli-darwin-amd64" => "applogger-cli" + end + end + end + EOF + + - name: Push formula to tap repository + if: env.TAP_TOKEN != '' + env: + TAP_REPO: ${{ vars.HOMEBREW_TAP_REPO }} + run: | + git clone "https://x-access-token:${TAP_TOKEN}@github.com/${TAP_REPO}.git" tap + mkdir -p tap/Formula + cp applogger-cli.rb tap/Formula/applogger-cli.rb + cd tap + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add Formula/applogger-cli.rb + git commit -m "chore: publish applogger-cli ${GITHUB_REF_NAME}" || echo "No formula changes" + git push origin HEAD:main + + publish-scoop: + name: Publish Scoop Bucket + runs-on: ubuntu-latest + needs: [release] + if: startsWith(github.ref, 'refs/tags/applogger-cli-v') && vars.SCOOP_BUCKET_REPO != '' + env: + SCOOP_TOKEN: ${{ secrets.SCOOP_BUCKET_TOKEN }} + steps: + - name: Download build artifacts + if: env.SCOOP_TOKEN != '' + uses: actions/download-artifact@v4 + with: + path: ./artifacts + + - name: Build Scoop manifest + if: env.SCOOP_TOKEN != '' + run: | + VERSION="${GITHUB_REF_NAME#applogger-cli-v}" + REPO_URL="https://github.com/${GITHUB_REPOSITORY}" + DOWNLOAD_BASE="${REPO_URL}/releases/download/${GITHUB_REF_NAME}" + WINDOWS_AMD64_SHA="$(sha256sum artifacts/applogger-cli-windows-amd64/applogger-cli-windows-amd64.exe | cut -d' ' -f1)" + + cat > applogger-cli.json < winget/DevZucca/AppLoggerCLI/${VERSION}/DevZucca.AppLoggerCLI.yaml < winget/DevZucca/AppLoggerCLI/${VERSION}/DevZucca.AppLoggerCLI.locale.en-US.yaml < winget/DevZucca/AppLoggerCLI/${VERSION}/DevZucca.AppLoggerCLI.installer.yaml < `VERSION_NAME` + - Se usa para publicar artefactos y para generar `AppLoggerVersion` automaticamente. +- CLI: `cli/VERSION` + - El workflow de CLI lo usa para construir versiones en ramas y valida que el tag release `applogger-cli-v*` coincida con ese valor. + +Regla de release: + +- SDK release tag: `vX.Y.Z...` (pipeline SDK) +- CLI release tag: `applogger-cli-vX.Y.Z...` (pipeline CLI) + --- ## AppLogger CLI (Operaciones) @@ -88,17 +102,14 @@ appLoggers/ ### Instalación Rápida (3 minutos) ```bash -# Linux / macOS -curl -L https://github.com/devzucca/appLoggers/releases/download/applogger-cli-v0.1.0/applogger-cli-linux-amd64 \ - -o /usr/local/bin/applogger-cli -chmod +x /usr/local/bin/applogger-cli +# Linux / macOS (instala la ultima release del CLI) +curl -fsSL https://raw.githubusercontent.com/devzucca/appLoggers/main/cli/install/install.sh | bash # Windows (PowerShell) -$url = "https://github.com/devzucca/appLoggers/releases/download/applogger-cli-v0.1.0/applogger-cli-windows-amd64.exe" -Invoke-WebRequest -Uri $url -OutFile "$env:ProgramFiles\applogger-cli.exe" +irm https://raw.githubusercontent.com/devzucca/appLoggers/main/cli/install/install.ps1 | iex # Verificar -applogger-cli --version +applogger-cli version --output json ``` ### Primeros Comandos @@ -196,7 +207,7 @@ applogger-cli telemetry agent-response \ | Android SDK | API 35 (compileSdk) | Android Studio → SDK Manager | | Gradle | 8.10.2 (usa el wrapper) | `cd sdk && ./gradlew --version` | | Git | 2.30+ | `git --version` | -| Go | 1.25+ (solo si editas el CLI) | `go version` | +| Go | 1.24+ (solo si editas el CLI) | `go version` | ### Paso 1 — Clonar el repositorio @@ -224,21 +235,21 @@ sdk.dir=C:\\Users\\TU_USUARIO\\AppData\\Local\\Android\\Sdk # ── Supabase (backend de logs) ────────────────────────────────────────── # Obtener de: https://supabase.com/dashboard → Settings → API -appLogger.url=https://TU-PROYECTO.supabase.co -appLogger.anonKey=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... +appLogger_url=https://TU-PROYECTO.supabase.co +appLogger_anonKey=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... # ── Modo Debug ────────────────────────────────────────────────────────── # true → logs en Logcat + envío al backend (desarrollo) # false → solo envío al backend, sin output local (producción) -appLogger.debug=true +appLogger_debug=true ``` | Variable | Obligatoria | Dónde obtenerla | |---|:---:|---| | `sdk.dir` | ✅ | Android Studio lo autocompleta, o ver `ANDROID_HOME` | -| `appLogger.url` | ✅ | [Supabase Dashboard](https://supabase.com/dashboard) → Settings → API → Project URL | -| `appLogger.anonKey` | ✅ | Supabase Dashboard → Settings → API → `anon` `public` key | -| `appLogger.debug` | ❌ | `true` para desarrollo, `false` para producción (default: `false`) | +| `appLogger_url` | ✅ | [Supabase Dashboard](https://supabase.com/dashboard) → Settings → API → Project URL | +| `appLogger_anonKey` | ✅ | Supabase Dashboard → Settings → API → `anon` `public` key | +| `appLogger_debug` | ❌ | `true` para desarrollo, `false` para producción (default: `false`) | > ⚠️ **`local.properties` está en `.gitignore`** — nunca se sube al repositorio. > Si clonás el repo y no existe, copiá desde `local.properties.example`. @@ -266,9 +277,9 @@ android { val file = rootProject.file("local.properties") if (file.exists()) load(file.inputStream()) } - buildConfigField("String", "LOGGER_URL", "\"${props["appLogger.url"] ?: ""}\"") - buildConfigField("String", "LOGGER_KEY", "\"${props["appLogger.anonKey"] ?: ""}\"") - buildConfigField("Boolean", "LOGGER_DEBUG", "${props["appLogger.debug"] ?: false}") + buildConfigField("String", "LOGGER_URL", "\"${props["appLogger_url"] ?: ""}\"") + buildConfigField("String", "LOGGER_KEY", "\"${props["appLogger_anonKey"] ?: ""}\"") + buildConfigField("Boolean", "LOGGER_DEBUG", "${props["appLogger_debug"] ?: false}") } } ``` @@ -372,7 +383,8 @@ class MyApp : Application() { config = AppLoggerConfig.Builder() .endpoint(BuildConfig.LOGGER_URL) .apiKey(BuildConfig.LOGGER_KEY) - .debugMode(BuildConfig.DEBUG) + .debugMode(BuildConfig.LOGGER_DEBUG) + .consoleOutput(BuildConfig.LOGGER_DEBUG) .batchSize(20) .flushIntervalSeconds(30) .build(), @@ -387,9 +399,15 @@ class MyApp : Application() { // 3. Usar en cualquier lugar — fire-and-forget AppLoggerSDK.error("PAYMENT", "Transaction failed", throwable) AppLoggerSDK.info("PLAYER", "Playback started", extra = mapOf("content_id" to "movie_123")) -AppLoggerSDK.warn("NETWORK", "Slow response", anomalyType = "HIGH_LATENCY") +AppLoggerSDK.info("PLAYER", "Recovering after error", throwable = e) // throwable opcional +AppLoggerSDK.warn("NETWORK", "Slow response", throwable = e, anomalyType = "HIGH_LATENCY") +AppLoggerSDK.debug("TAG", "Solo visible en debug", throwable = e) // throwable opcional AppLoggerSDK.metric("screen_load_time", 1234.0, "ms", tags = mapOf("screen" to "Home")) -AppLoggerSDK.debug("TAG", "Solo visible en debug") + +// 4. Extension functions — tag inferido automáticamente del nombre de clase +// (disponible en todos los targets via AppLoggerExtensions.kt) +this.logE(logger, "Playback failed", throwable = e) // tag → nombre de la clase actual +this.logW(logger, "Buffer low", anomalyType = "BUFFER_LOW") ``` ### iOS (Kotlin `iosMain`) @@ -414,7 +432,7 @@ AppLoggerIos.shared.metric("buffer_time", 420.0, "ms") | 4 | `docs/ES/migraciones/004_rls_policies.sql` | Políticas de seguridad (RLS) | | 5 | `docs/ES/migraciones/005_retention_policy.sql` | Retención automática de datos | -3. Copiá la **URL del proyecto** y la **anon key** a tu `local.properties` +1. Copiá la **URL del proyecto** y la **anon key** a tu `local.properties` --- @@ -447,7 +465,7 @@ Para que el pipeline funcione al 100%, configurá estos secrets en **GitHub → | Secret | Requerido por | Dónde obtenerlo | |---|---|---| -| `APPLOGGER_SUPABASE_URL` | Job `e2e` | Supabase Dashboard → Settings → API → Project URL | +| `appLogger_supabaseUrl` | Job `e2e` | Supabase Dashboard → Settings → API → Project URL | | `APPLOGGER_SUPABASE_ANON_KEY` | Job `e2e` | Supabase Dashboard → Settings → API → `anon` `public` key | | `APPLOGGER_SUPABASE_SERVICE_KEY` | Job `e2e` | Supabase Dashboard → Settings → API → `service_role` key | | `CODECOV_TOKEN` | Job `test` (opcional) | [codecov.io](https://codecov.io) → Settings → Token | diff --git a/cli/README.md b/cli/README.md index d0b3227..2d535cb 100644 --- a/cli/README.md +++ b/cli/README.md @@ -35,49 +35,139 @@ go mod tidy go run ./cmd/applogger-cli --syncbin-metadata --output json ``` +## Standard Installation + +One-line installers for the latest published CLI release: + +```bash +# Linux +curl -fsSL https://raw.githubusercontent.com/devzucca/appLoggers/main/cli/install/install.sh | bash + +# macOS +curl -fsSL https://raw.githubusercontent.com/devzucca/appLoggers/main/cli/install/install.sh | bash +``` + +```powershell +# Windows PowerShell +irm https://raw.githubusercontent.com/devzucca/appLoggers/main/cli/install/install.ps1 | iex +``` + +Notes: + +- The bash installer auto-detects Linux vs macOS and `amd64` vs `arm64`. +- The PowerShell installer installs `applogger-cli.exe` into the user profile and adds it to the user `PATH`. +- Both installers resolve the latest `applogger-cli-v*` GitHub Release automatically. +- To pin a specific release, set `APPLOGGER_CLI_VERSION`, for example `APPLOGGER_CLI_VERSION=applogger-cli-v0.1.0`. + ## Supabase Configuration (Environment Variables) The CLI reads Supabase configuration from environment variables: -- `APPLOGGER_SUPABASE_URL` (required) -- `APPLOGGER_SUPABASE_KEY` (required, service_role key for CLI reads) -- `APPLOGGER_SUPABASE_SCHEMA` (optional, default `public`) -- `APPLOGGER_SUPABASE_LOG_TABLE` (optional, default `app_logs`) -- `APPLOGGER_SUPABASE_METRIC_TABLE` (optional, default `app_metrics`) -- `APPLOGGER_SUPABASE_TIMEOUT_SECONDS` (optional, default `15`) +- `appLogger_supabaseUrl` (required) +- `appLogger_supabaseKey` (required, service_role key for CLI reads) +- `appLogger_supabaseSchema` (optional, default `public`) +- `appLogger_supabaseLogTable` (optional, default `app_logs`) +- `appLogger_supabaseMetricTable` (optional, default `app_metrics`) +- `appLogger_supabaseTimeoutSeconds` (optional, default `15`) Fallback aliases are supported for compatibility: +- `APPLOGGER_SUPABASE_URL` +- `APPLOGGER_SUPABASE_KEY` - `SUPABASE_URL` - `SUPABASE_KEY` +## Multi-Project Configuration + +For corporate setups with multiple telemetry apps, the CLI also supports a shared +project config file. This is the recommended model when the CLI will later be +hosted or orchestrated by a Wails desktop app and streamed over SSE. + +Selection precedence: + +1. `--project ` +2. `APPLOGGER_PROJECT` +3. Workspace autodetection via `workspace_roots` +4. `default_project` +5. Single configured project +6. Legacy environment variables (`appLogger_supabase*`, `APPLOGGER_SUPABASE_*`, `SUPABASE_*`) + +Config file resolution: + +- `--config ` +- `APPLOGGER_CONFIG` +- Default path: `os.UserConfigDir()/applogger/cli.json` + +Recommended JSON structure: + +```json +{ + "default_project": "klinema", + "projects": [ + { + "name": "klinema", + "display_name": "Klinema Mobile", + "workspace_roots": [ + "D:/workspace/klinema" + ], + "supabase": { + "url": "https://klinema.supabase.co", + "api_key_env": "APPLOGGER_KLINEMA_SUPABASE_KEY", + "schema": "public", + "logs_table": "app_logs", + "metrics_table": "app_metrics", + "timeout_seconds": 15 + } + }, + { + "name": "klinematv", + "display_name": "Klinema TV", + "workspace_roots": [ + "D:/workspace/klinematv" + ], + "supabase": { + "url": "https://klinematv.supabase.co", + "api_key_env": "APPLOGGER_KLINEMATV_SUPABASE_KEY" + } + } + ] +} +``` + +Operational guidance: + +- Keep `service_role` secrets outside the JSON file whenever possible by using `api_key_env`. +- Let Wails own the project registry and spawn the CLI with the same config model. +- SSE should transport resolved project context (`project`, `config_source`) rather than raw secrets. +- When only one project is configured, the CLI auto-selects it to keep local workflows simple. + ### Export Variables (PowerShell) ```powershell -$env:APPLOGGER_SUPABASE_URL="https://YOUR_PROJECT_REF.supabase.co" -$env:APPLOGGER_SUPABASE_KEY="YOUR_SUPABASE_SERVICE_ROLE_KEY" +$env:appLogger_supabaseUrl="https://YOUR_PROJECT_REF.supabase.co" +$env:appLogger_supabaseKey="YOUR_SUPABASE_SERVICE_ROLE_KEY" ``` ### Export Variables (CMD) ```cmd -set APPLOGGER_SUPABASE_URL=https://YOUR_PROJECT_REF.supabase.co -set APPLOGGER_SUPABASE_KEY=YOUR_SUPABASE_SERVICE_ROLE_KEY +set appLogger_supabaseUrl=https://YOUR_PROJECT_REF.supabase.co +set appLogger_supabaseKey=YOUR_SUPABASE_SERVICE_ROLE_KEY ``` ### Export Variables (Bash/Zsh) ```bash -export APPLOGGER_SUPABASE_URL="https://YOUR_PROJECT_REF.supabase.co" -export APPLOGGER_SUPABASE_KEY="YOUR_SUPABASE_SERVICE_ROLE_KEY" +export appLogger_supabaseUrl="https://YOUR_PROJECT_REF.supabase.co" +export appLogger_supabaseKey="YOUR_SUPABASE_SERVICE_ROLE_KEY" ``` ### If You Are Using Supabase MCP You can resolve values before export with: -1. `mcp_supabase_get_project_url` for `APPLOGGER_SUPABASE_URL` -2. `APPLOGGER_SUPABASE_KEY` must be provisioned from secure secrets storage (service_role) +1. `mcp_supabase_get_project_url` for `appLogger_supabaseUrl` +2. `appLogger_supabaseKey` must be provisioned from secure secrets storage (service_role) ## Examples @@ -107,10 +197,16 @@ applogger-cli telemetry agent-response \ # Health check for agents applogger-cli health --output json -# Placeholder telemetry command +# Explicit project selection +applogger-cli --project klinema telemetry query --source logs --severity error --output json + +# Workspace-based autodetection via APPLOGGER_CONFIG +APPLOGGER_CONFIG="$HOME/.config/applogger/cli.json" applogger-cli telemetry query --source logs --limit 25 --output json + +# Minimal telemetry query applogger-cli telemetry query -# Telemetry contract with filters (backend next phase) +# Telemetry contract with filters applogger-cli telemetry query \ --source logs \ --from 2026-03-01T00:00:00Z \ @@ -120,6 +216,13 @@ applogger-cli telemetry query \ --limit 25 \ --output json +# Query warning anomalies stored under extra.anomaly_type +applogger-cli telemetry query \ + --source logs \ + --anomaly-type slow_response \ + --limit 25 \ + --output json + # Query metrics source applogger-cli telemetry query \ --source metrics \ @@ -138,6 +241,13 @@ applogger-cli telemetry query \ - `tag`: logs only, group by `tag` - `name`: metrics only, group by metric `name` +### Log Payload Notes + +- Log queries include the `extra` object when present. +- `warn(..., anomalyType = "...")` is exposed through `extra.anomaly_type`. +- Use `--anomaly-type` to filter warning anomalies on the server side. +- Project-based responses include `project` and `config_source` when the CLI resolved a project profile. + ## Development ```bash @@ -147,10 +257,25 @@ go test ./... ## Next Milestones -- Phase 2: telemetry query engine and filters -- Phase 3: Supabase integration and aggregations +- Phase 3: richer telemetry presets and saved reports - Phase 4: installers and release automation ## Plugin Metadata Syncbin plugin metadata lives in `plugin-metadata.yaml`. + +## Release Distribution Contract + +- Published binaries come from GitHub Releases tagged as `applogger-cli-v*`. +- Source of truth for CLI base version: `cli/VERSION`. +- Current release assets: + - `applogger-cli-linux-amd64` + - `applogger-cli-linux-arm64` + - `applogger-cli-darwin-amd64` + - `applogger-cli-darwin-arm64` + - `applogger-cli-windows-amd64.exe` + - `manifests/homebrew/applogger-cli.rb` + - `manifests/scoop/applogger-cli.json` + - `manifests/winget/DevZucca.AppLoggerCLI*.yaml` +- Each asset is accompanied by a `.sha256` checksum file. +- Package manager manifests are generated automatically on every `applogger-cli-v*` tag release. diff --git a/cli/VERSION b/cli/VERSION new file mode 100644 index 0000000..6e8bf73 --- /dev/null +++ b/cli/VERSION @@ -0,0 +1 @@ +0.1.0 diff --git a/cli/install/install.ps1 b/cli/install/install.ps1 new file mode 100644 index 0000000..5fc53ab --- /dev/null +++ b/cli/install/install.ps1 @@ -0,0 +1,124 @@ +param( + [string]$Version = $env:APPLOGGER_CLI_VERSION, + [string]$InstallDir = $env:APPLOGGER_CLI_INSTALL_DIR, + [int]$DownloadRetries = 5, + [int]$RetryDelaySeconds = 2, + [int]$DownloadTimeoutSeconds = 120 +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$Repo = 'devzucca/appLoggers' + +function Write-Log { + param([string]$Message) + Write-Host "[applogger-cli] $Message" +} + +function Resolve-Version { + param([string]$RequestedVersion) + + if ($RequestedVersion) { + if ($RequestedVersion -notlike 'applogger-cli-v*') { + throw 'APPLOGGER_CLI_VERSION must match applogger-cli-v*.' + } + return $RequestedVersion + } + + $releases = Invoke-WithRetry -Action { + Invoke-RestMethod -Uri "https://api.github.com/repos/$Repo/releases?per_page=100" -TimeoutSec $DownloadTimeoutSeconds + } + $release = $releases | Where-Object { $_.tag_name -like 'applogger-cli-v*' } | Select-Object -First 1 + if (-not $release) { + throw 'Unable to resolve latest applogger-cli release tag.' + } + return $release.tag_name +} + +function Invoke-WithRetry { + param( + [scriptblock]$Action + ) + + $attempt = 1 + while ($attempt -le $DownloadRetries) { + try { + return & $Action + } + catch { + if ($attempt -ge $DownloadRetries) { + throw + } + Write-Log "Attempt $attempt failed; retrying in $RetryDelaySeconds second(s)..." + Start-Sleep -Seconds $RetryDelaySeconds + $attempt++ + } + } +} + +function Download-File { + param( + [string]$Uri, + [string]$OutFile + ) + + Invoke-WithRetry -Action { + Invoke-WebRequest -Uri $Uri -OutFile $OutFile -TimeoutSec $DownloadTimeoutSeconds + } +} + +function Ensure-PathContains { + param([string]$Directory) + + $userPath = [Environment]::GetEnvironmentVariable('Path', 'User') + $segments = @() + if ($userPath) { + $segments = $userPath -split ';' + } + + if ($segments -notcontains $Directory) { + $newPath = if ($userPath) { "$userPath;$Directory" } else { $Directory } + [Environment]::SetEnvironmentVariable('Path', $newPath, 'User') + Write-Log "Added $Directory to the user PATH. Restart PowerShell to pick it up in new sessions." + } +} + +if (-not $InstallDir) { + $InstallDir = Join-Path $env:LOCALAPPDATA 'Programs\AppLoggerCLI' +} + +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + +$tag = Resolve-Version -RequestedVersion $Version +$assetName = 'applogger-cli-windows-amd64.exe' +$checksumName = "$assetName.sha256" +$releaseBase = "https://github.com/$Repo/releases/download/$tag" + +New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null +$tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ("applogger-cli-" + [System.Guid]::NewGuid().ToString('N')) +New-Item -ItemType Directory -Force -Path $tempDir | Out-Null + +try { + $downloadPath = Join-Path $tempDir $assetName + $checksumPath = Join-Path $tempDir $checksumName + $finalPath = Join-Path $InstallDir 'applogger-cli.exe' + + Write-Log "Installing $assetName from $tag" + Download-File -Uri "$releaseBase/$assetName" -OutFile $downloadPath + Download-File -Uri "$releaseBase/$checksumName" -OutFile $checksumPath + + $expectedHash = (Get-Content -Path $checksumPath -Raw).Split([char[]]@(' ', "`t", "`r", "`n"), [System.StringSplitOptions]::RemoveEmptyEntries)[0].ToLowerInvariant() + $actualHash = (Get-FileHash -Path $downloadPath -Algorithm SHA256).Hash.ToLowerInvariant() + if ($expectedHash -ne $actualHash) { + throw 'Checksum mismatch for downloaded applogger-cli binary.' + } + + Move-Item -Force $downloadPath $finalPath + Ensure-PathContains -Directory $InstallDir + Write-Log "Installed to $finalPath" + & $finalPath version +} +finally { + Remove-Item -Recurse -Force $tempDir -ErrorAction SilentlyContinue +} \ No newline at end of file diff --git a/cli/install/install.sh b/cli/install/install.sh new file mode 100644 index 0000000..e0644fb --- /dev/null +++ b/cli/install/install.sh @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +set -euo pipefail + +REPO="devzucca/appLoggers" +INSTALL_DIR="${APPLOGGER_CLI_INSTALL_DIR:-}" +REQUESTED_VERSION="${APPLOGGER_CLI_VERSION:-}" +CURL_RETRY_MAX="${APPLOGGER_CLI_CURL_RETRY_MAX:-5}" +CURL_RETRY_DELAY="${APPLOGGER_CLI_CURL_RETRY_DELAY:-2}" +CURL_CONNECT_TIMEOUT="${APPLOGGER_CLI_CURL_CONNECT_TIMEOUT:-10}" +CURL_MAX_TIME="${APPLOGGER_CLI_CURL_MAX_TIME:-120}" +CURL_RETRY_MAX_TIME="${APPLOGGER_CLI_CURL_RETRY_MAX_TIME:-300}" + +log() { + printf '[applogger-cli] %s\n' "$*" +} + +fail() { + printf '[applogger-cli] ERROR: %s\n' "$*" >&2 + exit 1 +} + +need_cmd() { + command -v "$1" >/dev/null 2>&1 || fail "missing required command: $1" +} + +detect_os() { + case "$(uname -s)" in + Linux) printf 'linux' ;; + Darwin) printf 'darwin' ;; + *) fail "unsupported OS: $(uname -s)" ;; + esac +} + +detect_arch() { + case "$(uname -m)" in + x86_64|amd64) printf 'amd64' ;; + arm64|aarch64) printf 'arm64' ;; + *) fail "unsupported architecture: $(uname -m)" ;; + esac +} + +resolve_version() { + if [ -n "$REQUESTED_VERSION" ]; then + case "$REQUESTED_VERSION" in + applogger-cli-v*) ;; + *) fail "APPLOGGER_CLI_VERSION must match applogger-cli-v*" ;; + esac + printf '%s' "$REQUESTED_VERSION" + return + fi + + local api_url="https://api.github.com/repos/${REPO}/releases?per_page=100" + local releases + releases="$(download_text "$api_url")" + local tag + tag="$(printf '%s' "$releases" | grep -o '"tag_name": *"applogger-cli-v[^"]*"' | head -n 1 | sed 's/.*"\(applogger-cli-v[^"]*\)"/\1/')" + [ -n "$tag" ] || fail "unable to resolve latest applogger-cli release tag" + printf '%s' "$tag" +} + +download_text() { + local url="$1" + curl \ + --fail \ + --silent \ + --show-error \ + --location \ + --retry "$CURL_RETRY_MAX" \ + --retry-delay "$CURL_RETRY_DELAY" \ + --retry-max-time "$CURL_RETRY_MAX_TIME" \ + --connect-timeout "$CURL_CONNECT_TIMEOUT" \ + --max-time "$CURL_MAX_TIME" \ + "$url" +} + +download_file() { + local url="$1" + local out_path="$2" + curl \ + --fail \ + --silent \ + --show-error \ + --location \ + --retry "$CURL_RETRY_MAX" \ + --retry-delay "$CURL_RETRY_DELAY" \ + --retry-max-time "$CURL_RETRY_MAX_TIME" \ + --connect-timeout "$CURL_CONNECT_TIMEOUT" \ + --max-time "$CURL_MAX_TIME" \ + "$url" \ + -o "$out_path" +} + +resolve_install_dir() { + if [ -n "$INSTALL_DIR" ]; then + printf '%s' "$INSTALL_DIR" + return + fi + if [ -w /usr/local/bin ]; then + printf '/usr/local/bin' + else + printf '%s/.local/bin' "$HOME" + fi +} + +verify_checksum() { + local file_path="$1" + local checksum_path="$2" + + if command -v sha256sum >/dev/null 2>&1; then + (cd "$(dirname "$file_path")" && sha256sum -c "$(basename "$checksum_path")") + return + fi + + if command -v shasum >/dev/null 2>&1; then + local expected actual + expected="$(awk '{print $1}' "$checksum_path")" + actual="$(shasum -a 256 "$file_path" | awk '{print $1}')" + [ "$expected" = "$actual" ] || fail "checksum mismatch for $(basename "$file_path")" + return + fi + + fail "sha256 verifier not available (requires sha256sum or shasum)" +} + +main() { + need_cmd curl + need_cmd mktemp + + local os arch version asset_name checksum_name release_base install_dir tmp_dir tmp_asset tmp_checksum final_path + os="$(detect_os)" + arch="$(detect_arch)" + version="$(resolve_version)" + asset_name="applogger-cli-${os}-${arch}" + checksum_name="${asset_name}.sha256" + release_base="https://github.com/${REPO}/releases/download/${version}" + install_dir="$(resolve_install_dir)" + + tmp_dir="$(mktemp -d)" + trap 'rm -rf "$tmp_dir"' EXIT + tmp_asset="${tmp_dir}/${asset_name}" + tmp_checksum="${tmp_dir}/${checksum_name}" + final_path="${install_dir}/applogger-cli" + + mkdir -p "$install_dir" + + log "installing ${asset_name} from ${version}" + download_file "${release_base}/${asset_name}" "$tmp_asset" + download_file "${release_base}/${checksum_name}" "$tmp_checksum" + verify_checksum "$tmp_asset" "$tmp_checksum" + + chmod +x "$tmp_asset" + mv "$tmp_asset" "$final_path" + + log "installed to ${final_path}" + if ! printf '%s' ":${PATH}:" | grep -q ":${install_dir}:"; then + log "${install_dir} is not in PATH for this shell" + log "add this to your shell profile: export PATH=\"${install_dir}:\$PATH\"" + fi + + "$final_path" version +} + +main "$@" \ No newline at end of file diff --git a/cli/internal/cli/agent.go b/cli/internal/cli/agent.go index 08532a1..bb616d9 100644 --- a/cli/internal/cli/agent.go +++ b/cli/internal/cli/agent.go @@ -43,22 +43,24 @@ func newAgentCommand() *cobra.Command { payload := agentSchemaPayload{ Name: "applogger-cli", Version: buildVersion, - Recommendation: "Agents should prefer --output agent (TOON) and use --output json when strict JSON is required", + Recommendation: "Agents should prefer --output agent (TOON) and use --output json when strict JSON is required; when multiple telemetry apps exist, resolve the active project via --project, APPLOGGER_PROJECT, or workspace autodetection", DefaultOutput: "text", ContractVersion: "1.0.0", EnvVars: []string{ - "APPLOGGER_SUPABASE_URL", - "APPLOGGER_SUPABASE_KEY", - "APPLOGGER_SUPABASE_SCHEMA", - "APPLOGGER_SUPABASE_LOG_TABLE", - "APPLOGGER_SUPABASE_METRIC_TABLE", - "APPLOGGER_SUPABASE_TIMEOUT_SECONDS", + "APPLOGGER_CONFIG", + "APPLOGGER_PROJECT", + "appLogger_supabaseUrl", + "appLogger_supabaseKey", + "appLogger_supabaseSchema", + "appLogger_supabaseLogTable", + "appLogger_supabaseMetricTable", + "appLogger_supabaseTimeoutSeconds", }, Commands: []commandContract{ {Command: "--syncbin-metadata", Description: "Metadata discovery endpoint", OutputModes: []string{"text", "json", "agent"}, Stable: true}, {Command: "version", Description: "CLI build information", OutputModes: []string{"text", "json", "agent"}, Stable: true}, {Command: "capabilities", Description: "Feature discovery endpoint", OutputModes: []string{"text", "json", "agent"}, Stable: true}, - {Command: "health", Description: "Readiness endpoint", OutputModes: []string{"text", "json", "agent"}, Stable: true}, + {Command: "health", Description: "Readiness endpoint with resolved project context when available", OutputModes: []string{"text", "json", "agent"}, Stable: true}, {Command: "telemetry query", Description: "Telemetry query command backed by Supabase with optional aggregation", OutputModes: []string{"text", "json", "agent"}, Stable: false}, {Command: "telemetry agent-response", Description: "Compact TOON envelope dedicated to agent orchestration", OutputModes: []string{"agent"}, Stable: false}, }, diff --git a/cli/internal/cli/capabilities.go b/cli/internal/cli/capabilities.go index e8beacd..3d1d48a 100644 --- a/cli/internal/cli/capabilities.go +++ b/cli/internal/cli/capabilities.go @@ -15,6 +15,7 @@ type capabilityEntry struct { type capabilitiesPayload struct { Name string `json:"name"` Version string `json:"version"` + ConfigInputs []string `json:"config_inputs,omitempty"` OutputModes []string `json:"output_modes"` ExitCodes map[string]int `json:"exit_codes"` Capabilities []capabilityEntry `json:"capabilities"` @@ -22,9 +23,10 @@ type capabilitiesPayload struct { func buildCapabilitiesPayload() capabilitiesPayload { return capabilitiesPayload{ - Name: "applogger-cli", - Version: buildVersion, - OutputModes: []string{"text", "json", "agent"}, + Name: "applogger-cli", + Version: buildVersion, + ConfigInputs: []string{"environment", "project_config", "workspace_autodetect"}, + OutputModes: []string{"text", "json", "agent"}, ExitCodes: map[string]int{ "success": exitCodeSuccess, "error": exitCodeError, @@ -36,7 +38,7 @@ func buildCapabilitiesPayload() capabilitiesPayload { {Name: "capabilities", Description: "Machine-readable CLI capabilities", Stability: "stable"}, {Name: "health", Description: "Runtime readiness probe", Stability: "stable"}, {Name: "agent schema", Description: "Schema and execution contract for agent clients", Stability: "stable"}, - {Name: "telemetry query", Description: "Telemetry query endpoint backed by Supabase with optional aggregation", Stability: "preview"}, + {Name: "telemetry query", Description: "Telemetry query endpoint backed by Supabase with optional aggregation and multi-project resolution", Stability: "preview"}, {Name: "telemetry agent-response", Description: "Compact TOON response optimized for agent orchestration", Stability: "preview"}, }, } diff --git a/cli/internal/cli/health.go b/cli/internal/cli/health.go index e0496f4..2135b04 100644 --- a/cli/internal/cli/health.go +++ b/cli/internal/cli/health.go @@ -8,10 +8,12 @@ import ( ) type healthPayload struct { - OK bool `json:"ok"` - Status string `json:"status"` - Version string `json:"version"` - Timestamp string `json:"timestamp"` + OK bool `json:"ok"` + Status string `json:"status"` + Version string `json:"version"` + Project string `json:"project,omitempty"` + ConfigSource string `json:"config_source,omitempty"` + Timestamp string `json:"timestamp"` } func newHealthCommand() *cobra.Command { @@ -32,6 +34,10 @@ func newHealthCommand() *cobra.Command { Version: buildVersion, Timestamp: time.Now().UTC().Format(time.RFC3339), } + if cfg, err := loadSupabaseConfig(); err == nil { + payload.Project = cfg.Project + payload.ConfigSource = cfg.ConfigSource + } if outputFormat == "json" { return writeJSON(cmd.OutOrStdout(), payload) } diff --git a/cli/internal/cli/project_config.go b/cli/internal/cli/project_config.go new file mode 100644 index 0000000..61e7b14 --- /dev/null +++ b/cli/internal/cli/project_config.go @@ -0,0 +1,227 @@ +package cli + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "runtime" + "strings" +) + +type cliProjectFile struct { + DefaultProject string `json:"default_project"` + Projects []cliProjectProfile `json:"projects"` +} + +type cliProjectProfile struct { + Name string `json:"name"` + DisplayName string `json:"display_name,omitempty"` + WorkspaceRoots []string `json:"workspace_roots,omitempty"` + Supabase cliProjectSupabaseProfile `json:"supabase"` +} + +type cliProjectSupabaseProfile struct { + URL string `json:"url"` + APIKey string `json:"api_key,omitempty"` + APIKeyEnv string `json:"api_key_env,omitempty"` + Schema string `json:"schema,omitempty"` + LogsTable string `json:"logs_table,omitempty"` + MetricsTable string `json:"metrics_table,omitempty"` + TimeoutSeconds int `json:"timeout_seconds,omitempty"` +} + +func loadSupabaseConfigFromProjectConfig() (supabaseConfig, error) { + configPath, explicitConfigPath := resolveProjectConfigPath() + explicitProject := activeProjectName() + + if configPath == "" { + if explicitProject != "" { + return supabaseConfig{}, fmt.Errorf("project %q requested but no project config path is available; set --config or APPLOGGER_CONFIG", explicitProject) + } + return supabaseConfig{}, os.ErrNotExist + } + + _, statErr := os.Stat(configPath) + if statErr != nil { + if os.IsNotExist(statErr) && !explicitConfigPath && explicitProject == "" { + return supabaseConfig{}, os.ErrNotExist + } + if os.IsNotExist(statErr) { + return supabaseConfig{}, fmt.Errorf("project config file not found: %s", configPath) + } + return supabaseConfig{}, fmt.Errorf("failed to stat project config %s: %w", configPath, statErr) + } + + content, err := os.ReadFile(configPath) + if err != nil { + return supabaseConfig{}, fmt.Errorf("failed to read project config %s: %w", configPath, err) + } + + var file cliProjectFile + if err := json.Unmarshal(content, &file); err != nil { + return supabaseConfig{}, fmt.Errorf("invalid project config %s: %w", configPath, err) + } + if len(file.Projects) == 0 { + return supabaseConfig{}, fmt.Errorf("project config %s does not define any projects", configPath) + } + + selected, err := selectProjectProfile(file, explicitProject) + if err != nil { + return supabaseConfig{}, fmt.Errorf("failed to resolve AppLogger project from %s: %w", configPath, err) + } + + apiKey := strings.TrimSpace(selected.Supabase.APIKey) + if envName := strings.TrimSpace(selected.Supabase.APIKeyEnv); envName != "" { + apiKey = strings.TrimSpace(os.Getenv(envName)) + if apiKey == "" { + return supabaseConfig{}, fmt.Errorf("project %q requires secret env %s for Supabase API key", selected.Name, envName) + } + } + if apiKey == "" { + return supabaseConfig{}, fmt.Errorf("project %q is missing a Supabase API key; set supabase.api_key_env or supabase.api_key in %s", selected.Name, configPath) + } + + timeoutSeconds := selected.Supabase.TimeoutSeconds + if timeoutSeconds == 0 { + timeoutSeconds = 15 + } + + cfg := supabaseConfig{ + Project: selected.Name, + ConfigSource: "project_config", + URL: strings.TrimSpace(selected.Supabase.URL), + APIKey: apiKey, + Schema: strings.TrimSpace(selected.Supabase.Schema), + LogsTable: strings.TrimSpace(selected.Supabase.LogsTable), + MetricsTable: strings.TrimSpace(selected.Supabase.MetricsTable), + TimeoutSeconds: timeoutSeconds, + } + + if cfg.URL == "" { + return cfg, fmt.Errorf("project %q is missing supabase.url in %s", selected.Name, configPath) + } + if cfg.Schema == "" { + cfg.Schema = "public" + } + if cfg.LogsTable == "" { + cfg.LogsTable = "app_logs" + } + if cfg.MetricsTable == "" { + cfg.MetricsTable = "app_metrics" + } + if cfg.TimeoutSeconds < 1 || cfg.TimeoutSeconds > 120 { + return cfg, fmt.Errorf("project %q has invalid timeout %d in %s (expected 1..120)", selected.Name, cfg.TimeoutSeconds, configPath) + } + + return validateSupabaseConfig(cfg) +} + +func resolveProjectConfigPath() (string, bool) { + if value := strings.TrimSpace(configFilePath); value != "" { + return value, true + } + if value := strings.TrimSpace(os.Getenv("APPLOGGER_CONFIG")); value != "" { + return value, true + } + baseDir, err := os.UserConfigDir() + if err != nil { + return "", false + } + return filepath.Join(baseDir, "applogger", "cli.json"), false +} + +func activeProjectName() string { + if value := strings.TrimSpace(projectName); value != "" { + return value + } + return strings.TrimSpace(os.Getenv("APPLOGGER_PROJECT")) +} + +func selectProjectProfile(file cliProjectFile, explicitProject string) (cliProjectProfile, error) { + if explicitProject != "" { + for _, candidate := range file.Projects { + if strings.EqualFold(strings.TrimSpace(candidate.Name), explicitProject) { + return candidate, nil + } + } + return cliProjectProfile{}, fmt.Errorf("project %q was not found; available projects: %s", explicitProject, joinProjectNames(file.Projects)) + } + + if cwd, err := os.Getwd(); err == nil { + matches := make([]cliProjectProfile, 0) + for _, candidate := range file.Projects { + if projectMatchesWorkspace(candidate, cwd) { + matches = append(matches, candidate) + } + } + if len(matches) == 1 { + return matches[0], nil + } + if len(matches) > 1 { + return cliProjectProfile{}, fmt.Errorf("multiple projects match current workspace %q; use --project or APPLOGGER_PROJECT (%s)", cwd, joinProjectNames(matches)) + } + } + + if defaultProject := strings.TrimSpace(file.DefaultProject); defaultProject != "" { + for _, candidate := range file.Projects { + if strings.EqualFold(strings.TrimSpace(candidate.Name), defaultProject) { + return candidate, nil + } + } + return cliProjectProfile{}, fmt.Errorf("default_project %q was not found in configured projects", defaultProject) + } + + if len(file.Projects) == 1 { + return file.Projects[0], nil + } + + return cliProjectProfile{}, fmt.Errorf("multiple projects are configured and none matched the current workspace; use --project or APPLOGGER_PROJECT (%s)", joinProjectNames(file.Projects)) +} + +func projectMatchesWorkspace(profile cliProjectProfile, cwd string) bool { + for _, root := range profile.WorkspaceRoots { + if workspaceContainsPath(cwd, root) { + return true + } + } + return false +} + +func workspaceContainsPath(cwd string, root string) bool { + cleanCWD := normalizePath(cwd) + cleanRoot := normalizePath(root) + if cleanCWD == "" || cleanRoot == "" { + return false + } + if cleanCWD == cleanRoot { + return true + } + separator := string(filepath.Separator) + return strings.HasPrefix(cleanCWD, cleanRoot+separator) +} + +func normalizePath(value string) string { + trimmed := strings.TrimSpace(value) + if trimmed == "" { + return "" + } + cleaned := filepath.Clean(trimmed) + if absolute, err := filepath.Abs(cleaned); err == nil { + cleaned = absolute + } + if runtime.GOOS == "windows" { + cleaned = strings.ToLower(cleaned) + } + return cleaned +} + +func joinProjectNames(projects []cliProjectProfile) string { + names := make([]string, 0, len(projects)) + for _, project := range projects { + if name := strings.TrimSpace(project.Name); name != "" { + names = append(names, name) + } + } + return strings.Join(names, ", ") +} diff --git a/cli/internal/cli/root.go b/cli/internal/cli/root.go index ddac002..70ac629 100644 --- a/cli/internal/cli/root.go +++ b/cli/internal/cli/root.go @@ -14,6 +14,8 @@ var ( outputFormat string verbose bool syncbinMetadata bool + projectName string + configFilePath string ) var rootCmd = &cobra.Command{ @@ -72,6 +74,8 @@ func init() { rootCmd.PersistentFlags().StringVar(&outputFormat, "output", "text", "Output format: text|json|agent") rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose output") + rootCmd.PersistentFlags().StringVar(&projectName, "project", "", "Project profile to use from the AppLogger CLI config") + rootCmd.PersistentFlags().StringVar(&configFilePath, "config", "", "Path to the AppLogger CLI project config file") rootCmd.Flags().BoolVar(&syncbinMetadata, "syncbin-metadata", false, "Print Syncbin metadata") _ = rootCmd.Flags().MarkHidden("syncbin-metadata") diff --git a/cli/internal/cli/supabase.go b/cli/internal/cli/supabase.go index bf331ef..8d7912e 100644 --- a/cli/internal/cli/supabase.go +++ b/cli/internal/cli/supabase.go @@ -9,6 +9,8 @@ import ( ) type supabaseConfig struct { + Project string + ConfigSource string URL string APIKey string Schema string @@ -18,21 +20,22 @@ type supabaseConfig struct { } func loadSupabaseConfig() (supabaseConfig, error) { + if cfg, err := loadSupabaseConfigFromProjectConfig(); err == nil { + return cfg, nil + } else if !os.IsNotExist(err) { + return supabaseConfig{}, err + } + cfg := supabaseConfig{ - URL: firstNonEmptyEnv("APPLOGGER_SUPABASE_URL", "SUPABASE_URL"), - APIKey: firstNonEmptyEnv("APPLOGGER_SUPABASE_KEY", "SUPABASE_KEY"), - Schema: firstNonEmptyEnv("APPLOGGER_SUPABASE_SCHEMA"), - LogsTable: firstNonEmptyEnv("APPLOGGER_SUPABASE_LOG_TABLE"), - MetricsTable: firstNonEmptyEnv("APPLOGGER_SUPABASE_METRIC_TABLE"), + ConfigSource: "environment", + URL: firstNonEmptyEnv("appLogger_supabaseUrl", "APPLOGGER_SUPABASE_URL", "SUPABASE_URL"), + APIKey: firstNonEmptyEnv("appLogger_supabaseKey", "APPLOGGER_SUPABASE_KEY", "SUPABASE_KEY"), + Schema: firstNonEmptyEnv("appLogger_supabaseSchema", "APPLOGGER_SUPABASE_SCHEMA"), + LogsTable: firstNonEmptyEnv("appLogger_supabaseLogTable", "APPLOGGER_SUPABASE_LOG_TABLE"), + MetricsTable: firstNonEmptyEnv("appLogger_supabaseMetricTable", "APPLOGGER_SUPABASE_METRIC_TABLE"), TimeoutSeconds: 15, } - if cfg.URL == "" { - return cfg, fmt.Errorf("missing Supabase URL: set APPLOGGER_SUPABASE_URL or SUPABASE_URL") - } - if cfg.APIKey == "" { - return cfg, fmt.Errorf("missing Supabase API key: set APPLOGGER_SUPABASE_KEY or SUPABASE_KEY (service_role key required for CLI reads)") - } if cfg.Schema == "" { cfg.Schema = "public" } @@ -42,14 +45,27 @@ func loadSupabaseConfig() (supabaseConfig, error) { if cfg.MetricsTable == "" { cfg.MetricsTable = "app_metrics" } - if timeoutRaw := firstNonEmptyEnv("APPLOGGER_SUPABASE_TIMEOUT_SECONDS"); timeoutRaw != "" { + if timeoutRaw := firstNonEmptyEnv("appLogger_supabaseTimeoutSeconds", "APPLOGGER_SUPABASE_TIMEOUT_SECONDS"); timeoutRaw != "" { timeoutSeconds, err := strconv.Atoi(timeoutRaw) if err != nil || timeoutSeconds < 1 || timeoutSeconds > 120 { - return cfg, fmt.Errorf("invalid APPLOGGER_SUPABASE_TIMEOUT_SECONDS value %q (expected 1..120)", timeoutRaw) + return cfg, fmt.Errorf("invalid appLogger_supabaseTimeoutSeconds value %q (expected 1..120)", timeoutRaw) } cfg.TimeoutSeconds = timeoutSeconds } + return validateSupabaseConfig(cfg) +} + +func validateSupabaseConfig(cfg supabaseConfig) (supabaseConfig, error) { + if cfg.URL == "" { + return cfg, fmt.Errorf("missing Supabase URL: set appLogger_supabaseUrl, APPLOGGER_SUPABASE_URL, or SUPABASE_URL, or configure a project profile via APPLOGGER_CONFIG") + } + if cfg.APIKey == "" { + return cfg, fmt.Errorf("missing Supabase API key: set appLogger_supabaseKey, APPLOGGER_SUPABASE_KEY, or SUPABASE_KEY (service_role key required for CLI reads), or configure a project profile via APPLOGGER_CONFIG") + } + if cfg.TimeoutSeconds < 1 || cfg.TimeoutSeconds > 120 { + return cfg, fmt.Errorf("invalid appLogger_supabaseTimeoutSeconds value %d (expected 1..120)", cfg.TimeoutSeconds) + } if !strings.HasPrefix(strings.ToLower(cfg.URL), "http://") && !strings.HasPrefix(strings.ToLower(cfg.URL), "https://") { return cfg, fmt.Errorf("invalid Supabase URL %q (expected http/https)", cfg.URL) } diff --git a/cli/internal/cli/telemetry.go b/cli/internal/cli/telemetry.go index afce2ff..b5f55d6 100644 --- a/cli/internal/cli/telemetry.go +++ b/cli/internal/cli/telemetry.go @@ -16,15 +16,16 @@ func newTelemetryCommand() *cobra.Command { } var ( - sourceFlag string - aggregateFlag string - fromFlag string - toFlag string - severityFlag string - sessionFlag string - tagFlag string - nameFlag string - limitFlag int + sourceFlag string + aggregateFlag string + fromFlag string + toFlag string + severityFlag string + sessionFlag string + tagFlag string + nameFlag string + anomalyTypeFlag string + limitFlag int ) buildRequest := func() (telemetryQueryRequest, error) { @@ -86,17 +87,21 @@ func newTelemetryCommand() *cobra.Command { if strings.TrimSpace(nameFlag) != "" && source != "metrics" { return telemetryQueryRequest{}, newUsageError("--name is only valid when --source=metrics") } + if strings.TrimSpace(anomalyTypeFlag) != "" && source != "logs" { + return telemetryQueryRequest{}, newUsageError("--anomaly-type is only valid when --source=logs") + } return telemetryQueryRequest{ - Source: source, - Aggregate: aggregate, - From: fromFlag, - To: toFlag, - Severity: severity, - SessionID: strings.TrimSpace(sessionFlag), - Tag: strings.TrimSpace(tagFlag), - Name: strings.TrimSpace(nameFlag), - Limit: limitFlag, + Source: source, + Aggregate: aggregate, + From: fromFlag, + To: toFlag, + Severity: severity, + SessionID: strings.TrimSpace(sessionFlag), + Tag: strings.TrimSpace(tagFlag), + Name: strings.TrimSpace(nameFlag), + AnomalyType: strings.TrimSpace(anomalyTypeFlag), + Limit: limitFlag, }, nil } @@ -105,7 +110,13 @@ func newTelemetryCommand() *cobra.Command { if err != nil { return telemetryQueryResponse{}, err } - return queryTelemetry(context.Background(), cfg, req) + response, err := queryTelemetry(context.Background(), cfg, req) + if err != nil { + return telemetryQueryResponse{}, err + } + response.Project = cfg.Project + response.ConfigSource = cfg.ConfigSource + return response, nil } queryCmd := &cobra.Command{ @@ -184,6 +195,7 @@ func newTelemetryCommand() *cobra.Command { queryCmd.Flags().StringVar(&sessionFlag, "session-id", "", "Session UUID filter") queryCmd.Flags().StringVar(&tagFlag, "tag", "", "Tag filter (logs source only)") queryCmd.Flags().StringVar(&nameFlag, "name", "", "Metric name filter (metrics source only)") + queryCmd.Flags().StringVar(&anomalyTypeFlag, "anomaly-type", "", "Anomaly type filter (logs source only, e.g. slow_response)") queryCmd.Flags().IntVar(&limitFlag, "limit", 100, "Result size limit (1..1000)") agentResponseCmd.Flags().StringVar(&sourceFlag, "source", "logs", "Telemetry source: logs|metrics") @@ -194,6 +206,7 @@ func newTelemetryCommand() *cobra.Command { agentResponseCmd.Flags().StringVar(&sessionFlag, "session-id", "", "Session UUID filter") agentResponseCmd.Flags().StringVar(&tagFlag, "tag", "", "Tag filter (logs source only)") agentResponseCmd.Flags().StringVar(&nameFlag, "name", "", "Metric name filter (metrics source only)") + agentResponseCmd.Flags().StringVar(&anomalyTypeFlag, "anomaly-type", "", "Anomaly type filter (logs source only, e.g. slow_response)") agentResponseCmd.Flags().IntVar(&limitFlag, "limit", 100, "Result size limit (1..1000)") agentResponseCmd.Flags().IntVar(&previewLimitFlag, "preview-limit", 5, "Max rows included in rows_preview for agents (0..50)") diff --git a/cli/internal/cli/telemetry_agent_response.go b/cli/internal/cli/telemetry_agent_response.go index fc76d14..4c8eb0c 100644 --- a/cli/internal/cli/telemetry_agent_response.go +++ b/cli/internal/cli/telemetry_agent_response.go @@ -1,14 +1,16 @@ package cli type telemetryAgentResponse struct { - Kind string `json:"kind" toon:"kind"` - OK bool `json:"ok" toon:"ok"` - Source string `json:"source" toon:"source"` - Count int `json:"count" toon:"count"` - Request telemetryQueryRequest `json:"request" toon:"request"` - Summary *telemetryAggregation `json:"summary,omitempty" toon:"summary,omitempty"` - RowsPreview []map[string]any `json:"rows_preview,omitempty" toon:"rows_preview,omitempty"` - Hints []string `json:"hints" toon:"hints"` + Kind string `json:"kind" toon:"kind"` + OK bool `json:"ok" toon:"ok"` + Project string `json:"project,omitempty" toon:"project,omitempty"` + ConfigSource string `json:"config_source,omitempty" toon:"config_source,omitempty"` + Source string `json:"source" toon:"source"` + Count int `json:"count" toon:"count"` + Request telemetryQueryRequest `json:"request" toon:"request"` + Summary *telemetryAggregation `json:"summary,omitempty" toon:"summary,omitempty"` + RowsPreview []map[string]any `json:"rows_preview,omitempty" toon:"rows_preview,omitempty"` + Hints []string `json:"hints" toon:"hints"` } func buildTelemetryAgentResponse(resp telemetryQueryResponse, previewLimit int) telemetryAgentResponse { @@ -32,13 +34,15 @@ func buildTelemetryAgentResponse(resp telemetryQueryResponse, previewLimit int) } return telemetryAgentResponse{ - Kind: "telemetry_agent_response", - OK: resp.OK, - Source: resp.Source, - Count: resp.Count, - Request: resp.Request, - Summary: resp.Summary, - RowsPreview: rowsPreview, - Hints: hints, + Kind: "telemetry_agent_response", + OK: resp.OK, + Project: resp.Project, + ConfigSource: resp.ConfigSource, + Source: resp.Source, + Count: resp.Count, + Request: resp.Request, + Summary: resp.Summary, + RowsPreview: rowsPreview, + Hints: hints, } } diff --git a/cli/internal/cli/telemetry_client.go b/cli/internal/cli/telemetry_client.go index 9f32d8e..65fb3d6 100644 --- a/cli/internal/cli/telemetry_client.go +++ b/cli/internal/cli/telemetry_client.go @@ -12,24 +12,27 @@ import ( ) type telemetryQueryRequest struct { - Source string `json:"source" toon:"source"` - Aggregate string `json:"aggregate,omitempty" toon:"aggregate,omitempty"` - From string `json:"from,omitempty" toon:"from,omitempty"` - To string `json:"to,omitempty" toon:"to,omitempty"` - Severity string `json:"severity,omitempty" toon:"severity,omitempty"` - SessionID string `json:"session_id,omitempty" toon:"session_id,omitempty"` - Tag string `json:"tag,omitempty" toon:"tag,omitempty"` - Name string `json:"name,omitempty" toon:"name,omitempty"` - Limit int `json:"limit" toon:"limit"` + Source string `json:"source" toon:"source"` + Aggregate string `json:"aggregate,omitempty" toon:"aggregate,omitempty"` + From string `json:"from,omitempty" toon:"from,omitempty"` + To string `json:"to,omitempty" toon:"to,omitempty"` + Severity string `json:"severity,omitempty" toon:"severity,omitempty"` + SessionID string `json:"session_id,omitempty" toon:"session_id,omitempty"` + Tag string `json:"tag,omitempty" toon:"tag,omitempty"` + Name string `json:"name,omitempty" toon:"name,omitempty"` + AnomalyType string `json:"anomaly_type,omitempty" toon:"anomaly_type,omitempty"` + Limit int `json:"limit" toon:"limit"` } type telemetryQueryResponse struct { - OK bool `json:"ok" toon:"ok"` - Source string `json:"source" toon:"source"` - Count int `json:"count" toon:"count"` - Request telemetryQueryRequest `json:"request" toon:"request"` - Rows []map[string]any `json:"rows" toon:"rows"` - Summary *telemetryAggregation `json:"summary,omitempty" toon:"summary,omitempty"` + OK bool `json:"ok" toon:"ok"` + Project string `json:"project,omitempty" toon:"project,omitempty"` + ConfigSource string `json:"config_source,omitempty" toon:"config_source,omitempty"` + Source string `json:"source" toon:"source"` + Count int `json:"count" toon:"count"` + Request telemetryQueryRequest `json:"request" toon:"request"` + Rows []map[string]any `json:"rows" toon:"rows"` + Summary *telemetryAggregation `json:"summary,omitempty" toon:"summary,omitempty"` } func queryTelemetry(ctx context.Context, cfg supabaseConfig, req telemetryQueryRequest) (telemetryQueryResponse, error) { @@ -39,7 +42,7 @@ func queryTelemetry(ctx context.Context, cfg supabaseConfig, req telemetryQueryR } table := cfg.LogsTable - selectColumns := "id,created_at,level,tag,message,session_id,sdk_version" + selectColumns := "id,created_at,level,tag,message,session_id,sdk_version,extra" if req.Source == "metrics" { table = cfg.MetricsTable selectColumns = "id,created_at,name,value,unit,session_id,sdk_version" @@ -69,6 +72,9 @@ func queryTelemetry(ctx context.Context, cfg supabaseConfig, req telemetryQueryR if req.Severity != "" && req.Source == "logs" { query.Set("level", "eq."+strings.ToUpper(req.Severity)) } + if req.AnomalyType != "" && req.Source == "logs" { + query.Set("extra->>anomaly_type", "eq."+req.AnomalyType) + } base.RawQuery = query.Encode() httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, base.String(), nil) diff --git a/cli/plugin-metadata.yaml b/cli/plugin-metadata.yaml index 2f0b0ac..da5734e 100644 --- a/cli/plugin-metadata.yaml +++ b/cli/plugin-metadata.yaml @@ -44,13 +44,13 @@ dependencies: config: environment_variables: required: - - APPLOGGER_SUPABASE_URL - - APPLOGGER_SUPABASE_KEY + - appLogger_supabaseUrl + - appLogger_supabaseKey optional: - - APPLOGGER_SUPABASE_SCHEMA - - APPLOGGER_SUPABASE_LOG_TABLE - - APPLOGGER_SUPABASE_METRIC_TABLE - - APPLOGGER_SUPABASE_TIMEOUT_SECONDS + - appLogger_supabaseSchema + - appLogger_supabaseLogTable + - appLogger_supabaseMetricTable + - appLogger_supabaseTimeoutSeconds flags: global: - name: "--output" diff --git a/cli/tests/integration/contract_test.go b/cli/tests/integration/contract_test.go index b456733..2f3d0a4 100644 --- a/cli/tests/integration/contract_test.go +++ b/cli/tests/integration/contract_test.go @@ -4,6 +4,7 @@ import ( "encoding/json" "net/http" "net/http/httptest" + "os" "os/exec" "path/filepath" "runtime" @@ -167,8 +168,8 @@ func TestTelemetryQueryContractJSON(t *testing.T) { "--output", "json", ) cmd.Env = append(cmd.Env, - "APPLOGGER_SUPABASE_URL="+mockSupabase.URL, - "APPLOGGER_SUPABASE_KEY=test-key", + "appLogger_supabaseUrl="+mockSupabase.URL, + "appLogger_supabaseKey=test-key", ) out, err := cmd.CombinedOutput() if err != nil { @@ -227,8 +228,8 @@ func TestTelemetryAgentResponseTOON(t *testing.T) { "--preview-limit", "1", ) cmd.Env = append(cmd.Env, - "APPLOGGER_SUPABASE_URL="+mockSupabase.URL, - "APPLOGGER_SUPABASE_KEY=test-key", + "appLogger_supabaseUrl="+mockSupabase.URL, + "appLogger_supabaseKey=test-key", ) out, err := cmd.CombinedOutput() if err != nil { @@ -290,8 +291,8 @@ func TestTelemetryQueryMetricsNameFilterJSON(t *testing.T) { "--output", "json", ) cmd.Env = append(cmd.Env, - "APPLOGGER_SUPABASE_URL="+mockSupabase.URL, - "APPLOGGER_SUPABASE_KEY=test-key", + "appLogger_supabaseUrl="+mockSupabase.URL, + "appLogger_supabaseKey=test-key", ) out, err := cmd.CombinedOutput() if err != nil { @@ -306,6 +307,263 @@ func TestTelemetryQueryMetricsNameFilterJSON(t *testing.T) { } } +func TestTelemetryQueryLogsAnomalyTypeFilterAndExtraJSON(t *testing.T) { + binary := buildCLI(t) + mockSupabase := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/rest/v1/app_logs" { + http.Error(w, "unexpected path", http.StatusNotFound) + return + } + if r.URL.Query().Get("level") != "eq.WARN" { + http.Error(w, "unexpected level filter", http.StatusBadRequest) + return + } + if r.URL.Query().Get("extra->>anomaly_type") != "eq.slow_response" { + http.Error(w, "unexpected anomaly_type filter", http.StatusBadRequest) + return + } + if !strings.Contains(r.URL.RawQuery, "select=id%2Ccreated_at%2Clevel%2Ctag%2Cmessage%2Csession_id%2Csdk_version%2Cextra") { + http.Error(w, "expected extra in select columns", http.StatusBadRequest) + return + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`[ + {"id":"1","created_at":"2026-03-01T00:00:00Z","level":"WARN","tag":"network","message":"slow call","session_id":"s-1","sdk_version":"dev","extra":{"anomaly_type":"slow_response","latency_ms":"2500"}} + ]`)) + })) + defer mockSupabase.Close() + + cmd := exec.Command( + binary, + "telemetry", + "query", + "--source", "logs", + "--severity", "warn", + "--anomaly-type", "slow_response", + "--limit", "10", + "--output", "json", + ) + cmd.Env = append(cmd.Env, + "appLogger_supabaseUrl="+mockSupabase.URL, + "appLogger_supabaseKey=test-key", + ) + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("telemetry anomaly_type query failed: %v, output=%s", err, string(out)) + } + text := string(out) + if !strings.Contains(text, "\"anomaly_type\": \"slow_response\"") { + t.Fatalf("expected anomaly_type echo in response, output=%s", text) + } + if !strings.Contains(text, "\"extra\": {") { + t.Fatalf("expected extra object in response rows, output=%s", text) + } + if !strings.Contains(text, "\"latency_ms\": \"2500\"") { + t.Fatalf("expected extra payload content in response rows, output=%s", text) + } + if !strings.Contains(text, "\"severity\": \"warn\"") { + t.Fatalf("expected severity echo in request block, output=%s", text) + } +} + +func TestTelemetryQueryAnomalyTypeInvalidForMetrics(t *testing.T) { + binary := buildCLI(t) + cmd := exec.Command(binary, "telemetry", "query", "--source", "metrics", "--anomaly-type", "slow_response", "--output", "json") + out, err := cmd.CombinedOutput() + if err == nil { + t.Fatal("expected usage error for --anomaly-type with metrics source") + } + if cmd.ProcessState.ExitCode() != 2 { + t.Fatalf("expected exit code 2, got %d", cmd.ProcessState.ExitCode()) + } + if !strings.Contains(string(out), "\"error_kind\": \"usage_error\"") { + t.Fatalf("expected usage error envelope, output=%s", string(out)) + } +} + +func TestTelemetryQueryProjectProfileExplicitJSON(t *testing.T) { + binary := buildCLI(t) + workspace := t.TempDir() + configPath := filepath.Join(t.TempDir(), "applogger-cli.json") + writeProjectConfig(t, configPath, map[string]any{ + "projects": []map[string]any{ + { + "name": "klinema", + "workspace_roots": []string{workspace}, + "supabase": map[string]any{ + "url": "http://placeholder.invalid", + "api_key_env": "APPLOGGER_KLINEMA_SUPABASE_KEY", + }, + }, + }, + }) + + mockSupabase := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`[ {"id":"1","created_at":"2026-03-01T00:00:00Z","level":"ERROR","tag":"api","message":"boom"} ]`)) + })) + defer mockSupabase.Close() + + writeProjectConfig(t, configPath, map[string]any{ + "projects": []map[string]any{ + { + "name": "klinema", + "workspace_roots": []string{workspace}, + "supabase": map[string]any{ + "url": mockSupabase.URL, + "api_key_env": "APPLOGGER_KLINEMA_SUPABASE_KEY", + }, + }, + }, + }) + + cmd := exec.Command(binary, "--project", "klinema", "telemetry", "query", "--source", "logs", "--limit", "10", "--output", "json") + cmd.Dir = workspace + cmd.Env = append(os.Environ(), + "APPLOGGER_CONFIG="+configPath, + "APPLOGGER_KLINEMA_SUPABASE_KEY=test-key", + ) + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("telemetry query with explicit project failed: %v, output=%s", err, string(out)) + } + text := string(out) + if !strings.Contains(text, "\"project\": \"klinema\"") { + t.Fatalf("expected project name in response, output=%s", text) + } + if !strings.Contains(text, "\"config_source\": \"project_config\"") { + t.Fatalf("expected project config source in response, output=%s", text) + } +} + +func TestTelemetryQueryProjectProfileSingleProjectAutoSelectionJSON(t *testing.T) { + binary := buildCLI(t) + configPath := filepath.Join(t.TempDir(), "applogger-cli.json") + mockSupabase := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`[ {"id":"1","created_at":"2026-03-01T00:00:00Z","name":"response_time_ms","value":123.4,"unit":"ms"} ]`)) + })) + defer mockSupabase.Close() + + writeProjectConfig(t, configPath, map[string]any{ + "projects": []map[string]any{ + { + "name": "klinematv", + "supabase": map[string]any{ + "url": mockSupabase.URL, + "api_key_env": "APPLOGGER_KLINEMATV_SUPABASE_KEY", + }, + }, + }, + }) + + cmd := exec.Command(binary, "telemetry", "query", "--source", "metrics", "--name", "response_time_ms", "--limit", "10", "--output", "json") + cmd.Env = append(os.Environ(), + "APPLOGGER_CONFIG="+configPath, + "APPLOGGER_KLINEMATV_SUPABASE_KEY=test-key", + ) + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("telemetry query with single-project auto selection failed: %v, output=%s", err, string(out)) + } + text := string(out) + if !strings.Contains(text, "\"project\": \"klinematv\"") { + t.Fatalf("expected single configured project name in response, output=%s", text) + } + if !strings.Contains(text, "\"source\": \"metrics\"") { + t.Fatalf("expected metrics source in response, output=%s", text) + } +} + +func TestTelemetryAgentResponseProjectProfileTOON(t *testing.T) { + binary := buildCLI(t) + configPath := filepath.Join(t.TempDir(), "applogger-cli.json") + mockSupabase := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`[ + {"id":"1","created_at":"2026-03-01T00:00:00Z","level":"ERROR","tag":"api","message":"boom"} + ]`)) + })) + defer mockSupabase.Close() + + writeProjectConfig(t, configPath, map[string]any{ + "projects": []map[string]any{ + { + "name": "klinema", + "supabase": map[string]any{ + "url": mockSupabase.URL, + "api_key_env": "APPLOGGER_KLINEMA_SUPABASE_KEY", + }, + }, + }, + }) + + cmd := exec.Command(binary, "--project", "klinema", "telemetry", "agent-response", "--source", "logs", "--preview-limit", "1") + cmd.Env = append(os.Environ(), + "APPLOGGER_CONFIG="+configPath, + "APPLOGGER_KLINEMA_SUPABASE_KEY=test-key", + ) + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("agent-response with project profile failed: %v, output=%s", err, string(out)) + } + text := string(out) + if !strings.Contains(text, "project: klinema") { + t.Fatalf("expected project in TOON output, output=%s", text) + } + if !strings.Contains(text, "config_source: project_config") { + t.Fatalf("expected config_source in TOON output, output=%s", text) + } +} + +func TestTelemetryQueryProjectProfileAmbiguousWorkspaceFails(t *testing.T) { + binary := buildCLI(t) + workspace := t.TempDir() + configPath := filepath.Join(t.TempDir(), "applogger-cli.json") + writeProjectConfig(t, configPath, map[string]any{ + "projects": []map[string]any{ + { + "name": "klinema", + "workspace_roots": []string{workspace}, + "supabase": map[string]any{ + "url": "https://klinema.supabase.co", + "api_key_env": "APPLOGGER_KLINEMA_SUPABASE_KEY", + }, + }, + { + "name": "klinematv", + "workspace_roots": []string{workspace}, + "supabase": map[string]any{ + "url": "https://klinematv.supabase.co", + "api_key_env": "APPLOGGER_KLINEMATV_SUPABASE_KEY", + }, + }, + }, + }) + + cmd := exec.Command(binary, "telemetry", "query", "--output", "json") + cmd.Dir = workspace + cmd.Env = append(os.Environ(), "APPLOGGER_CONFIG="+configPath) + out, err := cmd.CombinedOutput() + if err == nil { + t.Fatal("expected ambiguous workspace resolution to fail") + } + if !strings.Contains(string(out), "multiple projects") || !strings.Contains(string(out), "use --project or APPLOGGER_PROJECT") { + t.Fatalf("expected ambiguity error, output=%s", string(out)) + } +} + +func writeProjectConfig(t *testing.T, path string, payload any) { + t.Helper() + content, err := json.Marshal(payload) + if err != nil { + t.Fatalf("failed to marshal project config: %v", err) + } + if err := os.WriteFile(path, content, 0o600); err != nil { + t.Fatalf("failed to write project config: %v", err) + } +} + func TestTelemetryQueryNameFilterInvalidForLogs(t *testing.T) { binary := buildCLI(t) cmd := exec.Command(binary, "telemetry", "query", "--source", "logs", "--name", "response_time_ms", "--output", "json") diff --git a/docs-investigation/investigation.md b/docs-investigation/investigation.md index 2cf07fa..d85387c 100644 --- a/docs-investigation/investigation.md +++ b/docs-investigation/investigation.md @@ -53,7 +53,7 @@ Implementar logging de forma ingenua puede convertir **la solución en el proble | Llamar al logger en el hilo principal | ANR (Application Not Responding) | | Sin control de volumen en WebSocket/gRPC | 10.000 eventos/minuto → colapso del backend | -### 1.3 Objetivo del Paquete +### 1.3 Objetivo del Paquete Diseñar `AppLogger` como un paquete Kotlin de código abierto que: @@ -509,14 +509,14 @@ enum class Platform(val isLowResource: Boolean) { ```properties # AppLogger — Configuración local de desarrollo (NO commitear) -appLogger.url=https://tu-proyecto.supabase.co -appLogger.anonKey=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... -appLogger.debug=true -appLogger.logToConsole=true -appLogger.batchSize=20 -appLogger.flushIntervalSeconds=30 -appLogger.lowStorageMode=false -appLogger.maxStackTraceLines=50 +appLogger_url=https://tu-proyecto.supabase.co +appLogger_anonKey=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... +appLogger_debug=true +appLogger_logToConsole=true +appLogger_batchSize=20 +appLogger_flushIntervalSeconds=30 +appLogger_lowStorageMode=false +appLogger_maxStackTraceLines=50 ``` ### 10.2 build.gradle.kts — Mapeo a BuildConfig @@ -527,11 +527,11 @@ android { val props = Properties().also { it.load(rootProject.file("local.properties").inputStream()) } - buildConfigField("String", "LOGGER_URL", "\"${props["appLogger.url"] ?: ""}\"") - buildConfigField("String", "LOGGER_KEY", "\"${props["appLogger.anonKey"] ?: ""}\"") - buildConfigField("Boolean", "LOGGER_DEBUG", "${props["appLogger.debug"] ?: false}") - buildConfigField("Boolean", "LOG_CONSOLE", "${props["appLogger.logToConsole"] ?: true}") - buildConfigField("Int", "LOGGER_BATCH", "${props["appLogger.batchSize"] ?: 20}") + buildConfigField("String", "LOGGER_URL", "\"${props["appLogger_url"] ?: ""}\"") + buildConfigField("String", "LOGGER_KEY", "\"${props["appLogger_anonKey"] ?: ""}\"") + buildConfigField("Boolean", "LOGGER_DEBUG", "${props["appLogger_debug"] ?: false}") + buildConfigField("Boolean", "LOG_CONSOLE", "${props["appLogger_logToConsole"] ?: true}") + buildConfigField("Int", "LOGGER_BATCH", "${props["appLogger_batchSize"] ?: 20}") } } ``` diff --git a/docs/ES/agents/applogger-cli-agent-operator/SKILL.md b/docs/ES/agents/applogger-cli-agent-operator/SKILL.md index 323375e..0b31e7a 100644 --- a/docs/ES/agents/applogger-cli-agent-operator/SKILL.md +++ b/docs/ES/agents/applogger-cli-agent-operator/SKILL.md @@ -29,6 +29,35 @@ This skill enables AI agents and automation systems to **safely, predictably, an ## Core Concepts +### 0. Instalacion bootstrap del CLI + +Si `applogger-cli` no existe aun en la maquina del agente, instalar primero: + +```bash +# Linux +curl -fsSL https://raw.githubusercontent.com/devzucca/appLoggers/main/cli/install/install.sh | bash + +# macOS (Intel / Apple Silicon) +curl -fsSL https://raw.githubusercontent.com/devzucca/appLoggers/main/cli/install/install.sh | bash +``` + +```powershell +# Windows PowerShell +irm https://raw.githubusercontent.com/devzucca/appLoggers/main/cli/install/install.ps1 | iex +``` + +Despues verificar: + +```bash +applogger-cli version --output json +``` + +Reglas: + +- Si el instalador cambia `PATH`, abrir una nueva shell o ejecutar por ruta absoluta una vez. +- Para fijar version: definir `APPLOGGER_CLI_VERSION=applogger-cli-vX.Y.Z` antes de instalar. +- En macOS y Linux el instalador exige verificacion SHA-256 y falla si no existe `sha256sum` ni `shasum`. + ### 1. Agent Contract Discovery Before executing any command, **always** discover capabilities and schema: @@ -46,6 +75,32 @@ applogger-cli health --output json **Why?** The CLI may have new commands, output format changes, or schema updates between versions. Discovery ensures forward compatibility. +### 1.1 Multi-Project Resolution (Corporate Mode) + +When operating multiple telemetry apps (for example `klinema` and `klinematv`), +agents must resolve the active project deterministically. + +Resolution precedence: + +1. `--project ` +2. `APPLOGGER_PROJECT` +3. Workspace autodetection via `workspace_roots` +4. `default_project` +5. Single configured project +6. Legacy environment variables (`appLogger_supabase*`, `APPLOGGER_SUPABASE_*`, `SUPABASE_*`) + +Project config path precedence: + +1. `--config ` +2. `APPLOGGER_CONFIG` +3. Default user config path (`os.UserConfigDir()/applogger/cli.json`) + +Rules for agents: + +- Prefer project profiles for production automation. +- Prefer `api_key_env` in the JSON config instead of inline secrets. +- Parse `project` and `config_source` from health/telemetry outputs for auditability. + ### 2. Three Output Modes (Choose Wisely) | Mode | Use When | Example | @@ -101,9 +156,9 @@ if ! command -v applogger-cli &> /dev/null; then exit 127 fi -# 2. Verify credentials are set -if [ -z "$APPLOGGER_SUPABASE_URL" ] || [ -z "$APPLOGGER_SUPABASE_KEY" ]; then - echo "FATAL: APPLOGGER_SUPABASE_URL or APPLOGGER_SUPABASE_KEY not set" +# 2. Verify project resolution inputs are set (project mode) OR legacy env vars exist +if [ -z "$APPLOGGER_CONFIG" ] && { [ -z "$appLogger_supabaseUrl" ] || [ -z "$appLogger_supabaseKey" ]; }; then + echo "FATAL: set APPLOGGER_CONFIG (recommended) or appLogger_supabaseUrl/appLogger_supabaseKey" exit 1 fi @@ -114,8 +169,8 @@ if [ -z "$VERSION" ]; then exit 1 fi -# 4. Verify backend is healthy -HEALTH=$(applogger-cli health --output json) +# 4. Verify backend is healthy (optionally pin project) +HEALTH=$(applogger-cli ${APPLOGGER_PROJECT:+--project "$APPLOGGER_PROJECT"} health --output json) if ! jq -e '.ok' <<< "$HEALTH" > /dev/null 2>&1; then echo "FATAL: Backend health check failed" echo "$HEALTH" | jq . @@ -125,6 +180,8 @@ fi echo "✓ Pre-flight check passed" echo " - CLI version: $VERSION" echo " - Backend: $(jq '.services.supabase' <<< "$HEALTH")" +echo " - Project: $(jq -r '.project // "legacy-env"' <<< "$HEALTH")" +echo " - Config source: $(jq -r '.config_source // "environment"' <<< "$HEALTH")" ``` ### Workflow 2: Safe Telemetry Query @@ -367,6 +424,14 @@ applogger-cli telemetry query \ --limit 25 \ --output json +# Filter warning anomalies stored in extra.anomaly_type +applogger-cli telemetry query \ + --source logs \ + --severity warn \ + --anomaly-type slow_response \ + --limit 25 \ + --output json + # Piping for further processing applogger-cli telemetry query \ --source metrics \ @@ -422,23 +487,23 @@ print(result['kind']) ```go import ( - "encoding/json" - "os/exec" + "encoding/json" + "os/exec" ) // Struct matching telemetry_agent_response type AgentResponse struct { - Kind string `json:"kind"` - OK bool `json:"ok"` - Source string `json:"source"` - Count int `json:"count"` + Kind string `json:"kind"` + OK bool `json:"ok"` + Source string `json:"source"` + Count int `json:"count"` } // Execute cmd := exec.Command("applogger-cli", "telemetry", "agent-response", - "--source", "logs", - "--aggregate", "severity", - "--output", "agent") + "--source", "logs", + "--aggregate", "severity", + "--output", "agent") output, _ := cmd.Output() @@ -626,20 +691,18 @@ jobs: steps: - name: Install CLI run: | - curl -L https://github.com/devzucca/appLoggers/releases/download/applogger-cli-v0.1.0/applogger-cli-linux-amd64 \ - -o /tmp/applogger-cli - chmod +x /tmp/applogger-cli + curl -fsSL https://raw.githubusercontent.com/devzucca/appLoggers/main/cli/install/install.sh | bash - name: Query errors (last 24h) env: - APPLOGGER_SUPABASE_URL: ${{ secrets.APPLOGGER_SUPABASE_URL }} - APPLOGGER_SUPABASE_KEY: ${{ secrets.APPLOGGER_SUPABASE_KEY }} + appLogger_supabaseUrl: ${{ secrets.APPLOGGER_SUPABASE_URL }} + appLogger_supabaseKey: ${{ secrets.APPLOGGER_SUPABASE_KEY }} run: | /tmp/applogger-cli telemetry query \ --source logs \ --severity error \ - --create-report audit-$(date +%Y%m%d).json \ - --output json + --output json \ + > audit-$(date +%Y%m%d).json - name: Upload report uses: actions/upload-artifact@v4 @@ -676,19 +739,18 @@ which applogger-cli echo $PATH # Reinstall -curl -L https://github.com/devzucca/appLoggers/releases/download/applogger-cli-v0.1.0/applogger-cli-linux-amd64 \ - -o /usr/local/bin/applogger-cli && chmod +x /usr/local/bin/applogger-cli +curl -fsSL https://raw.githubusercontent.com/devzucca/appLoggers/main/cli/install/install.sh | bash ``` ### "backend health check failed" ```bash # Verify credentials -echo "URL: $APPLOGGER_SUPABASE_URL" -echo "Key: ${APPLOGGER_SUPABASE_KEY:0:10}..." +echo "URL: $appLogger_supabaseUrl" +echo "Key: ${appLogger_supabaseKey:0:10}..." # Test connectivity -curl -s "$APPLOGGER_SUPABASE_URL/rest/v1/?apikey=$APPLOGGER_SUPABASE_KEY" | jq . +curl -s "$appLogger_supabaseUrl/rest/v1/?apikey=$appLogger_supabaseKey" | jq . # Check Supabase dashboard # https://supabase.com/dashboard → Status @@ -734,7 +796,7 @@ A: No. Each `applogger-cli` call hits Supabase. Cache in your agent if needed. | CLI Version | Node Version | Go | Status | |---|---|---|---| -| 0.1.0-alpha.0+ | N/A | 1.25+ | Current | +| 0.1.0-alpha.0+ | N/A | 1.24+ | Current | | 0.2.0+ (planned) | N/A | 1.26+ | Future | --- diff --git a/docs/ES/agents/applogger-cli-live-configuration/SKILL.md b/docs/ES/agents/applogger-cli-live-configuration/SKILL.md index 14f696b..9039492 100644 --- a/docs/ES/agents/applogger-cli-live-configuration/SKILL.md +++ b/docs/ES/agents/applogger-cli-live-configuration/SKILL.md @@ -24,15 +24,15 @@ Examples: ## Required CLI env -1. `APPLOGGER_SUPABASE_URL` -2. `APPLOGGER_SUPABASE_KEY` (service_role) +1. `appLogger_supabaseUrl` +2. `appLogger_supabaseKey` (service_role) ## Optional CLI env -1. `APPLOGGER_SUPABASE_SCHEMA` -2. `APPLOGGER_SUPABASE_LOG_TABLE` -3. `APPLOGGER_SUPABASE_METRIC_TABLE` -4. `APPLOGGER_SUPABASE_TIMEOUT_SECONDS` +1. `appLogger_supabaseSchema` +2. `appLogger_supabaseLogTable` +3. `appLogger_supabaseMetricTable` +4. `appLogger_supabaseTimeoutSeconds` ## Workflow diff --git a/docs/ES/agents/applogger-cli-live-configuration/references/cli-live-setup-runbook.md b/docs/ES/agents/applogger-cli-live-configuration/references/cli-live-setup-runbook.md index 56db418..4b7ea95 100644 --- a/docs/ES/agents/applogger-cli-live-configuration/references/cli-live-setup-runbook.md +++ b/docs/ES/agents/applogger-cli-live-configuration/references/cli-live-setup-runbook.md @@ -3,16 +3,16 @@ ## Windows (PowerShell) ```powershell -$env:APPLOGGER_SUPABASE_URL = "https://YOUR_PROJECT.supabase.co" -$env:APPLOGGER_SUPABASE_KEY = "YOUR_SERVICE_ROLE_KEY" +$env:appLogger_supabaseUrl = "https://YOUR_PROJECT.supabase.co" +$env:appLogger_supabaseKey = "YOUR_SERVICE_ROLE_KEY" applogger-cli health --output json ``` ## Linux/macOS (bash) ```bash -export APPLOGGER_SUPABASE_URL="https://YOUR_PROJECT.supabase.co" -export APPLOGGER_SUPABASE_KEY="YOUR_SERVICE_ROLE_KEY" +export appLogger_supabaseUrl="https://YOUR_PROJECT.supabase.co" +export appLogger_supabaseKey="YOUR_SERVICE_ROLE_KEY" applogger-cli health --output json ``` diff --git a/docs/ES/agents/applogger-guided-setup/SKILL.md b/docs/ES/agents/applogger-guided-setup/SKILL.md index 6a9802a..0cb4285 100644 --- a/docs/ES/agents/applogger-guided-setup/SKILL.md +++ b/docs/ES/agents/applogger-guided-setup/SKILL.md @@ -29,6 +29,11 @@ Do not use this skill when: 5. When handling configuration, verify `local.properties` keys first. 6. If AppLogger keys are missing, add only missing keys directly. 7. Never modify, rename, or delete unrelated existing variables in `local.properties`. +8. When `appLogger_debug=true`, AppLogger outputs automatically to Logcat — do not instruct the user to add any additional Logcat or logging configuration. +9. Never set `appLogger_debug=true` in production builds. +10. Use canonical imports from this SDK only: `com.applogger.core.*` and `com.applogger.transport.supabase.SupabaseTransport`. +11. Never suggest `com.applogger.sdk.*` imports; that package is not valid for this SDK version. +12. State Logcat behavior precisely: output is visible only when `isDebugMode=true` and `consoleOutput=true`. ## Workflow @@ -52,4 +57,4 @@ Do not use this skill when: 1. Start with required changes only. 2. Separate Android and iOS instructions clearly. 3. Explain where each snippet goes. -4. Include a short validation checklist at the end. \ No newline at end of file +4. Include a short validation checklist at the end. diff --git a/docs/ES/agents/applogger-guided-setup/references/android-setup.md b/docs/ES/agents/applogger-guided-setup/references/android-setup.md index 278179d..e8192dc 100644 --- a/docs/ES/agents/applogger-guided-setup/references/android-setup.md +++ b/docs/ES/agents/applogger-guided-setup/references/android-setup.md @@ -37,18 +37,42 @@ dependencies { If the project uses `local.properties`: -1. Check whether these keys already exist: `appLogger.url`, `appLogger.anonKey`, `appLogger.debug`. +1. Check whether these keys already exist: `appLogger_url`, `appLogger_anonKey`, `appLogger_debug`. 2. Add only missing keys. 3. Do not edit, remove, or rename any unrelated existing variable. Example append-only update: ```properties -appLogger.url=https://YOUR-PROJECT.supabase.co -appLogger.anonKey=YOUR_ANON_KEY -appLogger.debug=false +appLogger_url=https://YOUR-PROJECT.supabase.co +appLogger_anonKey=YOUR_ANON_KEY +appLogger_debug=false ``` +## Debug output behavior + +- Effective rule: Logcat output happens only when `isDebugMode=true` **and** `consoleOutput=true`. +- `appLogger_debug=true` usually enables Logcat because most setups map it to `debugMode` (and often to `consoleOutput`). +- No additional Logcat configuration, tag setup, or Android logger wrapper is needed. +- `appLogger_debug=false` (production default) disables Logcat output in the standard setup; no code change required. +- Do **not** set `debug=true` in production builds. + +## Canonical imports (Android) + +```kotlin +import com.applogger.core.AppLoggerConfig +import com.applogger.core.AppLoggerHealth +import com.applogger.core.AppLoggerSDK +import com.applogger.transport.supabase.SupabaseTransport +``` + +Do not use `com.applogger.sdk.*` imports. + +SDK source references (anti-hallucination): + +1. `sdk/logger-core/src/commonMain/kotlin/com/applogger/core/internal/AppLoggerImpl.kt` — Logcat/console output guard. +2. `sdk/logger-core/src/commonMain/kotlin/com/applogger/core/AppLoggerConfig.kt` — default `consoleOutput=true` in Builder. + ## Initialization pattern Preferred initialization point: custom `Application`. @@ -65,6 +89,7 @@ AppLoggerSDK.initialize( .endpoint(BuildConfig.LOGGER_URL) .apiKey(BuildConfig.LOGGER_KEY) .debugMode(BuildConfig.LOGGER_DEBUG) + .consoleOutput(BuildConfig.LOGGER_DEBUG) .batchSize(20) .flushIntervalSeconds(30) .build(), @@ -85,4 +110,4 @@ println("initialized=${health.isInitialized}, buffered=${health.bufferedEvents}" 1. Do not log PII. 2. Do not log tokens or API keys. -3. Keep `debugMode=false` outside local development. \ No newline at end of file +3. Keep `debugMode=false` outside local development. diff --git a/docs/ES/agents/applogger-guided-setup/references/ios-kmp-setup.md b/docs/ES/agents/applogger-guided-setup/references/ios-kmp-setup.md index 8acb2d5..1a4e829 100644 --- a/docs/ES/agents/applogger-guided-setup/references/ios-kmp-setup.md +++ b/docs/ES/agents/applogger-guided-setup/references/ios-kmp-setup.md @@ -65,9 +65,9 @@ If `local.properties` is present: Suggested keys: ```properties -appLogger.url=https://YOUR-PROJECT.supabase.co -appLogger.anonKey=YOUR_ANON_KEY -appLogger.debug=false +appLogger_url=https://YOUR-PROJECT.supabase.co +appLogger_anonKey=YOUR_ANON_KEY +appLogger_debug=false ``` ## Minimal verification @@ -83,4 +83,4 @@ println("initialized=${health.isInitialized}, buffered=${health.bufferedEvents}" 1. Do not propose native host setup outside KMP. 2. Keep secrets outside versioned files. -3. Validate connectivity and HTTPS when the transport is remote. \ No newline at end of file +3. Validate connectivity and HTTPS when the transport is remote. diff --git a/docs/ES/agents/applogger-instrumentation-design/SKILL.md b/docs/ES/agents/applogger-instrumentation-design/SKILL.md index 7eb6b4e..f4ebf59 100644 --- a/docs/ES/agents/applogger-instrumentation-design/SKILL.md +++ b/docs/ES/agents/applogger-instrumentation-design/SKILL.md @@ -18,6 +18,8 @@ Use this skill when the user needs: 1. Prioritize business-critical and reliability-critical signals. 2. Avoid noisy low-value logging. 3. Keep naming stable and searchable. +4. All log levels (`debug`, `info`, `warn`, `error`, `critical`) accept an optional `throwable: Throwable?` parameter — recommend it for any error or anomaly event. +5. For classes that hold an `AppLogger` reference, recommend `Any.logD/I/W/E/C(logger, ...)` from `AppLoggerExtensions` to avoid repeating the tag manually. ## Workflow @@ -37,4 +39,4 @@ Use this skill when the user needs: 1. Deliver a concise taxonomy table. 2. Include anti-patterns to avoid. -3. End with phased rollout guidance. \ No newline at end of file +3. End with phased rollout guidance. diff --git a/docs/ES/agents/applogger-instrumentation-design/references/event-taxonomy.md b/docs/ES/agents/applogger-instrumentation-design/references/event-taxonomy.md index 09739f5..3dca62e 100644 --- a/docs/ES/agents/applogger-instrumentation-design/references/event-taxonomy.md +++ b/docs/ES/agents/applogger-instrumentation-design/references/event-taxonomy.md @@ -14,4 +14,4 @@ Use levels consistently: 2. `warn` for recoverable anomalies. 3. `error` for user-visible failures. 4. `critical` for app-blocking failures. -5. `metric` for quantitative performance. \ No newline at end of file +5. `metric` for quantitative performance. diff --git a/docs/ES/agents/applogger-instrumentation-design/references/metric-guidelines.md b/docs/ES/agents/applogger-instrumentation-design/references/metric-guidelines.md index 9410f0f..0867075 100644 --- a/docs/ES/agents/applogger-instrumentation-design/references/metric-guidelines.md +++ b/docs/ES/agents/applogger-instrumentation-design/references/metric-guidelines.md @@ -9,4 +9,4 @@ Examples: 1. `screen_load_time` in `ms` 2. `api_latency` in `ms` -3. `startup_duration` in `ms` \ No newline at end of file +3. `startup_duration` in `ms` diff --git a/docs/ES/agents/applogger-instrumentation-design/references/tag-conventions.md b/docs/ES/agents/applogger-instrumentation-design/references/tag-conventions.md index de23c11..7880016 100644 --- a/docs/ES/agents/applogger-instrumentation-design/references/tag-conventions.md +++ b/docs/ES/agents/applogger-instrumentation-design/references/tag-conventions.md @@ -12,4 +12,4 @@ Rules: 1. Keep tag set finite. 2. Avoid dynamic values in tags. -3. Put variable context in `extra` fields. \ No newline at end of file +3. Put variable context in `extra` fields. diff --git a/docs/ES/agents/applogger-integration-validation/SKILL.md b/docs/ES/agents/applogger-integration-validation/SKILL.md index 2529669..f360579 100644 --- a/docs/ES/agents/applogger-integration-validation/SKILL.md +++ b/docs/ES/agents/applogger-integration-validation/SKILL.md @@ -33,4 +33,4 @@ Use this skill when integration seems complete and needs objective validation. 1. Report pass/fail per check. 2. Include evidence links or artifacts. -3. End with clear release recommendation. \ No newline at end of file +3. End with clear release recommendation. diff --git a/docs/ES/agents/applogger-integration-validation/references/acceptance-gates.md b/docs/ES/agents/applogger-integration-validation/references/acceptance-gates.md index 8a50d48..1fb5826 100644 --- a/docs/ES/agents/applogger-integration-validation/references/acceptance-gates.md +++ b/docs/ES/agents/applogger-integration-validation/references/acceptance-gates.md @@ -6,4 +6,4 @@ All gates must pass: 2. Events are delivered. 3. No forbidden sensitive data in logs. 4. Health snapshot does not indicate persistent degradation. -5. local.properties policy is respected (only missing keys added; unrelated keys untouched). \ No newline at end of file +5. local.properties policy is respected (only missing keys added; unrelated keys untouched). diff --git a/docs/ES/agents/applogger-integration-validation/references/qa-evidence.md b/docs/ES/agents/applogger-integration-validation/references/qa-evidence.md index 804ee26..3931f73 100644 --- a/docs/ES/agents/applogger-integration-validation/references/qa-evidence.md +++ b/docs/ES/agents/applogger-integration-validation/references/qa-evidence.md @@ -5,4 +5,4 @@ Collect at least: 1. Build/test output. 2. Health snapshot values. 3. Backend evidence of received events. -4. Config source proof (without exposing secrets). \ No newline at end of file +4. Config source proof (without exposing secrets). diff --git a/docs/ES/agents/applogger-integration-validation/references/smoke-tests.md b/docs/ES/agents/applogger-integration-validation/references/smoke-tests.md index 494ce55..58fe848 100644 --- a/docs/ES/agents/applogger-integration-validation/references/smoke-tests.md +++ b/docs/ES/agents/applogger-integration-validation/references/smoke-tests.md @@ -4,4 +4,6 @@ 2. Emit one `info`, one `warn`, one `error`, one `metric`. 3. Trigger manual flush. 4. Verify health snapshot before and after flush. -5. Confirm events arrive in backend. \ No newline at end of file +5. Confirm events arrive in backend. +6. Verify canonical imports compile: `com.applogger.core.*` and `com.applogger.transport.supabase.*`. +7. Verify Logcat condition explicitly: visible only with `isDebugMode=true` and `consoleOutput=true`. diff --git a/docs/ES/agents/applogger-production-hardening/SKILL.md b/docs/ES/agents/applogger-production-hardening/SKILL.md index 6cc1cc9..13e37b6 100644 --- a/docs/ES/agents/applogger-production-hardening/SKILL.md +++ b/docs/ES/agents/applogger-production-hardening/SKILL.md @@ -38,4 +38,4 @@ Use this skill when the integration already works and needs production quality: 1. Separate required hardening from optional tuning. 2. Provide concrete parameter values. -3. End with release-ready checklist. \ No newline at end of file +3. End with release-ready checklist. diff --git a/docs/ES/agents/applogger-production-hardening/references/release-checklist.md b/docs/ES/agents/applogger-production-hardening/references/release-checklist.md index 8e26b6e..4620322 100644 --- a/docs/ES/agents/applogger-production-hardening/references/release-checklist.md +++ b/docs/ES/agents/applogger-production-hardening/references/release-checklist.md @@ -4,4 +4,4 @@ 2. Keys load from non-committed source. 3. `debugMode=false` in release. 4. One startup event arrives in backend. -5. Health snapshot stable after smoke flow. \ No newline at end of file +5. Health snapshot stable after smoke flow. diff --git a/docs/ES/agents/applogger-production-hardening/references/runtime-tuning.md b/docs/ES/agents/applogger-production-hardening/references/runtime-tuning.md index a4ed671..af1ae25 100644 --- a/docs/ES/agents/applogger-production-hardening/references/runtime-tuning.md +++ b/docs/ES/agents/applogger-production-hardening/references/runtime-tuning.md @@ -8,4 +8,4 @@ Suggested baseline: 4. `bufferOverflowPolicy=DISCARD_OLDEST` 5. `offlinePersistenceMode=NONE` -Tune values after observing health and delivery behavior. \ No newline at end of file +Tune values after observing health and delivery behavior. diff --git a/docs/ES/agents/applogger-production-hardening/references/security-and-pii.md b/docs/ES/agents/applogger-production-hardening/references/security-and-pii.md index d98045e..dd9fdf7 100644 --- a/docs/ES/agents/applogger-production-hardening/references/security-and-pii.md +++ b/docs/ES/agents/applogger-production-hardening/references/security-and-pii.md @@ -8,4 +8,4 @@ `local.properties` rule: 1. Add missing AppLogger keys only. -2. Do not alter unrelated existing keys. \ No newline at end of file +2. Do not alter unrelated existing keys. diff --git a/docs/ES/agents/applogger-project-integration/SKILL.md b/docs/ES/agents/applogger-project-integration/SKILL.md index 0902151..33663b5 100644 --- a/docs/ES/agents/applogger-project-integration/SKILL.md +++ b/docs/ES/agents/applogger-project-integration/SKILL.md @@ -28,6 +28,9 @@ Do not use this skill when: 4. Keep iOS guidance KMP-only. 5. If `local.properties` is used, verify required AppLogger keys first. 6. Add only missing AppLogger keys and keep all existing unrelated variables untouched. +7. Use canonical SDK packages only: `com.applogger.core.*` and `com.applogger.transport.supabase.SupabaseTransport`. +8. Never generate `com.applogger.sdk.*` imports. +9. Validate Logcat guidance with the exact condition: `isDebugMode && consoleOutput`. ## Workflow @@ -52,4 +55,4 @@ Do not use this skill when: 1. Explain why each integration point was chosen. 2. Distinguish required changes from optional improvements. 3. Call out assumptions and unknowns. -4. End with a short validation plan. \ No newline at end of file +4. End with a short validation plan. diff --git a/docs/ES/agents/applogger-project-integration/references/android-patterns.md b/docs/ES/agents/applogger-project-integration/references/android-patterns.md index 2810c43..f1a1483 100644 --- a/docs/ES/agents/applogger-project-integration/references/android-patterns.md +++ b/docs/ES/agents/applogger-project-integration/references/android-patterns.md @@ -21,4 +21,4 @@ Use `AppLoggerHealth.snapshot()` after initialization and after one emitted even 1. Is there already a wrapper around logs? 2. Should AppLogger be called directly or behind an app-specific facade? -3. Where are secrets loaded today? \ No newline at end of file +3. Where are secrets loaded today? diff --git a/docs/ES/agents/applogger-project-integration/references/integration-playbook.md b/docs/ES/agents/applogger-project-integration/references/integration-playbook.md index eb6e29b..ebec710 100644 --- a/docs/ES/agents/applogger-project-integration/references/integration-playbook.md +++ b/docs/ES/agents/applogger-project-integration/references/integration-playbook.md @@ -12,7 +12,7 @@ 1. Android: `Application.onCreate()` or the main DI/bootstrap layer. 2. KMP iOS: shared Kotlin bootstrap invoked from app startup code. -3. Shared business logic: helper wrappers in `commonMain` for repeated usage. +3. Shared business logic: use `AppLoggerExtensions` (`Any.logD/I/W/E/C`) in `commonMain` for repeated usage — tag is inferred automatically from the class name. ## First-pass integration policy @@ -31,13 +31,48 @@ Required keys: -1. `appLogger.url` -2. `appLogger.anonKey` -3. `appLogger.debug` +1. `appLogger_url` +2. `appLogger_anonKey` +3. `appLogger_debug` + +## Canonical imports and packages + +Use only these package roots in Android integration code: + +1. `com.applogger.core.AppLoggerSDK` +2. `com.applogger.core.AppLoggerConfig` +3. `com.applogger.core.AppLoggerHealth` +4. `com.applogger.transport.supabase.SupabaseTransport` + +Do not use `com.applogger.sdk.*` imports. + +## Canonical initialization snippet (Android) + +```kotlin +val transport = SupabaseTransport( + endpoint = BuildConfig.LOGGER_URL, + apiKey = BuildConfig.LOGGER_KEY +) + +AppLoggerSDK.initialize( + context = this, + config = AppLoggerConfig.Builder() + .endpoint(BuildConfig.LOGGER_URL) + .apiKey(BuildConfig.LOGGER_KEY) + .debugMode(BuildConfig.LOGGER_DEBUG) + .consoleOutput(BuildConfig.LOGGER_DEBUG) + .batchSize(20) + .flushIntervalSeconds(30) + .build(), + transport = transport +) +``` + +Logcat visibility rule: output is shown only when `isDebugMode=true` and `consoleOutput=true`. ## What not to do on first pass 1. Do not replace every existing logger call in the whole codebase. 2. Do not add PII to logs. 3. Do not add AppLogger to unrelated layers without a reason. -4. Do not introduce platform-specific iOS host code if the project is KMP. \ No newline at end of file +4. Do not introduce platform-specific iOS host code if the project is KMP. diff --git a/docs/ES/agents/applogger-project-integration/references/ios-kmp-patterns.md b/docs/ES/agents/applogger-project-integration/references/ios-kmp-patterns.md index 55a6cfe..b00a0e6 100644 --- a/docs/ES/agents/applogger-project-integration/references/ios-kmp-patterns.md +++ b/docs/ES/agents/applogger-project-integration/references/ios-kmp-patterns.md @@ -20,4 +20,4 @@ Use `AppLoggerHealth.snapshot()` from Kotlin after initialization and after one 1. Is the project truly KMP-first for iOS? 2. Where are remote endpoint and keys injected? -3. Should logging happen directly in `iosMain` or in shared abstractions? \ No newline at end of file +3. Should logging happen directly in `iosMain` or in shared abstractions? diff --git a/docs/ES/agents/applogger-runtime-troubleshooting/SKILL.md b/docs/ES/agents/applogger-runtime-troubleshooting/SKILL.md index 5ead6b2..1c93cd3 100644 --- a/docs/ES/agents/applogger-runtime-troubleshooting/SKILL.md +++ b/docs/ES/agents/applogger-runtime-troubleshooting/SKILL.md @@ -47,4 +47,4 @@ Do not use this skill when: 1. Show evidence for each suspected cause. 2. Give exact remediation steps. -3. End with a revalidation checklist. \ No newline at end of file +3. End with a revalidation checklist. diff --git a/docs/ES/agents/applogger-runtime-troubleshooting/references/checklist-android.md b/docs/ES/agents/applogger-runtime-troubleshooting/references/checklist-android.md index c93c580..f657b6b 100644 --- a/docs/ES/agents/applogger-runtime-troubleshooting/references/checklist-android.md +++ b/docs/ES/agents/applogger-runtime-troubleshooting/references/checklist-android.md @@ -4,4 +4,4 @@ 2. Verify INTERNET and ACCESS_NETWORK_STATE permissions. 3. Verify BuildConfig values are sourced from expected keys. 4. Emit one `info` and one `error` test event. -5. Inspect `AppLoggerHealth.snapshot()` before and after flush. \ No newline at end of file +5. Inspect `AppLoggerHealth.snapshot()` before and after flush. diff --git a/docs/ES/agents/applogger-runtime-troubleshooting/references/checklist-ios-kmp.md b/docs/ES/agents/applogger-runtime-troubleshooting/references/checklist-ios-kmp.md index c4a796e..58cac5d 100644 --- a/docs/ES/agents/applogger-runtime-troubleshooting/references/checklist-ios-kmp.md +++ b/docs/ES/agents/applogger-runtime-troubleshooting/references/checklist-ios-kmp.md @@ -4,4 +4,4 @@ 2. Verify AppLogger keys are loaded correctly. 3. Emit one `info` and one `error` event. 4. Inspect `AppLoggerHealth.snapshot()` values. -5. Confirm remote endpoint uses HTTPS. \ No newline at end of file +5. Confirm remote endpoint uses HTTPS. diff --git a/docs/ES/agents/applogger-runtime-troubleshooting/references/common-failures.md b/docs/ES/agents/applogger-runtime-troubleshooting/references/common-failures.md index ee34dca..c404ed7 100644 --- a/docs/ES/agents/applogger-runtime-troubleshooting/references/common-failures.md +++ b/docs/ES/agents/applogger-runtime-troubleshooting/references/common-failures.md @@ -6,8 +6,16 @@ 4. Missing network permissions on Android. 5. Initialization never called. 6. AppLogger keys missing in `local.properties`. +7. Wrong SDK imports (`com.applogger.sdk.*` instead of `com.applogger.core.*`). +8. Misreading Logcat behavior: expecting console output with `isDebugMode=false`. +9. Passing `throwable` as 3rd positional argument to `warn()` when `anomalyType` is also intended — pass by name: `warn(tag, message, throwable = e, anomalyType = "TYPE")`. Fix policy for `local.properties`: 1. Add only missing keys. -2. Preserve unrelated keys exactly as they are. \ No newline at end of file +2. Preserve unrelated keys exactly as they are. + +Logcat rule to verify during debugging: + +1. Output is shown only when `isDebugMode=true` and `consoleOutput=true`. +2. No additional Android logger wrapper is required for AppLogger console output. diff --git a/docs/ES/agents/applogger-runtime-troubleshooting/references/health-diagnostics.md b/docs/ES/agents/applogger-runtime-troubleshooting/references/health-diagnostics.md index 1b855d5..60b479d 100644 --- a/docs/ES/agents/applogger-runtime-troubleshooting/references/health-diagnostics.md +++ b/docs/ES/agents/applogger-runtime-troubleshooting/references/health-diagnostics.md @@ -13,4 +13,4 @@ Interpretation: 1. `isInitialized=false`: bootstrap path not executed. 2. `transportAvailable=false`: endpoint or connectivity issue. 3. `bufferedEvents` increasing continuously: delivery blocked. -4. high drop counters: overflow policy and capacity issue. \ No newline at end of file +4. high drop counters: overflow policy and capacity issue. diff --git a/docs/ES/agents/applogger-sdk-live-configuration/SKILL.md b/docs/ES/agents/applogger-sdk-live-configuration/SKILL.md index 449395a..723fb21 100644 --- a/docs/ES/agents/applogger-sdk-live-configuration/SKILL.md +++ b/docs/ES/agents/applogger-sdk-live-configuration/SKILL.md @@ -22,12 +22,26 @@ Examples: 3. Add only missing AppLogger keys. 4. Never rename/delete unrelated existing keys. 5. Never print full secrets in output. +6. Use canonical imports only: `com.applogger.core.*` and `com.applogger.transport.supabase.SupabaseTransport`. +7. Never suggest `com.applogger.sdk.*` imports. ## Required SDK config keys -1. `appLogger.url` -2. `appLogger.anonKey` -3. `appLogger.debug` +1. `appLogger_url` +2. `appLogger_anonKey` +3. `appLogger_debug` + +## Debug output behavior + +1. Logcat output on Android appears only when `isDebugMode=true` and `consoleOutput=true`. +2. In standard setups, `appLogger_debug` maps to both `debugMode` and `consoleOutput`, so `appLogger_debug=true` enables Logcat output. +3. No additional Logcat configuration, logging wrapper, or Android logger setup is required. +4. Never set `appLogger_debug=true` in production builds. + +Source of truth in AppLoggers SDK: + +1. `sdk/logger-core/src/commonMain/kotlin/com/applogger/core/internal/AppLoggerImpl.kt` — console emission guard is `if (config.isDebugMode && config.consoleOutput)`. +2. `sdk/logger-core/src/commonMain/kotlin/com/applogger/core/AppLoggerConfig.kt` — Builder default is `consoleOutput = true`. ## Workflow diff --git a/docs/ES/agents/applogger-sdk-live-configuration/references/local-properties-live-checklist.md b/docs/ES/agents/applogger-sdk-live-configuration/references/local-properties-live-checklist.md index cc67671..23e0364 100644 --- a/docs/ES/agents/applogger-sdk-live-configuration/references/local-properties-live-checklist.md +++ b/docs/ES/agents/applogger-sdk-live-configuration/references/local-properties-live-checklist.md @@ -8,9 +8,9 @@ ## Required keys -1. `appLogger.url` -2. `appLogger.anonKey` -3. `appLogger.debug` +1. `appLogger_url` +2. `appLogger_anonKey` +3. `appLogger_debug` — in the standard mapping, set `true` to enable `debugMode` and `consoleOutput` together (Logcat output) ## Edit rules @@ -23,3 +23,5 @@ 1. Build config fields map keys correctly. 2. SDK initializes with endpoint/apiKey. 3. Smoke log call compiles. +4. Android imports resolve under `com.applogger.core.*` and `com.applogger.transport.supabase.*`. +5. If Logcat behavior is required, verify both flags: `isDebugMode=true` and `consoleOutput=true`. diff --git a/docs/ES/cli/INSTALLATION.md b/docs/ES/cli/INSTALLATION.md index 408667d..8e678eb 100644 --- a/docs/ES/cli/INSTALLATION.md +++ b/docs/ES/cli/INSTALLATION.md @@ -1,7 +1,7 @@ # AppLogger CLI — Guía de Instalación -**Última actualización**: 2026-03-19 -**Versión mínima**: Go 1.25+ (si compilas desde fuente) +**Última actualización**: 2026-03-20 +**Versión mínima**: Go 1.24+ (si compilas desde fuente) **Plataformas soportadas**: Windows, macOS, Linux (x86_64, ARM64) --- @@ -19,21 +19,76 @@ ## Instalación Rápida -### Opción 1: Descargar Binario (Recomendado) +### Opción 1: Instalador estándar de una línea (Recomendado) + +```bash +# Linux +curl -fsSL https://raw.githubusercontent.com/devzucca/appLoggers/main/cli/install/install.sh | bash + +# macOS (Intel y Apple Silicon) +curl -fsSL https://raw.githubusercontent.com/devzucca/appLoggers/main/cli/install/install.sh | bash +``` + +```powershell +# Windows PowerShell +irm https://raw.githubusercontent.com/devzucca/appLoggers/main/cli/install/install.ps1 | iex +``` + +Este flujo: + +- resuelve la última release `applogger-cli-v*` +- detecta plataforma y arquitectura +- descarga el binario correcto +- valida checksum SHA-256 de forma obligatoria +- deja el binario listo para usar +- aplica política de red con reintentos y timeouts para descargas + +Notas de hardening del instalador: + +- Linux/macOS: requiere `sha256sum` o `shasum`; si no existe verificador, la instalación falla. +- Windows: usa TLS 1.2 y reintentos de descarga con timeout configurable. + +Parámetros de red (opcionales): + +- Bash installer: + - `APPLOGGER_CLI_CURL_RETRY_MAX` (default `5`) + - `APPLOGGER_CLI_CURL_RETRY_DELAY` (default `2`) + - `APPLOGGER_CLI_CURL_CONNECT_TIMEOUT` (default `10`) + - `APPLOGGER_CLI_CURL_MAX_TIME` (default `120`) + - `APPLOGGER_CLI_CURL_RETRY_MAX_TIME` (default `300`) +- PowerShell installer: + - `-DownloadRetries` (default `5`) + - `-RetryDelaySeconds` (default `2`) + - `-DownloadTimeoutSeconds` (default `120`) + +Para fijar una versión específica: + +```bash +APPLOGGER_CLI_VERSION=applogger-cli-v0.1.0 curl -fsSL https://raw.githubusercontent.com/devzucca/appLoggers/main/cli/install/install.sh | bash +``` + +```powershell +$env:APPLOGGER_CLI_VERSION = 'applogger-cli-v0.1.0' +irm https://raw.githubusercontent.com/devzucca/appLoggers/main/cli/install/install.ps1 | iex +``` + +### Opción 2: Descargar Binario (Manual) ```bash # Linux / macOS -curl -L https://github.com/devzucca/appLoggers/releases/download/applogger-cli-v0.1.0/applogger-cli-linux-amd64 -o applogger-cli +VERSION="applogger-cli-vX.Y.Z" +curl -L "https://github.com/devzucca/appLoggers/releases/download/${VERSION}/applogger-cli-linux-amd64" -o applogger-cli chmod +x applogger-cli sudo mv applogger-cli /usr/local/bin/ # Windows (PowerShell) -$url = "https://github.com/devzucca/appLoggers/releases/download/applogger-cli-v0.1.0/applogger-cli-windows-amd64.exe" +$version = "applogger-cli-vX.Y.Z" +$url = "https://github.com/devzucca/appLoggers/releases/download/$version/applogger-cli-windows-amd64.exe" $output = "$env:ProgramFiles\applogger-cli.exe" Invoke-WebRequest -Uri $url -OutFile $output ``` -### Opción 2: Compilar desde Fuente +### Opción 3: Compilar desde Fuente ```bash git clone https://github.com/devzucca/appLoggers.git @@ -42,12 +97,19 @@ go build -o applogger-cli ./cmd/applogger-cli sudo mv applogger-cli /usr/local/bin/ ``` -### Opción 3: Homebrew (macOS, Linux) +### Opción 4: Homebrew / Scoop / Winget (manifiestos de publicación) -```bash -# (Próximamente - en desarrollo) -# brew install applogger-cli -``` +Desde `applogger-cli-v*`, el workflow de release genera y publica estos manifiestos como assets: + +- `manifests/homebrew/applogger-cli.rb` +- `manifests/scoop/applogger-cli.json` +- `manifests/winget/DevZucca.AppLoggerCLI*.yaml` + +Esto deja la publicación lista para: + +- Tap Homebrew propio +- Bucket Scoop propio +- PR al repositorio `microsoft/winget-pkgs` --- @@ -63,7 +125,7 @@ sudo mv applogger-cli /usr/local/bin/ 4. Abre **PowerShell** o **CMD** y verifica: ```powershell -applogger-cli --version +applogger-cli version --output json ``` #### B. Agregar a PATH (PowerShell) @@ -107,23 +169,24 @@ Move-Item applogger-cli.exe "$env:ProgramFiles\applogger-cli.exe" ```bash # Detectar arquitectura ARCH=$(uname -m) # "arm64" (Apple Silicon) o "x86_64" (Intel) +VERSION="applogger-cli-vX.Y.Z" # Descargar curl -L \ - https://github.com/devzucca/appLoggers/releases/download/applogger-cli-v0.1.0/applogger-cli-darwin-${ARCH} \ + "https://github.com/devzucca/appLoggers/releases/download/${VERSION}/applogger-cli-darwin-${ARCH}" \ -o applogger-cli chmod +x applogger-cli sudo mv applogger-cli /usr/local/bin/ # Verificar -applogger-cli --version +applogger-cli version --output json ``` #### B. Compilar desde Fuente ```bash -# 1. Asegúrate de tener Go 1.25+ +# 1. Asegúrate de tener Go 1.24+ go version # 2. Clonar y compilar @@ -136,12 +199,22 @@ sudo mv applogger-cli /usr/local/bin/ chmod +x /usr/local/bin/applogger-cli ``` -#### C. Homebrew (Cuando esté disponible) +#### C. Homebrew (con tap propio) ```bash +# 1) copiar el formula generado desde los assets de release +# 2) publicarlo en tu tap (ej: devzucca/homebrew-applogger) brew install devzucca/apploggers/applogger-cli ``` +#### D. Winget (publicación comunitaria) + +```powershell +# usar los manifiestos winget generados en el release +# y abrir PR a microsoft/winget-pkgs +winget install DevZucca.AppLoggerCLI +``` + --- ### 🐧 Linux @@ -151,17 +224,18 @@ brew install devzucca/apploggers/applogger-cli ```bash # Detectar arquitectura ARCH=$(dpkg --print-architecture) # "amd64", "arm64", etc. +VERSION="applogger-cli-vX.Y.Z" # Descargar curl -L \ - https://github.com/devzucca/appLoggers/releases/download/applogger-cli-v0.1.0/applogger-cli-linux-${ARCH} \ + "https://github.com/devzucca/appLoggers/releases/download/${VERSION}/applogger-cli-linux-${ARCH}" \ -o applogger-cli chmod +x applogger-cli sudo mv applogger-cli /usr/local/bin/ # Verificar -applogger-cli --version +applogger-cli version --output json ``` #### B. Compilar desde Fuente @@ -169,7 +243,7 @@ applogger-cli --version ```bash # 1. Instalar Go (si no lo tienes) sudo apt-get update -sudo apt-get install -y golang-1.25 # O descargar desde golang.org/dl +sudo apt-get install -y golang # O descargar desde golang.org/dl # 2. Clonar y compilar git clone https://github.com/devzucca/appLoggers.git @@ -211,7 +285,7 @@ docker run applogger-cli:latest --version | Herramienta | Versión mínima | Verificar con | |---|---|---| -| Go | 1.25 | `go version` | +| Go | 1.24 | `go version` | | Git | 2.30+ | `git --version` | | GNU Make | 4.3+ (opcional) | `make --version` | @@ -237,7 +311,7 @@ sudo mv applogger-cli /usr/local/bin/ chmod +x /usr/local/bin/applogger-cli # 6. Verificar -applogger-cli --version +applogger-cli version --output json ``` ### Compilar para Otras Plataformas @@ -272,8 +346,8 @@ El CLI necesita acceso a tu proyecto Supabase para consultar logs y métricas. # 2. Selecciona tu proyecto # 3. Settings → API → copia los valores -$env:APPLOGGER_SUPABASE_URL = "https://TU_PROJECT_REF.supabase.co" -$env:APPLOGGER_SUPABASE_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." +$env:appLogger_supabaseUrl = "https://TU_PROJECT_REF.supabase.co" +$env:appLogger_supabaseKey = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." # Verifica que funcionó applogger-cli health --output json @@ -282,8 +356,8 @@ applogger-cli health --output json #### CMD (Windows) ```cmd -set APPLOGGER_SUPABASE_URL=https://TU_PROJECT_REF.supabase.co -set APPLOGGER_SUPABASE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... +set appLogger_supabaseUrl=https://TU_PROJECT_REF.supabase.co +set appLogger_supabaseKey=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... REM Verifica applogger-cli health --output json @@ -292,8 +366,8 @@ applogger-cli health --output json #### Bash / Zsh (macOS, Linux) ```bash -export APPLOGGER_SUPABASE_URL="https://TU_PROJECT_REF.supabase.co" -export APPLOGGER_SUPABASE_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." +export appLogger_supabaseUrl="https://TU_PROJECT_REF.supabase.co" +export appLogger_supabaseKey="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." # Verifica applogger-cli health --output json @@ -304,8 +378,8 @@ applogger-cli health --output json **Bash/Zsh:** ```bash # Agregar a ~/.bashrc o ~/.zshrc -echo 'export APPLOGGER_SUPABASE_URL="https://TU_PROJECT_REF.supabase.co"' >> ~/.bashrc -echo 'export APPLOGGER_SUPABASE_KEY="..."' >> ~/.bashrc +echo 'export appLogger_supabaseUrl="https://TU_PROJECT_REF.supabase.co"' >> ~/.bashrc +echo 'export appLogger_supabaseKey="..."' >> ~/.bashrc source ~/.bashrc ``` @@ -314,8 +388,8 @@ source ~/.bashrc # Agregar a tu perfil PowerShell $profile # te muestra la ruta Add-Content -Path $profile -Value @" -`$env:APPLOGGER_SUPABASE_URL = "https://TU_PROJECT_REF.supabase.co" -`$env:APPLOGGER_SUPABASE_KEY = "..." +`$env:appLogger_supabaseUrl = "https://TU_PROJECT_REF.supabase.co" +`$env:appLogger_supabaseKey = "..." "@ ``` @@ -323,12 +397,12 @@ Add-Content -Path $profile -Value @" | Variable | Default | Propósito | |---|---|---| -| `APPLOGGER_SUPABASE_URL` | — | URL de tu proyecto Supabase | -| `APPLOGGER_SUPABASE_KEY` | — | Llave `service_role` para consultas del CLI | -| `APPLOGGER_SUPABASE_SCHEMA` | `public` | Esquema en PostgreSQL | -| `APPLOGGER_SUPABASE_LOG_TABLE` | `app_logs` | Tabla de logs | -| `APPLOGGER_SUPABASE_METRIC_TABLE` | `app_metrics` | Tabla de métricas | -| `APPLOGGER_SUPABASE_TIMEOUT_SECONDS` | `15` | Timeout HTTP (1-120) | +| `appLogger_supabaseUrl` | — | URL de tu proyecto Supabase | +| `appLogger_supabaseKey` | — | Llave `service_role` para consultas del CLI | +| `appLogger_supabaseSchema` | `public` | Esquema en PostgreSQL | +| `appLogger_supabaseLogTable` | `app_logs` | Tabla de logs | +| `appLogger_supabaseMetricTable` | `app_metrics` | Tabla de métricas | +| `appLogger_supabaseTimeoutSeconds` | `15` | Timeout HTTP (1-120) | > Seguridad: usa `service_role` solo en backend/entornos de operaciones. > El SDK movil debe usar anon key y nunca exponer `service_role`. @@ -340,8 +414,8 @@ Add-Content -Path $profile -Value @" ### 1. Verificar Instalación ```bash -applogger-cli --version -# Output: applogger-cli v0.1.0-alpha.0 +applogger-cli version --output json +# Output: {"name":"applogger-cli","version":"applogger-cli-vX.Y.Z",...} applogger-cli --syncbin-metadata # Output: JSON con metadatos Syncbin @@ -378,7 +452,8 @@ applogger-cli telemetry query \ ls -la /usr/local/bin/applogger-cli # Si no existe, descargarlo nuevamente -curl -L https://github.com/devzucca/appLoggers/releases/download/applogger-cli-v0.1.0/applogger-cli-linux-amd64 \ +VERSION="applogger-cli-vX.Y.Z" +curl -L "https://github.com/devzucca/appLoggers/releases/download/${VERSION}/applogger-cli-linux-amd64" \ -o /usr/local/bin/applogger-cli chmod +x /usr/local/bin/applogger-cli ``` @@ -391,18 +466,18 @@ chmod +x /usr/local/bin/applogger-cli chmod +x /usr/local/bin/applogger-cli ``` -### "health check failed: APPLOGGER_SUPABASE_URL not set" +### "health check failed: appLogger_supabaseUrl not set" **Causa**: Las variables de entorno no están configuradas. ```bash # Verifica si están cargadas -echo $APPLOGGER_SUPABASE_URL -echo $APPLOGGER_SUPABASE_KEY +echo $appLogger_supabaseUrl +echo $appLogger_supabaseKey # Si están vacías, configúralas -export APPLOGGER_SUPABASE_URL="..." -export APPLOGGER_SUPABASE_KEY="..." +export appLogger_supabaseUrl="..." +export appLogger_supabaseKey="..." ``` ### "GOARCH and OS env vars not allowed" @@ -423,7 +498,7 @@ go build -o applogger-cli ./cmd/applogger-cli 1. Ve a [Supabase Dashboard](https://supabase.com/dashboard) 2. Settings → API 3. Copia la llave `service_role` nuevamente -4. Reconfigura: `export APPLOGGER_SUPABASE_KEY="..."` +4. Reconfigura: `export appLogger_supabaseKey="..."` --- diff --git a/docs/ES/cli/README.md b/docs/ES/cli/README.md index f058c12..9af1f29 100644 --- a/docs/ES/cli/README.md +++ b/docs/ES/cli/README.md @@ -24,6 +24,7 @@ - [Instalación Rápida](#instalación-rápida) - [Configuración Supabase Detallada](#configuración-supabase-detallada) +- [Selección de Proyecto](#selección-de-proyecto) - [Comandos Principales](#comandos-principales) - [Consultas de Telemetría](#consultas-de-telemetría) - [Salidas (text/json/agent)](#salidas-texjsonarent) @@ -51,16 +52,54 @@ usuario operativo del CLI, hardening y troubleshooting), ver: - [SUPABASE_CONFIGURATION.md](./SUPABASE_CONFIGURATION.md) +## Selección de Proyecto + +El CLI ya soporta operación multi-proyecto para escenarios donde una misma +estación o futura app en Wails debe consultar telemetría de varias apps +distintas (`klinema`, `klinematv`, etc.). + +Precedencia de resolución: + +1. `--project` +2. `APPLOGGER_PROJECT` +3. Detección por `workspace_roots` desde `APPLOGGER_CONFIG` +4. `default_project` +5. Único proyecto configurado +6. Variables legacy `appLogger_supabase*`, `APPLOGGER_SUPABASE_*`, `SUPABASE_*` + +Variables nuevas: + +- `APPLOGGER_CONFIG`: ruta al archivo JSON de proyectos +- `APPLOGGER_PROJECT`: nombre del proyecto activo + +Ejemplo rápido: + +```bash +applogger-cli --project klinema telemetry query --source logs --severity error --output json +``` + ```bash # Linux / macOS -curl -L https://github.com/devzucca/appLoggers/releases/download/applogger-cli-v0.1.0/applogger-cli-linux-amd64 \ - -o /usr/local/bin/applogger-cli -chmod +x /usr/local/bin/applogger-cli +curl -fsSL https://raw.githubusercontent.com/devzucca/appLoggers/main/cli/install/install.sh | bash + +# Verificar +applogger-cli version --output json +``` + +```powershell +# Windows PowerShell +irm https://raw.githubusercontent.com/devzucca/appLoggers/main/cli/install/install.ps1 | iex # Verificar -applogger-cli --version +applogger-cli version --output json ``` +Notas: + +- En macOS se usa el mismo instalador `bash`; detecta Intel vs Apple Silicon. +- En Linux detecta `amd64` vs `arm64`. +- En Windows instala en el perfil del usuario y actualiza `PATH`. + --- ## Comandos Principales @@ -172,6 +211,7 @@ applogger-cli telemetry query \ [--aggregate MODE] \ [--severity LEVEL] \ # logs only [--tag NAME] \ # logs only + [--anomaly-type TYPE] \ # logs only (extra.anomaly_type) [--session-id UUID] \ [--name METRIC_NAME] \ # metrics only [--limit N] \ @@ -186,11 +226,12 @@ applogger-cli telemetry query \ | `--from` | ❌ | RFC3339 | `--from 2026-03-01T00:00:00Z` | | `--to` | ❌ | RFC3339 | `--to 2026-03-02T00:00:00Z` | | `--aggregate` | ❌ | `none`, `hour`, `severity`, `tag`, `session`, `name` | `--aggregate severity` | -| `--severity` | ❌ (logs) | `debug`, `info`, `warn`, `error` | `--severity error` | +| `--severity` | ❌ (logs) | `debug`, `info`, `warn`, `error`, `critical`, `metric` | `--severity error` | | `--tag` | ❌ (logs) | texto libre | `--tag PAYMENT` | +| `--anomaly-type` | ❌ (logs) | texto libre | `--anomaly-type slow_response` | | `--session-id` | ❌ | UUID | `--session-id 550e8400-e29b-41d4-a716-446655440000` | | `--name` | ❌ (metrics) | texto libre | `--name response_time_ms` | -| `--limit` | ❌ | 1-1000 (default: 25) | `--limit 50` | +| `--limit` | ❌ | 1-1000 (default: 100) | `--limit 50` | | `--output` | ❌ | `text`, `json`, `agent` | `--output json` | #### Ejemplos @@ -233,6 +274,21 @@ applogger-cli telemetry query \ --to 2026-03-19T23:59:59Z ``` +**E. Warnings por tipo de anomalia** +```bash +applogger-cli telemetry query \ + --source logs \ + --severity warn \ + --anomaly-type slow_response \ + --limit 25 \ + --output json +``` + +Notas operativas: + +- Las consultas de `logs` devuelven el campo `extra` cuando existe. +- `anomaly_type` no es una columna top-level; vive dentro de `extra.anomaly_type`. + --- ### `applogger-cli telemetry agent-response` @@ -249,6 +305,7 @@ applogger-cli telemetry agent-response \ [--to TIMESTAMP] \ [--severity LEVEL] \ [--tag NAME] \ + [--anomaly-type TYPE] \ [--session-id UUID] \ [--name METRIC_NAME] \ [--limit N] \ @@ -259,7 +316,7 @@ applogger-cli telemetry agent-response \ | Parámetro | Default | Valores | Propósito | |---|---|---|---| -| `--preview-limit` | 1 | 0-50 | Filas de muestra en `rows_preview` | +| `--preview-limit` | 5 | 0-50 | Filas de muestra en `rows_preview` | #### Salida (TOON Format) @@ -312,17 +369,16 @@ Por defecto, amigable para lectura. ```bash $ applogger-cli telemetry query --source logs --severity error --limit 2 -Total logs: 2145 -Filter: severity=error -Showing 2 of 2145 results - -[1] ERROR (2026-03-19T10:30:00Z) — PAYMENT - Message: Transaction declined - Session: 550e8400-e29b-41d4-a716-446655440000 - -[2] ERROR (2026-03-19T10:29:15Z) — AUTH - Message: Invalid credentials - Session: 550e8400-e29b-41d4-a716-446655440001 +source=logs +count=2 +aggregate=none +from= +to= +severity=error +session_id= +tag= +name= +limit=2 ``` ### 2. **json** (Parser/Script) @@ -338,6 +394,7 @@ JSON valido compatible con cualquier lenguaje. "source": "logs", "aggregate": "none", "severity": "error", + "anomaly_type": "", "limit": 2 }, "rows": [ @@ -500,26 +557,29 @@ Ver [AGENT_OPERATOR_SKILL.md](../agents/applogger-cli-agent-operator/SKILL.md) p | Variable | Propósito | Ejemplo | |---|---|---| -| `APPLOGGER_SUPABASE_URL` | URL del proyecto | `https://project.supabase.co` | -| `APPLOGGER_SUPABASE_KEY` | API key service_role (solo backend/operaciones) | `eyJhbGc...` | +| `appLogger_supabaseUrl` | URL del proyecto | `https://project.supabase.co` | +| `appLogger_supabaseKey` | API key service_role (solo backend/operaciones) | `eyJhbGc...` | ### Opcionales | Variable | Default | Propósito | |---|---|---| -| `APPLOGGER_SUPABASE_SCHEMA` | `public` | Esquema PostgreSQL | -| `APPLOGGER_SUPABASE_LOG_TABLE` | `app_logs` | Nombre tabla logs | -| `APPLOGGER_SUPABASE_METRIC_TABLE` | `app_metrics` | Nombre tabla métricas | -| `APPLOGGER_SUPABASE_TIMEOUT_SECONDS` | `15` | Timeout HTTP (1-120) | +| `appLogger_supabaseSchema` | `public` | Esquema PostgreSQL | +| `appLogger_supabaseLogTable` | `app_logs` | Nombre tabla logs | +| `appLogger_supabaseMetricTable` | `app_metrics` | Nombre tabla métricas | +| `appLogger_supabaseTimeoutSeconds` | `15` | Timeout HTTP (1-120) | ### Backwards Compatibility Fallback aliases para compatibilidad: -- `SUPABASE_URL` → `APPLOGGER_SUPABASE_URL` -- `SUPABASE_KEY` → `APPLOGGER_SUPABASE_KEY` + +- `APPLOGGER_SUPABASE_URL` → `appLogger_supabaseUrl` +- `APPLOGGER_SUPABASE_KEY` → `appLogger_supabaseKey` +- `SUPABASE_URL` → `appLogger_supabaseUrl` +- `SUPABASE_KEY` → `appLogger_supabaseKey` > Importante: el CLI requiere `service_role` para consultas `SELECT` con RLS activa. -> No uses anon/publishable key en `APPLOGGER_SUPABASE_KEY`. +> No uses anon/publishable key en `appLogger_supabaseKey`. --- diff --git a/docs/ES/cli/SUPABASE_CONFIGURATION.md b/docs/ES/cli/SUPABASE_CONFIGURATION.md index c5985e3..09a4ccf 100644 --- a/docs/ES/cli/SUPABASE_CONFIGURATION.md +++ b/docs/ES/cli/SUPABASE_CONFIGURATION.md @@ -54,15 +54,93 @@ Desde Supabase Dashboard: Variables requeridas para CLI: +- `appLogger_supabaseUrl` +- `appLogger_supabaseKey` (service_role) + +Aliases compatibles: + - `APPLOGGER_SUPABASE_URL` -- `APPLOGGER_SUPABASE_KEY` (service_role) +- `APPLOGGER_SUPABASE_KEY` +- `SUPABASE_URL` +- `SUPABASE_KEY` Opcionales: -- `APPLOGGER_SUPABASE_SCHEMA` (default `public`) -- `APPLOGGER_SUPABASE_LOG_TABLE` (default `app_logs`) -- `APPLOGGER_SUPABASE_METRIC_TABLE` (default `app_metrics`) -- `APPLOGGER_SUPABASE_TIMEOUT_SECONDS` (default `15`) +- `appLogger_supabaseSchema` (default `public`) +- `appLogger_supabaseLogTable` (default `app_logs`) +- `appLogger_supabaseMetricTable` (default `app_metrics`) +- `appLogger_supabaseTimeoutSeconds` (default `15`) + +## Paso 2b - Configuracion multi-proyecto para CLI + Wails + +Cuando el CLI opere varias aplicaciones de telemetria distintas +(`klinema`, `klinematv`, etc.), la configuracion recomendada ya no es un solo +par global de variables, sino un registro de proyectos compartido entre el CLI +y la futura app de escritorio en Wails. + +Variables de control: + +- `APPLOGGER_CONFIG`: ruta al archivo JSON de proyectos. +- `APPLOGGER_PROJECT`: seleccion explicita del proyecto activo. +- `--config`: override por linea de comandos. +- `--project`: override por linea de comandos. + +Ruta default del archivo: + +- Windows: `%AppData%/applogger/cli.json` +- Linux/macOS: `$(os.UserConfigDir)/applogger/cli.json` + +Ejemplo recomendado: + +```json +{ + "default_project": "klinema", + "projects": [ + { + "name": "klinema", + "display_name": "Klinema Mobile", + "workspace_roots": [ + "D:/workspace/klinema" + ], + "supabase": { + "url": "https://klinema.supabase.co", + "api_key_env": "APPLOGGER_KLINEMA_SUPABASE_KEY", + "schema": "public", + "logs_table": "app_logs", + "metrics_table": "app_metrics", + "timeout_seconds": 15 + } + }, + { + "name": "klinematv", + "display_name": "Klinema TV", + "workspace_roots": [ + "D:/workspace/klinematv" + ], + "supabase": { + "url": "https://klinematv.supabase.co", + "api_key_env": "APPLOGGER_KLINEMATV_SUPABASE_KEY" + } + } + ] +} +``` + +Precedencia de resolucion: + +1. `--project` +2. `APPLOGGER_PROJECT` +3. Matching por `workspace_roots` contra el directorio actual +4. `default_project` +5. Unico proyecto configurado +6. Fallback legacy a `appLogger_supabase*` / `APPLOGGER_SUPABASE_*` / `SUPABASE_*` + +Practica corporativa recomendada: + +- Guardar solo URL y metadata en el JSON. +- Guardar el `service_role` en variables de entorno o secreto del sistema usando `api_key_env`. +- Hacer que Wails administre el registro de proyectos y lance el CLI con el mismo modelo. +- En SSE transmitir el proyecto resuelto y nunca la credencial cruda. ## Paso 3 - Crear usuario operativo del CLI @@ -87,12 +165,12 @@ sudo install -m 600 -o root -g root /dev/null /etc/applogger-cli.env Contenido sugerido de `/etc/applogger-cli.env`: ```env -APPLOGGER_SUPABASE_URL=https://YOUR_PROJECT.supabase.co -APPLOGGER_SUPABASE_KEY=YOUR_SERVICE_ROLE_KEY -APPLOGGER_SUPABASE_SCHEMA=public -APPLOGGER_SUPABASE_LOG_TABLE=app_logs -APPLOGGER_SUPABASE_METRIC_TABLE=app_metrics -APPLOGGER_SUPABASE_TIMEOUT_SECONDS=15 +appLogger_supabaseUrl=https://YOUR_PROJECT.supabase.co +appLogger_supabaseKey=YOUR_SERVICE_ROLE_KEY +appLogger_supabaseSchema=public +appLogger_supabaseLogTable=app_logs +appLogger_supabaseMetricTable=app_metrics +appLogger_supabaseTimeoutSeconds=15 ``` ### Windows @@ -106,8 +184,8 @@ Opciones recomendadas: PowerShell para sesion actual: ```powershell -$env:APPLOGGER_SUPABASE_URL = "https://YOUR_PROJECT.supabase.co" -$env:APPLOGGER_SUPABASE_KEY = "YOUR_SERVICE_ROLE_KEY" +$env:appLogger_supabaseUrl = "https://YOUR_PROJECT.supabase.co" +$env:appLogger_supabaseKey = "YOUR_SERVICE_ROLE_KEY" ``` ### CI/CD @@ -120,10 +198,21 @@ Ejemplo GitHub Actions: ```yaml env: - APPLOGGER_SUPABASE_URL: ${{ secrets.APPLOGGER_SUPABASE_URL }} - APPLOGGER_SUPABASE_KEY: ${{ secrets.APPLOGGER_SUPABASE_SERVICE_ROLE_KEY }} + appLogger_supabaseUrl: ${{ secrets.APPLOGGER_SUPABASE_URL }} + appLogger_supabaseKey: ${{ secrets.APPLOGGER_SUPABASE_KEY }} ``` +Para runners multi-proyecto, tambien puede inyectarse: + +```yaml +env: + APPLOGGER_CONFIG: /opt/applogger/cli.json + APPLOGGER_PROJECT: klinema + APPLOGGER_KLINEMA_SUPABASE_KEY: ${{ secrets.APPLOGGER_KLINEMA_SUPABASE_KEY }} +``` + +Para `act` en local, definir el mismo par en `.act.secrets`. Si el workflow referencia un secret ausente, `act` lo inyecta vacio. + ## Paso 4 - Verificacion operativa del CLI Comandos de verificacion minima: @@ -160,7 +249,7 @@ Causas probables: Acciones: -1. Verificar key cargada en `APPLOGGER_SUPABASE_KEY`. +1. Verificar key cargada en `appLogger_supabaseKey`. 2. Validar migraciones 004 y 006 aplicadas. ### Error de tabla no encontrada @@ -173,7 +262,7 @@ Accion: ### Timeouts -- Ajustar `APPLOGGER_SUPABASE_TIMEOUT_SECONDS` (1..120). +- Ajustar `appLogger_supabaseTimeoutSeconds` (1..120). - Reducir rango temporal y limite en query. ## Matriz de responsabilidades diff --git a/docs/ES/cli/references/BEST_PRACTICES.md b/docs/ES/cli/references/BEST_PRACTICES.md index 9c9496d..c9439ae 100644 --- a/docs/ES/cli/references/BEST_PRACTICES.md +++ b/docs/ES/cli/references/BEST_PRACTICES.md @@ -13,8 +13,8 @@ ```bash # Usar variables de entorno (nunca hardcodear) -export APPLOGGER_SUPABASE_URL="https://project.supabase.co" -export APPLOGGER_SUPABASE_KEY="$(aws secretsmanager get-secret-value --secret-id applogger-key | jq -r .SecretString)" +export appLogger_supabaseUrl="https://project.supabase.co" +export appLogger_supabaseKey="$(aws secretsmanager get-secret-value --secret-id applogger-key | jq -r .SecretString)" applogger-cli health --output json ``` @@ -29,12 +29,12 @@ kubectl create secret generic applogger-credentials \ ```yaml # Pod config env: - - name: APPLOGGER_SUPABASE_URL + - name: appLogger_supabaseUrl valueFrom: secretKeyRef: name: applogger-credentials key: supabase-url - - name: APPLOGGER_SUPABASE_KEY + - name: appLogger_supabaseKey valueFrom: secretKeyRef: name: applogger-credentials @@ -54,7 +54,7 @@ echo "key=eyJhbGc..." > /etc/applogger.conf # ❌ ¡DANGEROUS! ps aux | grep applogger-cli # ❌ Secrets expuestos # ❌ Loguear credenciales -echo "Using key: $APPLOGGER_SUPABASE_KEY" # ❌ Logs sensibles +echo "Using key: $appLogger_supabaseKey" # ❌ Logs sensibles ``` ### 2. Manejo de Errores @@ -251,7 +251,7 @@ log_query "logs" applogger-cli telemetry query >> /tmp/log.txt # Formato inconsistente # ❌ Loguear datos sensibles -echo "Query ran with credentials: $APPLOGGER_SUPABASE_KEY" >> log.txt # ❌ SECRETOS +echo "Query ran with credentials: $appLogger_supabaseKey" >> log.txt # ❌ SECRETOS # ❌ Sin trazabilidad applogger-cli health && echo "Health check OK" # No se sabe quién, cuándo, por qué @@ -403,12 +403,12 @@ spec: --output json > /tmp/audit.json echo "Audit complete" env: - - name: APPLOGGER_SUPABASE_URL + - name: appLogger_supabaseUrl valueFrom: secretKeyRef: name: applogger-secrets key: url - - name: APPLOGGER_SUPABASE_KEY + - name: appLogger_supabaseKey valueFrom: secretKeyRef: name: applogger-secrets @@ -429,8 +429,8 @@ RUN go build -o /usr/local/bin/applogger-cli ./cmd/applogger-cli FROM alpine:latest RUN apk add --no-cache ca-certificates COPY --from=builder /usr/local/bin/applogger-cli /usr/local/bin/ -ENV APPLOGGER_SUPABASE_URL="" -ENV APPLOGGER_SUPABASE_KEY="" +ENV appLogger_supabaseUrl="" +ENV appLogger_supabaseKey="" ENTRYPOINT ["applogger-cli"] CMD ["--help"] ``` @@ -448,8 +448,8 @@ jobs: audit: runs-on: ubuntu-latest env: - APPLOGGER_SUPABASE_URL: ${{ secrets.APPLOGGER_SUPABASE_URL }} - APPLOGGER_SUPABASE_KEY: ${{ secrets.APPLOGGER_SUPABASE_KEY }} + appLogger_supabaseUrl: ${{ secrets.APPLOGGER_SUPABASE_URL }} + appLogger_supabaseKey: ${{ secrets.APPLOGGER_SUPABASE_KEY }} steps: - name: Download CLI run: | diff --git a/docs/ES/desarrollo/integration-guide.md b/docs/ES/desarrollo/integration-guide.md index f57f555..1515096 100644 --- a/docs/ES/desarrollo/integration-guide.md +++ b/docs/ES/desarrollo/integration-guide.md @@ -106,33 +106,33 @@ Todas las variables se colocan en `local.properties` (no commiteable) y se mapea | Variable | Tipo | Valor por defecto | Descripción | |---|---|---|---| -| `appLogger.url` | String | `""` | Endpoint del backend (Supabase URL o URL propia) | -| `appLogger.anonKey` | String | `""` | API key de autenticación (anon key de Supabase) | -| `appLogger.debug` | Boolean | `false` | Modo debug: logs van a Logcat en vez de backend | -| `appLogger.logToConsole` | Boolean | `true` | Mostrar logs en Logcat (solo en debug) | -| `appLogger.batchSize` | Int | `20` | Número de eventos por batch antes de enviar (1-100) | -| `appLogger.flushIntervalSeconds` | Int | `30` | Intervalo máximo en segundos antes de flush automático (5-300) | -| `appLogger.maxStackTraceLines` | Int | `50` | Líneas máximas de stack trace (Mobile: 50, TV: 5) | -| `appLogger.lowStorageMode` | Boolean | `false` | Reduce buffer local y stack traces (para TV o dispositivos low-RAM) | -| `appLogger.verboseTransport` | Boolean | `false` | Log detallado de cada batch enviado (solo debug) | -| `appLogger.userId` | String | `null` | UUID anónimo del usuario (solo con consentimiento explícito) | -| `appLogger.bufferSizeStrategy` | String | `FIXED` | Estrategia de tamaño: `FIXED`, `ADAPTIVE_TO_RAM`, `ADAPTIVE_TO_LOG_RATE` | -| `appLogger.bufferOverflowPolicy` | String | `DISCARD_OLDEST` | Política ante overflow: `DISCARD_OLDEST`, `DISCARD_NEWEST`, `PRIORITY_AWARE` | -| `appLogger.offlinePersistenceMode` | String | `NONE` | Persistencia offline: `NONE`, `CRITICAL_ONLY`, `ALL` | +| `appLogger_url` | String | `""` | Endpoint del backend (Supabase URL o URL propia) | +| `appLogger_anonKey` | String | `""` | API key de autenticación (anon key de Supabase) | +| `appLogger_debug` | Boolean | `false` | Modo debug: logs van a Logcat en vez de backend | +| `appLogger_logToConsole` | Boolean | `true` | Mostrar logs en Logcat (solo en debug) | +| `appLogger_batchSize` | Int | `20` | Número de eventos por batch antes de enviar (1-100) | +| `appLogger_flushIntervalSeconds` | Int | `30` | Intervalo máximo en segundos antes de flush automático (5-300) | +| `appLogger_maxStackTraceLines` | Int | `50` | Líneas máximas de stack trace (Mobile: 50, TV: 5) | +| `appLogger_lowStorageMode` | Boolean | `false` | Reduce buffer local y stack traces (para TV o dispositivos low-RAM) | +| `appLogger_verboseTransport` | Boolean | `false` | Log detallado de cada batch enviado (solo debug) | +| `appLogger_userId` | String | `null` | UUID anónimo del usuario (solo con consentimiento explícito) | +| `appLogger_bufferSizeStrategy` | String | `FIXED` | Estrategia de tamaño: `FIXED`, `ADAPTIVE_TO_RAM`, `ADAPTIVE_TO_LOG_RATE` | +| `appLogger_bufferOverflowPolicy` | String | `DISCARD_OLDEST` | Política ante overflow: `DISCARD_OLDEST`, `DISCARD_NEWEST`, `PRIORITY_AWARE` | +| `appLogger_offlinePersistenceMode` | String | `NONE` | Persistencia offline: `NONE`, `CRITICAL_ONLY`, `ALL` | ### 3.2 Ejemplo completo de `local.properties` ```properties # AppLogger — NUNCA commitear este archivo -appLogger.url=https://tu-proyecto.supabase.co -appLogger.anonKey=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.tu_anon_key_aqui -appLogger.debug=true -appLogger.logToConsole=true -appLogger.batchSize=20 -appLogger.flushIntervalSeconds=30 -appLogger.maxStackTraceLines=50 -appLogger.lowStorageMode=false -appLogger.verboseTransport=false +appLogger_url=https://tu-proyecto.supabase.co +appLogger_anonKey=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.tu_anon_key_aqui +appLogger_debug=true +appLogger_logToConsole=true +appLogger_batchSize=20 +appLogger_flushIntervalSeconds=30 +appLogger_maxStackTraceLines=50 +appLogger_lowStorageMode=false +appLogger_verboseTransport=false ``` > **Verificar que `local.properties` está en `.gitignore`:** @@ -155,18 +155,18 @@ android { if (file.exists()) load(file.inputStream()) } - buildConfigField("String", "LOGGER_URL", "\"${props["appLogger.url"] ?: ""}\"") - buildConfigField("String", "LOGGER_KEY", "\"${props["appLogger.anonKey"] ?: ""}\"") - buildConfigField("Boolean", "LOGGER_DEBUG_MODE", "${props["appLogger.debug"] ?: false}") - buildConfigField("Boolean", "LOGGER_CONSOLE_OUTPUT", "${props["appLogger.logToConsole"] ?: true}") - buildConfigField("Int", "LOGGER_BATCH_SIZE", "${props["appLogger.batchSize"] ?: 20}") - buildConfigField("Int", "LOGGER_FLUSH_INTERVAL", "${props["appLogger.flushIntervalSeconds"] ?: 30}") - buildConfigField("Int", "LOGGER_MAX_STACK", "${props["appLogger.maxStackTraceLines"] ?: 50}") - buildConfigField("Boolean", "LOGGER_LOW_STORAGE", "${props["appLogger.lowStorageMode"] ?: false}") - buildConfigField("Boolean", "LOGGER_VERBOSE", "${props["appLogger.verboseTransport"] ?: false}") - buildConfigField("String", "LOGGER_BUFFER_STRATEGY", "\"${props["appLogger.bufferSizeStrategy"] ?: "FIXED"}\"") - buildConfigField("String", "LOGGER_OVERFLOW_POLICY", "\"${props["appLogger.bufferOverflowPolicy"] ?: "DISCARD_OLDEST"}\"") - buildConfigField("String", "LOGGER_PERSISTENCE_MODE", "\"${props["appLogger.offlinePersistenceMode"] ?: "NONE"}\"") + buildConfigField("String", "LOGGER_URL", "\"${props["appLogger_url"] ?: ""}\"") + buildConfigField("String", "LOGGER_KEY", "\"${props["appLogger_anonKey"] ?: ""}\"") + buildConfigField("Boolean", "LOGGER_DEBUG_MODE", "${props["appLogger_debug"] ?: false}") + buildConfigField("Boolean", "LOGGER_CONSOLE_OUTPUT", "${props["appLogger_logToConsole"] ?: true}") + buildConfigField("Int", "LOGGER_BATCH_SIZE", "${props["appLogger_batchSize"] ?: 20}") + buildConfigField("Int", "LOGGER_FLUSH_INTERVAL", "${props["appLogger_flushIntervalSeconds"] ?: 30}") + buildConfigField("Int", "LOGGER_MAX_STACK", "${props["appLogger_maxStackTraceLines"] ?: 50}") + buildConfigField("Boolean", "LOGGER_LOW_STORAGE", "${props["appLogger_lowStorageMode"] ?: false}") + buildConfigField("Boolean", "LOGGER_VERBOSE", "${props["appLogger_verboseTransport"] ?: false}") + buildConfigField("String", "LOGGER_BUFFER_STRATEGY", "\"${props["appLogger_bufferSizeStrategy"] ?: "FIXED"}\"") + buildConfigField("String", "LOGGER_OVERFLOW_POLICY", "\"${props["appLogger_bufferOverflowPolicy"] ?: "DISCARD_OLDEST"}\"") + buildConfigField("String", "LOGGER_PERSISTENCE_MODE", "\"${props["appLogger_offlinePersistenceMode"] ?: "NONE"}\"") } } ``` @@ -182,23 +182,23 @@ Para el pipeline de producción, las credenciales se inyectan como secrets: LOGGER_URL: ${{ secrets.LOGGER_URL }} LOGGER_KEY: ${{ secrets.LOGGER_ANON_KEY }} run: | - echo "appLogger.url=$LOGGER_URL" >> local.properties - echo "appLogger.anonKey=$LOGGER_KEY" >> local.properties - echo "appLogger.debug=false" >> local.properties - echo "appLogger.logToConsole=false" >> local.properties + echo "appLogger_url=$LOGGER_URL" >> local.properties + echo "appLogger_anonKey=$LOGGER_KEY" >> local.properties + echo "appLogger_debug=false" >> local.properties + echo "appLogger_logToConsole=false" >> local.properties ./gradlew assembleRelease ``` ### 3.5 Comportamiento según configuración -| `appLogger.debug` | `appLogger.logToConsole` | Resultado | +| `appLogger_debug` | `appLogger_logToConsole` | Resultado | |---|---|---| | `true` | `true` | Logs a Logcat + backend (doble envío) | | `true` | `false` | Solo a Logcat (desarrollo sin red) | | `false` | `true` | Solo a backend (producción con verbose) | | `false` | `false` | Solo a backend (producción normal) | -Para desactivar completamente el envío de datos (modo offline-only), no configurar `appLogger.url` o dejarlo vacío. El SDK operará solo con SQLite local. +Para desactivar completamente el envío de datos (modo offline-only), no configurar `appLogger_url` o dejarlo vacío. El SDK operará solo con SQLite local. --- @@ -279,13 +279,16 @@ Todo esto ocurre **fuera del hilo principal**. // Debug — solo visible en modo debug (Logcat) AppLoggerSDK.debug("TAG", "Mensaje de depuración") AppLoggerSDK.debug("TAG", "Con datos extra", extra = mapOf("key" to "value")) +AppLoggerSDK.debug("TAG", "Estado inesperado", throwable = e) // stacktrace opcional // Info — flujos normales de la app AppLoggerSDK.info("PLAYER", "Playback started") AppLoggerSDK.info("PLAYER", "Buffering", extra = mapOf("buffer_ms" to 500)) +AppLoggerSDK.info("PLAYER", "Recovery attempt", throwable = e) // stacktrace opcional // Warn — comportamiento inesperado pero no fatal AppLoggerSDK.warn("NETWORK", "Slow response detected", anomalyType = "HIGH_LATENCY") +AppLoggerSDK.warn("NETWORK", "Slow response", throwable = e, anomalyType = "HIGH_LATENCY") // con stacktrace // Error — fallos que el usuario probablemente nota AppLoggerSDK.error("PAYMENT", "Transaction failed", throwable = exception) @@ -297,7 +300,41 @@ AppLoggerSDK.critical("AUTH", "Token refresh failed completely", throwable = exc AppLoggerSDK.metric("screen_load_time", 1234.0, "ms", tags = mapOf("screen" to "HomeScreen")) ``` -### 5.2 Qué loguear en cada nivel +### 5.2 Extension Functions — Tag Automático + +El módulo `logger-core` incluye extension functions en `commonMain` que eliminan el tag manual en clases con logger inyectado. El tag se infiere del nombre simple de la clase (`this::class.simpleName`). + +```kotlin +import com.applogger.core.logD +import com.applogger.core.logI +import com.applogger.core.logW +import com.applogger.core.logE +import com.applogger.core.logC +import com.applogger.core.logTag + +class PlaybackRepository(private val logger: AppLogger) { + + fun load(id: String) { + this.logI(logger, "Loading content", extra = mapOf("content_id" to id)) + // Tag automático → "PlaybackRepository" + } + + fun handleError(t: Throwable) { + this.logE(logger, "Content load failed", throwable = t) + } + + fun warnRetry(t: Throwable) { + this.logW(logger, "Retrying segment", throwable = t, anomalyType = "SEGMENT_RETRY") + } +} +``` + +También se pueden usar los shorthands directos sobre `AppLogger` cuando el tag es explícito: + +```kotlin +logger.logE("PLAYER", "Segment failed", throwable = e) +logger.logW("NETWORK", "Timeout", anomalyType = "TIMEOUT") +``` | Nivel | Cuándo usarlo | Ejemplos | |---|---|---| @@ -308,7 +345,7 @@ AppLoggerSDK.metric("screen_load_time", 1234.0, "ms", tags = mapOf("screen" to " | `critical` | Fallos que bloquean la app | Corrupción de estado, fallo de inicialización | | `metric` | Datos cuantitativos de performance | Tiempos de carga, uso de memoria, buffer | -### 5.3 Buenas Prácticas de Contenido +### 5.4 Buenas Prácticas de Contenido ```kotlin // ✅ Loguear contexto técnico, no datos del usuario @@ -601,7 +638,7 @@ Para requisitos más estrictos (ej. banca), se recomienda: ```kotlin // El valor de LOGGER_DEBUG_MODE viene de local.properties → build.gradle -// En desarrollo: appLogger.debug=true → consola +// En desarrollo: appLogger_debug=true → consola // En release: la variable no existe o es false → remoto AppLoggerConfig.Builder() diff --git a/docs/ES/paquete/CHANGELOG.md b/docs/ES/paquete/CHANGELOG.md index f7b7bf0..52066b4 100644 --- a/docs/ES/paquete/CHANGELOG.md +++ b/docs/ES/paquete/CHANGELOG.md @@ -8,6 +8,17 @@ El formato sigue [Keep a Changelog](https://keepachangelog.com/es/1.0.0/) y el p ## [Unreleased] +### Added +- `throwable: Throwable? = null` opcional en `debug()` e `info()` — permite capturar stack traces + en niveles no-críticos sin cambios breaking en call-sites existentes. +- `throwable: Throwable? = null` opcional en `warn()` — parámetro insertado antes de `anomalyType`; + todas las llamadas existentes con named params no se ven afectadas. +- **`AppLoggerExtensions.kt`** (commonMain, todos los targets) — extension functions que reducen boilerplate: + - `AppLogger.logD/I/W/E/C(tag, message, throwable?, extra?)` — shorthands sobre el objeto logger. + - `Any.logTag()` — derivación automática del tag desde el nombre de clase. + - `Any.logD/I/W/E/C(logger, message, throwable?, extra?)` — extensiones sobre cualquier objeto + con tag inferido automáticamente (`this::class.simpleName`). + ### Planned - Módulo `logger-transport-firebase` — transporte a Firebase Realtime Database - `SqliteOfflineBuffer` — persistencia FIFO en SQLite usando el esquema SQLDelight ya definido (`offline_logs`) diff --git a/docs/ES/paquete/README.md b/docs/ES/paquete/README.md index 0843f43..f6507d3 100644 --- a/docs/ES/paquete/README.md +++ b/docs/ES/paquete/README.md @@ -52,7 +52,8 @@ class MyApp : Application() { config = AppLoggerConfig.Builder() .endpoint(BuildConfig.LOGGER_URL) .apiKey(BuildConfig.LOGGER_KEY) - .debugMode(BuildConfig.DEBUG) + .debugMode(BuildConfig.LOGGER_DEBUG) + .consoleOutput(BuildConfig.LOGGER_DEBUG) .build() ) } diff --git a/docs/ES/paquete/architecture.md b/docs/ES/paquete/architecture.md index 2902371..d1acbbe 100644 --- a/docs/ES/paquete/architecture.md +++ b/docs/ES/paquete/architecture.md @@ -66,6 +66,7 @@ appLoggers/ │ ├── commonMain/kotlin/ │ │ └── com/applogger/core/ │ │ ├── AppLogger.kt (trait público) +│ │ ├── AppLoggerExtensions.kt (extension functions logD/I/W/E/C) │ │ ├── LogTransport.kt (trait de transporte) │ │ ├── LogBuffer.kt (trait de buffer) │ │ ├── LogFormatter.kt (trait de formato) @@ -216,9 +217,9 @@ Cada dependencia es **inyectable**: en producción se instancian las implementac * - Las llamadas a [debug] no hacen nada en modo producción. */ interface AppLogger { - fun debug(tag: String, message: String, extra: Map? = null) - fun info(tag: String, message: String, extra: Map? = null) - fun warn(tag: String, message: String, anomalyType: String? = null, extra: Map? = null) + fun debug(tag: String, message: String, throwable: Throwable? = null, extra: Map? = null) + fun info(tag: String, message: String, throwable: Throwable? = null, extra: Map? = null) + fun warn(tag: String, message: String, throwable: Throwable? = null, anomalyType: String? = null, extra: Map? = null) fun error(tag: String, message: String, throwable: Throwable? = null, extra: Map? = null) fun critical(tag: String, message: String, throwable: Throwable? = null, extra: Map? = null) fun metric(name: String, value: Double, unit: String, tags: Map? = null) @@ -226,6 +227,49 @@ interface AppLogger { } ``` +### 4.1.1 `AppLoggerExtensions` — Extension Functions (commonMain) + +`AppLoggerExtensions.kt` extiende tanto `AppLogger` como `Any` con helpers que reducen boilerplate. +Disponible en **todos los targets** (Android, iOS, JVM) sin dependencias adicionales. + +```kotlin +// logger-core/src/commonMain/kotlin/com/applogger/core/AppLoggerExtensions.kt + +// ── Shorthands sobre AppLogger (tag explícito) ────────────────────────────── +fun AppLogger.logD(tag: String, message: String, throwable: Throwable? = null, extra: Map? = null) +fun AppLogger.logI(tag: String, message: String, throwable: Throwable? = null, extra: Map? = null) +fun AppLogger.logW(tag: String, message: String, throwable: Throwable? = null, anomalyType: String? = null, extra: Map? = null) +fun AppLogger.logE(tag: String, message: String, throwable: Throwable? = null, extra: Map? = null) +fun AppLogger.logC(tag: String, message: String, throwable: Throwable? = null, extra: Map? = null) + +// ── Extensión sobre Any (tag inferido del nombre de clase) ────────────────── +fun Any.logTag(): String // → this::class.simpleName ?: "Anonymous" + +fun Any.logD(logger: AppLogger, message: String, throwable: Throwable? = null, extra: Map? = null) +fun Any.logI(logger: AppLogger, message: String, throwable: Throwable? = null, extra: Map? = null) +fun Any.logW(logger: AppLogger, message: String, throwable: Throwable? = null, anomalyType: String? = null, extra: Map? = null) +fun Any.logE(logger: AppLogger, message: String, throwable: Throwable? = null, extra: Map? = null) +fun Any.logC(logger: AppLogger, message: String, throwable: Throwable? = null, extra: Map? = null) +``` + +**Uso recomendado:** + +```kotlin +class PlayerController(private val logger: AppLogger) { + fun onError(t: Throwable) { + // Tag inferido automáticamente → "PlayerController" + this.logE(logger, "Playback failed", throwable = t, extra = mapOf("codec" to "h264")) + } + + fun onStart() { + // Shorthand sin inferencia de tag + logger.logI("PLAYER", "Playback started") + } +} +``` + +--- + ### 4.2 `LogTransport` — Contrato de Transporte ```kotlin @@ -395,17 +439,17 @@ object AppLoggerSDK : AppLogger { override fun error(tag: String, message: String, throwable: Throwable?, extra: Map?) = instance.error(tag, message, throwable, extra) - override fun info(tag: String, message: String, extra: Map?) = - instance.info(tag, message, extra) + override fun info(tag: String, message: String, throwable: Throwable?, extra: Map?) = + instance.info(tag, message, throwable, extra) - override fun warn(tag: String, message: String, anomalyType: String?, extra: Map?) = - instance.warn(tag, message, anomalyType, extra) + override fun warn(tag: String, message: String, throwable: Throwable?, anomalyType: String?, extra: Map?) = + instance.warn(tag, message, throwable, anomalyType, extra) override fun critical(tag: String, message: String, throwable: Throwable?, extra: Map?) = instance.critical(tag, message, throwable, extra) - override fun debug(tag: String, message: String, extra: Map?) = - instance.debug(tag, message, extra) + override fun debug(tag: String, message: String, throwable: Throwable?, extra: Map?) = + instance.debug(tag, message, throwable, extra) override fun metric(name: String, value: Double, unit: String, tags: Map?) = instance.metric(name, value, unit, tags) @@ -433,9 +477,9 @@ object AppLoggerSDK : AppLogger { * Garantiza que llamadas tempranas al SDK (en ContentProviders, etc.) no crasheen. */ internal class NoOpLogger : AppLogger { - override fun debug(tag: String, message: String, extra: Map?) = Unit - override fun info(tag: String, message: String, extra: Map?) = Unit - override fun warn(tag: String, message: String, anomalyType: String?, extra: Map?) = Unit + override fun debug(tag: String, message: String, throwable: Throwable?, extra: Map?) = Unit + override fun info(tag: String, message: String, throwable: Throwable?, extra: Map?) = Unit + override fun warn(tag: String, message: String, throwable: Throwable?, anomalyType: String?, extra: Map?) = Unit override fun error(tag: String, message: String, throwable: Throwable?, extra: Map?) = Unit override fun critical(tag: String, message: String, throwable: Throwable?, extra: Map?) = Unit override fun metric(name: String, value: Double, unit: String, tags: Map?) = Unit diff --git a/docs/ES/paquete/dev-environment.md b/docs/ES/paquete/dev-environment.md index ca49beb..87ae4dd 100644 --- a/docs/ES/paquete/dev-environment.md +++ b/docs/ES/paquete/dev-environment.md @@ -181,7 +181,7 @@ Editar con los valores reales: sdk.dir=C\:\\Users\\\\AppData\\Local\\Android\\Sdk # ─── Supabase ──────────────────────────────────────────────── -APPLOGGER_SUPABASE_URL=https://hqvkrsmlphjnkefpfpzg.supabase.co +appLogger_supabaseUrl=https://hqvkrsmlphjnkefpfpzg.supabase.co APPLOGGER_SUPABASE_ANON_KEY= APPLOGGER_SUPABASE_SERVICE_KEY= @@ -311,7 +311,9 @@ act push -W .github/workflows/ci.yml --job test |---|---|---| | Composite actions en v4 | Puede fallar con `unsupported object type` al resolver tags | Usar `./gradlew` directamente (Sección 6) | | Android SDK | La imagen `act-latest` no incluye Android SDK | Solo correr jobs que no requieran compilación Android | +| Permisos de `gradlew` en Windows | `act` puede perder el execute bit al copiar desde NTFS al contenedor Linux | Mantener `chmod +x ./gradlew` en los workflows | | CodeQL | Requiere autenticación GitHub y no funciona en local | Solo corre en CI remoto | +| Upload de artifacts | `actions/upload-artifact` puede fallar sin `ACTIONS_RUNTIME_TOKEN` | Validar compilación y tests localmente; dejar upload para CI remoto | | Secretos | `.act.secrets` requiere valores reales para tests e2e | Usar mocks para desarrollo local | > **Recomendación:** Para el desarrollo diario, usar las verificaciones manuales de la Sección 6. Reservar `act` para verificar cambios en los propios archivos `.github/workflows/`. @@ -382,7 +384,7 @@ Verificar que `JAVA_HOME` apunta a JDK 17 (no 21, no 25). El CI usa estrictament ### Supabase e2e tests fallan localmente -Los tests en `SupabaseE2ETest.kt` requieren credenciales reales en `local.properties`. Asegurarse de que `APPLOGGER_SUPABASE_URL`, `APPLOGGER_SUPABASE_ANON_KEY`, y `APPLOGGER_SUPABASE_SERVICE_KEY` estén configurados. Los tests e2e solo corren en CI cuando se hace push a `main`. +Los tests en `SupabaseE2ETest.kt` requieren credenciales reales en `local.properties`. Asegurarse de que `appLogger_supabaseUrl`, `APPLOGGER_SUPABASE_ANON_KEY`, y `APPLOGGER_SUPABASE_SERVICE_KEY` estén configurados. Los tests e2e solo corren en CI cuando se hace push a `main`. --- diff --git a/local.properties.example b/local.properties.example index ad87836..ce27661 100644 --- a/local.properties.example +++ b/local.properties.example @@ -14,11 +14,11 @@ sdk.dir=C:\\Users\\TU_USUARIO\\AppData\\Local\\Android\\Sdk # ── Supabase (backend de logs) ────────────────────────────────────────────── -# Obtener de: https://supabase.com/dashboard → Settings → API -appLogger.url=https://TU-PROYECTO.supabase.co -appLogger.anonKey=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... +# Obtener de: https://supabase.com/dashboard -> Settings -> API +appLogger_url=https://TU-PROYECTO.supabase.co +appLogger_anonKey=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... # ── Modo Debug ────────────────────────────────────────────────────────────── -# true → logs en Logcat + envío al backend (desarrollo) -# false → solo envío al backend, sin output local (producción) -appLogger.debug=true +# true -> logs en Logcat + envio al backend (desarrollo) +# false -> solo envio al backend, sin output local (produccion) +appLogger_debug=true diff --git a/sdk/logger-core/build.gradle.kts b/sdk/logger-core/build.gradle.kts index 8dbdf3e..184a600 100644 --- a/sdk/logger-core/build.gradle.kts +++ b/sdk/logger-core/build.gradle.kts @@ -1,4 +1,5 @@ import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFramework +import java.io.File plugins { alias(libs.plugins.kotlin.multiplatform) @@ -8,6 +9,34 @@ plugins { `maven-publish` } +val sdkVersionName = providers.gradleProperty("VERSION_NAME").orElse("0.0.0-dev") +val generatedVersionDir = layout.buildDirectory.dir("generated/version/commonMain/kotlin") + +val generateAppLoggerVersion by tasks.registering { + val outDir = generatedVersionDir.get().asFile + outputs.dir(outDir) + + doLast { + val pkgDir = File(outDir, "com/applogger/core") + pkgDir.mkdirs() + + File(pkgDir, "AppLoggerVersion.kt").writeText( + """ + package com.applogger.core + + /** + * Single source of truth for the SDK version. + * + * Generated from Gradle property `VERSION_NAME`. + */ + object AppLoggerVersion { + const val NAME = "${sdkVersionName.get()}" + } + """.trimIndent() + ) + } +} + kotlin { androidTarget { compilations.all { @@ -34,6 +63,8 @@ kotlin { jvm() sourceSets { + getByName("commonMain").kotlin.srcDir(generatedVersionDir) + commonMain.dependencies { implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.serialization.json) @@ -73,6 +104,11 @@ kotlin { } } +tasks.matching { it.name.startsWith("compile") && it.name.contains("Kotlin") } + .configureEach { + dependsOn(generateAppLoggerVersion) + } + android { namespace = "com.applogger.core" compileSdk = libs.versions.compileSdk.get().toInt() diff --git a/sdk/logger-core/src/androidMain/kotlin/com/applogger/core/AppLoggerSDK.kt b/sdk/logger-core/src/androidMain/kotlin/com/applogger/core/AppLoggerSDK.kt index 6378d66..b60167d 100644 --- a/sdk/logger-core/src/androidMain/kotlin/com/applogger/core/AppLoggerSDK.kt +++ b/sdk/logger-core/src/androidMain/kotlin/com/applogger/core/AppLoggerSDK.kt @@ -108,14 +108,19 @@ object AppLoggerSDK : AppLogger { ) } - override fun debug(tag: String, message: String, extra: Map?) = - instance.debug(tag, message, extra) - - override fun info(tag: String, message: String, extra: Map?) = - instance.info(tag, message, extra) - - override fun warn(tag: String, message: String, anomalyType: String?, extra: Map?) = - instance.warn(tag, message, anomalyType, extra) + override fun debug(tag: String, message: String, throwable: Throwable?, extra: Map?) = + instance.debug(tag, message, throwable, extra) + + override fun info(tag: String, message: String, throwable: Throwable?, extra: Map?) = + instance.info(tag, message, throwable, extra) + + override fun warn( + tag: String, + message: String, + throwable: Throwable?, + anomalyType: String?, + extra: Map? + ) = instance.warn(tag, message, throwable, anomalyType, extra) override fun error(tag: String, message: String, throwable: Throwable?, extra: Map?) = instance.error(tag, message, throwable, extra) diff --git a/sdk/logger-core/src/commonMain/kotlin/com/applogger/core/AppLogger.kt b/sdk/logger-core/src/commonMain/kotlin/com/applogger/core/AppLogger.kt index bf1afc5..456cdd3 100644 --- a/sdk/logger-core/src/commonMain/kotlin/com/applogger/core/AppLogger.kt +++ b/sdk/logger-core/src/commonMain/kotlin/com/applogger/core/AppLogger.kt @@ -25,28 +25,37 @@ interface AppLogger { * * @param tag Short identifier for the source (e.g. class or screen name). * @param message Human-readable description. + * @param throwable Optional exception whose stack trace will be captured. * @param extra Optional key-value metadata attached to the event. */ - fun debug(tag: String, message: String, extra: Map? = null) + fun debug(tag: String, message: String, throwable: Throwable? = null, extra: Map? = null) /** * Logs an informational message. * * @param tag Short identifier for the source. * @param message Human-readable description. + * @param throwable Optional exception whose stack trace will be captured. * @param extra Optional key-value metadata. */ - fun info(tag: String, message: String, extra: Map? = null) + fun info(tag: String, message: String, throwable: Throwable? = null, extra: Map? = null) /** * Logs a warning. Use for recoverable anomalies. * * @param tag Short identifier for the source. * @param message Human-readable description. + * @param throwable Optional exception whose stack trace will be captured. * @param anomalyType Optional classification of the anomaly (e.g. "slow_response"). * @param extra Optional key-value metadata. */ - fun warn(tag: String, message: String, anomalyType: String? = null, extra: Map? = null) + fun warn( + tag: String, + message: String, + throwable: Throwable? = null, + anomalyType: String? = null, + extra: Map? = null + ) /** * Logs an error. Triggers immediate batch flush. diff --git a/sdk/logger-core/src/commonMain/kotlin/com/applogger/core/AppLoggerExtensions.kt b/sdk/logger-core/src/commonMain/kotlin/com/applogger/core/AppLoggerExtensions.kt new file mode 100644 index 0000000..2d7923f --- /dev/null +++ b/sdk/logger-core/src/commonMain/kotlin/com/applogger/core/AppLoggerExtensions.kt @@ -0,0 +1,95 @@ +package com.applogger.core + +/** + * Kotlin extension functions for [AppLogger] that infer the tag from the calling class name. + * + * These helpers reduce boilerplate in classes that already hold a logger reference. + * The tag is derived from the simple class name of the receiver. + * + * ## Usage + * ```kotlin + * class PlayerController(private val logger: AppLogger) { + * fun start() { + * logger.logI("PLAYER", "Playback started") + * logger.logW("PLAYER", "Buffer low", anomalyType = "BUFFER_LOW") + * logger.logE("PLAYER", "Playback failed", throwable = e) + * } + * } + * ``` + * + * ## Against-the-receiver style (tag auto-inferred as class name) + * ```kotlin + * class AuthRepository(private val logger: AppLogger) { + * fun login() { + * this.logD(logger, "Login attempt") + * this.logE(logger, "Login failed", throwable = e) + * } + * } + * ``` + * + * @see AppLogger for the full low-level API. + */ + +// ─── Convenience methods on AppLogger (no tag inference) ───────────────────── + +/** Logs a debug message. Shorthand for [AppLogger.debug]. */ +fun AppLogger.logD(tag: String, message: String, throwable: Throwable? = null, extra: Map? = null) = + debug(tag, message, throwable, extra) + +/** Logs an info message. Shorthand for [AppLogger.info]. */ +fun AppLogger.logI(tag: String, message: String, throwable: Throwable? = null, extra: Map? = null) = + info(tag, message, throwable, extra) + +/** Logs a warning. Shorthand for [AppLogger.warn]. */ +fun AppLogger.logW( + tag: String, + message: String, + throwable: Throwable? = null, + anomalyType: String? = null, + extra: Map? = null +) = warn(tag, message, throwable, anomalyType, extra) + +/** Logs an error. Shorthand for [AppLogger.error]. */ +fun AppLogger.logE(tag: String, message: String, throwable: Throwable? = null, extra: Map? = null) = + error(tag, message, throwable, extra) + +/** Logs a critical/fatal event. Shorthand for [AppLogger.critical]. */ +fun AppLogger.logC(tag: String, message: String, throwable: Throwable? = null, extra: Map? = null) = + critical(tag, message, throwable, extra) + +// ─── Tag-inferring extensions on Any ────────────────────────────────────────── + +/** + * Returns a safe tag derived from the simple class name of the receiver, + * suitable for use as an [AppLogger] tag. + * + * Anonymous or lambda classes return `"Anonymous"`. Tags longer than 100 + * characters are truncated (SDK limit). + */ +fun Any.logTag(): String = + this::class.simpleName?.take(100) ?: "Anonymous" + +/** Logs a debug message, inferring the tag from the receiver's class name. */ +fun Any.logD(logger: AppLogger, message: String, throwable: Throwable? = null, extra: Map? = null) = + logger.debug(logTag(), message, throwable, extra) + +/** Logs an info message, inferring the tag from the receiver's class name. */ +fun Any.logI(logger: AppLogger, message: String, throwable: Throwable? = null, extra: Map? = null) = + logger.info(logTag(), message, throwable, extra) + +/** Logs a warning, inferring the tag from the receiver's class name. */ +fun Any.logW( + logger: AppLogger, + message: String, + throwable: Throwable? = null, + anomalyType: String? = null, + extra: Map? = null +) = logger.warn(logTag(), message, throwable, anomalyType, extra) + +/** Logs an error, inferring the tag from the receiver's class name. */ +fun Any.logE(logger: AppLogger, message: String, throwable: Throwable? = null, extra: Map? = null) = + logger.error(logTag(), message, throwable, extra) + +/** Logs a critical event, inferring the tag from the receiver's class name. */ +fun Any.logC(logger: AppLogger, message: String, throwable: Throwable? = null, extra: Map? = null) = + logger.critical(logTag(), message, throwable, extra) diff --git a/sdk/logger-core/src/commonMain/kotlin/com/applogger/core/AppLoggerVersion.kt b/sdk/logger-core/src/commonMain/kotlin/com/applogger/core/AppLoggerVersion.kt deleted file mode 100644 index 3f74bc9..0000000 --- a/sdk/logger-core/src/commonMain/kotlin/com/applogger/core/AppLoggerVersion.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.applogger.core - -/** - * Single source of truth for the SDK version. - * - * Embedded in every [com.applogger.core.model.LogEvent.sdkVersion] field. - * Update this constant when publishing a new release. - */ -object AppLoggerVersion { - /** Semantic version string following [semver.org](https://semver.org). */ - const val NAME = "0.1.1-alpha.3" -} diff --git a/sdk/logger-core/src/commonMain/kotlin/com/applogger/core/internal/AppLoggerImpl.kt b/sdk/logger-core/src/commonMain/kotlin/com/applogger/core/internal/AppLoggerImpl.kt index 6857e8b..a765f11 100644 --- a/sdk/logger-core/src/commonMain/kotlin/com/applogger/core/internal/AppLoggerImpl.kt +++ b/sdk/logger-core/src/commonMain/kotlin/com/applogger/core/internal/AppLoggerImpl.kt @@ -18,20 +18,26 @@ internal class AppLoggerImpl( @Volatile private var userId: String? = null - override fun debug(tag: String, message: String, extra: Map?) { - process(LogLevel.DEBUG, tag, message, extra = extra) + override fun debug(tag: String, message: String, throwable: Throwable?, extra: Map?) { + process(LogLevel.DEBUG, tag, message, throwable, extra = extra) } - override fun info(tag: String, message: String, extra: Map?) { - process(LogLevel.INFO, tag, message, extra = extra) + override fun info(tag: String, message: String, throwable: Throwable?, extra: Map?) { + process(LogLevel.INFO, tag, message, throwable, extra = extra) } - override fun warn(tag: String, message: String, anomalyType: String?, extra: Map?) { + override fun warn( + tag: String, + message: String, + throwable: Throwable?, + anomalyType: String?, + extra: Map? + ) { val mergedExtra = buildMap { extra?.forEach { (k, v) -> put(k, v.toString()) } anomalyType?.let { put("anomaly_type", it) } }.ifEmpty { null } - process(LogLevel.WARN, tag, message, extraStr = mergedExtra) + process(LogLevel.WARN, tag, message, throwable, extraStr = mergedExtra) } override fun error(tag: String, message: String, throwable: Throwable?, extra: Map?) { diff --git a/sdk/logger-core/src/commonMain/kotlin/com/applogger/core/internal/NoOpLogger.kt b/sdk/logger-core/src/commonMain/kotlin/com/applogger/core/internal/NoOpLogger.kt index be4764b..e4fbec2 100644 --- a/sdk/logger-core/src/commonMain/kotlin/com/applogger/core/internal/NoOpLogger.kt +++ b/sdk/logger-core/src/commonMain/kotlin/com/applogger/core/internal/NoOpLogger.kt @@ -8,9 +8,15 @@ import com.applogger.core.AppLogger * Garantiza que llamadas tempranas no crasheen. */ internal class NoOpLogger : AppLogger { - override fun debug(tag: String, message: String, extra: Map?) = Unit - override fun info(tag: String, message: String, extra: Map?) = Unit - override fun warn(tag: String, message: String, anomalyType: String?, extra: Map?) = Unit + override fun debug(tag: String, message: String, throwable: Throwable?, extra: Map?) = Unit + override fun info(tag: String, message: String, throwable: Throwable?, extra: Map?) = Unit + override fun warn( + tag: String, + message: String, + throwable: Throwable?, + anomalyType: String?, + extra: Map? + ) = Unit override fun error(tag: String, message: String, throwable: Throwable?, extra: Map?) = Unit override fun critical(tag: String, message: String, throwable: Throwable?, extra: Map?) = Unit override fun metric(name: String, value: Double, unit: String, tags: Map?) = Unit diff --git a/sdk/logger-core/src/iosMain/kotlin/com/applogger/core/AppLoggerIos.kt b/sdk/logger-core/src/iosMain/kotlin/com/applogger/core/AppLoggerIos.kt index ad82ab4..e10015a 100644 --- a/sdk/logger-core/src/iosMain/kotlin/com/applogger/core/AppLoggerIos.kt +++ b/sdk/logger-core/src/iosMain/kotlin/com/applogger/core/AppLoggerIos.kt @@ -72,14 +72,20 @@ class AppLoggerIos private constructor() : AppLogger { } } - override fun debug(tag: String, message: String, extra: Map?) = - instance.debug(tag, message, extra) - - override fun info(tag: String, message: String, extra: Map?) = - instance.info(tag, message, extra) - - override fun warn(tag: String, message: String, anomalyType: String?, extra: Map?) = - instance.warn(tag, message, anomalyType, extra) + override fun debug(tag: String, message: String, throwable: Throwable?, extra: Map?) = + instance.debug(tag, message, throwable, extra) + + override fun info(tag: String, message: String, throwable: Throwable?, extra: Map?) = + instance.info(tag, message, throwable, extra) + + override fun warn( + tag: String, + message: String, + throwable: Throwable?, + anomalyType: String?, + extra: Map? + ) = + instance.warn(tag, message, throwable, anomalyType, extra) override fun error(tag: String, message: String, throwable: Throwable?, extra: Map?) = instance.error(tag, message, throwable, extra) diff --git a/sdk/logger-core/src/jvmTest/kotlin/com/applogger/core/AppLoggerExtensionsTest.kt b/sdk/logger-core/src/jvmTest/kotlin/com/applogger/core/AppLoggerExtensionsTest.kt new file mode 100644 index 0000000..62467c9 --- /dev/null +++ b/sdk/logger-core/src/jvmTest/kotlin/com/applogger/core/AppLoggerExtensionsTest.kt @@ -0,0 +1,162 @@ +package com.applogger.core + +import com.applogger.core.model.LogLevel +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class AppLoggerExtensionsTest { + + // ── Minimal in-test spy, avoids logger-test circular dep ────────────────── + + data class Capture( + val level: LogLevel, + val tag: String, + val message: String, + val throwable: Throwable? = null, + val extra: Map? = null + ) + + class SpyLogger : AppLogger { + val calls = mutableListOf() + override fun debug(tag: String, message: String, throwable: Throwable?, extra: Map?) = + calls.add(Capture(LogLevel.DEBUG, tag, message, throwable, extra)).let {} + override fun info(tag: String, message: String, throwable: Throwable?, extra: Map?) = + calls.add(Capture(LogLevel.INFO, tag, message, throwable, extra)).let {} + override fun warn(tag: String, message: String, throwable: Throwable?, anomalyType: String?, extra: Map?) = + calls.add(Capture(LogLevel.WARN, tag, message, throwable, extra)).let {} + override fun error(tag: String, message: String, throwable: Throwable?, extra: Map?) = + calls.add(Capture(LogLevel.ERROR, tag, message, throwable, extra)).let {} + override fun critical(tag: String, message: String, throwable: Throwable?, extra: Map?) = + calls.add(Capture(LogLevel.CRITICAL, tag, message, throwable, extra)).let {} + override fun metric(name: String, value: Double, unit: String, tags: Map?) = Unit + override fun flush() = Unit + } + + private lateinit var spy: SpyLogger + + @BeforeEach + fun setup() { + spy = SpyLogger() + } + + // ── logTag() ────────────────────────────────────────────────────────────── + + @Test + fun `logTag returns simple class name`() { + assertEquals("AppLoggerExtensionsTest", this.logTag()) + } + + @Test + fun `logTag on anonymous object returns Anonymous`() { + val anon = object : Any() {} + assertEquals("Anonymous", anon.logTag()) + } + + // ── AppLogger shorthand extensions ──────────────────────────────────────── + + @Test + fun `logD delegates to debug with correct tag and message`() { + spy.logD("TAG", "debug message") + assertEquals(1, spy.calls.size) + spy.calls[0].also { + assertEquals(LogLevel.DEBUG, it.level) + assertEquals("TAG", it.tag) + assertEquals("debug message", it.message) + assertNull(it.throwable) + } + } + + @Test + fun `logI delegates to info with throwable`() { + val e = RuntimeException("info-error") + spy.logI("TAG", "info message", throwable = e) + assertEquals(LogLevel.INFO, spy.calls[0].level) + assertEquals(e, spy.calls[0].throwable) + } + + @Test + fun `logW delegates to warn with anomalyType and throwable`() { + val e = Exception("slow") + spy.logW("NETWORK", "Slow response", throwable = e, anomalyType = "HIGH_LATENCY") + assertEquals(LogLevel.WARN, spy.calls[0].level) + assertEquals(e, spy.calls[0].throwable) + } + + @Test + fun `logE delegates to error`() { + val e = RuntimeException("payment failed") + spy.logE("PAYMENT", "Transaction error", throwable = e) + assertEquals(LogLevel.ERROR, spy.calls[0].level) + assertEquals("payment failed", spy.calls[0].throwable?.message) + } + + @Test + fun `logC delegates to critical`() { + spy.logC("AUTH", "Token refresh failed") + assertEquals(LogLevel.CRITICAL, spy.calls[0].level) + assertEquals("AUTH", spy.calls[0].tag) + } + + @Test + fun `logD passes extra map`() { + spy.logD("TAG", "msg", extra = mapOf("key" to "value")) + assertEquals("value", spy.calls[0].extra?.get("key")) + } + + // ── Any tag-inferring extensions ────────────────────────────────────────── + + @Test + fun `Any logD infers tag from class name`() { + this.logD(spy, "debug via any") + assertEquals("AppLoggerExtensionsTest", spy.calls[0].tag) + assertEquals("debug via any", spy.calls[0].message) + assertEquals(LogLevel.DEBUG, spy.calls[0].level) + } + + @Test + fun `Any logI infers tag and captures throwable`() { + val e = RuntimeException("any-info-error") + this.logI(spy, "info via any", throwable = e) + assertEquals("AppLoggerExtensionsTest", spy.calls[0].tag) + assertEquals(e, spy.calls[0].throwable) + } + + @Test + fun `Any logW infers tag with anomalyType and throwable`() { + val e = Exception("warn via any") + this.logW(spy, "warn message", throwable = e, anomalyType = "ANOMALY") + assertEquals("AppLoggerExtensionsTest", spy.calls[0].tag) + assertEquals(e, spy.calls[0].throwable) + assertEquals(LogLevel.WARN, spy.calls[0].level) + } + + @Test + fun `Any logE infers tag and records error`() { + val e = RuntimeException("error via any") + this.logE(spy, "error message", throwable = e) + assertEquals("AppLoggerExtensionsTest", spy.calls[0].tag) + assertEquals(e, spy.calls[0].throwable) + assertEquals(LogLevel.ERROR, spy.calls[0].level) + } + + @Test + fun `Any logC infers tag and records critical`() { + this.logC(spy, "critical message") + assertEquals("AppLoggerExtensionsTest", spy.calls[0].tag) + assertEquals("critical message", spy.calls[0].message) + assertEquals(LogLevel.CRITICAL, spy.calls[0].level) + } + + @Test + fun `Any logD with extra map passes metadata`() { + this.logD(spy, "with extra", extra = mapOf("key" to "value")) + assertEquals("value", spy.calls[0].extra?.get("key")) + } + + @Test + fun `null throwable results in null in captured call`() { + this.logI(spy, "no exception", throwable = null) + assertNull(spy.calls[0].throwable) + } +} diff --git a/sdk/logger-core/src/jvmTest/kotlin/com/applogger/core/internal/AppLoggerImplTest.kt b/sdk/logger-core/src/jvmTest/kotlin/com/applogger/core/internal/AppLoggerImplTest.kt index 9e3912c..d21a605 100644 --- a/sdk/logger-core/src/jvmTest/kotlin/com/applogger/core/internal/AppLoggerImplTest.kt +++ b/sdk/logger-core/src/jvmTest/kotlin/com/applogger/core/internal/AppLoggerImplTest.kt @@ -216,6 +216,60 @@ class AppLoggerImplTest { assertNull(fakeTransport.sentEvents[0].userId) } + // ── Throwable propagation on non-critical levels ─────────────────────────── + + @Test + fun `debug throwable is captured in debug mode`() = runBlocking { + logger = createLogger(buildConfig(debugMode = true)) + val exception = IllegalStateException("unexpected state") + logger.debug("TAG", "debug with throwable", throwable = exception) + delay(200) + processor.sendBatch() + + assertEquals(1, fakeTransport.sentEvents.size) + val event = fakeTransport.sentEvents[0] + assertNotNull(event.throwableInfo) + assertEquals("IllegalStateException", event.throwableInfo?.type) + assertEquals("unexpected state", event.throwableInfo?.message) + } + + @Test + fun `info throwable is captured in throwableInfo`() = runBlocking { + logger = createLogger(buildConfig(debugMode = false)) + val exception = RuntimeException("info-level anomaly") + logger.info("PLAYER", "Recovered after error", throwable = exception) + delay(200) + processor.sendBatch() + + assertEquals(1, fakeTransport.sentEvents.size) + assertNotNull(fakeTransport.sentEvents[0].throwableInfo) + assertEquals("RuntimeException", fakeTransport.sentEvents[0].throwableInfo?.type) + } + + @Test + fun `warn throwable is captured alongside anomalyType`() = runBlocking { + logger = createLogger(buildConfig(debugMode = false)) + val exception = Exception("timeout") + logger.warn("NETWORK", "Slow response", throwable = exception, anomalyType = "HIGH_LATENCY") + delay(200) + processor.sendBatch() + + val event = fakeTransport.sentEvents[0] + assertNotNull(event.throwableInfo) + assertEquals("Exception", event.throwableInfo?.type) + assertEquals("HIGH_LATENCY", event.extra?.get("anomaly_type")) + } + + @Test + fun `debug without throwable produces null throwableInfo`() = runBlocking { + logger = createLogger(buildConfig(debugMode = true)) + logger.debug("TAG", "no throwable") + delay(200) + processor.sendBatch() + + assertNull(fakeTransport.sentEvents[0].throwableInfo) + } + /** * Simple recording transport for tests. */ diff --git a/sdk/logger-test/src/commonMain/kotlin/com/applogger/test/InMemoryLogger.kt b/sdk/logger-test/src/commonMain/kotlin/com/applogger/test/InMemoryLogger.kt index 8447eae..b14714d 100644 --- a/sdk/logger-test/src/commonMain/kotlin/com/applogger/test/InMemoryLogger.kt +++ b/sdk/logger-test/src/commonMain/kotlin/com/applogger/test/InMemoryLogger.kt @@ -38,16 +38,22 @@ class InMemoryLogger : AppLogger { val lastError get() = _logs.lastOrNull { it.level == LogLevel.ERROR } val lastCritical get() = _logs.lastOrNull { it.level == LogLevel.CRITICAL } - override fun debug(tag: String, message: String, extra: Map?) { - _logs.add(LogEntry(LogLevel.DEBUG, tag, message, extra = extra)) + override fun debug(tag: String, message: String, throwable: Throwable?, extra: Map?) { + _logs.add(LogEntry(LogLevel.DEBUG, tag, message, throwable, extra)) } - override fun info(tag: String, message: String, extra: Map?) { - _logs.add(LogEntry(LogLevel.INFO, tag, message, extra = extra)) + override fun info(tag: String, message: String, throwable: Throwable?, extra: Map?) { + _logs.add(LogEntry(LogLevel.INFO, tag, message, throwable, extra)) } - override fun warn(tag: String, message: String, anomalyType: String?, extra: Map?) { - _logs.add(LogEntry(LogLevel.WARN, tag, message, extra = extra)) + override fun warn( + tag: String, + message: String, + throwable: Throwable?, + anomalyType: String?, + extra: Map? + ) { + _logs.add(LogEntry(LogLevel.WARN, tag, message, throwable, extra)) } override fun error(tag: String, message: String, throwable: Throwable?, extra: Map?) { diff --git a/sdk/logger-test/src/commonMain/kotlin/com/applogger/test/NoOpTestLogger.kt b/sdk/logger-test/src/commonMain/kotlin/com/applogger/test/NoOpTestLogger.kt index efdefbd..4580437 100644 --- a/sdk/logger-test/src/commonMain/kotlin/com/applogger/test/NoOpTestLogger.kt +++ b/sdk/logger-test/src/commonMain/kotlin/com/applogger/test/NoOpTestLogger.kt @@ -9,9 +9,15 @@ import com.applogger.core.AppLogger * the subject under test. */ class NoOpTestLogger : AppLogger { - override fun debug(tag: String, message: String, extra: Map?) = Unit - override fun info(tag: String, message: String, extra: Map?) = Unit - override fun warn(tag: String, message: String, anomalyType: String?, extra: Map?) = Unit + override fun debug(tag: String, message: String, throwable: Throwable?, extra: Map?) = Unit + override fun info(tag: String, message: String, throwable: Throwable?, extra: Map?) = Unit + override fun warn( + tag: String, + message: String, + throwable: Throwable?, + anomalyType: String?, + extra: Map? + ) = Unit override fun error(tag: String, message: String, throwable: Throwable?, extra: Map?) = Unit override fun critical(tag: String, message: String, throwable: Throwable?, extra: Map?) = Unit override fun metric(name: String, value: Double, unit: String, tags: Map?) = Unit diff --git a/sdk/logger-test/src/jvmTest/kotlin/com/applogger/test/InMemoryLoggerTest.kt b/sdk/logger-test/src/jvmTest/kotlin/com/applogger/test/InMemoryLoggerTest.kt index bae79ea..7847365 100644 --- a/sdk/logger-test/src/jvmTest/kotlin/com/applogger/test/InMemoryLoggerTest.kt +++ b/sdk/logger-test/src/jvmTest/kotlin/com/applogger/test/InMemoryLoggerTest.kt @@ -117,4 +117,34 @@ class InMemoryLoggerTest { assertEquals(1, snapshot.size) // Snapshot doesn't grow assertEquals(2, logger.logs.size) // But actual logs does } + + // ── Throwable capture on non-critical levels ─────────────────────────────── + + @Test + fun `debug throwable is captured in LogEntry`() { + val exception = IllegalStateException("debug error") + logger.debug("TAG", "debug msg", throwable = exception) + assertEquals(exception, logger.logs.first().throwable) + } + + @Test + fun `info throwable is captured in LogEntry`() { + val exception = RuntimeException("info error") + logger.info("TAG", "info msg", throwable = exception) + assertEquals(exception, logger.logs.first().throwable) + } + + @Test + fun `warn throwable is captured in LogEntry`() { + val exception = Exception("warn error") + logger.warn("TAG", "warn msg", throwable = exception, anomalyType = "SLOW") + assertEquals(exception, logger.logs.first().throwable) + assertEquals(1, logger.warnCount) + } + + @Test + fun `null throwable is absent in LogEntry`() { + logger.info("TAG", "no throwable") + assertNull(logger.logs.first().throwable) + } } diff --git a/sdk/logger-transport-supabase/src/jvmTest/kotlin/com/applogger/transport/supabase/SupabaseE2ETest.kt b/sdk/logger-transport-supabase/src/jvmTest/kotlin/com/applogger/transport/supabase/SupabaseE2ETest.kt index b67faed..a547d2c 100644 --- a/sdk/logger-transport-supabase/src/jvmTest/kotlin/com/applogger/transport/supabase/SupabaseE2ETest.kt +++ b/sdk/logger-transport-supabase/src/jvmTest/kotlin/com/applogger/transport/supabase/SupabaseE2ETest.kt @@ -18,11 +18,13 @@ import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable * Test E2E real contra Supabase. * * Solo se ejecuta si las variables de entorno están configuradas: + * appLogger_supabaseUrl=https://tu-proyecto.supabase.co * APPLOGGER_SUPABASE_URL=https://tu-proyecto.supabase.co * APPLOGGER_SUPABASE_ANON_KEY=eyJhbGc... * APPLOGGER_SUPABASE_SERVICE_KEY=eyJhbGc... (para lectura/verificación) * * Para ejecutar: + * $env:appLogger_supabaseUrl = "https://xxx.supabase.co" * $env:APPLOGGER_SUPABASE_URL = "https://xxx.supabase.co" * $env:APPLOGGER_SUPABASE_ANON_KEY = "eyJ..." * $env:APPLOGGER_SUPABASE_SERVICE_KEY = "eyJ..." @@ -34,7 +36,8 @@ import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable class SupabaseE2ETest { companion object { - private val url = System.getenv("APPLOGGER_SUPABASE_URL") ?: "" + private val url = System.getenv("appLogger_supabaseUrl")?.takeIf { it.isNotBlank() } + ?: (System.getenv("APPLOGGER_SUPABASE_URL") ?: "") private val anonKey = System.getenv("APPLOGGER_SUPABASE_ANON_KEY") ?: "" private val serviceKey = System.getenv("APPLOGGER_SUPABASE_SERVICE_KEY") ?: "" diff --git a/sdk/sample/build.gradle.kts b/sdk/sample/build.gradle.kts index 5d37f01..171f380 100644 --- a/sdk/sample/build.gradle.kts +++ b/sdk/sample/build.gradle.kts @@ -10,8 +10,15 @@ android { defaultConfig { minSdk = libs.versions.minSdk.get().toInt() - buildConfigField("String", "LOGGER_URL", "\"${project.findProperty("appLogger.url") ?: ""}\"") - buildConfigField("String", "LOGGER_KEY", "\"${project.findProperty("appLogger.anonKey") ?: ""}\"") + val loggerUrl = project.findProperty("appLogger_url") + ?: project.findProperty("appLogger.url") + ?: "" + val loggerKey = project.findProperty("appLogger_anonKey") + ?: project.findProperty("appLogger.anonKey") + ?: "" + + buildConfigField("String", "LOGGER_URL", "\"${loggerUrl}\"") + buildConfigField("String", "LOGGER_KEY", "\"${loggerKey}\"") } buildFeatures { buildConfig = true }