|
6 | 6 | assertStringIncludes, |
7 | 7 | } from "@std/assert"; |
8 | 8 | import { createFederationDebugger } from "./mod.tsx"; |
9 | | -import type { FederationDebuggerAuth } from "./mod.tsx"; |
| 9 | +import type { FederationDebuggerAuth, SerializedLogRecord } from "./mod.tsx"; |
10 | 10 | import type { |
11 | 11 | Federation, |
12 | 12 | FederationFetchOptions, |
@@ -774,3 +774,164 @@ test("auth password: logout clears session cookie", async () => { |
774 | 774 | assert(setCookie != null); |
775 | 775 | assertStringIncludes(setCookie!, "Max-Age=0"); |
776 | 776 | }); |
| 777 | + |
| 778 | +// ---------- Sink property tests ---------- |
| 779 | + |
| 780 | +test("createFederationDebugger exposes a sink property", () => { |
| 781 | + const { federation } = createMockFederation(); |
| 782 | + const exporter = createMockExporter(); |
| 783 | + const dbg = createFederationDebugger(federation, { exporter }); |
| 784 | + assertNotEquals(dbg.sink, null); |
| 785 | + assertNotEquals(dbg.sink, undefined); |
| 786 | + assertEquals(typeof dbg.sink, "function"); |
| 787 | +}); |
| 788 | + |
| 789 | +test("simplified overload exposes a sink property", () => { |
| 790 | + const originalProvider = trace.getTracerProvider(); |
| 791 | + try { |
| 792 | + const { federation } = createMockFederation(); |
| 793 | + const dbg = createFederationDebugger(federation); |
| 794 | + assertNotEquals(dbg.sink, null); |
| 795 | + assertEquals(typeof dbg.sink, "function"); |
| 796 | + } finally { |
| 797 | + trace.setGlobalTracerProvider(originalProvider); |
| 798 | + } |
| 799 | +}); |
| 800 | + |
| 801 | +// ---------- Log collection tests ---------- |
| 802 | + |
| 803 | +test("sink collects logs by traceId and API returns them", async () => { |
| 804 | + const { federation } = createMockFederation(); |
| 805 | + const exporter = createMockExporter(); |
| 806 | + const dbg = createFederationDebugger(federation, { exporter }); |
| 807 | + |
| 808 | + // Simulate log records with traceId in properties |
| 809 | + const traceId = "aaaa1111bbbb2222cccc3333dddd4444"; |
| 810 | + dbg.sink({ |
| 811 | + category: ["fedify", "federation", "http"], |
| 812 | + level: "info", |
| 813 | + message: ["GET ", "/users/alice", ": ", "200"], |
| 814 | + rawMessage: "{method} {path}: {status}", |
| 815 | + timestamp: Date.now(), |
| 816 | + properties: { |
| 817 | + traceId, |
| 818 | + spanId: "1234567890abcdef", |
| 819 | + method: "GET", |
| 820 | + path: "/users/alice", |
| 821 | + status: 200, |
| 822 | + }, |
| 823 | + }); |
| 824 | + |
| 825 | + // Check via API |
| 826 | + const request = new Request( |
| 827 | + `https://example.com/__debug__/api/logs/${traceId}`, |
| 828 | + ); |
| 829 | + const response = await dbg.fetch(request, { contextData: undefined }); |
| 830 | + assertEquals(response.status, 200); |
| 831 | + assertEquals(response.headers.get("content-type"), "application/json"); |
| 832 | + const logs = (await response.json()) as SerializedLogRecord[]; |
| 833 | + assertEquals(logs.length, 1); |
| 834 | + assertEquals(logs[0].level, "info"); |
| 835 | + assertEquals(logs[0].message, "GET /users/alice: 200"); |
| 836 | + assertEquals(logs[0].category[0], "fedify"); |
| 837 | + // traceId and spanId should be excluded from properties |
| 838 | + assertEquals("traceId" in logs[0].properties, false); |
| 839 | + assertEquals("spanId" in logs[0].properties, false); |
| 840 | + // Other properties should be preserved |
| 841 | + assertEquals(logs[0].properties.method, "GET"); |
| 842 | +}); |
| 843 | + |
| 844 | +test("sink ignores log records without traceId", async () => { |
| 845 | + const { federation } = createMockFederation(); |
| 846 | + const exporter = createMockExporter(); |
| 847 | + const dbg = createFederationDebugger(federation, { exporter }); |
| 848 | + |
| 849 | + // Log without traceId — should be silently ignored |
| 850 | + dbg.sink({ |
| 851 | + category: ["app"], |
| 852 | + level: "debug", |
| 853 | + message: ["some message"], |
| 854 | + rawMessage: "some message", |
| 855 | + timestamp: Date.now(), |
| 856 | + properties: {}, |
| 857 | + }); |
| 858 | + |
| 859 | + // No logs should be stored for any traceId |
| 860 | + const request = new Request( |
| 861 | + "https://example.com/__debug__/api/logs/nonexistent", |
| 862 | + ); |
| 863 | + const response = await dbg.fetch(request, { contextData: undefined }); |
| 864 | + assertEquals(response.status, 200); |
| 865 | + const logs = (await response.json()) as SerializedLogRecord[]; |
| 866 | + assertEquals(logs.length, 0); |
| 867 | +}); |
| 868 | + |
| 869 | +test("multiple logs for the same trace are grouped", async () => { |
| 870 | + const { federation } = createMockFederation(); |
| 871 | + const exporter = createMockExporter(); |
| 872 | + const dbg = createFederationDebugger(federation, { exporter }); |
| 873 | + |
| 874 | + const traceId = "aaaa1111bbbb2222cccc3333dddd4444"; |
| 875 | + for (let i = 0; i < 5; i++) { |
| 876 | + dbg.sink({ |
| 877 | + category: ["fedify"], |
| 878 | + level: "info", |
| 879 | + message: [`log ${i}`], |
| 880 | + rawMessage: `log ${i}`, |
| 881 | + timestamp: Date.now() + i, |
| 882 | + properties: { traceId, spanId: "abcdef1234567890" }, |
| 883 | + }); |
| 884 | + } |
| 885 | + |
| 886 | + const request = new Request( |
| 887 | + `https://example.com/__debug__/api/logs/${traceId}`, |
| 888 | + ); |
| 889 | + const response = await dbg.fetch(request, { contextData: undefined }); |
| 890 | + const logs = (await response.json()) as SerializedLogRecord[]; |
| 891 | + assertEquals(logs.length, 5); |
| 892 | + assertEquals(logs[0].message, "log 0"); |
| 893 | + assertEquals(logs[4].message, "log 4"); |
| 894 | +}); |
| 895 | + |
| 896 | +test("trace detail page shows log records", async () => { |
| 897 | + const { federation } = createMockFederation(); |
| 898 | + const exporter = createMockExporter(); |
| 899 | + const dbg = createFederationDebugger(federation, { exporter }); |
| 900 | + |
| 901 | + const traceId = "aaaa1111bbbb2222cccc3333dddd4444"; |
| 902 | + dbg.sink({ |
| 903 | + category: ["fedify", "federation"], |
| 904 | + level: "warning", |
| 905 | + message: ["Something went wrong"], |
| 906 | + rawMessage: "Something went wrong", |
| 907 | + timestamp: 1700000000000, |
| 908 | + properties: { traceId, spanId: "1234567890abcdef" }, |
| 909 | + }); |
| 910 | + |
| 911 | + const request = new Request( |
| 912 | + `https://example.com/__debug__/traces/${traceId}`, |
| 913 | + ); |
| 914 | + const response = await dbg.fetch(request, { contextData: undefined }); |
| 915 | + assertEquals(response.status, 200); |
| 916 | + const html = await response.text(); |
| 917 | + assertStringIncludes(html, "Logs"); |
| 918 | + assertStringIncludes(html, "Something went wrong"); |
| 919 | + assertStringIncludes(html, "warning"); |
| 920 | + assertStringIncludes(html, "fedify.federation"); |
| 921 | + // "1" and "log record" are separated by HTML tags |
| 922 | + assertStringIncludes(html, "log record"); |
| 923 | +}); |
| 924 | + |
| 925 | +test("trace detail page shows empty log message", async () => { |
| 926 | + const { federation } = createMockFederation(); |
| 927 | + const exporter = createMockExporter(); |
| 928 | + const dbg = createFederationDebugger(federation, { exporter }); |
| 929 | + |
| 930 | + const request = new Request( |
| 931 | + "https://example.com/__debug__/traces/0000000000000000", |
| 932 | + ); |
| 933 | + const response = await dbg.fetch(request, { contextData: undefined }); |
| 934 | + assertEquals(response.status, 200); |
| 935 | + const html = await response.text(); |
| 936 | + assertStringIncludes(html, "No logs captured for this trace."); |
| 937 | +}); |
0 commit comments